server.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. package webapi
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "net/http"
  8. "golang.org/x/sync/errgroup"
  9. "github.com/mk6i/open-oscar-server/server/webapi/handlers"
  10. "github.com/mk6i/open-oscar-server/server/webapi/middleware"
  11. "github.com/mk6i/open-oscar-server/state"
  12. )
  13. func NewServer(listeners []string, logger *slog.Logger, handler Handler, apiKeyValidator middleware.APIKeyValidator, sessionManager *state.WebAPISessionManager) *Server {
  14. servers := make([]*http.Server, 0, len(listeners))
  15. authMiddleware := middleware.NewAuthMiddleware(apiKeyValidator, logger)
  16. authHandler := &handlers.AuthHandler{
  17. AuthService: handler.AuthService,
  18. CookieBaker: handler.CookieBaker,
  19. Logger: logger,
  20. }
  21. sessionHandler := &handlers.SessionHandler{
  22. SessionManager: sessionManager,
  23. OSCARSessionManager: handler.SessionRetriever.(handlers.SessionManager),
  24. OSCARAuthService: handler.AuthService,
  25. BuddyListRegistry: handler.BuddyListRegistry,
  26. BuddyBroadcaster: handler.BuddyBroadcaster,
  27. FeedbagService: handler.FeedbagService,
  28. BuddyListManager: handler.BuddyListManager.(*handlers.BuddyListManager),
  29. Logger: logger,
  30. OServiceService: handler.OServiceService,
  31. RecalcWarning: handler.RecalcWarning,
  32. LowerWarnLevel: handler.LowerWarnLevel,
  33. ChatSessionManager: handler.ChatSessionManager,
  34. }
  35. eventsHandler := &handlers.EventsHandler{
  36. SessionManager: sessionManager,
  37. Logger: logger,
  38. }
  39. presenceHandler := &handlers.PresenceHandler{
  40. SessionManager: sessionManager,
  41. FeedbagService: handler.FeedbagService,
  42. BuddyBroadcaster: handler.BuddyBroadcaster,
  43. LocateService: handler.LocateService,
  44. Logger: logger,
  45. }
  46. buddyListHandler := &handlers.BuddyListHandler{
  47. BuddyListManager: handler.BuddyListManager.(*handlers.BuddyListManager),
  48. Logger: logger,
  49. FeedbagService: handler.FeedbagService,
  50. }
  51. messagingHandler := &handlers.MessagingHandler{
  52. SessionManager: sessionManager,
  53. ICBMService: handler.ICBMService,
  54. Logger: logger,
  55. }
  56. preferenceHandler := &handlers.PreferenceHandler{
  57. SessionManager: sessionManager,
  58. FeedbagService: handler.FeedbagService,
  59. Logger: logger,
  60. }
  61. oscarBridgeHandler := &handlers.OSCARBridgeHandler{
  62. SessionManager: sessionManager,
  63. OSCARAuthService: handler.AuthService,
  64. CookieBaker: handler.CookieBaker,
  65. Config: handler.OSCARConfig,
  66. Logger: logger,
  67. }
  68. for _, l := range listeners {
  69. mux := http.NewServeMux()
  70. // Exact root only. Pattern "GET /" matches every GET path in Go 1.22+ (prefix /), which
  71. // would steal /getAggregated and other lifestream URLs before stubs/404.
  72. mux.HandleFunc("GET /{$}", handler.GetHelloWorldHandler)
  73. // Authentication endpoint (public - no API key required for user login)
  74. // Using pattern with explicit method for Go 1.22+ routing
  75. mux.HandleFunc("POST /auth/clientLogin", func(w http.ResponseWriter, r *http.Request) {
  76. // Set CORS headers for public endpoint
  77. w.Header().Set("Access-Control-Allow-Origin", "*")
  78. w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
  79. w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  80. authHandler.ClientLogin(w, r)
  81. })
  82. // Handle OPTIONS for CORS preflight
  83. mux.HandleFunc("OPTIONS /auth/clientLogin", func(w http.ResponseWriter, r *http.Request) {
  84. w.Header().Set("Access-Control-Allow-Origin", "*")
  85. w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
  86. w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  87. w.WriteHeader(http.StatusNoContent)
  88. })
  89. mux.HandleFunc("GET /auth/getToken", func(w http.ResponseWriter, r *http.Request) {
  90. w.Header().Set("Access-Control-Allow-Origin", "*")
  91. w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
  92. w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  93. authHandler.GetToken(w, r)
  94. })
  95. mux.HandleFunc("OPTIONS /auth/getToken", func(w http.ResponseWriter, r *http.Request) {
  96. w.Header().Set("Access-Control-Allow-Origin", "*")
  97. w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
  98. w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  99. w.WriteHeader(http.StatusNoContent)
  100. })
  101. // Web AIM navigates the browser here on File > Logout; clear SSO state
  102. // and redirect to the login screen.
  103. mux.HandleFunc("GET /auth/logout", authHandler.Logout)
  104. mux.HandleFunc("GET /_cqr/login/login.psp", authHandler.LoginPSP)
  105. mux.HandleFunc("POST /_cqr/login/login.psp", authHandler.LoginPSP)
  106. // Authenticated Web AIM API endpoints
  107. // SessionInstance management - supports multiple auth methods (k, a, ts+sig_sha256)
  108. mux.Handle("GET /aim/startSession", authMiddleware.AuthenticateFlexible(
  109. authMiddleware.CORSMiddleware(
  110. http.HandlerFunc(sessionHandler.StartSession))))
  111. // End session - uses aimsid for auth, no k required
  112. mux.Handle("GET /aim/endSession", authMiddleware.AuthenticateFlexible(
  113. authMiddleware.CORSMiddleware(
  114. authMiddleware.RequireSession(sessionManager, sessionHandler.EndSession))))
  115. // Event fetching - uses aimsid for auth, no k required
  116. mux.Handle("GET /aim/fetchEvents", authMiddleware.AuthenticateFlexible(
  117. authMiddleware.CORSMiddleware(
  118. authMiddleware.RequireSession(sessionManager, eventsHandler.FetchEvents))))
  119. // Add temp buddy - uses aimsid for auth
  120. mux.Handle("GET /aim/addTempBuddy", authMiddleware.AuthenticateFlexible(
  121. authMiddleware.CORSMiddleware(
  122. authMiddleware.RequireSession(sessionManager, buddyListHandler.AddTempBuddy))))
  123. mux.Handle("GET /aim/removeTempBuddy", authMiddleware.AuthenticateFlexible(
  124. authMiddleware.CORSMiddleware(
  125. authMiddleware.RequireSession(sessionManager, buddyListHandler.RemoveTempBuddy))))
  126. aimStub := &handlers.AimStubHandler{Logger: logger}
  127. aimRoute := func(h http.HandlerFunc) http.Handler {
  128. return authMiddleware.AuthenticateFlexible(
  129. authMiddleware.CORSMiddleware(http.HandlerFunc(h)))
  130. }
  131. mux.Handle("GET /aim/setForwardDomain", aimRoute(aimStub.SetForwardDomain))
  132. mux.Handle("GET /aim/getData", aimRoute(aimStub.GetData))
  133. conversationStub := &handlers.ConversationStubHandler{
  134. SessionManager: sessionManager,
  135. Logger: logger,
  136. }
  137. mux.Handle("GET /conversation/update", aimRoute(conversationStub.Update))
  138. mux.Handle("GET /conversation/close", aimRoute(conversationStub.Close))
  139. mux.Handle("GET /imlog/markRead", aimRoute(conversationStub.MarkRead))
  140. mux.Handle("GET /imlog/fetchStoredIMs", authMiddleware.AuthenticateFlexible(
  141. authMiddleware.CORSMiddleware(
  142. authMiddleware.RequireSession(sessionManager, conversationStub.FetchStoredIMs))))
  143. // Presence and buddy list
  144. // GetPresence supports aimsid-based auth, so we use flexible auth
  145. mux.Handle("GET /presence/get", authMiddleware.AuthenticateFlexible(
  146. authMiddleware.CORSMiddleware(
  147. authMiddleware.RequireSession(sessionManager, presenceHandler.GetPresence))))
  148. buddyListRoute := func(h func(http.ResponseWriter, *http.Request, *state.WebAPISession)) http.Handler {
  149. return authMiddleware.AuthenticateFlexible(
  150. authMiddleware.CORSMiddleware(
  151. authMiddleware.RequireSession(sessionManager, h)))
  152. }
  153. mux.Handle("GET /buddylist/addBuddy", buddyListRoute(buddyListHandler.AddBuddy))
  154. mux.Handle("GET /buddylist/addGroup", buddyListRoute(buddyListHandler.AddGroup))
  155. mux.Handle("GET /buddylist/removeBuddy", buddyListRoute(buddyListHandler.RemoveBuddy))
  156. mux.Handle("GET /buddylist/removeGroup", buddyListRoute(buddyListHandler.RemoveGroup))
  157. mux.Handle("GET /buddylist/renameGroup", buddyListRoute(buddyListHandler.RenameGroup))
  158. mux.Handle("GET /buddylist/moveBuddy", buddyListRoute(buddyListHandler.MoveBuddy))
  159. mux.Handle("GET /buddylist/setBuddyAttribute", buddyListRoute(buddyListHandler.SetBuddyAttribute))
  160. mux.Handle("GET /buddylist/setGroupAttribute", buddyListRoute(buddyListHandler.SetGroupAttribute))
  161. // sendIM supports aimsid-based auth, so we use flexible auth.
  162. // The Web AIM client POSTs the message body (non-IE browsers); IE uses GET.
  163. sendIMHandler := authMiddleware.AuthenticateFlexible(
  164. authMiddleware.CORSMiddleware(
  165. authMiddleware.RequireSession(sessionManager, messagingHandler.SendIM)))
  166. mux.Handle("GET /im/sendIM", sendIMHandler)
  167. mux.Handle("POST /im/sendIM", sendIMHandler)
  168. mux.Handle("GET /im/setTyping", authMiddleware.AuthenticateFlexible(
  169. authMiddleware.CORSMiddleware(
  170. authMiddleware.RequireSession(sessionManager, messagingHandler.SetTyping))))
  171. // SetState only requires aimsid, no k parameter needed
  172. mux.Handle("GET /presence/setState", authMiddleware.AuthenticateFlexible(
  173. authMiddleware.CORSMiddleware(
  174. authMiddleware.RequireSession(sessionManager, presenceHandler.SetState))))
  175. // These presence endpoints support aimsid-based auth where k is not required
  176. mux.Handle("GET /presence/setStatus", authMiddleware.AuthenticateFlexible(
  177. authMiddleware.CORSMiddleware(
  178. authMiddleware.RequireSession(sessionManager, presenceHandler.SetStatus))))
  179. mux.Handle("GET /presence/setProfile", authMiddleware.AuthenticateFlexible(
  180. authMiddleware.CORSMiddleware(
  181. authMiddleware.RequireSession(sessionManager, presenceHandler.SetProfile))))
  182. mux.Handle("GET /presence/getProfile", authMiddleware.AuthenticateFlexible(
  183. authMiddleware.CORSMiddleware(
  184. authMiddleware.RequireSession(sessionManager, presenceHandler.GetProfile))))
  185. mux.HandleFunc("GET /presence/icon", presenceHandler.Icon)
  186. // These endpoints support aimsid-based auth, so we use a flexible auth approach
  187. mux.Handle("GET /preference/set", authMiddleware.AuthenticateFlexible(
  188. authMiddleware.CORSMiddleware(
  189. authMiddleware.RequireSession(sessionManager, preferenceHandler.SetPreferences))))
  190. mux.Handle("GET /preference/get", authMiddleware.AuthenticateFlexible(
  191. authMiddleware.CORSMiddleware(
  192. authMiddleware.RequireSession(sessionManager, preferenceHandler.GetPreferences))))
  193. mux.Handle("GET /preference/setPermitDeny", authMiddleware.AuthenticateFlexible(
  194. authMiddleware.CORSMiddleware(
  195. authMiddleware.RequireSession(sessionManager, preferenceHandler.SetPermitDeny))))
  196. mux.Handle("GET /preference/getPermitDeny", authMiddleware.AuthenticateFlexible(
  197. authMiddleware.CORSMiddleware(
  198. authMiddleware.RequireSession(sessionManager, preferenceHandler.GetPermitDeny))))
  199. // OSCAR Bridge endpoint
  200. mux.Handle("GET /aim/startOSCARSession", authMiddleware.Authenticate(
  201. authMiddleware.CORSMiddleware(
  202. http.HandlerFunc(oscarBridgeHandler.StartOSCARSession))))
  203. // Expressions endpoint (for buddy icons, etc.)
  204. expressionsHandler := handlers.NewExpressionsHandler(logger)
  205. mux.Handle("GET /expressions/get", authMiddleware.AuthenticateFlexible(
  206. authMiddleware.CORSMiddleware(
  207. http.HandlerFunc(expressionsHandler.Get))))
  208. // Web AIM calls lifestream/* on the API host (e.g. /lifestream/getUserDetails).
  209. lifestreamStub := &handlers.UserInfoStubHandler{Logger: logger}
  210. lifestreamRoute := func(h http.HandlerFunc) http.Handler {
  211. return authMiddleware.AuthenticateFlexible(
  212. authMiddleware.CORSMiddleware(http.HandlerFunc(h)))
  213. }
  214. // getUserDetails returns a minimal AIM identity. Every other lifestream/*
  215. // method is an unimplemented social-feed feature; the subtree catch-all
  216. // acknowledges them with an empty 200 so the client doesn't error.
  217. mux.Handle("GET /lifestream/getUserDetails", lifestreamRoute(lifestreamStub.GetUserDetails))
  218. mux.Handle("GET /lifestream/", lifestreamRoute(lifestreamStub.EmptyOK))
  219. // Unmatched paths (pattern "/" matches anything not covered by routes above).
  220. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  221. logger.Debug("webapi 404", "method", r.Method, "path", r.URL.Path)
  222. handlers.SendError(w, http.StatusNotFound, "not found")
  223. })
  224. servers = append(servers, &http.Server{
  225. Addr: l,
  226. Handler: middleware.RequestLogger(logger, mux),
  227. })
  228. }
  229. return &Server{
  230. servers: servers,
  231. logger: logger,
  232. }
  233. }
  234. // Server hosts an HTTP endpoint capable of handling AIM-style Kerberos
  235. // authentication. The messages are structured as SNACs transmitted over HTTP.
  236. type Server struct {
  237. servers []*http.Server
  238. logger *slog.Logger
  239. }
  240. func (s *Server) ListenAndServe() error {
  241. if len(s.servers) == 0 {
  242. s.logger.Debug("no webapi listeners defined")
  243. return nil
  244. }
  245. ctx, cancel := context.WithCancel(context.Background())
  246. defer cancel()
  247. g, _ := errgroup.WithContext(ctx)
  248. for _, server := range s.servers {
  249. g.Go(func() error {
  250. s.logger.Info("starting server", "addr", server.Addr)
  251. if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
  252. cancel()
  253. return fmt.Errorf("unable to start webapi server: %w", err)
  254. }
  255. return nil
  256. })
  257. }
  258. return g.Wait()
  259. }
  260. func (s *Server) Shutdown(ctx context.Context) error {
  261. if len(s.servers) > 0 {
  262. for _, srv := range s.servers {
  263. _ = srv.Shutdown(ctx)
  264. }
  265. s.logger.Info("shutdown complete")
  266. }
  267. return nil
  268. }