Rewrite to use fasthttp

This commit is contained in:
2025-10-10 20:19:50 +02:00
parent 2c727632b8
commit a1f568cbe6
4 changed files with 108 additions and 125 deletions

View File

@@ -9,7 +9,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"net"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@@ -20,6 +19,8 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
logger "git.site.quack-lab.dev/dave/cylogger" logger "git.site.quack-lab.dev/dave/cylogger"
"github.com/fasthttp/router"
"github.com/valyala/fasthttp"
) )
const ( const (
@@ -43,8 +44,7 @@ type SSO struct {
scopes []string scopes []string
db DB db DB
mu sync.Mutex mu sync.Mutex
server *http.Server router *router.Router
mux *http.ServeMux
state string state string
callbackChan chan struct { callbackChan chan struct {
code string code string
@@ -79,10 +79,10 @@ func NewSSO(clientID, redirectURI string, scopes []string) (*SSO, error) {
return s, nil return s, nil
} }
// SetMuxer allows the SSO to use an existing HTTP muxer instead of creating its own server // SetRouter allows the SSO to use an existing fasthttp router
func (s *SSO) SetMuxer(mux *http.ServeMux) { func (s *SSO) SetRouter(r *router.Router) {
s.mux = mux s.router = r
logger.Debug("SSO configured to use existing HTTP muxer") logger.Debug("SSO configured to use existing fasthttp router")
} }
func (s *SSO) initDB() error { func (s *SSO) initDB() error {
@@ -181,19 +181,12 @@ func (s *SSO) startAuthFlow(ctx context.Context, characterName string) error {
logger.Info("Waiting for authentication...") logger.Info("Waiting for authentication...")
// Setup callback handling // Setup callback handling
if s.mux != nil { if s.router == nil {
logger.Debug("Using existing HTTP muxer for callback handling") logger.Error("No router configured for callback handling")
s.setupCallbackHandler() return errors.New("no router configured for callback handling")
} 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)
} }
logger.Debug("Using fasthttp router for callback handling")
s.setupCallbackHandler()
// Wait for callback // Wait for callback
logger.Debug("Waiting for authentication 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) 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) { func (s *SSO) handleCallback(ctx *fasthttp.RequestCtx) {
logger.Debug("Received callback request: %s %s", r.Method, r.URL.String()) s.processCallback(
if r.Method != http.MethodGet { ctx.IsGet(),
logger.Warning("Invalid callback method: %s", r.Method) string(ctx.QueryArgs().Peek("code")),
w.WriteHeader(http.StatusMethodNotAllowed) 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 { s.callbackChan <- struct {
code string code string
state string state string
@@ -266,13 +273,10 @@ func (s *SSO) handleCallback(w http.ResponseWriter, r *http.Request) {
}{"", "", errors.New("method not allowed")} }{"", "", errors.New("method not allowed")}
return return
} }
q := r.URL.Query()
code := q.Get("code") if code == "" || state == "" || state != s.state {
st := q.Get("state") logger.Error("Invalid SSO response: code=%s, state=%s, expected_state=%s", code, state, s.state)
if code == "" || st == "" || st != s.state { writeResponse(http.StatusBadRequest, "Invalid SSO response")
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"))
s.callbackChan <- struct { s.callbackChan <- struct {
code string code string
state string state string
@@ -280,52 +284,15 @@ func (s *SSO) handleCallback(w http.ResponseWriter, r *http.Request) {
}{"", "", errors.New("invalid state")} }{"", "", errors.New("invalid state")}
return return
} }
logger.Info("Received valid callback, exchanging token for code: %s", code) logger.Info("Received valid callback, exchanging token for code: %s", code)
w.Header().Set("Content-Type", "text/html") setContentType("text/html")
_, _ = w.Write([]byte("<html><body><h1>Login successful!</h1><p>You can close this window.</p></body></html>")) writeResponse(http.StatusOK, "<html><body><h1>Login successful!</h1><p>You can close this window.</p></body></html>")
s.callbackChan <- struct { s.callbackChan <- struct {
code string code string
state string state string
err error err error
}{code, st, nil} }{code, state, 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
} }
func (s *SSO) waitForCallback() (code, state string, err error) { func (s *SSO) waitForCallback() (code, state string, err error) {

18
go.mod
View File

@@ -1,23 +1,31 @@
module go-eve-pi module go-eve-pi
go 1.23.6 go 1.24.0
toolchain go1.24.8
require ( require (
git.site.quack-lab.dev/dave/cylogger v1.4.0 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/joho/godotenv v1.5.1
github.com/valyala/fasthttp v1.67.0
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.31.0
) )
require ( require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/hexops/valast v1.5.0 // indirect github.com/hexops/valast v1.5.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // 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 github.com/mattn/go-sqlite3 v1.14.22 // indirect
golang.org/x/mod v0.17.0 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
golang.org/x/sync v0.9.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/text v0.20.0 // indirect golang.org/x/mod v0.27.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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 mvdan.cc/gofumpt v0.4.0 // indirect
) )

30
go.sum
View File

@@ -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 h1:3Ca7V5JWvruARJd5S8xDFwW9LnZ9QInqkYLRdrEFvuY=
git.site.quack-lab.dev/dave/cylogger v1.4.0/go.mod h1:wctgZplMvroA4X6p8f4B/LaCKtiBcT1Pp+L14kcS8jk= 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 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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/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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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/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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= github.com/valyala/fasthttp v1.67.0 h1:tqKlJMUP6iuNG8hGjK/s9J4kadH7HLV4ijEcPGsezac=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= github.com/valyala/fasthttp v1.67.0/go.mod h1:qYSIpqt/0XNmShgo/8Aq8E3UYWVVwNS2QYmzd8WIEPM=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=

72
main.go
View File

@@ -4,14 +4,15 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"net/http"
"os" "os"
"os/signal" "os/signal"
"strings" "strings"
logger "git.site.quack-lab.dev/dave/cylogger" logger "git.site.quack-lab.dev/dave/cylogger"
"github.com/fasthttp/router"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/valyala/fasthttp"
) )
type Options struct { type Options struct {
@@ -46,61 +47,54 @@ func main() {
return return
} }
// Setup HTTP server // Setup fasthttp router
mux := http.NewServeMux() r := router.New()
// Configure SSO to use existing muxer // Configure SSO to use existing fasthttp router
sso.SetMuxer(mux) sso.SetRouter(r)
// Add your own routes // Add your own routes
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { r.GET("/", func(ctx *fasthttp.RequestCtx) {
w.WriteHeader(http.StatusOK) ctx.SetStatusCode(fasthttp.StatusOK)
w.Write([]byte("EVE PI Server Running")) ctx.WriteString("EVE PI Server Running")
}) })
server := &http.Server{ r.GET("/login/{character}", func(ctx *fasthttp.RequestCtx) {
Addr: ":3000", charName := ctx.UserValue("character")
Handler: mux, 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") logger.Info("Starting web server on :3000")
go func() { 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) 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 // Listen for SIGINT and gracefully shut down the server
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt) signal.Notify(sigCh, os.Interrupt)
<-sigCh <-sigCh
logger.Info("SIGINT received, shutting down web server gracefully...") logger.Info("SIGINT received, shutting down web server gracefully...")
logger.Info("Web server shut down cleanly")
if err := server.Shutdown(context.Background()); err != nil {
logger.Error("Error shutting down server: %v", err)
} else {
logger.Info("Web server shut down cleanly")
}
} }
func LoadOptions() (Options, error) { func LoadOptions() (Options, error) {