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 } } }