peer management HTTP API (#81)

* feature: create account for a newly registered user

* feature: finalize user auth flow

* feature: create protected API with JWT

* chore: cleanup http server

* feature: add UI assets

* chore: update react UI

* refactor: move account not exists -> create to AccountManager

* chore: update UI

* chore: return only peers on peers endpoint

* chore: add UI path to the config

* chore: remove ui from management

* chore: remove unused Docker comamnds

* docs: update management config sample

* fix: store creation

* feature: introduce peer response to the HTTP api

* fix: lint errors

* feature: add setup-keys HTTP endpoint

* fix: return empty json arrays in HTTP API

* feature: add new peer response fields
This commit is contained in:
Mikhail Bragin 2021-08-12 12:49:10 +02:00 committed by GitHub
parent d5af5f1878
commit 3c47a3c408
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 633 additions and 394 deletions

6
go.mod
View File

@ -3,14 +3,15 @@ module github.com/wiretrustee/wiretrustee
go 1.16
require (
github.com/auth0/go-jwt-middleware v1.0.1
github.com/cenkalti/backoff/v4 v4.1.0
github.com/codegangsta/negroni v1.0.0
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2
github.com/google/uuid v1.2.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/sessions v1.2.1
github.com/joho/godotenv v1.3.0
github.com/kardianos/service v1.2.0
github.com/onsi/ginkgo v1.16.4
github.com/onsi/gomega v1.13.0
@ -20,7 +21,6 @@ require (
github.com/spf13/cobra v1.1.3
github.com/vishvananda/netlink v1.1.0
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210506160403-92e472f520a5

17
go.sum
View File

@ -19,6 +19,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/auth0/go-jwt-middleware v1.0.1 h1:/fsQ4vRr4zod1wKReUH+0A3ySRjGiT9G34kypO/EKwI=
github.com/auth0/go-jwt-middleware v1.0.1/go.mod h1:YSeUX3z6+TF2H+7padiEqNJ73Zy9vXW72U//IgN0BIM=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
@ -48,6 +50,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@ -60,6 +64,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
@ -97,6 +103,9 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@ -130,8 +139,6 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
@ -145,6 +152,7 @@ github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozI
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kardianos/service v1.2.0 h1:bGuZ/epo3vrt8IPC7mnKQolqFeYJb7Cs8Rk4PSOBB/g=
@ -254,6 +262,9 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@ -276,6 +287,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=

View File

@ -25,9 +25,9 @@
"HttpConfig": {
"LetsEncryptDomain": "",
"Address": "0.0.0.0:3000",
"AuthDomain": "<PASTE YOUR AUTH0 DOMAIN HERE>",
"AuthClientId": "<PASTE YOUR AUTH0 CLIENT ID HERE>",
"AuthClientSecret": "<PASTE YOUR AUTH0 SECRET>",
"AuthCallback": "http://localhost:3000/callback"
"AuthIssuer": "<PASTE YOUR AUTH0 ISSUER HERE>",
"AuthAudience": "<PASTE YOUR AUTH0 AUDIENCE HERE>",
"AuthKeysLocation": "<PASTE YOUR JWT KEY SET location>",
"UIFilesLocation": "/var/lib/wiretrustee/ui/"
}
}

View File

@ -57,6 +57,12 @@ var (
}
}
store, err := server.NewStore(config.Datadir)
if err != nil {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
accountManager := server.NewManager(store)
var opts []grpc.ServerOption
var httpServer *http.Server
@ -65,15 +71,15 @@ var (
transportCredentials := credentials.NewTLS(certManager.TLSConfig())
opts = append(opts, grpc.Creds(transportCredentials))
httpServer = http.NewHttpsServer(config.HttpConfig, certManager)
httpServer = http.NewHttpsServer(config.HttpConfig, certManager, accountManager)
} else {
httpServer = http.NewHttpServer(config.HttpConfig)
httpServer = http.NewHttpServer(config.HttpConfig, accountManager)
}
opts = append(opts, grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
grpcServer := grpc.NewServer(opts...)
server, err := grpc2.NewServer(config)
server, err := grpc2.NewServer(config, accountManager)
if err != nil {
log.Fatalf("failed creating new server: %v", err)
}

View File

@ -82,6 +82,83 @@ func (manager *AccountManager) GetPeersForAPeer(peerKey string) ([]*Peer, error)
return res, nil
}
//GetAccount returns an existing account or error (NotFound) if doesn't exist
func (manager *AccountManager) GetAccount(accountId string) (*Account, error) {
manager.mux.Lock()
defer manager.mux.Unlock()
account, err := manager.Store.GetAccount(accountId)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed retrieving account")
}
return account, nil
}
// GetOrCreateAccount returns an existing account or creates a new one if doesn't exist
func (manager *AccountManager) GetOrCreateAccount(accountId string) (*Account, error) {
manager.mux.Lock()
defer manager.mux.Unlock()
_, err := manager.Store.GetAccount(accountId)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
return manager.createAccount(accountId)
} else {
// other error
return nil, err
}
}
account, err := manager.Store.GetAccount(accountId)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed retrieving account")
}
return account, nil
}
//AccountExists checks whether account exists (returns true) or not (returns false)
func (manager *AccountManager) AccountExists(accountId string) (*bool, error) {
manager.mux.Lock()
defer manager.mux.Unlock()
var res bool
_, err := manager.Store.GetAccount(accountId)
if err != nil {
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
res = false
return &res, nil
} else {
return nil, err
}
}
res = true
return &res, nil
}
// AddAccount generates a new Account with a provided accountId and saves to the Store
func (manager *AccountManager) AddAccount(accountId string) (*Account, error) {
manager.mux.Lock()
defer manager.mux.Unlock()
return manager.createAccount(accountId)
}
func (manager *AccountManager) createAccount(accountId string) (*Account, error) {
account, _ := newAccountWithId(accountId)
err := manager.Store.SaveAccount(account)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed creating account")
}
return account, nil
}
// AddPeer adds a new peer to the Store.
// Each Account has a list of pre-authorised SetupKey and if no Account has a given key err wit ha code codes.Unauthenticated
// will be returned, meaning the key is invalid
@ -96,7 +173,7 @@ func (manager *AccountManager) AddPeer(setupKey string, peerKey string) (*Peer,
var sk *SetupKey
if len(setupKey) == 0 {
// Empty setup key, create a new account for it.
account, sk = manager.newAccount()
account, sk = newAccount()
} else {
sk = &SetupKey{Key: setupKey}
account, err = manager.Store.GetAccountBySetupKey(sk.Key)
@ -129,20 +206,28 @@ func (manager *AccountManager) AddPeer(setupKey string, peerKey string) (*Peer,
}
// newAccount creates a new Account with a default SetupKey (doesn't store in a Store)
func (manager *AccountManager) newAccount() (*Account, *SetupKey) {
// newAccountWithId creates a new Account with a default SetupKey (doesn't store in a Store) and provided id
func newAccountWithId(accountId string) (*Account, *SetupKey) {
log.Debugf("creating new account")
accountId := uuid.New().String()
setupKeyId := uuid.New().String()
setupKeys := make(map[string]*SetupKey)
setupKey := &SetupKey{Key: setupKeyId}
setupKeys[setupKeyId] = setupKey
network := &Network{Id: uuid.New().String(), Net: net.IPNet{}, Dns: ""}
network := &Network{
Id: uuid.New().String(),
Net: net.IPNet{IP: net.ParseIP("100.64.0.0"), Mask: net.IPMask{255, 192, 0, 0}},
Dns: ""}
peers := make(map[string]*Peer)
log.Debugf("created new account %s with setup key %s", accountId, setupKeyId)
return &Account{Id: accountId, SetupKeys: setupKeys, Network: network, Peers: peers}, setupKey
}
// newAccount creates a new Account with a default SetupKey (doesn't store in a Store)
func newAccount() (*Account, *SetupKey) {
accountId := uuid.New().String()
return newAccountWithId(accountId)
}

View File

@ -25,10 +25,14 @@ type Config struct {
type HttpServerConfig struct {
LetsEncryptDomain string
Address string
AuthDomain string
AuthClientId string
AuthClientSecret string
AuthCallback string
// AuthAudience identifies the recipients that the JWT is intended for (aud in JWT)
AuthAudience string
// AuthIssuer identifies principal that issued the JWT.
AuthIssuer string
// AuthKeysLocation is a location of JWT key set containing the public keys used to verify JWT
AuthKeysLocation string
// UIFilesLocation is the location of static UI files for management frontend
UIFilesLocation string
}
// Host represents a Wiretrustee host (e.g. STUN, TURN, Signal)

View File

@ -41,9 +41,11 @@ func restore(file string) (*FileStore, error) {
if _, err := os.Stat(file); os.IsNotExist(err) {
// create a new FileStore if previously didn't exist (e.g. first run)
s := &FileStore{
Accounts: make(map[string]*Account),
mux: sync.Mutex{},
storeFile: file,
Accounts: make(map[string]*Account),
mux: sync.Mutex{},
SetupKeyId2AccountId: make(map[string]string),
PeerKeyId2AccountId: make(map[string]string),
storeFile: file,
}
err = s.persist(file)
@ -148,7 +150,7 @@ func (s *FileStore) GetAccount(accountId string) (*Account, error) {
account, accountFound := s.Accounts[accountId]
if !accountFound {
return nil, status.Errorf(codes.Internal, "account not found")
return nil, status.Errorf(codes.NotFound, "account not found")
}
return account, nil

View File

@ -34,21 +34,17 @@ type UpdateChannelMessage struct {
}
// NewServer creates a new Management server
func NewServer(config *server.Config) (*Server, error) {
func NewServer(config *server.Config, accountManager *server.AccountManager) (*Server, error) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
}
store, err := server.NewStore(config.Datadir)
if err != nil {
return nil, err
}
return &Server{
wgKey: key,
// peerKey -> event channel
peerChannels: make(map[string]chan *UpdateChannelMessage),
channelsMux: &sync.Mutex{},
accountManager: server.NewManager(store),
accountManager: accountManager,
config: config,
}, nil
}

View File

@ -1,96 +0,0 @@
package handler
import (
"context"
"github.com/coreos/go-oidc"
"github.com/gorilla/sessions"
middleware2 "github.com/wiretrustee/wiretrustee/management/server/http/middleware"
"log"
"net/http"
)
// Callback handler used to receive a callback from the identity provider
type Callback struct {
authenticator *middleware2.Authenticator
sessionStore sessions.Store
}
func NewCallback(authenticator *middleware2.Authenticator, sessionStore sessions.Store) *Callback {
return &Callback{
authenticator: authenticator,
sessionStore: sessionStore,
}
}
// ServeHTTP checks the user session, verifies the state, verifies the token, stores user profile in a session,
// and in case of the successful auth redirects user to the main page
func (h *Callback) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session, err := h.sessionStore.Get(r, "auth-session")
if err != nil {
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
//http.Error(w, err.Error(), http.StatusInternalServerError)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
if r.URL.Query().Get("state") != session.Values["state"] {
//todo redirect to the error page stating: "error authenticating plz try to login once again"
//http.Error(w, "invalid state parameter", http.StatusBadRequest)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
token, err := h.authenticator.Config.Exchange(context.TODO(), r.URL.Query().Get("code"))
if err != nil {
log.Printf("no token found: %v", err)
//todo redirect to the error page stating: "error authenticating plz try to login once again"
//w.WriteHeader(http.StatusUnauthorized)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
//http.Error(w, "no id_token field in oauth2 token.", http.StatusInternalServerError)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
oidcConfig := &oidc.Config{
ClientID: h.authenticator.Config.ClientID,
}
idToken, err := h.authenticator.Provider.Verifier(oidcConfig).Verify(context.TODO(), rawIDToken)
if err != nil {
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
//http.Error(w, "failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// get the userInfo from the token
var profile map[string]interface{}
if err := idToken.Claims(&profile); err != nil {
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
//http.Error(w, err.Error(), http.StatusInternalServerError)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
session.Values["id_token"] = rawIDToken
session.Values["access_token"] = token.AccessToken
session.Values["profile"] = profile
err = session.Save(r, w)
if err != nil {
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
//http.Error(w, err.Error(), http.StatusInternalServerError)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
// redirect to logged in page
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}

View File

@ -1,44 +0,0 @@
package handler
import (
"fmt"
"github.com/gorilla/sessions"
"io"
"net/http"
"strings"
)
// Dashboard is a handler of the main page of the app (dashboard)
type Dashboard struct {
sessionStore sessions.Store
}
func NewDashboard(sessionStore sessions.Store) *Dashboard {
return &Dashboard{
sessionStore: sessionStore,
}
}
// ServeHTTP verifies if user is authenticated and returns a user dashboard
func (h *Dashboard) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session, err := h.sessionStore.Get(r, "auth-session")
if err != nil {
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
//http.Error(w, err.Error(), http.StatusInternalServerError)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
//todo get user account and relevant data to show
profile := session.Values["profile"].(map[string]interface{})
name := profile["name"]
w.WriteHeader(200)
_, err = io.Copy(w, strings.NewReader("hello "+fmt.Sprintf("%v", name)))
if err != nil {
return
}
//template.RenderTemplate(w, "dashboard", session.Values["profile"])
}

View File

@ -1,58 +0,0 @@
package handler
import (
"crypto/rand"
"encoding/base64"
"github.com/gorilla/sessions"
middleware2 "github.com/wiretrustee/wiretrustee/management/server/http/middleware"
"io/fs"
"net/http"
)
// Login handler used to login a user
type Login struct {
authenticator *middleware2.Authenticator
sessionStore sessions.Store
}
func NewLogin(authenticator *middleware2.Authenticator, sessionStore sessions.Store) *Login {
return &Login{
authenticator: authenticator,
sessionStore: sessionStore,
}
}
// ServeHTTP generates a new session state for a user and redirects the user to the auth URL
func (h *Login) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Generate random state
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
state := base64.StdEncoding.EncodeToString(b)
session, err := h.sessionStore.Get(r, "auth-session")
if err != nil {
switch err.(type) {
case *fs.PathError:
// a case when session doesn't exist in the store but was sent by the client in the cookie -> create new session ID
// it appears that in this case session is always non empty object
session.ID = "" //nolint
default:
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
session.Values["state"] = state //nolint
err = session.Save(r, w) //nolint
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
url := h.authenticator.Config.AuthCodeURL(state)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}

View File

@ -1,48 +0,0 @@
package handler
import (
"net/http"
"net/url"
)
// Logout logs out a user
type Logout struct {
authDomain string
authClientId string
}
func NewLogout(authDomain string, authClientId string) *Logout {
return &Logout{authDomain: authDomain, authClientId: authClientId}
}
// ServeHTTP redirects user to teh auth identity provider logout URL
func (h *Logout) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logoutUrl, err := url.Parse("https://" + h.authDomain)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
logoutUrl.Path += "/v2/logout"
parameters := url.Values{}
var scheme string
if r.TLS == nil {
scheme = "http"
} else {
scheme = "https"
}
returnTo, err := url.Parse(scheme + "://" + r.Host + "/login")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
parameters.Add("returnTo", returnTo.String())
parameters.Add("client_id", h.authClientId)
logoutUrl.RawQuery = parameters.Encode()
http.Redirect(w, r, logoutUrl.String(), http.StatusTemporaryRedirect)
}

View File

@ -0,0 +1,67 @@
package handler
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/management/server"
"net/http"
"time"
)
// Peers is a handler that returns peers of the account
type Peers struct {
accountManager *server.AccountManager
}
// PeerResponse is a response sent to the client
type PeerResponse struct {
Name string
IP string
Connected bool
LastSeen time.Time
Os string
}
func NewPeers(accountManager *server.AccountManager) *Peers {
return &Peers{
accountManager: accountManager,
}
}
func (h *Peers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
accountId := extractAccountIdFromRequestContext(r)
//new user -> create a new account
account, err := h.accountManager.GetOrCreateAccount(accountId)
if err != nil {
log.Errorf("failed getting user account %s: %v", accountId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
respBody := []*PeerResponse{}
for _, peer := range account.Peers {
respBody = append(respBody, &PeerResponse{
Name: peer.Key,
IP: peer.IP.String(),
LastSeen: time.Now(),
Connected: false,
Os: "Ubuntu 21.04 (Hirsute Hippo)",
})
}
err = json.NewEncoder(w).Encode(respBody)
if err != nil {
log.Errorf("failed encoding account peers %s: %v", accountId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
case http.MethodOptions:
default:
http.Error(w, "", http.StatusNotFound)
}
}

View File

@ -0,0 +1,58 @@
package handler
import (
"encoding/json"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/management/server"
"net/http"
)
// SetupKeys is a handler that returns a list of setup keys of the account
type SetupKeys struct {
accountManager *server.AccountManager
}
// SetupKeyResponse is a response sent to the client
type SetupKeyResponse struct {
Key string
}
func NewSetupKeysHandler(accountManager *server.AccountManager) *SetupKeys {
return &SetupKeys{
accountManager: accountManager,
}
}
func (h *SetupKeys) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
accountId := extractAccountIdFromRequestContext(r)
//new user -> create a new account
account, err := h.accountManager.GetOrCreateAccount(accountId)
if err != nil {
log.Errorf("failed getting user account %s: %v", accountId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json")
respBody := []*SetupKeyResponse{}
for _, key := range account.SetupKeys {
respBody = append(respBody, &SetupKeyResponse{
Key: key.Key,
})
}
err = json.NewEncoder(w).Encode(respBody)
if err != nil {
log.Errorf("failed encoding account peers %s: %v", accountId, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
case http.MethodOptions:
default:
http.Error(w, "", http.StatusNotFound)
}
}

View File

@ -0,0 +1,15 @@
package handler
import (
"github.com/golang-jwt/jwt"
"net/http"
)
// extractAccountIdFromRequestContext extracts accountId from the request context previously filled by the JWT token (after auth)
func extractAccountIdFromRequestContext(r *http.Request) string {
token := r.Context().Value("user").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
//actually a user id but for now we have a 1 to 1 mapping.
return claims["sub"].(string)
}

View File

@ -1,39 +0,0 @@
package middleware
import (
"context"
"golang.org/x/oauth2"
"log"
"github.com/coreos/go-oidc"
)
type Authenticator struct {
Provider *oidc.Provider
Config oauth2.Config
Ctx context.Context
}
func NewAuthenticator(authDomain string, authClientId string, authClientSecret string, authCallback string) (*Authenticator, error) {
ctx := context.Background()
provider, err := oidc.NewProvider(ctx, "https://"+authDomain+"/")
if err != nil {
log.Printf("failed to get provider: %v", err)
return nil, err
}
conf := oauth2.Config{
ClientID: authClientId,
ClientSecret: authClientSecret,
RedirectURL: authCallback,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile"},
}
return &Authenticator{
Provider: provider,
Config: conf,
Ctx: ctx,
}, nil
}

View File

@ -1,31 +0,0 @@
package middleware
import (
"github.com/gorilla/sessions"
"net/http"
)
type AuthMiddleware struct {
sessionStore sessions.Store
}
func NewAuth(sessionStore sessions.Store) *AuthMiddleware {
return &AuthMiddleware{sessionStore: sessionStore}
}
func (am *AuthMiddleware) IsAuthenticated(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
session, err := am.sessionStore.Get(r, "auth-session")
if err != nil {
//todo redirect to the error page stating: "error occurred plz try again later and a link to login"
//http.Error(w, err.Error(), http.StatusInternalServerError)
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
if _, ok := session.Values["profile"]; !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
} else {
next(w, r)
}
}

View File

@ -0,0 +1,92 @@
package middleware
import (
"encoding/json"
"errors"
"github.com/golang-jwt/jwt"
"net/http"
)
//Jwks is a collection of JSONWebKeys obtained from Config.HttpServerConfig.AuthKeysLocation
type Jwks struct {
Keys []JSONWebKeys `json:"keys"`
}
//JSONWebKeys is a representation of a Jason Web Key
type JSONWebKeys struct {
Kty string `json:"kty"`
Kid string `json:"kid"`
Use string `json:"use"`
N string `json:"n"`
E string `json:"e"`
X5c []string `json:"x5c"`
}
//NewJwtMiddleware creates new middleware to verify the JWT token sent via Authorization header
func NewJwtMiddleware(issuer string, audience string, keysLocation string) (*JWTMiddleware, error) {
keys, err := getPemKeys(keysLocation)
if err != nil {
return nil, err
}
return New(Options{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
// Verify 'aud' claim
checkAud := token.Claims.(jwt.MapClaims).VerifyAudience(audience, false)
if !checkAud {
return token, errors.New("invalid audience")
}
// Verify 'issuer' claim
checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(issuer, false)
if !checkIss {
return token, errors.New("invalid issuer")
}
cert, err := getPemCert(token, keys)
if err != nil {
panic(err.Error())
}
result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert))
return result, nil
},
SigningMethod: jwt.SigningMethodRS256,
EnableAuthOnOptions: true,
}), nil
}
func getPemKeys(keysLocation string) (*Jwks, error) {
resp, err := http.Get(keysLocation)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var jwks = &Jwks{}
err = json.NewDecoder(resp.Body).Decode(jwks)
if err != nil {
return jwks, err
}
return jwks, err
}
func getPemCert(token *jwt.Token, jwks *Jwks) (string, error) {
cert := ""
for k := range jwks.Keys {
if token.Header["kid"] == jwks.Keys[k].Kid {
cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----"
}
}
if cert == "" {
err := errors.New("unable to find appropriate key")
return cert, err
}
return cert, nil
}

View File

@ -0,0 +1,237 @@
package middleware
import (
"context"
"errors"
"fmt"
"github.com/golang-jwt/jwt"
"log"
"net/http"
"strings"
)
// A function called whenever an error is encountered
type errorHandler func(w http.ResponseWriter, r *http.Request, err string)
// TokenExtractor is a function that takes a request as input and returns
// either a token or an error. An error should only be returned if an attempt
// to specify a token was found, but the information was somehow incorrectly
// formed. In the case where a token is simply not present, this should not
// be treated as an error. An empty string should be returned in that case.
type TokenExtractor func(r *http.Request) (string, error)
// Options is a struct for specifying configuration options for the middleware.
type Options struct {
// The function that will return the Key to validate the JWT.
// It can be either a shared secret or a public key.
// Default value: nil
ValidationKeyGetter jwt.Keyfunc
// The name of the property in the request where the user information
// from the JWT will be stored.
// Default value: "user"
UserProperty string
// The function that will be called when there's an error validating the token
// Default value:
ErrorHandler errorHandler
// A boolean indicating if the credentials are required or not
// Default value: false
CredentialsOptional bool
// A function that extracts the token from the request
// Default: FromAuthHeader (i.e., from Authorization header as bearer token)
Extractor TokenExtractor
// Debug flag turns on debugging output
// Default: false
Debug bool
// When set, all requests with the OPTIONS method will use authentication
// Default: false
EnableAuthOnOptions bool
// When set, the middelware verifies that tokens are signed with the specific signing algorithm
// If the signing method is not constant the ValidationKeyGetter callback can be used to implement additional checks
// Important to avoid security issues described here: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
// Default: nil
SigningMethod jwt.SigningMethod
}
type JWTMiddleware struct {
Options Options
}
func OnError(w http.ResponseWriter, r *http.Request, err string) {
http.Error(w, err, http.StatusUnauthorized)
}
// New constructs a new Secure instance with supplied options.
func New(options ...Options) *JWTMiddleware {
var opts Options
if len(options) == 0 {
opts = Options{}
} else {
opts = options[0]
}
if opts.UserProperty == "" {
opts.UserProperty = "user"
}
if opts.ErrorHandler == nil {
opts.ErrorHandler = OnError
}
if opts.Extractor == nil {
opts.Extractor = FromAuthHeader
}
return &JWTMiddleware{
Options: opts,
}
}
func (m *JWTMiddleware) logf(format string, args ...interface{}) {
if m.Options.Debug {
log.Printf(format, args...)
}
}
// HandlerWithNext is a special implementation for Negroni, but could be used elsewhere.
func (m *JWTMiddleware) HandlerWithNext(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
err := m.CheckJWT(w, r)
// If there was an error, do not call next.
if err == nil && next != nil {
next(w, r)
}
}
func (m *JWTMiddleware) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Let secure process the request. If it returns an error,
// that indicates the request should not continue.
err := m.CheckJWT(w, r)
// If there was an error, do not continue.
if err != nil {
return
}
h.ServeHTTP(w, r)
})
}
// FromAuthHeader is a "TokenExtractor" that takes a give request and extracts
// the JWT token from the Authorization header.
func FromAuthHeader(r *http.Request) (string, error) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return "", nil // No error, just no token
}
// TODO: Make this a bit more robust, parsing-wise
authHeaderParts := strings.Fields(authHeader)
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
return "", errors.New("Authorization header format must be Bearer {token}")
}
return authHeaderParts[1], nil
}
// FromParameter returns a function that extracts the token from the specified
// query string parameter
func FromParameter(param string) TokenExtractor {
return func(r *http.Request) (string, error) {
return r.URL.Query().Get(param), nil
}
}
// FromFirst returns a function that runs multiple token extractors and takes the
// first token it finds
func FromFirst(extractors ...TokenExtractor) TokenExtractor {
return func(r *http.Request) (string, error) {
for _, ex := range extractors {
token, err := ex(r)
if err != nil {
return "", err
}
if token != "" {
return token, nil
}
}
return "", nil
}
}
func (m *JWTMiddleware) CheckJWT(w http.ResponseWriter, r *http.Request) error {
if !m.Options.EnableAuthOnOptions {
if r.Method == "OPTIONS" {
return nil
}
}
// Use the specified token extractor to extract a token from the request
token, err := m.Options.Extractor(r)
// If debugging is turned on, log the outcome
if err != nil {
m.logf("Error extracting JWT: %v", err)
} else {
m.logf("Token extracted: %s", token)
}
// If an error occurs, call the error handler and return an error
if err != nil {
m.Options.ErrorHandler(w, r, err.Error())
return fmt.Errorf("Error extracting token: %w", err)
}
// If the token is empty...
if token == "" {
// Check if it was required
if m.Options.CredentialsOptional {
m.logf(" No credentials found (CredentialsOptional=true)")
// No error, just no token (and that is ok given that CredentialsOptional is true)
return nil
}
// If we get here, the required token is missing
errorMsg := "Required authorization token not found"
m.Options.ErrorHandler(w, r, errorMsg)
m.logf(" Error: No credentials found (CredentialsOptional=false)")
return fmt.Errorf(errorMsg)
}
// Now parse the token
parsedToken, err := jwt.Parse(token, m.Options.ValidationKeyGetter)
// Check if there was an error in parsing...
if err != nil {
m.logf("error parsing token: %v", err)
m.Options.ErrorHandler(w, r, err.Error())
return fmt.Errorf("Error parsing token: %w", err)
}
if m.Options.SigningMethod != nil && m.Options.SigningMethod.Alg() != parsedToken.Header["alg"] {
message := fmt.Sprintf("Expected %s signing method but token specified %s",
m.Options.SigningMethod.Alg(),
parsedToken.Header["alg"])
m.logf("Error validating token algorithm: %s", message)
m.Options.ErrorHandler(w, r, errors.New(message).Error())
return fmt.Errorf("Error validating token algorithm: %s", message)
}
// Check if the parsed token is valid...
if !parsedToken.Valid {
m.logf("Token is invalid")
m.Options.ErrorHandler(w, r, "The token isn't valid")
return errors.New("token is invalid")
}
m.logf("JWT: %v", parsedToken)
// If we get here, everything worked and we can set the
// user property in context.
newRequest := r.WithContext(context.WithValue(r.Context(), m.Options.UserProperty, parsedToken)) //nolint
// Update the current request with the new context information.
*r = *newRequest
return nil
}

View File

@ -2,40 +2,38 @@ package http
import (
"context"
"encoding/gob"
log "github.com/sirupsen/logrus"
s "github.com/wiretrustee/wiretrustee/management/server"
handler2 "github.com/wiretrustee/wiretrustee/management/server/http/handler"
middleware2 "github.com/wiretrustee/wiretrustee/management/server/http/middleware"
"github.com/wiretrustee/wiretrustee/management/server/http/handler"
"github.com/wiretrustee/wiretrustee/management/server/http/middleware"
"golang.org/x/crypto/acme/autocert"
"net/http"
"path/filepath"
"time"
"github.com/codegangsta/negroni"
"github.com/gorilla/sessions"
)
type Server struct {
server *http.Server
config *s.HttpServerConfig
certManager *autocert.Manager
server *http.Server
config *s.HttpServerConfig
certManager *autocert.Manager
accountManager *s.AccountManager
}
// NewHttpsServer creates a new HTTPs server (with HTTPS support)
// The listening address will be :443 no matter what was specified in s.HttpServerConfig.Address
func NewHttpsServer(config *s.HttpServerConfig, certManager *autocert.Manager) *Server {
func NewHttpsServer(config *s.HttpServerConfig, certManager *autocert.Manager, accountManager *s.AccountManager) *Server {
server := &http.Server{
Addr: config.Address,
WriteTimeout: time.Second * 15,
ReadTimeout: time.Second * 15,
IdleTimeout: time.Second * 60,
}
return &Server{server: server, config: config, certManager: certManager}
return &Server{server: server, config: config, certManager: certManager, accountManager: accountManager}
}
// NewHttpServer creates a new HTTP server (without HTTPS)
func NewHttpServer(config *s.HttpServerConfig) *Server {
return NewHttpsServer(config, nil)
func NewHttpServer(config *s.HttpServerConfig, accountManager *s.AccountManager) *Server {
return NewHttpsServer(config, nil, accountManager)
}
// Stop stops the http server
@ -50,25 +48,23 @@ func (s *Server) Stop(ctx context.Context) error {
// Start defines http handlers and starts the http server. Blocks until server is shutdown.
func (s *Server) Start() error {
sessionStore := sessions.NewFilesystemStore("", []byte("something-very-secret"))
authenticator, err := middleware2.NewAuthenticator(s.config.AuthDomain, s.config.AuthClientId, s.config.AuthClientSecret, s.config.AuthCallback)
jwtMiddleware, err := middleware.NewJwtMiddleware(s.config.AuthIssuer, s.config.AuthAudience, s.config.AuthKeysLocation)
if err != nil {
log.Errorf("failed cerating authentication middleware %v", err)
return err
}
gob.Register(map[string]interface{}{})
r := http.NewServeMux()
s.server.Handler = r
r.Handle("/login", handler2.NewLogin(authenticator, sessionStore))
r.Handle("/logout", handler2.NewLogout(s.config.AuthDomain, s.config.AuthClientId))
r.Handle("/callback", handler2.NewCallback(authenticator, sessionStore))
r.Handle("/dashboard", negroni.New(
negroni.HandlerFunc(middleware2.NewAuth(sessionStore).IsAuthenticated),
negroni.Wrap(handler2.NewDashboard(sessionStore))),
)
// serve public website
uiPath := filepath.Clean(s.config.UIFilesLocation)
fs := http.FileServer(http.Dir(uiPath))
r.Handle("/", fs)
fsStatic := http.FileServer(http.Dir(filepath.Join(uiPath, "static/")))
r.Handle("/static/", http.StripPrefix("/static/", fsStatic))
r.Handle("/api/peers", jwtMiddleware.Handler(handler.NewPeers(s.accountManager)))
r.Handle("/api/setup-keys", jwtMiddleware.Handler(handler.NewSetupKeysHandler(s.accountManager)))
http.Handle("/", r)
if s.certManager != nil {

View File

@ -1,21 +0,0 @@
package template
import (
"html/template"
"net/http"
"os"
"path/filepath"
)
func RenderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
cwd, _ := os.Getwd()
t, err := template.ParseFiles(filepath.Join(cwd, "./routes/"+tmpl+"/"+tmpl+".html"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

View File

@ -426,7 +426,12 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", ":0")
Expect(err).NotTo(HaveOccurred())
s := grpc.NewServer()
mgmtServer, err := grpc2.NewServer(config)
store, err := server.NewStore(config.Datadir)
if err != nil {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
accountManager := server.NewManager(store)
mgmtServer, err := grpc2.NewServer(config, accountManager)
Expect(err).NotTo(HaveOccurred())
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
go func() {