server.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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. // Create authentication middleware
  16. authMiddleware := middleware.NewAuthMiddleware(apiKeyValidator, logger)
  17. // Create handlers
  18. authHandler := &handlers.AuthHandler{
  19. AuthService: handler.AuthService,
  20. CookieBaker: handler.CookieBaker,
  21. UserManager: handler.TOCConfigStore,
  22. Logger: logger,
  23. }
  24. sessionHandler := &handlers.SessionHandler{
  25. SessionManager: sessionManager,
  26. OSCARSessionManager: handler.SessionRetriever.(handlers.SessionManager),
  27. OSCARAuthService: handler.AuthService,
  28. BuddyListService: nil,
  29. BuddyListRegistry: handler.BuddyListRegistry,
  30. BuddyBroadcaster: handler.BuddyBroadcaster,
  31. FeedbagRetriever: handler.FeedbagRetriever,
  32. OSCARBuddyService: handler.BuddyService,
  33. BuddyListManager: handler.BuddyListManager.(*handlers.BuddyListManager),
  34. Logger: logger,
  35. OServiceService: handler.OServiceService,
  36. RecalcWarning: handler.RecalcWarning,
  37. LowerWarnLevel: handler.LowerWarnLevel,
  38. ChatSessionManager: handler.ChatSessionManager,
  39. }
  40. eventsHandler := &handlers.EventsHandler{
  41. SessionManager: sessionManager,
  42. Logger: logger,
  43. }
  44. presenceHandler := &handlers.PresenceHandler{
  45. SessionManager: sessionManager,
  46. SessionRetriever: handler.SessionRetriever,
  47. FeedbagRetriever: handler.FeedbagRetriever,
  48. BuddyBroadcaster: handler.BuddyBroadcaster,
  49. ProfileManager: handler.ProfileManager,
  50. RelationshipFetcher: handler.RelationshipFetcher,
  51. Logger: logger,
  52. }
  53. buddyListHandler := handlers.NewBuddyListHandler(
  54. sessionManager,
  55. handler.BuddyListManager.(*handlers.BuddyListManager),
  56. logger,
  57. handler.FeedbagService,
  58. )
  59. // Phase 2: Messaging handler
  60. messagingHandler := &handlers.MessagingHandler{
  61. SessionManager: sessionManager,
  62. MessageRelayer: handler.MessageRelayer,
  63. OfflineMessageManager: handler.OfflineMessageManager,
  64. SessionRetriever: handler.SessionRetriever,
  65. RelationshipFetcher: handler.RelationshipFetcher,
  66. Logger: logger,
  67. }
  68. // Phase 3: Preference handler
  69. preferenceHandler := &handlers.PreferenceHandler{
  70. SessionManager: sessionManager,
  71. PreferenceManager: handler.PreferenceManager,
  72. PermitDenyManager: handler.PermitDenyManager,
  73. Logger: logger,
  74. }
  75. // Phase 4: OSCAR Bridge handler
  76. oscarBridgeHandler := &handlers.OSCARBridgeHandler{
  77. SessionManager: sessionManager,
  78. OSCARAuthService: handler.AuthService,
  79. CookieBaker: handler.CookieBaker,
  80. BridgeStore: handler.OSCARBridgeStore,
  81. Config: handler.OSCARConfig,
  82. Logger: logger,
  83. }
  84. // Phase 5: Chat handler
  85. chatHandler := &handlers.ChatHandler{
  86. SessionManager: sessionManager,
  87. ChatManager: handler.ChatManager,
  88. Logger: logger,
  89. }
  90. for _, l := range listeners {
  91. mux := http.NewServeMux()
  92. // Exact root only. Pattern "GET /" matches every GET path in Go 1.22+ (prefix /), which
  93. // would steal /getAggregated and other lifestream URLs before stubs/404.
  94. mux.HandleFunc("GET /{$}", handler.GetHelloWorldHandler)
  95. // Authentication endpoint (public - no API key required for user login)
  96. // Using pattern with explicit method for Go 1.22+ routing
  97. mux.HandleFunc("POST /auth/clientLogin", func(w http.ResponseWriter, r *http.Request) {
  98. // Set CORS headers for public endpoint
  99. w.Header().Set("Access-Control-Allow-Origin", "*")
  100. w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
  101. w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  102. authHandler.ClientLogin(w, r)
  103. })
  104. // Handle OPTIONS for CORS preflight
  105. mux.HandleFunc("OPTIONS /auth/clientLogin", func(w http.ResponseWriter, r *http.Request) {
  106. w.Header().Set("Access-Control-Allow-Origin", "*")
  107. w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
  108. w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  109. w.WriteHeader(http.StatusNoContent)
  110. })
  111. mux.HandleFunc("GET /auth/getToken", func(w http.ResponseWriter, r *http.Request) {
  112. w.Header().Set("Access-Control-Allow-Origin", "*")
  113. w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
  114. w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  115. authHandler.GetToken(w, r)
  116. })
  117. mux.HandleFunc("OPTIONS /auth/getToken", func(w http.ResponseWriter, r *http.Request) {
  118. w.Header().Set("Access-Control-Allow-Origin", "*")
  119. w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
  120. w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  121. w.WriteHeader(http.StatusNoContent)
  122. })
  123. mux.HandleFunc("GET /_cqr/login/login.psp", authHandler.LoginPSP)
  124. mux.HandleFunc("POST /_cqr/login/login.psp", authHandler.LoginPSP)
  125. // Authenticated Web AIM API endpoints
  126. // SessionInstance management - supports multiple auth methods (k, a, ts+sig_sha256)
  127. mux.Handle("GET /aim/startSession", authMiddleware.AuthenticateFlexible(
  128. authMiddleware.CORSMiddleware(
  129. http.HandlerFunc(sessionHandler.StartSession))))
  130. // End session - uses aimsid for auth, no k required
  131. mux.Handle("GET /aim/endSession", authMiddleware.AuthenticateFlexible(
  132. authMiddleware.CORSMiddleware(
  133. http.HandlerFunc(sessionHandler.EndSession))))
  134. // Event fetching - uses aimsid for auth, no k required
  135. mux.Handle("GET /aim/fetchEvents", authMiddleware.AuthenticateFlexible(
  136. authMiddleware.CORSMiddleware(
  137. http.HandlerFunc(eventsHandler.FetchEvents))))
  138. // Add temp buddy - uses aimsid for auth
  139. mux.Handle("GET /aim/addTempBuddy", authMiddleware.AuthenticateFlexible(
  140. authMiddleware.CORSMiddleware(
  141. buddyListHandler.SessionMiddleware(buddyListHandler.AddTempBuddy))))
  142. mux.Handle("GET /aim/removeTempBuddy", authMiddleware.AuthenticateFlexible(
  143. authMiddleware.CORSMiddleware(
  144. buddyListHandler.SessionMiddleware(buddyListHandler.RemoveTempBuddy))))
  145. aimStub := &handlers.AimStubHandler{Logger: logger}
  146. aimRoute := func(h http.HandlerFunc) http.Handler {
  147. return authMiddleware.AuthenticateFlexible(
  148. authMiddleware.CORSMiddleware(http.HandlerFunc(h)))
  149. }
  150. mux.Handle("GET /aim/setForwardDomain", aimRoute(aimStub.SetForwardDomain))
  151. mux.Handle("GET /aim/getData", aimRoute(aimStub.GetData))
  152. conversationStub := &handlers.ConversationStubHandler{
  153. SessionManager: sessionManager,
  154. Logger: logger,
  155. }
  156. mux.Handle("GET /conversation/update", aimRoute(conversationStub.Update))
  157. mux.Handle("GET /conversation/close", aimRoute(conversationStub.Close))
  158. mux.Handle("GET /imlog/markRead", aimRoute(conversationStub.MarkRead))
  159. mux.Handle("GET /imlog/fetchStoredIMs", aimRoute(conversationStub.FetchStoredIMs))
  160. // Presence and buddy list
  161. // GetPresence supports aimsid-based auth, so we use flexible auth
  162. mux.Handle("GET /presence/get", authMiddleware.AuthenticateFlexible(
  163. authMiddleware.CORSMiddleware(
  164. http.HandlerFunc(presenceHandler.GetPresence))))
  165. mux.Handle("/buddylist/", authMiddleware.AuthenticateFlexible(
  166. authMiddleware.CORSMiddleware(buddyListHandler)))
  167. // Phase 2: Messaging endpoints
  168. // sendIM supports aimsid-based auth, so we use flexible auth.
  169. // The Web AIM client POSTs the message body (non-IE browsers); IE uses GET.
  170. sendIMHandler := authMiddleware.AuthenticateFlexible(
  171. authMiddleware.CORSMiddleware(
  172. http.HandlerFunc(messagingHandler.SendIM)))
  173. mux.Handle("GET /im/sendIM", sendIMHandler)
  174. mux.Handle("POST /im/sendIM", sendIMHandler)
  175. mux.Handle("GET /im/setTyping", authMiddleware.Authenticate(
  176. authMiddleware.CORSMiddleware(
  177. http.HandlerFunc(messagingHandler.SetTyping))))
  178. // Phase 2: Presence management endpoints
  179. // SetState only requires aimsid, no k parameter needed
  180. mux.Handle("GET /presence/setState", authMiddleware.AuthenticateFlexible(
  181. authMiddleware.CORSMiddleware(
  182. http.HandlerFunc(presenceHandler.SetState))))
  183. // These presence endpoints support aimsid-based auth where k is not required
  184. mux.Handle("GET /presence/setStatus", authMiddleware.AuthenticateFlexible(
  185. authMiddleware.CORSMiddleware(
  186. http.HandlerFunc(presenceHandler.SetStatus))))
  187. mux.Handle("GET /presence/setProfile", authMiddleware.AuthenticateFlexible(
  188. authMiddleware.CORSMiddleware(
  189. http.HandlerFunc(presenceHandler.SetProfile))))
  190. mux.Handle("GET /presence/getProfile", authMiddleware.AuthenticateFlexible(
  191. authMiddleware.CORSMiddleware(
  192. http.HandlerFunc(presenceHandler.GetProfile))))
  193. // Phase 2: Presence icon endpoint (no auth required)
  194. mux.HandleFunc("GET /presence/icon", presenceHandler.Icon)
  195. // Phase 3: Preference management endpoints
  196. // These endpoints support aimsid-based auth, so we use a flexible auth approach
  197. mux.Handle("GET /preference/set", authMiddleware.AuthenticateFlexible(
  198. authMiddleware.CORSMiddleware(
  199. http.HandlerFunc(preferenceHandler.SetPreferences))))
  200. mux.Handle("GET /preference/get", authMiddleware.AuthenticateFlexible(
  201. authMiddleware.CORSMiddleware(
  202. http.HandlerFunc(preferenceHandler.GetPreferences))))
  203. mux.Handle("GET /preference/setPermitDeny", authMiddleware.AuthenticateFlexible(
  204. authMiddleware.CORSMiddleware(
  205. http.HandlerFunc(preferenceHandler.SetPermitDeny))))
  206. mux.Handle("GET /preference/getPermitDeny", authMiddleware.AuthenticateFlexible(
  207. authMiddleware.CORSMiddleware(
  208. http.HandlerFunc(preferenceHandler.GetPermitDeny))))
  209. // Phase 4: Advanced Features
  210. // OSCAR Bridge endpoint
  211. mux.Handle("GET /aim/startOSCARSession", authMiddleware.Authenticate(
  212. authMiddleware.CORSMiddleware(
  213. http.HandlerFunc(oscarBridgeHandler.StartOSCARSession))))
  214. // Expressions endpoint (for buddy icons, etc.)
  215. expressionsHandler := handlers.NewExpressionsHandler(logger)
  216. mux.Handle("GET /expressions/get", authMiddleware.AuthenticateFlexible(
  217. authMiddleware.CORSMiddleware(
  218. http.HandlerFunc(expressionsHandler.Get))))
  219. // Web AIM calls lifestream/* on the API host (e.g. /lifestream/getUserDetails).
  220. lifestreamStub := &handlers.UserInfoStubHandler{Logger: logger}
  221. lifestreamRoute := func(h http.HandlerFunc) http.Handler {
  222. return authMiddleware.AuthenticateFlexible(
  223. authMiddleware.CORSMiddleware(http.HandlerFunc(h)))
  224. }
  225. mux.Handle("GET /lifestream/getUserDetails", lifestreamRoute(lifestreamStub.GetUserDetails))
  226. mux.Handle("GET /lifestream/getLocationsFollowing", lifestreamRoute(lifestreamStub.GetLocationsFollowing))
  227. for _, p := range []string{
  228. "getAggregated",
  229. "getNotifications",
  230. "getSingle",
  231. "getNotificationFilter",
  232. "heyGetNotifications",
  233. "heyMarkNotifications",
  234. "deleteNotification",
  235. "commonsFollow",
  236. "commonsUnfollow",
  237. "tdAddService",
  238. "tdRemoveService",
  239. "setUserPreference",
  240. "getActivity",
  241. "addComment",
  242. "deleteComment",
  243. "deleteActivity",
  244. "addLike",
  245. "deleteLike",
  246. "setNotificationFilter",
  247. "heyTakeAction",
  248. } {
  249. mux.Handle("GET /lifestream/"+p, lifestreamRoute(lifestreamStub.EmptyOK))
  250. }
  251. // Phase 5: Chat room endpoints
  252. // All chat endpoints use aimsid for authentication
  253. mux.Handle("GET /chat/createAndJoinChat", authMiddleware.AuthenticateFlexible(
  254. authMiddleware.CORSMiddleware(
  255. http.HandlerFunc(chatHandler.CreateAndJoinChat))))
  256. mux.Handle("GET /chat/sendMessage", authMiddleware.AuthenticateFlexible(
  257. authMiddleware.CORSMiddleware(
  258. http.HandlerFunc(chatHandler.SendMessage))))
  259. mux.Handle("GET /chat/setTyping", authMiddleware.AuthenticateFlexible(
  260. authMiddleware.CORSMiddleware(
  261. http.HandlerFunc(chatHandler.SetTyping))))
  262. mux.Handle("GET /chat/leaveChat", authMiddleware.AuthenticateFlexible(
  263. authMiddleware.CORSMiddleware(
  264. http.HandlerFunc(chatHandler.LeaveChat))))
  265. // Unmatched paths (pattern "/" matches anything not covered by routes above).
  266. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  267. logger.Debug("webapi 404", "method", r.Method, "path", r.URL.Path)
  268. handlers.SendError(w, http.StatusNotFound, "not found")
  269. })
  270. servers = append(servers, &http.Server{
  271. Addr: l,
  272. Handler: middleware.RequestLogger(logger, mux),
  273. })
  274. }
  275. return &Server{
  276. servers: servers,
  277. logger: logger,
  278. }
  279. }
  280. // Server hosts an HTTP endpoint capable of handling AIM-style Kerberos
  281. // authentication. The messages are structured as SNACs transmitted over HTTP.
  282. type Server struct {
  283. servers []*http.Server
  284. logger *slog.Logger
  285. }
  286. func (s *Server) ListenAndServe() error {
  287. if len(s.servers) == 0 {
  288. s.logger.Debug("no webapi listeners defined")
  289. return nil
  290. }
  291. ctx, cancel := context.WithCancel(context.Background())
  292. defer cancel()
  293. g, _ := errgroup.WithContext(ctx)
  294. for _, server := range s.servers {
  295. g.Go(func() error {
  296. s.logger.Info("starting server", "addr", server.Addr)
  297. if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
  298. cancel()
  299. return fmt.Errorf("unable to start webapi server: %w", err)
  300. }
  301. return nil
  302. })
  303. }
  304. return g.Wait()
  305. }
  306. func (s *Server) Shutdown(ctx context.Context) error {
  307. if len(s.servers) > 0 {
  308. for _, srv := range s.servers {
  309. _ = srv.Shutdown(ctx)
  310. }
  311. s.logger.Info("shutdown complete")
  312. }
  313. return nil
  314. }