|
|
@@ -7,8 +7,10 @@ import (
|
|
|
"fmt"
|
|
|
"log/slog"
|
|
|
"net/http"
|
|
|
+ "strings"
|
|
|
"time"
|
|
|
|
|
|
+ "github.com/rs/cors"
|
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
|
|
"github.com/mk6i/open-oscar-server/config"
|
|
|
@@ -16,6 +18,14 @@ import (
|
|
|
"github.com/mk6i/open-oscar-server/wire"
|
|
|
)
|
|
|
|
|
|
+// allowAnyOrigin allows every origin through AllowOriginFunc, which echoes the
|
|
|
+// request origin back. Leaving AllowedOrigins empty would also allow everything,
|
|
|
+// but the library answers that with a literal "*".
|
|
|
+func allowAnyOrigin(opts cors.Options) cors.Options {
|
|
|
+ opts.AllowOriginFunc = func(string) bool { return true }
|
|
|
+ return opts
|
|
|
+}
|
|
|
+
|
|
|
func NewServer(listeners []string, logger *slog.Logger, handler Handler, sessionManager *SessionManager) *Server {
|
|
|
servers := make([]*http.Server, 0, len(listeners))
|
|
|
|
|
|
@@ -75,209 +85,109 @@ func NewServer(listeners []string, logger *slog.Logger, handler Handler, session
|
|
|
Logger: logger,
|
|
|
}
|
|
|
|
|
|
- shutdownCtx, shutdownCancel := context.WithCancel(context.Background())
|
|
|
-
|
|
|
- for _, l := range listeners {
|
|
|
- mux := http.NewServeMux()
|
|
|
-
|
|
|
- // CORSMiddleware wraps the session layer rather than the other way around,
|
|
|
- // so that responses the session layer rejects (400 missing aimsid, 401
|
|
|
- // expired session) still carry Access-Control-Allow-Origin. A browser
|
|
|
- // blocks a cross-origin response without that header, and the Web AIM
|
|
|
- // client reads the resulting status-0 empty response as a CORS failure and
|
|
|
- // permanently switches its whole request pipeline to JSONP.
|
|
|
- //
|
|
|
- // oscarRoute charges the request against the rate class for (foodGroup,
|
|
|
- // subGroup) before the handler runs; sessionRoute and stubRoute reach no
|
|
|
- // food group and so are not rate limited here.
|
|
|
- oscarRoute := func(foodGroup uint16, subGroup uint16, h SessionHandlerFunc) http.Handler {
|
|
|
- return authMiddleware.CORSMiddleware(
|
|
|
- authMiddleware.RequireSession(sessionManager,
|
|
|
- rateLimiter.OSCAR(foodGroup, subGroup)(h)))
|
|
|
- }
|
|
|
- sessionRoute := func(h SessionHandlerFunc) http.Handler {
|
|
|
- return authMiddleware.CORSMiddleware(
|
|
|
- authMiddleware.RequireSession(sessionManager, h))
|
|
|
- }
|
|
|
- stubRoute := func(h http.HandlerFunc) http.Handler {
|
|
|
- return authMiddleware.CORSMiddleware(h)
|
|
|
- }
|
|
|
-
|
|
|
- mux.Handle("GET /{$}", http.HandlerFunc(handler.GetHelloWorldHandler))
|
|
|
+ expressionsHandler := NewExpressionsHandler(
|
|
|
+ handler.IconSource, handler.BARTService, handler.FeedbagService, logger)
|
|
|
|
|
|
- // Unauthenticated and outside every middleware: Flash Player fetches the
|
|
|
- // policy before it has a session, and refuses to look at a redirect or an
|
|
|
- // error envelope.
|
|
|
- mux.Handle("GET /crossdomain.xml", &CrossDomainPolicyHandler{Logger: logger})
|
|
|
+ crossDomainHandler := &CrossDomainPolicyHandler{Logger: logger}
|
|
|
+ conversationStub := &ConversationStubHandler{Logger: logger}
|
|
|
+ lifestreamStub := &UserInfoStubHandler{Logger: logger}
|
|
|
+ serviceStub := &ServiceStubHandler{Logger: logger}
|
|
|
|
|
|
- mux.Handle("POST /auth/clientLogin", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- // Set CORS headers for public endpoint
|
|
|
- w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
- w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
|
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
|
+ oscarRoute := func(foodGroup uint16, subGroup uint16, h SessionHandlerFunc) http.Handler {
|
|
|
+ return authMiddleware.RequireSession(sessionManager,
|
|
|
+ rateLimiter.OSCAR(foodGroup, subGroup)(h))
|
|
|
+ }
|
|
|
+ sessionRoute := func(h SessionHandlerFunc) http.Handler {
|
|
|
+ return authMiddleware.RequireSession(sessionManager, h)
|
|
|
+ }
|
|
|
|
|
|
- authHandler.ClientLogin(w, r)
|
|
|
- }))
|
|
|
+ loginPSP := http.HandlerFunc(authHandler.LoginPSP)
|
|
|
+ startSession := http.HandlerFunc(aimHandler.StartSession)
|
|
|
+ sendIM := oscarRoute(wire.ICBM, wire.ICBMChannelMsgToHost, messagingHandler.SendIM)
|
|
|
+ memberDirUpdate := oscarRoute(wire.Locate, wire.LocateSetDirInfo, memberDirHandler.Update)
|
|
|
|
|
|
- // Handle OPTIONS for CORS preflight
|
|
|
- mux.HandleFunc("OPTIONS /auth/clientLogin", func(w http.ResponseWriter, r *http.Request) {
|
|
|
- w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
- w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
|
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
|
- w.WriteHeader(http.StatusNoContent)
|
|
|
- })
|
|
|
+ corsHandler := cors.New(corsOptions(logger, handler.AllowedOrigins))
|
|
|
|
|
|
- mux.Handle("GET /auth/getToken", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
- w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
|
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
|
- authHandler.GetToken(w, r)
|
|
|
- }))
|
|
|
-
|
|
|
- mux.HandleFunc("OPTIONS /auth/getToken", func(w http.ResponseWriter, r *http.Request) {
|
|
|
- w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
- w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
|
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
|
- w.WriteHeader(http.StatusNoContent)
|
|
|
- })
|
|
|
+ shutdownCtx, shutdownCancel := context.WithCancel(context.Background())
|
|
|
|
|
|
- // No SSO cookie is involved, so this sits outside the session middleware.
|
|
|
- // Both methods, since clients differ on which they use.
|
|
|
- getInfo := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
- w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
|
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
|
- authHandler.GetInfo(w, r)
|
|
|
- })
|
|
|
- mux.Handle("GET /auth/getInfo", getInfo)
|
|
|
- mux.Handle("POST /auth/getInfo", getInfo)
|
|
|
-
|
|
|
- mux.HandleFunc("OPTIONS /auth/getInfo", func(w http.ResponseWriter, r *http.Request) {
|
|
|
- w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
- w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
|
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
|
- w.WriteHeader(http.StatusNoContent)
|
|
|
- })
|
|
|
+ for _, l := range listeners {
|
|
|
+ mux := http.NewServeMux()
|
|
|
|
|
|
- // Web AIM navigates the browser here on File > Logout; clear SSO state
|
|
|
- // and redirect to the login screen.
|
|
|
- mux.Handle("GET /auth/logout", http.HandlerFunc(authHandler.Logout))
|
|
|
+ mux.HandleFunc("GET /{$}", handler.GetHelloWorldHandler)
|
|
|
+ mux.Handle("GET /crossdomain.xml", crossDomainHandler)
|
|
|
|
|
|
- // Wrapped in CORS: besides the browser navigation that renders the form,
|
|
|
- // the client fetches this cross-origin for its client2Web SSO handoff, and
|
|
|
- // a response without Access-Control-Allow-Origin reaches it as an ioError.
|
|
|
- loginPSP := authMiddleware.CORSMiddleware(http.HandlerFunc(authHandler.LoginPSP))
|
|
|
+ mux.HandleFunc("POST /auth/clientLogin", authHandler.ClientLogin)
|
|
|
+ mux.HandleFunc("GET /auth/getToken", authHandler.GetToken)
|
|
|
+ mux.HandleFunc("GET /auth/getInfo", authHandler.GetInfo)
|
|
|
+ mux.HandleFunc("POST /auth/getInfo", authHandler.GetInfo)
|
|
|
+ mux.HandleFunc("GET /auth/logout", authHandler.Logout)
|
|
|
mux.Handle("GET /_cqr/login/login.psp", loginPSP)
|
|
|
mux.Handle("POST /_cqr/login/login.psp", loginPSP)
|
|
|
|
|
|
- startSession := authMiddleware.CORSMiddleware(http.HandlerFunc(aimHandler.StartSession))
|
|
|
mux.Handle("GET /aim/startSession", startSession)
|
|
|
mux.Handle("POST /aim/startSession", startSession)
|
|
|
-
|
|
|
mux.Handle("GET /aim/endSession", sessionRoute(aimHandler.EndSession))
|
|
|
-
|
|
|
mux.Handle("GET /aim/fetchEvents", sessionRoute(aimHandler.FetchEvents))
|
|
|
+ mux.HandleFunc("GET /aim/startOSCARSession", aimHandler.StartOSCARSession)
|
|
|
|
|
|
- mux.Handle("GET /aim/addTempBuddy", oscarRoute(wire.Buddy, wire.BuddyAddTempBuddies, aimHandler.AddTempBuddy))
|
|
|
- mux.Handle("GET /aim/removeTempBuddy", oscarRoute(wire.Buddy, wire.BuddyDelTempBuddies, aimHandler.RemoveTempBuddy))
|
|
|
-
|
|
|
- mux.Handle("GET /aim/setForwardDomain", stubRoute(aimHandler.SetForwardDomain))
|
|
|
- mux.Handle("GET /aim/getData", stubRoute(aimHandler.GetData))
|
|
|
- mux.Handle("GET /aim/reportAction", stubRoute(aimHandler.ReportAction))
|
|
|
-
|
|
|
- // OSCAR Bridge endpoint. Hands off to a BOS session rather than reaching
|
|
|
- // a food group, so there is no OSCAR budget to charge.
|
|
|
- mux.Handle("GET /aim/startOSCARSession",
|
|
|
- authMiddleware.CORSMiddleware(http.HandlerFunc(aimHandler.StartOSCARSession)))
|
|
|
-
|
|
|
- conversationStub := &ConversationStubHandler{
|
|
|
- Logger: logger,
|
|
|
- }
|
|
|
- mux.Handle("GET /conversation/update", stubRoute(conversationStub.Update))
|
|
|
- mux.Handle("GET /conversation/close", stubRoute(conversationStub.Close))
|
|
|
- mux.Handle("GET /imlog/markRead", stubRoute(conversationStub.MarkRead))
|
|
|
- mux.Handle("GET /imlog/fetchStoredIMs", sessionRoute(conversationStub.FetchStoredIMs))
|
|
|
-
|
|
|
- // Presence and buddy list
|
|
|
mux.Handle("GET /presence/get", oscarRoute(wire.Feedbag, wire.FeedbagQuery, presenceHandler.GetPresence))
|
|
|
+ mux.Handle("GET /presence/setState", oscarRoute(wire.OService, wire.OServiceSetUserInfoFields, presenceHandler.SetState))
|
|
|
+ mux.Handle("GET /presence/setStatus", oscarRoute(wire.OService, wire.OServiceSetUserInfoFields, presenceHandler.SetStatus))
|
|
|
+ mux.Handle("GET /presence/getProfile", oscarRoute(wire.Locate, wire.LocateUserInfoQuery, presenceHandler.GetProfile))
|
|
|
+ mux.Handle("GET /presence/setProfile", oscarRoute(wire.Locate, wire.LocateSetInfo, presenceHandler.SetProfile))
|
|
|
+ mux.HandleFunc("GET /presence/icon", presenceHandler.Icon)
|
|
|
|
|
|
mux.Handle("GET /buddylist/addBuddy", oscarRoute(wire.Feedbag, wire.FeedbagInsertItem, buddyListHandler.AddBuddy))
|
|
|
mux.Handle("GET /buddylist/addGroup", oscarRoute(wire.Feedbag, wire.FeedbagInsertItem, buddyListHandler.AddGroup))
|
|
|
mux.Handle("GET /buddylist/removeBuddy", oscarRoute(wire.Feedbag, wire.FeedbagDeleteItem, buddyListHandler.RemoveBuddy))
|
|
|
mux.Handle("GET /buddylist/removeGroup", oscarRoute(wire.Feedbag, wire.FeedbagDeleteItem, buddyListHandler.RemoveGroup))
|
|
|
- mux.Handle("GET /buddylist/renameGroup", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, buddyListHandler.RenameGroup))
|
|
|
mux.Handle("GET /buddylist/moveBuddy", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, buddyListHandler.MoveBuddy))
|
|
|
+ mux.Handle("GET /buddylist/renameGroup", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, buddyListHandler.RenameGroup))
|
|
|
mux.Handle("GET /buddylist/setBuddyAttribute", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, buddyListHandler.SetBuddyAttribute))
|
|
|
mux.Handle("GET /buddylist/setGroupAttribute", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, buddyListHandler.SetGroupAttribute))
|
|
|
+ mux.Handle("GET /aim/addTempBuddy", oscarRoute(wire.Buddy, wire.BuddyAddTempBuddies, aimHandler.AddTempBuddy))
|
|
|
+ mux.Handle("GET /aim/removeTempBuddy", oscarRoute(wire.Buddy, wire.BuddyDelTempBuddies, aimHandler.RemoveTempBuddy))
|
|
|
|
|
|
// The Web AIM client POSTs the message body (non-IE browsers); IE uses GET.
|
|
|
- sendIMHandler := oscarRoute(wire.ICBM, wire.ICBMChannelMsgToHost, messagingHandler.SendIM)
|
|
|
- mux.Handle("GET /im/sendIM", sendIMHandler)
|
|
|
- mux.Handle("POST /im/sendIM", sendIMHandler)
|
|
|
-
|
|
|
+ mux.Handle("GET /im/sendIM", sendIM)
|
|
|
+ mux.Handle("POST /im/sendIM", sendIM)
|
|
|
mux.Handle("GET /im/setTyping", oscarRoute(wire.ICBM, wire.ICBMClientEvent, messagingHandler.SetTyping))
|
|
|
-
|
|
|
- mux.Handle("GET /presence/setState", oscarRoute(wire.OService, wire.OServiceSetUserInfoFields, presenceHandler.SetState))
|
|
|
-
|
|
|
- mux.Handle("GET /presence/setStatus", oscarRoute(wire.OService, wire.OServiceSetUserInfoFields, presenceHandler.SetStatus))
|
|
|
- mux.Handle("GET /presence/setProfile", oscarRoute(wire.Locate, wire.LocateSetInfo, presenceHandler.SetProfile))
|
|
|
- mux.Handle("GET /presence/getProfile", oscarRoute(wire.Locate, wire.LocateUserInfoQuery, presenceHandler.GetProfile))
|
|
|
-
|
|
|
- // Unauthenticated, like /expressions/get below: buddy icons load as plain
|
|
|
- // <img> sources that carry no aimsid.
|
|
|
- mux.Handle("GET /presence/icon", http.HandlerFunc(presenceHandler.Icon))
|
|
|
+ mux.Handle("GET /imlog/fetchStoredIMs", sessionRoute(conversationStub.FetchStoredIMs))
|
|
|
+ mux.HandleFunc("GET /imlog/markRead", conversationStub.MarkRead)
|
|
|
+ mux.HandleFunc("GET /conversation/update", conversationStub.Update)
|
|
|
+ mux.HandleFunc("GET /conversation/close", conversationStub.Close)
|
|
|
|
|
|
mux.Handle("GET /memberDir/search", oscarRoute(wire.ODir, wire.ODirInfoQuery, memberDirHandler.Search))
|
|
|
mux.Handle("GET /memberDir/get", oscarRoute(wire.Locate, wire.LocateGetDirInfo, memberDirHandler.Get))
|
|
|
- memberDirUpdate := oscarRoute(wire.Locate, wire.LocateSetDirInfo, memberDirHandler.Update)
|
|
|
mux.Handle("GET /memberDir/update", memberDirUpdate)
|
|
|
mux.Handle("POST /memberDir/update", memberDirUpdate)
|
|
|
|
|
|
- mux.Handle("GET /preference/set", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, preferenceHandler.SetPreferences))
|
|
|
mux.Handle("GET /preference/get", oscarRoute(wire.Feedbag, wire.FeedbagQuery, preferenceHandler.GetPreferences))
|
|
|
- mux.Handle("GET /preference/setPermitDeny", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, preferenceHandler.SetPermitDeny))
|
|
|
+ mux.Handle("GET /preference/set", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, preferenceHandler.SetPreferences))
|
|
|
mux.Handle("GET /preference/getPermitDeny", oscarRoute(wire.Feedbag, wire.FeedbagQuery, preferenceHandler.GetPermitDeny))
|
|
|
+ mux.Handle("GET /preference/setPermitDeny", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, preferenceHandler.SetPermitDeny))
|
|
|
|
|
|
- // Expressions endpoint (for buddy icons, etc.).
|
|
|
- expressionsHandler := NewExpressionsHandler(
|
|
|
- handler.IconSource, handler.BARTService, handler.FeedbagService, logger)
|
|
|
- mux.Handle("GET /expressions/get",
|
|
|
- authMiddleware.CORSMiddleware(
|
|
|
- http.HandlerFunc(expressionsHandler.Get)))
|
|
|
- // WithBinaryBody: the body is the raw image, and a missed parameter lookup
|
|
|
- // would otherwise feed it to ParseForm and consume it.
|
|
|
+ mux.HandleFunc("GET /expressions/get", expressionsHandler.Get)
|
|
|
mux.Handle("POST /expressions/upload",
|
|
|
WithBinaryBody(oscarRoute(wire.BART, wire.BARTUploadQuery, expressionsHandler.Upload)))
|
|
|
|
|
|
- // Web AIM calls lifestream/* on the API host (e.g. /lifestream/getUserDetails).
|
|
|
- lifestreamStub := &UserInfoStubHandler{Logger: logger}
|
|
|
- // getUserDetails returns a minimal AIM identity and getServices the service
|
|
|
- // list behind it. Every other lifestream/* method is an unimplemented
|
|
|
- // social-feed feature; the subtree catch-all acknowledges them with an
|
|
|
- // empty 200 so the client doesn't error.
|
|
|
- mux.Handle("GET /lifestream/getUserDetails", stubRoute(lifestreamStub.GetUserDetails))
|
|
|
- mux.Handle("GET /lifestream/getServices", stubRoute(lifestreamStub.GetServices))
|
|
|
- mux.Handle("GET /lifestream/heyGetNotifications", stubRoute(lifestreamStub.HeyGetNotifications))
|
|
|
- mux.Handle("GET /lifestream/", stubRoute(lifestreamStub.EmptyOK))
|
|
|
-
|
|
|
- // The client probes for a linked Google Talk account as soon as the
|
|
|
- // session comes up, and its callback dereferences response.data unless
|
|
|
- // the status says the service is absent.
|
|
|
- serviceStub := &ServiceStubHandler{Logger: logger}
|
|
|
- mux.Handle("GET /service/getAttributes", stubRoute(serviceStub.GetAttributes))
|
|
|
-
|
|
|
- mux.Handle("OPTIONS /", authMiddleware.CORSMiddleware(
|
|
|
- http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})))
|
|
|
-
|
|
|
- mux.Handle("/", authMiddleware.CORSMiddleware(
|
|
|
- http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- logger.Debug("webapi 404", "method", r.Method, "path", r.URL.Path)
|
|
|
- SendError(w, r, http.StatusNotFound, "not found")
|
|
|
- })))
|
|
|
+ mux.HandleFunc("GET /aim/setForwardDomain", aimHandler.SetForwardDomain)
|
|
|
+ mux.HandleFunc("GET /aim/getData", aimHandler.GetData)
|
|
|
+ mux.HandleFunc("GET /aim/reportAction", aimHandler.ReportAction)
|
|
|
+ mux.HandleFunc("GET /lifestream/getUserDetails", lifestreamStub.GetUserDetails)
|
|
|
+ mux.HandleFunc("GET /lifestream/getServices", lifestreamStub.GetServices)
|
|
|
+ mux.HandleFunc("GET /lifestream/heyGetNotifications", lifestreamStub.HeyGetNotifications)
|
|
|
+ mux.HandleFunc("GET /lifestream/", lifestreamStub.EmptyOK)
|
|
|
+ mux.HandleFunc("GET /service/getAttributes", serviceStub.GetAttributes)
|
|
|
+
|
|
|
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
|
+ logger.Debug("webapi 404", "method", r.Method, "path", r.URL.Path)
|
|
|
+ SendError(w, r, http.StatusNotFound, "not found")
|
|
|
+ })
|
|
|
|
|
|
servers = append(servers, &http.Server{
|
|
|
Addr: l,
|
|
|
- Handler: RequestLogger(logger, mux),
|
|
|
+ Handler: RequestLogger(logger, corsHandler.Handler(mux)),
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -429,6 +339,7 @@ type Handler struct {
|
|
|
BuddyBroadcaster BuddyBroadcaster
|
|
|
BuddyService BuddyService
|
|
|
BOSListener config.ListenerGroup
|
|
|
+ AllowedOrigins []string
|
|
|
BuddyListManager *BuddyListManager
|
|
|
RecalcWarning func(ctx context.Context, instance *state.SessionInstance) error
|
|
|
LowerWarnLevel func(ctx context.Context, instance *state.SessionInstance)
|
|
|
@@ -454,3 +365,34 @@ func (h Handler) GetHelloWorldHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
}
|
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
|
}
|
|
|
+
|
|
|
+// corsOptions maps the configured origin allowlist onto rs/cors.
|
|
|
+func corsOptions(logger *slog.Logger, allowedOrigins []string) cors.Options {
|
|
|
+ opts := cors.Options{
|
|
|
+ AllowCredentials: false,
|
|
|
+ AllowedMethods: []string{http.MethodGet, http.MethodPost},
|
|
|
+ AllowedHeaders: []string{"Content-Type", "Authorization"},
|
|
|
+ MaxAge: 3600,
|
|
|
+ }
|
|
|
+
|
|
|
+ origins := make([]string, 0, len(allowedOrigins))
|
|
|
+ for _, origin := range allowedOrigins {
|
|
|
+ origin = strings.TrimSpace(origin)
|
|
|
+ if origin == "" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if origin == "*" {
|
|
|
+ logger.Info("WEBAPI_ALLOWED_ORIGINS is *, allowing browser calls from any origin")
|
|
|
+ return allowAnyOrigin(opts)
|
|
|
+ }
|
|
|
+ origins = append(origins, origin)
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(origins) == 0 {
|
|
|
+ logger.Info("WEBAPI_ALLOWED_ORIGINS is not set, allowing browser calls from any origin")
|
|
|
+ return allowAnyOrigin(opts)
|
|
|
+ }
|
|
|
+
|
|
|
+ opts.AllowedOrigins = origins
|
|
|
+ return opts
|
|
|
+}
|