From 2fe21169bbda5a536a1c19209501e9bcf32a8c85 Mon Sep 17 00:00:00 2001 From: Samuel Lorch Date: Sat, 11 Mar 2023 14:54:21 +0100 Subject: [PATCH] Improve Session / Auth, APICall --- client/src/App.vue | 31 +++++++++---- client/src/api.ts | 18 ++++++-- cmd/main.go | 4 +- pkg/definitions/config.go | 1 - pkg/jsonrpc/handler.go | 7 ++- pkg/server/api.go | 11 +++-- pkg/server/server.go | 3 +- pkg/server/session.go | 97 ++++----------------------------------- pkg/server/websocket.go | 7 +-- pkg/session/cookie.go | 10 ++++ pkg/session/session.go | 93 +++++++++++++++++++++++++++++++++++++ 11 files changed, 167 insertions(+), 115 deletions(-) create mode 100644 pkg/session/cookie.go create mode 100644 pkg/session/session.go diff --git a/client/src/App.vue b/client/src/App.vue index 1ed034c..26f31c4 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -36,24 +36,37 @@ async function tryLogin() { } async function tryLogout() { - logout(); + console.info("Logging out..."); authState = AuthState.Unauthenticated; + logout(); } -function deAuthenticatedCallback() { +function UnauthorizedCallback() { console.info("Unauthenticated"); authState = AuthState.Unauthenticated; } -onMounted(async() => { - setup(deAuthenticatedCallback); +async function checkAuth() { + console.info("Checking Auth State..."); let res = await checkAuthentication(); authState = res.auth; loginDisabled = false; if (authState === AuthState.Authenticated) { console.info("Already Authenticated ", authState); + } else if (res.error == null) { + console.info("Unauthorized"); } else console.info("Check Authentication error",res.error); +} + +onMounted(async() => { + setup(UnauthorizedCallback); + await checkAuth(); + setInterval(function () { + if (authState === AuthState.Authenticated) { + checkAuth(); + } + }.bind(this), 120000); }); @@ -97,11 +110,11 @@ onMounted(async() => {

nfSense Login

-
diff --git a/client/src/api.ts b/client/src/api.ts index 9d89950..51d3c03 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -5,19 +5,24 @@ const httpTransport = new HTTPTransport("http://"+ window.location.host +"/api") const manager = new RequestManager([httpTransport], () => crypto.randomUUID()); const client = new Client(manager); -let deAuthenticatedCallback; +let UnauthorizedCallback: Function; -export function setup(_deAuthenticatedCallback: () => void) { - deAuthenticatedCallback = _deAuthenticatedCallback; +export function setup(_UnauthorizedCallback: () => void) { + UnauthorizedCallback = _UnauthorizedCallback; } export async function apiCall(method: string, params: Record): Promise{ + console.debug("Starting API Call..."); try { const result = await client.request({method, params}); console.debug("api call result", result); return { Data: result, Error: null}; } catch (ex){ - console.debug("api call epic fail", ex); + if (ex == "Error: Unauthorized") { + UnauthorizedCallback(); + } else { + console.debug("api call epic fail", ex); + } return { Data: null, Error: ex}; } } @@ -57,7 +62,10 @@ export async function checkAuthentication() { } } else window.localStorage.setItem("commit_hash", response.data.commit_hash); return {auth: 2, error: null}; - } catch (error) { + } catch (error: any) { + if (error.response.status == 401) { + return {auth: 0, error: null}; + } return {auth: 0, error: error}; } } \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 4d488d1..4919841 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,7 +34,7 @@ func main() { os.Exit(1) } - slog.Info("Validating Config") + slog.Info("Validating Config...") if *applyPtr { slog.Info("Applying Config...") @@ -54,7 +54,7 @@ func main() { slog.Info("Starting Webserver...") server.StartWebserver(conf, apiHandler) - slog.Info("Ready") + slog.Info("Ready.") // Handle Exit Signal sigChan := make(chan os.Signal, 1) diff --git a/pkg/definitions/config.go b/pkg/definitions/config.go index f1faa7a..8061a42 100644 --- a/pkg/definitions/config.go +++ b/pkg/definitions/config.go @@ -14,7 +14,6 @@ type Config struct { func ValidateConfig(conf *Config) error { val := validator.New() - slog.Info("Registering validator") val.RegisterValidation("test", nilIfOtherNil) return val.Struct(conf) } diff --git a/pkg/jsonrpc/handler.go b/pkg/jsonrpc/handler.go index 7aae6f3..d6598fe 100644 --- a/pkg/jsonrpc/handler.go +++ b/pkg/jsonrpc/handler.go @@ -10,6 +10,7 @@ import ( "runtime/debug" "golang.org/x/exp/slog" + "nfsense.net/nfsense/pkg/session" ) type Handler struct { @@ -25,7 +26,7 @@ func NewHandler(maxRequestSize int64) *Handler { } } -func (h *Handler) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) error { +func (h *Handler) HandleRequest(ctx context.Context, s *session.Session, r io.Reader, w io.Writer) error { defer func() { if r := recover(); r != nil { slog.Error("Recovered Panic Handling JSONRPC Request", fmt.Errorf("%v", r), "stack", debug.Stack()) @@ -52,6 +53,10 @@ func (h *Handler) HandleRequest(ctx context.Context, r io.Reader, w io.Writer) e return respondError(w, req.ID, ErrMethodNotFound, fmt.Errorf("Unsupported Jsonrpc version %v", req.Jsonrpc)) } + if s == nil { + return respondError(w, req.ID, 401, fmt.Errorf("Unauthorized")) + } + method, ok := h.methods[req.Method] if !ok { return respondError(w, req.ID, ErrMethodNotFound, fmt.Errorf("Unknown Method %v", req.Method)) diff --git a/pkg/server/api.go b/pkg/server/api.go index 4986853..eb5cd81 100644 --- a/pkg/server/api.go +++ b/pkg/server/api.go @@ -8,14 +8,17 @@ import ( "time" "golang.org/x/exp/slog" + "nfsense.net/nfsense/pkg/session" ) func HandleAPI(w http.ResponseWriter, r *http.Request) { - _, s := GetSession(r) + slog.Info("Api Handler hit") + _, s := session.GetSession(r) if s == nil { + // Fallthrough after so that jsonrpc can still deliver a valid jsonrpc error w.WriteHeader(http.StatusUnauthorized) - return } + defer func() { if r := recover(); r != nil { slog.Error("Recovered Panic Handling HTTP API Request", fmt.Errorf("%v", r), "stack", debug.Stack()) @@ -23,10 +26,10 @@ func HandleAPI(w http.ResponseWriter, r *http.Request) { return } }() - ctx, cancel := context.WithTimeout(context.WithValue(r.Context(), SessionKey, s), time.Second*10) + ctx, cancel := context.WithTimeout(context.WithValue(r.Context(), session.SessionKey, s), time.Second*10) defer cancel() - err := apiHandler.HandleRequest(ctx, r.Body, w) + err := apiHandler.HandleRequest(ctx, s, r.Body, w) if err != nil { slog.Error("Handling HTTP API Request", err) } diff --git a/pkg/server/server.go b/pkg/server/server.go index 51b99fe..25f4b6f 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -10,6 +10,7 @@ import ( "nfsense.net/nfsense/pkg/definitions" "nfsense.net/nfsense/pkg/jsonrpc" + "nfsense.net/nfsense/pkg/session" ) var server http.Server @@ -32,7 +33,7 @@ func StartWebserver(conf *definitions.Config, _apiHandler *jsonrpc.Handler) { stopCleanup = make(chan struct{}) - go CleanupSessions(stopCleanup) + go session.CleanupSessions(stopCleanup) go func() { if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { diff --git a/pkg/server/session.go b/pkg/server/session.go index 908174f..b785bb7 100644 --- a/pkg/server/session.go +++ b/pkg/server/session.go @@ -4,94 +4,17 @@ import ( "encoding/json" "io" "net/http" - "runtime/debug" - "sync" "time" - "github.com/google/uuid" "golang.org/x/exp/slog" + "nfsense.net/nfsense/pkg/session" ) -type SessionKeyType string - -const SessionKey SessionKeyType = "session" -const SessionCookieName string = "session" - -type Session struct { - Username string - Expires time.Time - // TODO Add []websocket.Conn pointer to close all active websockets, alternativly do this via context cancelation -} - type LoginRequest struct { Username string `json:"username"` Password string `json:"password"` } -type SessionResponse struct { - CommitHash string `json:"commit_hash"` -} - -var sessionsSync sync.Mutex -var sessions map[string]*Session = map[string]*Session{} - -var CommitHash = func() string { - if info, ok := debug.ReadBuildInfo(); ok { - for _, setting := range info.Settings { - if setting.Key == "vcs.revision" { - return setting.Value - } - } - } - return "asd" -}() - -func GetSession(r *http.Request) (string, *Session) { - c, err := r.Cookie("session") - if err != nil { - return "", nil - } - s, ok := sessions[c.Value] - if ok { - return c.Value, s - } - return "", nil -} - -func GenerateSession(w http.ResponseWriter, username string) { - id := uuid.New().String() - expires := time.Now().Add(time.Minute * 5) - sessionsSync.Lock() - defer sessionsSync.Unlock() - sessions[id] = &Session{ - Username: username, - Expires: expires, - } - http.SetCookie(w, &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: expires}) -} - -func CleanupSessions(stop chan struct{}) { - tick := time.NewTicker(time.Minute) - for { - select { - case <-tick.C: - ids := []string{} - sessionsSync.Lock() - for id, s := range sessions { - if time.Now().After(s.Expires) { - ids = append(ids, id) - } - } - for _, id := range ids { - delete(sessions, id) - } - sessionsSync.Unlock() - case <-stop: - return - } - } -} - func HandleLogin(w http.ResponseWriter, r *http.Request) { buf, err := io.ReadAll(r.Body) if err != nil { @@ -106,7 +29,7 @@ func HandleLogin(w http.ResponseWriter, r *http.Request) { } if req.Username == "admin" && req.Password == "12345" { slog.Info("User Login Successfull") - GenerateSession(w, req.Username) + session.GenerateSession(w, req.Username) w.WriteHeader(http.StatusOK) return } @@ -114,25 +37,21 @@ func HandleLogin(w http.ResponseWriter, r *http.Request) { } func HandleLogout(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: "", Expires: time.Now()}) + http.SetCookie(w, session.GetCookie("", time.Now())) w.WriteHeader(http.StatusOK) } func HandleSession(w http.ResponseWriter, r *http.Request) { - id, s := GetSession(r) + id, s := session.GetSession(r) if s == nil { w.WriteHeader(http.StatusUnauthorized) return } - sessionsSync.Lock() - defer sessionsSync.Unlock() - if s != nil { - s.Expires = time.Now().Add(time.Minute * 5) - } - http.SetCookie(w, &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: s.Expires}) + session.ExtendSession(s) + http.SetCookie(w, session.GetCookie(id, s.Expires)) w.WriteHeader(http.StatusOK) - resp := SessionResponse{ - CommitHash: CommitHash, + resp := session.SessionResponse{ + CommitHash: session.CommitHash, } res, err := json.Marshal(resp) if err != nil { diff --git a/pkg/server/websocket.go b/pkg/server/websocket.go index 92e60cb..86299a6 100644 --- a/pkg/server/websocket.go +++ b/pkg/server/websocket.go @@ -9,17 +9,18 @@ import ( "time" "golang.org/x/exp/slog" + "nfsense.net/nfsense/pkg/session" "nhooyr.io/websocket" ) func HandleWebsocketAPI(w http.ResponseWriter, r *http.Request) { - _, s := GetSession(r) + _, s := session.GetSession(r) if s == nil { w.WriteHeader(http.StatusUnauthorized) return } - ctx, cancel := context.WithCancel(context.WithValue(r.Context(), SessionKey, s)) + ctx, cancel := context.WithCancel(context.WithValue(r.Context(), session.SessionKey, s)) defer cancel() c, err := websocket.Accept(w, r, nil) if err != nil { @@ -51,7 +52,7 @@ func HandleWebsocketAPI(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(ctx, time.Second*10) defer cancel() - err := apiHandler.HandleRequest(ctx, bytes.NewReader(m), w) + err := apiHandler.HandleRequest(ctx, s, bytes.NewReader(m), w) if err != nil { slog.Error("Handling Websocket API Request", err) } diff --git a/pkg/session/cookie.go b/pkg/session/cookie.go new file mode 100644 index 0000000..32da76e --- /dev/null +++ b/pkg/session/cookie.go @@ -0,0 +1,10 @@ +package session + +import ( + "net/http" + "time" +) + +func GetCookie(value string, expires time.Time) *http.Cookie { + return &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: value, Expires: expires} +} diff --git a/pkg/session/session.go b/pkg/session/session.go new file mode 100644 index 0000000..83b7b9d --- /dev/null +++ b/pkg/session/session.go @@ -0,0 +1,93 @@ +package session + +import ( + "net/http" + "runtime/debug" + "sync" + "time" + + "github.com/google/uuid" +) + +type SessionKeyType string + +const SessionKey SessionKeyType = "session" +const SessionCookieName string = "session" + +type Session struct { + Username string + Expires time.Time + // TODO Add []websocket.Conn pointer to close all active websockets, alternativly do this via context cancelation +} + +type SessionResponse struct { + CommitHash string `json:"commit_hash"` +} + +var sessionsSync sync.Mutex +var sessions map[string]*Session = map[string]*Session{} + +var CommitHash = func() string { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + return setting.Value + } + } + } + return "asd" +}() + +func ExtendSession(s *Session) { + sessionsSync.Lock() + defer sessionsSync.Unlock() + if s != nil { + s.Expires = time.Now().Add(time.Minute * 5) + } +} + +func GetSession(r *http.Request) (string, *Session) { + c, err := r.Cookie("session") + if err != nil { + return "", nil + } + s, ok := sessions[c.Value] + if ok { + return c.Value, s + } + return "", nil +} + +func GenerateSession(w http.ResponseWriter, username string) { + id := uuid.New().String() + expires := time.Now().Add(time.Minute * 5) + sessionsSync.Lock() + defer sessionsSync.Unlock() + sessions[id] = &Session{ + Username: username, + Expires: expires, + } + http.SetCookie(w, &http.Cookie{Name: SessionCookieName, HttpOnly: true, SameSite: http.SameSiteStrictMode, Value: id, Expires: expires}) +} + +func CleanupSessions(stop chan struct{}) { + tick := time.NewTicker(time.Minute) + for { + select { + case <-tick.C: + ids := []string{} + sessionsSync.Lock() + for id, s := range sessions { + if time.Now().After(s.Expires) { + ids = append(ids, id) + } + } + for _, id := range ids { + delete(sessions, id) + } + sessionsSync.Unlock() + case <-stop: + return + } + } +}