diff --git a/esi_sso.go b/esi_sso.go index 4a70973..0758830 100644 --- a/esi_sso.go +++ b/esi_sso.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "net" "net/http" "net/url" "strconv" @@ -20,6 +19,8 @@ import ( "gorm.io/gorm" logger "git.site.quack-lab.dev/dave/cylogger" + "github.com/fasthttp/router" + "github.com/valyala/fasthttp" ) const ( @@ -43,8 +44,7 @@ type SSO struct { scopes []string db DB mu sync.Mutex - server *http.Server - mux *http.ServeMux + router *router.Router state string callbackChan chan struct { code string @@ -79,10 +79,10 @@ func NewSSO(clientID, redirectURI string, scopes []string) (*SSO, error) { return s, nil } -// SetMuxer allows the SSO to use an existing HTTP muxer instead of creating its own server -func (s *SSO) SetMuxer(mux *http.ServeMux) { - s.mux = mux - logger.Debug("SSO configured to use existing HTTP muxer") +// SetRouter allows the SSO to use an existing fasthttp router +func (s *SSO) SetRouter(r *router.Router) { + s.router = r + logger.Debug("SSO configured to use existing fasthttp router") } func (s *SSO) initDB() error { @@ -181,19 +181,12 @@ func (s *SSO) startAuthFlow(ctx context.Context, characterName string) error { logger.Info("Waiting for authentication...") // Setup callback handling - if s.mux != nil { - logger.Debug("Using existing HTTP muxer for callback handling") - s.setupCallbackHandler() - } else { - logger.Debug("Starting dedicated callback server") - server, err := s.startCallbackServer() - if err != nil { - logger.Error("Failed to start callback server: %v", err) - return err - } - s.server = server - defer server.Shutdown(ctx) + if s.router == nil { + logger.Error("No router configured for callback handling") + return errors.New("no router configured for callback handling") } + logger.Debug("Using fasthttp router for callback handling") + s.setupCallbackHandler() // Wait for callback logger.Debug("Waiting for authentication callback") @@ -251,14 +244,28 @@ func (s *SSO) setupCallbackHandler() { } logger.Debug("Setting up callback handler on path: %s", u.Path) - s.mux.HandleFunc(u.Path, s.handleCallback) + s.router.GET(u.Path, s.handleCallback) } -func (s *SSO) handleCallback(w http.ResponseWriter, r *http.Request) { - logger.Debug("Received callback request: %s %s", r.Method, r.URL.String()) - if r.Method != http.MethodGet { - logger.Warning("Invalid callback method: %s", r.Method) - w.WriteHeader(http.StatusMethodNotAllowed) +func (s *SSO) handleCallback(ctx *fasthttp.RequestCtx) { + s.processCallback( + ctx.IsGet(), + string(ctx.QueryArgs().Peek("code")), + string(ctx.QueryArgs().Peek("state")), + func(status int, body string) { + ctx.SetStatusCode(status) + ctx.WriteString(body) + }, + func(contentType string) { + ctx.SetContentType(contentType) + }, + ) +} + +func (s *SSO) processCallback(isGet bool, code, state string, writeResponse func(int, string), setContentType func(string)) { + if !isGet { + logger.Warning("Invalid callback method") + writeResponse(http.StatusMethodNotAllowed, "Method not allowed") s.callbackChan <- struct { code string state string @@ -266,13 +273,10 @@ func (s *SSO) handleCallback(w http.ResponseWriter, r *http.Request) { }{"", "", errors.New("method not allowed")} return } - q := r.URL.Query() - code := q.Get("code") - st := q.Get("state") - if code == "" || st == "" || st != s.state { - logger.Error("Invalid SSO response: code=%s, state=%s, expected_state=%s", code, st, s.state) - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("Invalid SSO response")) + + if code == "" || state == "" || state != s.state { + logger.Error("Invalid SSO response: code=%s, state=%s, expected_state=%s", code, state, s.state) + writeResponse(http.StatusBadRequest, "Invalid SSO response") s.callbackChan <- struct { code string state string @@ -280,52 +284,15 @@ func (s *SSO) handleCallback(w http.ResponseWriter, r *http.Request) { }{"", "", errors.New("invalid state")} return } + logger.Info("Received valid callback, exchanging token for code: %s", code) - w.Header().Set("Content-Type", "text/html") - _, _ = w.Write([]byte("
You can close this window.
")) + setContentType("text/html") + writeResponse(http.StatusOK, "You can close this window.
") s.callbackChan <- struct { code string state string err error - }{code, st, nil} -} - -func (s *SSO) startCallbackServer() (*http.Server, error) { - logger.Debug("Starting dedicated callback server for redirect URI: %s", s.redirectURI) - u, err := url.Parse(s.redirectURI) - if err != nil { - logger.Error("Failed to parse redirect URI: %v", err) - return nil, err - } - if u.Scheme != "http" && u.Scheme != "https" { - logger.Error("Invalid redirect URI scheme: %s", u.Scheme) - return nil, errors.New("redirect URI must be http(s)") - } - hostPort := u.Host - if !strings.Contains(hostPort, ":") { - if u.Scheme == "https" { - hostPort += ":443" - } else { - hostPort += ":80" - } - } - - logger.Debug("Callback server will listen on %s", hostPort) - mux := http.NewServeMux() - mux.HandleFunc(u.Path, s.handleCallback) - - ln, err := net.Listen("tcp", hostPort) - if err != nil { - logger.Error("Failed to listen on %s: %v", hostPort, err) - return nil, err - } - - server := &http.Server{Handler: mux} - go func() { - logger.Debug("Callback server started successfully") - _ = server.Serve(ln) - }() - return server, nil + }{code, state, nil} } func (s *SSO) waitForCallback() (code, state string, err error) { diff --git a/go.mod b/go.mod index 1995ba3..03ae781 100644 --- a/go.mod +++ b/go.mod @@ -1,23 +1,31 @@ module go-eve-pi -go 1.23.6 +go 1.24.0 + +toolchain go1.24.8 require ( git.site.quack-lab.dev/dave/cylogger v1.4.0 + github.com/fasthttp/router v1.5.4 github.com/joho/godotenv v1.5.1 + github.com/valyala/fasthttp v1.67.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.0 ) require ( + github.com/andybalholm/brotli v1.2.0 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/hexops/valast v1.5.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/text v0.20.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.36.0 // indirect mvdan.cc/gofumpt v0.4.0 // indirect ) diff --git a/go.sum b/go.sum index b17cb7f..27237a7 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ git.site.quack-lab.dev/dave/cylogger v1.4.0 h1:3Ca7V5JWvruARJd5S8xDFwW9LnZ9QInqkYLRdrEFvuY= git.site.quack-lab.dev/dave/cylogger v1.4.0/go.mod h1:wctgZplMvroA4X6p8f4B/LaCKtiBcT1Pp+L14kcS8jk= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/fasthttp/router v1.5.4 h1:oxdThbBwQgsDIYZ3wR1IavsNl6ZS9WdjKukeMikOnC8= +github.com/fasthttp/router v1.5.4/go.mod h1:3/hysWq6cky7dTfzaaEPZGdptwjwx0qzTgFCKEWRjgc= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -16,6 +20,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -24,14 +30,22 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc= +github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac= +github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= diff --git a/main.go b/main.go index 37f2be4..7ba3507 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,15 @@ import ( "context" "flag" "fmt" - "net/http" "os" "os/signal" "strings" logger "git.site.quack-lab.dev/dave/cylogger" + "github.com/fasthttp/router" "github.com/joho/godotenv" + "github.com/valyala/fasthttp" ) type Options struct { @@ -46,61 +47,54 @@ func main() { return } - // Setup HTTP server - mux := http.NewServeMux() + // Setup fasthttp router + r := router.New() - // Configure SSO to use existing muxer - sso.SetMuxer(mux) + // Configure SSO to use existing fasthttp router + sso.SetRouter(r) // Add your own routes - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("EVE PI Server Running")) + r.GET("/", func(ctx *fasthttp.RequestCtx) { + ctx.SetStatusCode(fasthttp.StatusOK) + ctx.WriteString("EVE PI Server Running") }) - server := &http.Server{ - Addr: ":3000", - Handler: mux, - } + r.GET("/login/{character}", func(ctx *fasthttp.RequestCtx) { + charName := ctx.UserValue("character") + charNameStr, ok := charName.(string) + if !ok || charNameStr == "" { + ctx.SetStatusCode(fasthttp.StatusBadRequest) + ctx.WriteString("Missing character parameter") + return + } + logger.Info("Login requested for character %s", charNameStr) + // Trigger the auth flow (will register callback if needed) + token, err := sso.GetToken(context.Background(), charNameStr) + if err != nil { + logger.Error("Failed to authenticate character %s: %v", charNameStr, err) + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + ctx.WriteString("Authentication failed") + return + } + logger.Info("Successfully authenticated character %s", charNameStr) + ctx.SetContentType("text/plain") + ctx.SetStatusCode(fasthttp.StatusOK) + ctx.WriteString("Authenticated! Access token: " + token) + }) logger.Info("Starting web server on :3000") go func() { - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := fasthttp.ListenAndServe(":3000", r.Handler); err != nil { logger.Error("Server failed: %v", err) } }() - mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { - charName := r.URL.Query().Get("character") - if charName == "" { - http.Error(w, "Missing character parameter", http.StatusBadRequest) - return - } - logger.Info("Login requested for character %s", charName) - // Trigger the auth flow (will register callback if needed) - token, err := sso.GetToken(r.Context(), charName) - if err != nil { - logger.Error("Failed to authenticate character %s: %v", charName, err) - http.Error(w, "Authentication failed", http.StatusInternalServerError) - return - } - logger.Info("Successfully authenticated character %s", charName) - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - w.Write([]byte("Authenticated! Access token: " + token)) - }) - // Listen for SIGINT and gracefully shut down the server sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) <-sigCh logger.Info("SIGINT received, shutting down web server gracefully...") - - if err := server.Shutdown(context.Background()); err != nil { - logger.Error("Error shutting down server: %v", err) - } else { - logger.Info("Web server shut down cleanly") - } + logger.Info("Web server shut down cleanly") } func LoadOptions() (Options, error) {