commit
38f9b00829
@ -0,0 +1 @@
|
||||
mmb
|
@ -0,0 +1,10 @@
|
||||
TARGET=mmb
|
||||
LDFLAGS=-w -s
|
||||
|
||||
.PHONY: all
|
||||
all:
|
||||
go build -v -ldflags="$(LDFLAGS)" -o "$(TARGET)"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(RM) $(TARGET)
|
@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
type authString string
|
||||
|
||||
func (s authString) split() []string {
|
||||
return strings.SplitN(string(s), ":", 2)
|
||||
}
|
||||
|
||||
func (s authString) Username() string {
|
||||
return s.split()[0]
|
||||
}
|
||||
|
||||
func (s authString) Password() string {
|
||||
ss := s.split()
|
||||
if len(ss) < 2 {
|
||||
return ""
|
||||
}
|
||||
return ss[1]
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Listen string `long:"listen" description:"Listen on address" default:":8080" env:"HTTP_LISTEN"`
|
||||
LogLevel string `long:"log-level" description:"Set log level" default:"info" env:"LOG_LEVEL"`
|
||||
ManagementCredentials authString `long:"management-credentials" description:"Management credentials" env:"MANAGEMENT_CREDENTIALS" required:"true"`
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
module git.wownero.com/onionltd/monero-multisig-broker
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/hashicorp/go-uuid v1.0.1
|
||||
github.com/jessevdk/go-flags v1.4.0
|
||||
github.com/labstack/echo/v4 v4.1.17
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sethvargo/go-password v0.2.0
|
||||
github.com/stretchr/testify v1.6.1 // indirect
|
||||
go.uber.org/zap v1.16.0
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
|
||||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 // indirect
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
)
|
@ -0,0 +1,88 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/labstack/echo/v4 v4.1.17 h1:PQIBaRplyRy3OjwILGkPg89JRtH2x5bssi59G2EL3fo=
|
||||
github.com/labstack/echo/v4 v4.1.17/go.mod h1:Tn2yRQL/UclUalpb5rPdXDevbkJ+lp/2svdyFBg6CHQ=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
|
||||
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
|
||||
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88=
|
||||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05 h1:l9eKDCWy9n7C5NAiQAMvDePh0vyLAweR6LcSUVXFUGg=
|
||||
gopkg.in/validator.v2 v2.0.0-20200605151824-2b28d334fa05/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
@ -0,0 +1,47 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var DefaultConfig = zap.Config{
|
||||
Encoding: "console",
|
||||
Level: zap.NewAtomicLevelAt(zap.DebugLevel),
|
||||
OutputPaths: []string{"stderr"},
|
||||
ErrorOutputPaths: []string{"stderr"},
|
||||
EncoderConfig: zapcore.EncoderConfig{
|
||||
MessageKey: "message",
|
||||
|
||||
LevelKey: "level",
|
||||
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||
|
||||
TimeKey: "time",
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
|
||||
NameKey: "name",
|
||||
EncodeName: zapcore.FullNameEncoder,
|
||||
},
|
||||
}
|
||||
|
||||
func translateLogLevel(s string) zapcore.Level {
|
||||
switch strings.ToLower(s) {
|
||||
case "debug":
|
||||
return zap.DebugLevel
|
||||
case "info":
|
||||
return zap.InfoLevel
|
||||
case "warning":
|
||||
return zap.WarnLevel
|
||||
case "error":
|
||||
return zap.ErrorLevel
|
||||
default:
|
||||
return zap.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultConfigWithLogLevel(l string) zap.Config {
|
||||
cfg := DefaultConfig
|
||||
cfg.Level = zap.NewAtomicLevelAt(translateLogLevel(l))
|
||||
return cfg
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.wownero.com/onionltd/monero-multisig-broker/logger"
|
||||
"git.wownero.com/onionltd/monero-multisig-broker/members"
|
||||
"git.wownero.com/onionltd/monero-multisig-broker/messagelog"
|
||||
"github.com/jessevdk/go-flags"
|
||||
"github.com/labstack/echo/v4"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func run() error {
|
||||
cfg, err := setupConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rootLogger, err := setupLogger(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpdLogger := rootLogger.Named("httpd")
|
||||
|
||||
router := setupRouter()
|
||||
|
||||
server := server{
|
||||
logger: httpdLogger,
|
||||
config: cfg,
|
||||
router: router,
|
||||
messageLog: messagelog.New(),
|
||||
memberDB: members.NewDatabase(),
|
||||
}
|
||||
server.routes()
|
||||
|
||||
// Handle termination signals
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
<-sigCh
|
||||
rootLogger.Warn("received a termination signal")
|
||||
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_ = router.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := router.Start(cfg.Listen); err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
rootLogger.Error("http server error", zap.Error(err))
|
||||
die()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupConfig() (*config, error) {
|
||||
cfg := &config{}
|
||||
parser := flags.NewParser(cfg, flags.HelpFlag)
|
||||
if _, err := parser.Parse(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse configuration: %s", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func setupLogger(cfg *config) (*zap.Logger, error) {
|
||||
return logger.DefaultConfigWithLogLevel(cfg.LogLevel).Build()
|
||||
}
|
||||
|
||||
func setupRouter() *echo.Echo {
|
||||
e := echo.New()
|
||||
e.HideBanner = true
|
||||
e.HidePort = true
|
||||
return e
|
||||
}
|
||||
|
||||
func die() {
|
||||
p, _ := os.FindProcess(os.Getpid())
|
||||
_ = p.Signal(os.Interrupt)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package members
|
||||
|
||||
import "sync"
|
||||
|
||||
type Member struct {
|
||||
Nickname string
|
||||
Token string
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
members map[string]Member
|
||||
membersLock sync.RWMutex
|
||||
}
|
||||
|
||||
func (d *Database) AddMember(member Member) {
|
||||
// TODO: validate member!
|
||||
d.membersLock.Lock()
|
||||
d.members[member.Token] = member
|
||||
d.membersLock.Unlock()
|
||||
}
|
||||
|
||||
func (d *Database) GetMember(token string) (Member, bool) {
|
||||
d.membersLock.RLock()
|
||||
defer d.membersLock.RUnlock()
|
||||
member, ok := d.members[token]
|
||||
return member, ok
|
||||
}
|
||||
|
||||
func NewDatabase() *Database {
|
||||
return &Database{
|
||||
members: map[string]Member{},
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package messagelog
|
||||
|
||||
type ErrInvalidMessage struct {
|
||||
}
|
||||
|
||||
type ErrTopicNotFound struct {
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package messagelog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Log struct {
|
||||
log map[string][]Message
|
||||
logLock sync.RWMutex
|
||||
}
|
||||
|
||||
func (ml *Log) AddTopic(topic string, capacity int) {
|
||||
ml.logLock.Lock()
|
||||
ml.log[topic] = make([]Message, 0, capacity)
|
||||
ml.logLock.Unlock()
|
||||
}
|
||||
|
||||
func (ml *Log) Push(topic string, message Message) error {
|
||||
if err := message.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
ml.logLock.Lock()
|
||||
defer ml.logLock.Unlock()
|
||||
log, ok := ml.log[topic]
|
||||
if !ok {
|
||||
return errors.New("topic doesn't exist")
|
||||
}
|
||||
message.Index = len(log)
|
||||
ml.log[topic] = append(log, message)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ml *Log) List(topic string, offset int) ([]Message, error) {
|
||||
ml.logLock.RLock()
|
||||
defer ml.logLock.RUnlock()
|
||||
log, ok := ml.log[topic]
|
||||
if !ok {
|
||||
return nil, errors.New("topic doesn't exist")
|
||||
}
|
||||
if offset >= len(log) {
|
||||
return []Message{}, nil
|
||||
}
|
||||
return log[offset:], nil
|
||||
}
|
||||
|
||||
func New() *Log {
|
||||
return &Log{
|
||||
log: map[string][]Message{},
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package messagelog
|
||||
|
||||
import "gopkg.in/validator.v2"
|
||||
|
||||
type Message struct {
|
||||
Index int `json:"index"`
|
||||
Sender string `json:"sender" validate:"min=1"`
|
||||
ContentType string `json:"content_type" validate:"min=1"`
|
||||
Content interface{} `json:"content" validate:"nonnil"`
|
||||
}
|
||||
|
||||
func (m Message) validate() error {
|
||||
// TODO: Use ErrInvalidMessage!
|
||||
return validator.Validate(m)
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
func (s *server) routes() {
|
||||
s.router.Use(s.httpLogger())
|
||||
s.router.HTTPErrorHandler = s.defaultErrorHandler
|
||||
|
||||
s.router.POST("/api/v1/multisig", s.handleInitMultisig(), s.authManager())
|
||||
s.router.GET("/api/v1/multisig/:topic", s.handleListMessages(), s.authMember())
|
||||
s.router.PUT("/api/v1/multisig/:topic", s.handlePushMessage(), s.authMember())
|
||||
}
|
@ -0,0 +1,239 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 <nickname> <nickname>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
response="$(curl -u admin:pass -s -X POST -H "Content-Type: application/json" \
|
||||
--data '{"members": {"'$1'":{}, "'$2'":{}}}' \
|
||||
http://localhost:8080/api/v1/multisig)"
|
||||
|
||||
echo $response | jq -r .secrets.$1 | base32 -d | jq '. | {"'$1'": .}'
|
||||
echo $response | jq -r .secrets.$2 | base32 -d | jq '. | {"'$2'": .}'
|
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 <topic> <token>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -H "Authorization: Bearer $2" -s -H "Content-Type: application/json" \
|
||||
http://localhost:8080/api/v1/multisig/$1 \
|
||||
| jq .
|
@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 <topic> <token>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
message='{"content_type": "application/json","content":{"body":"hello world!"}}'
|
||||
|
||||
curl -H "Authorization: Bearer $2" -s -X PUT -H "Content-Type: application/json" \
|
||||
--data "$message" \
|
||||
http://localhost:8080/api/v1/multisig/$1
|
Loading…
Reference in new issue