Initial commit

master
Onion Ltd 4 years ago
commit 38f9b00829
Signed by: onionltd
GPG Key ID: E4B6CAC49B242A44

1
.gitignore vendored

@ -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…
Cancel
Save