tulip/cmd/purple-tulip/server/server.go

124 lines
3.4 KiB
Go

package server
import (
"bytes"
"fmt"
"github.com/1f349/cache"
"github.com/1f349/mjwt"
"github.com/1f349/tulip/database"
"github.com/1f349/tulip/issuer"
"github.com/1f349/tulip/oauth"
"github.com/1f349/tulip/theme"
"github.com/julienschmidt/httprouter"
"github.com/rs/cors"
"log"
"net/http"
"strings"
"sync/atomic"
"time"
)
type HttpServer struct {
Server *http.Server
r *httprouter.Router
conf atomic.Pointer[Conf]
manager atomic.Pointer[issuer.Manager]
signer mjwt.Signer
flowState *cache.Cache[string, flowStateData]
services atomic.Pointer[map[string]AllowedClient]
}
type flowStateData struct {
sso *issuer.WellKnownOIDC
target AllowedClient
}
func NewHttpServer(conf Conf, db *database.DB, controller *oauth.Controller, signer mjwt.Signer) *HttpServer {
r := httprouter.New()
// remove last slash from baseUrl
{
l := len(conf.BaseUrl)
if conf.BaseUrl[l-1] == '/' {
conf.BaseUrl = conf.BaseUrl[:l-1]
}
}
hs := &HttpServer{
Server: &http.Server{
Addr: conf.Listen,
Handler: r,
ReadTimeout: time.Minute,
ReadHeaderTimeout: time.Minute,
WriteTimeout: time.Minute,
IdleTimeout: time.Minute,
MaxHeaderBytes: 2500,
},
r: r,
signer: signer,
flowState: cache.New[string, flowStateData](),
}
err := hs.UpdateConfig(conf)
if err != nil {
log.Fatalln("Failed to load initial config:", err)
return nil
}
r.GET("/", func(rw http.ResponseWriter, req *http.Request, _ httprouter.Params) {
rw.WriteHeader(http.StatusOK)
_, _ = fmt.Fprintln(rw, "What is this?")
})
r.GET("/popup", hs.flowPopup)
r.POST("/popup", hs.flowPopupPost)
r.GET("/callback", hs.flowCallback)
r.GET("/theme/style.css", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
http.ServeContent(rw, req, "style.css", time.Now(), bytes.NewReader(theme.DefaultThemeCss))
})
// setup CORS options for `/verify` and `/refresh` endpoints
var corsAccessControl = cors.New(cors.Options{
AllowOriginFunc: func(origin string) bool {
load := hs.services.Load()
_, ok := (*load)[strings.TrimSuffix(origin, "/")]
return ok
},
AllowedMethods: []string{http.MethodPost, http.MethodOptions},
AllowedHeaders: []string{"Content-Type"},
AllowCredentials: true,
})
// `/verify` and `/refresh` need CORS headers to be usable on other domains
r.POST("/verify", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
corsAccessControl.ServeHTTP(rw, req, func(writer http.ResponseWriter, request *http.Request) {
hs.verifyHandler(rw, req, params)
})
})
r.POST("/refresh", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
corsAccessControl.ServeHTTP(rw, req, func(writer http.ResponseWriter, request *http.Request) {
hs.refreshHandler(rw, req, params)
})
})
r.OPTIONS("/refresh", func(rw http.ResponseWriter, req *http.Request, params httprouter.Params) {
corsAccessControl.ServeHTTP(rw, req, func(_ http.ResponseWriter, _ *http.Request) {})
})
return hs
}
func (h *HttpServer) UpdateConfig(conf Conf) error {
m, err := issuer.NewManager(conf.SsoServices)
if err != nil {
return fmt.Errorf("failed to reload SSO service manager: %w", err)
}
clientLookup := make(map[string]AllowedClient)
for _, i := range conf.AllowedClients {
clientLookup[i.Url.String()] = i
}
h.conf.Store(&conf)
h.manager.Store(m)
h.services.Store(&clientLookup)
return nil
}