refactor: move grpc and http APIs to separate packages

This commit is contained in:
braginini
2021-08-07 13:51:17 +02:00
parent 08d44b1d5f
commit 9f0c86c28e
11 changed files with 39 additions and 36 deletions

View File

@@ -1,8 +1,9 @@
package server
package grpc
import (
"context"
"fmt"
"github.com/wiretrustee/wiretrustee/management/server"
"sync"
"time"
@@ -17,12 +18,12 @@ import (
// Server an instance of a Management server
type Server struct {
accountManager *AccountManager
accountManager *server.AccountManager
wgKey wgtypes.Key
proto.UnimplementedManagementServiceServer
peerChannels map[string]chan *UpdateChannelMessage
channelsMux *sync.Mutex
config *Config
config *server.Config
}
// AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.30.30.1/32)
@@ -33,12 +34,12 @@ type UpdateChannelMessage struct {
}
// NewServer creates a new Management server
func NewServer(config *Config) (*Server, error) {
func NewServer(config *server.Config) (*Server, error) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
}
store, err := NewStore(config.Datadir)
store, err := server.NewStore(config.Datadir)
if err != nil {
return nil, err
}
@@ -47,7 +48,7 @@ func NewServer(config *Config) (*Server, error) {
// peerKey -> event channel
peerChannels: make(map[string]chan *UpdateChannelMessage),
channelsMux: &sync.Mutex{},
accountManager: NewManager(store),
accountManager: server.NewManager(store),
config: config,
}, nil
}
@@ -152,7 +153,7 @@ func (s *Server) RegisterPeer(ctx context.Context, req *proto.RegisterPeerReques
for _, remotePeer := range peers {
if channel, ok := s.peerChannels[remotePeer.Key]; ok {
// exclude notified peer and add ourselves
peersToSend := []*Peer{peer}
peersToSend := []*server.Peer{peer}
for _, p := range peers {
if remotePeer.Key != p.Key {
peersToSend = append(peersToSend, p)
@@ -166,17 +167,17 @@ func (s *Server) RegisterPeer(ctx context.Context, req *proto.RegisterPeerReques
return &proto.RegisterPeerResponse{}, nil
}
func toResponseProto(configProto Protocol) proto.HostConfig_Protocol {
func toResponseProto(configProto server.Protocol) proto.HostConfig_Protocol {
switch configProto {
case UDP:
case server.UDP:
return proto.HostConfig_UDP
case DTLS:
case server.DTLS:
return proto.HostConfig_DTLS
case HTTP:
case server.HTTP:
return proto.HostConfig_HTTP
case HTTPS:
case server.HTTPS:
return proto.HostConfig_HTTPS
case TCP:
case server.TCP:
return proto.HostConfig_TCP
default:
//mbragin: todo something better?
@@ -184,7 +185,7 @@ func toResponseProto(configProto Protocol) proto.HostConfig_Protocol {
}
}
func toSyncResponse(config *Config, peer *Peer, peers []*Peer) *proto.SyncResponse {
func toSyncResponse(config *server.Config, peer *server.Peer, peers []*server.Peer) *proto.SyncResponse {
var stuns []*proto.HostConfig
for _, stun := range config.Stuns {
@@ -267,7 +268,7 @@ func (s *Server) closeUpdatesChannel(peerKey string) {
}
// sendInitialSync sends initial proto.SyncResponse to the peer requesting synchronization
func (s *Server) sendInitialSync(peerKey wgtypes.Key, peer *Peer, srv proto.ManagementService_SyncServer) error {
func (s *Server) sendInitialSync(peerKey wgtypes.Key, peer *server.Peer, srv proto.ManagementService_SyncServer) error {
peers, err := s.accountManager.GetPeersForAPeer(peer.Key)
if err != nil {

View File

@@ -0,0 +1,96 @@
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

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,58 @@
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

@@ -0,0 +1,48 @@
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,39 @@
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

@@ -0,0 +1,31 @@
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,91 @@
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"
"golang.org/x/crypto/acme/autocert"
"net/http"
"time"
"github.com/codegangsta/negroni"
"github.com/gorilla/sessions"
)
type Server struct {
server *http.Server
config *s.HttpServerConfig
certManager *autocert.Manager
}
// 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 {
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}
}
// NewHttpServer creates a new HTTP server (without HTTPS)
func NewHttpServer(config *s.HttpServerConfig) *Server {
return NewHttpsServer(config, nil)
}
// Stop stops the http server
func (s *Server) Stop(ctx context.Context) error {
err := s.server.Shutdown(ctx)
if err != nil {
return err
}
return nil
}
// 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)
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))),
)
http.Handle("/", r)
if s.certManager != nil {
// if HTTPS is enabled we reuse the listener from the cert manager
listener := s.certManager.Listener()
log.Infof("http server listening on %s", listener.Addr())
if err = http.Serve(listener, s.certManager.HTTPHandler(r)); err != nil {
log.Errorf("failed to serve https server: %v", err)
return err
}
} else {
log.Infof("http server listening on %s", s.server.Addr)
if err = s.server.ListenAndServe(); err != nil {
log.Errorf("failed to serve http server: %v", err)
return err
}
}
return nil
}

View File

@@ -0,0 +1,21 @@
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

@@ -3,6 +3,7 @@ package server_test
import (
"context"
server "github.com/wiretrustee/wiretrustee/management/server"
grpc2 "github.com/wiretrustee/wiretrustee/management/server/grpc"
"io/ioutil"
"math/rand"
"net"
@@ -425,7 +426,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", ":0")
Expect(err).NotTo(HaveOccurred())
s := grpc.NewServer()
mgmtServer, err := server.NewServer(config)
mgmtServer, err := grpc2.NewServer(config)
Expect(err).NotTo(HaveOccurred())
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
go func() {