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

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