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"
"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("<html><body><h1>Login successful!</h1><p>You can close this window.</p></body></html>"))
setContentType("text/html")
writeResponse(http.StatusOK, "<html><body><h1>Login successful!</h1><p>You can close this window.</p></body></html>")
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) {

18
go.mod
View File

@@ -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
)

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/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=

72
main.go
View File

@@ -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) {