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.
240 lines
5.6 KiB
240 lines
5.6 KiB
package main
|
|
|
|
import (
|
|
"encoding/base32"
|
|
"encoding/json"
|
|
"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) 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"`
|
|
}
|
|
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 c.JSON(http.StatusBadRequest, map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
// TODO: validate input!
|
|
|
|
topic, err := uuid.GenerateUUID()
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
|
"error": "failed to generate UUID",
|
|
})
|
|
}
|
|
|
|
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 c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
|
"error": "failed to generate a token",
|
|
})
|
|
}
|
|
|
|
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,
|
|
}
|
|
encoded, err := encodeSecret(secret)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
|
"error": "failed to encode secret data",
|
|
})
|
|
}
|
|
memberSecrets[nickname] = encoded
|
|
}
|
|
|
|
// TODO: put initial capacity in the config.
|
|
s.messageLog.AddTopic(topic, 128)
|
|
|
|
out := output{
|
|
Topic: topic,
|
|
Secrets: memberSecrets,
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, 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 c.JSON(http.StatusBadRequest, map[string]interface{}{
|
|
"error": "offset is not a number",
|
|
})
|
|
}
|
|
|
|
messages, err := s.messageLog.List(topic, int(offset))
|
|
if err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, messages)
|
|
}
|
|
}
|
|
|
|
func (s *server) handlePushMessage() echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
message := messagelog.Message{}
|
|
|
|
if err := c.Bind(&message); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]interface{}{
|
|
"error": "invalid message format",
|
|
})
|
|
}
|
|
|
|
member, ok := c.Get("member").(members.Member)
|
|
if !ok {
|
|
return c.JSON(http.StatusInternalServerError, map[string]interface{}{
|
|
"error": "internal server error",
|
|
})
|
|
}
|
|
message.Sender = member.Nickname
|
|
|
|
topic := c.Param("topic")
|
|
if err := s.messageLog.Push(topic, message); err != nil {
|
|
return c.JSON(http.StatusNotFound, map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.String(http.StatusNoContent, "")
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|