You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
284 lines
7.0 KiB
284 lines
7.0 KiB
package main
|
|
|
|
import (
|
|
"encoding/base32"
|
|
"encoding/json"
|
|
"fmt"
|
|
"git.wownero.com/onionltd/monero-multisig-broker/members"
|
|
"git.wownero.com/onionltd/monero-multisig-broker/messagelog"
|
|
"github.com/hashicorp/go-uuid"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
"github.com/sethvargo/go-password/password"
|
|
"go.uber.org/zap"
|
|
"net/http"
|
|
"strconv"
|
|
)
|
|
|
|
type server struct {
|
|
logger *zap.Logger
|
|
router *echo.Echo
|
|
config *config
|
|
messageLog *messagelog.Log
|
|
memberDB *members.Database
|
|
}
|
|
|
|
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
s.router.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (s *server) defaultErrorHandler(err error, c echo.Context) {
|
|
code := http.StatusInternalServerError
|
|
if he, ok := err.(*echo.HTTPError); ok {
|
|
code = he.Code
|
|
} else {
|
|
code = http.StatusInternalServerError
|
|
}
|
|
if !c.Response().Committed {
|
|
if c.Request().Method == http.MethodHead {
|
|
_ = c.NoContent(code)
|
|
} else {
|
|
_ = c.JSON(code, map[string]interface{}{
|
|
"error": http.StatusText(code),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *server) response(c echo.Context, status int, data interface{}) error {
|
|
return c.JSON(status, data)
|
|
}
|
|
|
|
func (s *server) responseOK(c echo.Context, data interface{}) error {
|
|
return s.response(c, http.StatusOK, data)
|
|
}
|
|
|
|
func (s *server) responseCreated(c echo.Context, data interface{}) error {
|
|
return s.response(c, http.StatusCreated, data)
|
|
}
|
|
|
|
func (s *server) responseNoContent(c echo.Context) error {
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *server) error(c echo.Context, status int, err error) error {
|
|
type genericError struct {
|
|
Error string `json:"error"`
|
|
}
|
|
return c.JSON(status, genericError{Error: err.Error()})
|
|
}
|
|
|
|
func (s *server) errorBadRequest(c echo.Context, err error) error {
|
|
s.logger.Info("client error", zap.Error(err))
|
|
return s.error(c, http.StatusBadRequest, err)
|
|
}
|
|
|
|
func (s *server) errorNotFound(c echo.Context, err error) error {
|
|
s.logger.Info("client error", zap.Error(err))
|
|
return s.error(c, http.StatusNotFound, err)
|
|
}
|
|
|
|
func (s *server) errorInternal(c echo.Context, err error) error {
|
|
s.logger.Error("server error", zap.Error(err))
|
|
// Overwrite error so not to leak internal errors to clients
|
|
err = fmt.Errorf("internal server error")
|
|
return s.error(c, http.StatusInternalServerError, err)
|
|
}
|
|
|
|
func (s *server) handleInitMultisig() echo.HandlerFunc {
|
|
type member struct{}
|
|
|
|
type input struct {
|
|
Members map[string]member `json:"members"`
|
|
}
|
|
|
|
type secret struct {
|
|
Host string `json:"host"`
|
|
Topic string `json:"topic"`
|
|
Token string `json:"token"`
|
|
Nickname string `json:"nickname"`
|
|
}
|
|
encodeSecret := func(s secret) (string, error) {
|
|
b, err := json.Marshal(s)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base32.StdEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
type output struct {
|
|
Topic string `json:"topic"`
|
|
Secrets map[string]string `json:"secrets"`
|
|
}
|
|
|
|
return func(c echo.Context) error {
|
|
in := &input{}
|
|
|
|
if err := c.Bind(in); err != nil {
|
|
return s.errorBadRequest(c, err)
|
|
}
|
|
|
|
// TODO: validate input!
|
|
|
|
// Check if input contains manager's username
|
|
managerNickname := s.config.ManagementCredentials.Username()
|
|
if _, ok := in.Members[managerNickname]; ok {
|
|
return s.errorBadRequest(c, fmt.Errorf("username `%s` already taken", managerNickname))
|
|
}
|
|
in.Members[managerNickname] = member{}
|
|
|
|
topic, err := uuid.GenerateUUID()
|
|
if err != nil {
|
|
return s.errorInternal(c, err)
|
|
}
|
|
|
|
memberSecrets := map[string]string{}
|
|
for nickname, _ := range in.Members {
|
|
// TODO: put password length in config.
|
|
token, err := password.Generate(18, 6, 0, false, false)
|
|
if err != nil {
|
|
return s.errorInternal(c, err)
|
|
}
|
|
|
|
member := members.Member{
|
|
Nickname: nickname,
|
|
Token: token,
|
|
}
|
|
s.memberDB.AddMember(member)
|
|
|
|
secret := secret{
|
|
// TODO: put URL in the config.
|
|
Host: c.Request().Host,
|
|
Topic: topic,
|
|
Token: token,
|
|
Nickname: nickname,
|
|
}
|
|
encoded, err := encodeSecret(secret)
|
|
if err != nil {
|
|
return s.errorInternal(c, err)
|
|
}
|
|
memberSecrets[nickname] = encoded
|
|
}
|
|
|
|
// TODO: put initial capacity in the config.
|
|
s.messageLog.AddTopic(topic, 128)
|
|
|
|
out := output{
|
|
Topic: topic,
|
|
Secrets: memberSecrets,
|
|
}
|
|
|
|
return s.responseCreated(c, out)
|
|
}
|
|
}
|
|
|
|
func (s *server) handleListMessages() echo.HandlerFunc {
|
|
offsetToNumber := func(s string) (int64, error) {
|
|
if s == "" {
|
|
return 0, nil
|
|
}
|
|
return strconv.ParseInt(s, 10, 64)
|
|
}
|
|
|
|
return func(c echo.Context) error {
|
|
topic := c.Param("topic")
|
|
offset, err := offsetToNumber(c.QueryParam("offset"))
|
|
if err != nil {
|
|
return s.errorBadRequest(c, fmt.Errorf("offset is not a number"))
|
|
}
|
|
|
|
messages, err := s.messageLog.List(topic, int(offset))
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case *messagelog.ErrInvalidMessage:
|
|
return s.errorBadRequest(c, err)
|
|
case *messagelog.ErrTopicNotFound:
|
|
return s.errorNotFound(c, err)
|
|
}
|
|
return s.errorInternal(c, err)
|
|
}
|
|
|
|
return s.responseOK(c, messages)
|
|
}
|
|
}
|
|
|
|
func (s *server) handlePushMessage() echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
message := messagelog.Message{}
|
|
|
|
if err := c.Bind(&message); err != nil {
|
|
return s.errorBadRequest(c, fmt.Errorf("invalid message format"))
|
|
}
|
|
|
|
member, ok := c.Get("member").(members.Member)
|
|
if !ok {
|
|
return s.errorInternal(c, fmt.Errorf("missing member information from the middleware"))
|
|
}
|
|
message.Sender = member.Nickname
|
|
|
|
topic := c.Param("topic")
|
|
if err := s.messageLog.Push(topic, message); err != nil {
|
|
switch err.(type) {
|
|
case *messagelog.ErrInvalidMessage:
|
|
return s.errorBadRequest(c, err)
|
|
case *messagelog.ErrTopicNotFound:
|
|
return s.errorNotFound(c, err)
|
|
}
|
|
return s.errorInternal(c, err)
|
|
}
|
|
|
|
return s.responseNoContent(c)
|
|
}
|
|
}
|
|
|
|
func (s *server) authMember() echo.MiddlewareFunc {
|
|
return middleware.KeyAuth(func(authKey string, c echo.Context) (bool, error) {
|
|
member, ok := s.memberDB.GetMember(authKey)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
c.Set("member", member)
|
|
return true, nil
|
|
})
|
|
}
|
|
|
|
func (s *server) authManager() echo.MiddlewareFunc {
|
|
return middleware.BasicAuth(func(authUsername, authPassword string, c echo.Context) (bool, error) {
|
|
user := s.config.ManagementCredentials.Username()
|
|
pass := s.config.ManagementCredentials.Password()
|
|
if user != authUsername || pass != authPassword {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
})
|
|
}
|
|
|
|
func (s *server) httpLogger() echo.MiddlewareFunc {
|
|
return func(h echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
err := h(c)
|
|
statusCode := 0
|
|
if v, ok := err.(*echo.HTTPError); ok {
|
|
statusCode = v.Code
|
|
} else {
|
|
statusCode = c.Response().Status
|
|
}
|
|
|
|
if writer := s.logger.Check(zap.InfoLevel, ""); writer != nil {
|
|
writer.Write(
|
|
zap.Reflect("request", map[string]interface{}{
|
|
"method": c.Request().Method,
|
|
"path": c.Request().URL.RequestURI(),
|
|
"user_agent": c.Request().UserAgent(),
|
|
}),
|
|
zap.Reflect("response", map[string]interface{}{
|
|
"code": statusCode,
|
|
"status": http.StatusText(statusCode),
|
|
}),
|
|
)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
}
|