server.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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. UserManager: handler.UserManager,
  20. TokenStore: handler.TokenStore,
  21. Logger: logger,
  22. DisableAuth: handler.OSCARConfig.IsAuthDisabled(),
  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. BuddyListManager: handler.BuddyListManager.(*handlers.BuddyListManager),
  32. TokenStore: handler.TokenStore,
  33. Logger: logger,
  34. }
  35. eventsHandler := &handlers.EventsHandler{
  36. SessionManager: sessionManager,
  37. Logger: logger,
  38. }
  39. presenceHandler := &handlers.PresenceHandler{
  40. SessionManager: sessionManager,
  41. SessionRetriever: handler.SessionRetriever,
  42. FeedbagRetriever: handler.FeedbagRetriever,
  43. BuddyBroadcaster: handler.BuddyBroadcaster,
  44. ProfileManager: handler.ProfileManager,
  45. RelationshipFetcher: handler.RelationshipFetcher,
  46. Logger: logger,
  47. }
  48. buddyListHandler := &handlers.BuddyListHandler{
  49. SessionManager: sessionManager,
  50. FeedbagManager: handler.FeedbagManager,
  51. Logger: logger,
  52. }
  53. // Phase 2: Messaging handler
  54. messagingHandler := &handlers.MessagingHandler{
  55. SessionManager: sessionManager,
  56. MessageRelayer: handler.MessageRelayer,
  57. OfflineMessageManager: handler.OfflineMessageManager,
  58. SessionRetriever: handler.SessionRetriever,
  59. RelationshipFetcher: handler.RelationshipFetcher,
  60. Logger: logger,
  61. }
  62. // Phase 3: Preference handler
  63. preferenceHandler := &handlers.PreferenceHandler{
  64. SessionManager: sessionManager,
  65. PreferenceManager: handler.PreferenceManager,
  66. PermitDenyManager: handler.PermitDenyManager,
  67. Logger: logger,
  68. }
  69. // Phase 4: OSCAR Bridge handler
  70. oscarBridgeHandler := &handlers.OSCARBridgeHandler{
  71. SessionManager: sessionManager,
  72. OSCARAuthService: handler.AuthService,
  73. CookieBaker: handler.CookieBaker,
  74. BridgeStore: handler.OSCARBridgeStore,
  75. Config: handler.OSCARConfig,
  76. Logger: logger,
  77. }
  78. // Phase 5: Chat handler
  79. chatHandler := &handlers.ChatHandler{
  80. SessionManager: sessionManager,
  81. ChatManager: handler.ChatManager,
  82. Logger: logger,
  83. }
  84. for _, l := range listeners {
  85. mux := http.NewServeMux()
  86. // Public endpoint (no auth required for hello world)
  87. mux.HandleFunc("GET /", handler.GetHelloWorldHandler)
  88. // Authentication endpoint (public - no API key required for user login)
  89. // Using pattern with explicit method for Go 1.22+ routing
  90. mux.HandleFunc("POST /auth/clientLogin", func(w http.ResponseWriter, r *http.Request) {
  91. // Set CORS headers for public endpoint
  92. w.Header().Set("Access-Control-Allow-Origin", "*")
  93. w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
  94. w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
  95. authHandler.ClientLogin(w, r)
  96. })
  97. // Handle OPTIONS for CORS preflight
  98. mux.HandleFunc("OPTIONS /auth/clientLogin", func(w http.ResponseWriter, r *http.Request) {
  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. w.WriteHeader(http.StatusNoContent)
  103. })
  104. // Authenticated Web AIM API endpoints
  105. // SessionInstance management - supports multiple auth methods (k, a, ts+sig_sha256)
  106. mux.Handle("GET /aim/startSession", authMiddleware.AuthenticateFlexible(
  107. authMiddleware.CORSMiddleware(
  108. http.HandlerFunc(sessionHandler.StartSession))))
  109. // End session - uses aimsid for auth, no k required
  110. mux.Handle("GET /aim/endSession", authMiddleware.AuthenticateFlexible(
  111. authMiddleware.CORSMiddleware(
  112. http.HandlerFunc(sessionHandler.EndSession))))
  113. // Event fetching - uses aimsid for auth, no k required
  114. mux.Handle("GET /aim/fetchEvents", authMiddleware.AuthenticateFlexible(
  115. authMiddleware.CORSMiddleware(
  116. http.HandlerFunc(eventsHandler.FetchEvents))))
  117. // Add temp buddy - uses aimsid for auth
  118. mux.Handle("GET /aim/addTempBuddy", authMiddleware.AuthenticateFlexible(
  119. authMiddleware.CORSMiddleware(
  120. http.HandlerFunc(buddyListHandler.AddTempBuddy))))
  121. // Presence and buddy list
  122. // GetPresence supports aimsid-based auth, so we use flexible auth
  123. mux.Handle("GET /presence/get", authMiddleware.AuthenticateFlexible(
  124. authMiddleware.CORSMiddleware(
  125. http.HandlerFunc(presenceHandler.GetPresence))))
  126. mux.Handle("GET /buddylist/addBuddy", authMiddleware.Authenticate(
  127. authMiddleware.CORSMiddleware(
  128. http.HandlerFunc(buddyListHandler.AddBuddy))))
  129. // Phase 2: Messaging endpoints
  130. // sendIM supports aimsid-based auth, so we use flexible auth
  131. mux.Handle("GET /im/sendIM", authMiddleware.AuthenticateFlexible(
  132. authMiddleware.CORSMiddleware(
  133. http.HandlerFunc(messagingHandler.SendIM))))
  134. mux.Handle("GET /im/setTyping", authMiddleware.Authenticate(
  135. authMiddleware.CORSMiddleware(
  136. http.HandlerFunc(messagingHandler.SetTyping))))
  137. // Phase 2: Presence management endpoints
  138. // SetState only requires aimsid, no k parameter needed
  139. mux.Handle("GET /presence/setState", authMiddleware.AuthenticateFlexible(
  140. authMiddleware.CORSMiddleware(
  141. http.HandlerFunc(presenceHandler.SetState))))
  142. // These presence endpoints support aimsid-based auth where k is not required
  143. mux.Handle("GET /presence/setStatus", authMiddleware.AuthenticateFlexible(
  144. authMiddleware.CORSMiddleware(
  145. http.HandlerFunc(presenceHandler.SetStatus))))
  146. mux.Handle("GET /presence/setProfile", authMiddleware.AuthenticateFlexible(
  147. authMiddleware.CORSMiddleware(
  148. http.HandlerFunc(presenceHandler.SetProfile))))
  149. mux.Handle("GET /presence/getProfile", authMiddleware.AuthenticateFlexible(
  150. authMiddleware.CORSMiddleware(
  151. http.HandlerFunc(presenceHandler.GetProfile))))
  152. // Phase 2: Presence icon endpoint (no auth required)
  153. mux.HandleFunc("GET /presence/icon", presenceHandler.Icon)
  154. // Phase 3: Preference management endpoints
  155. // These endpoints support aimsid-based auth, so we use a flexible auth approach
  156. mux.Handle("GET /preference/set", authMiddleware.AuthenticateFlexible(
  157. authMiddleware.CORSMiddleware(
  158. http.HandlerFunc(preferenceHandler.SetPreferences))))
  159. mux.Handle("GET /preference/get", authMiddleware.AuthenticateFlexible(
  160. authMiddleware.CORSMiddleware(
  161. http.HandlerFunc(preferenceHandler.GetPreferences))))
  162. mux.Handle("GET /preference/setPermitDeny", authMiddleware.AuthenticateFlexible(
  163. authMiddleware.CORSMiddleware(
  164. http.HandlerFunc(preferenceHandler.SetPermitDeny))))
  165. mux.Handle("GET /preference/getPermitDeny", authMiddleware.AuthenticateFlexible(
  166. authMiddleware.CORSMiddleware(
  167. http.HandlerFunc(preferenceHandler.GetPermitDeny))))
  168. // Phase 4: Advanced Features
  169. // OSCAR Bridge endpoint
  170. mux.Handle("GET /aim/startOSCARSession", authMiddleware.Authenticate(
  171. authMiddleware.CORSMiddleware(
  172. http.HandlerFunc(oscarBridgeHandler.StartOSCARSession))))
  173. // Expressions endpoint (for buddy icons, etc.)
  174. expressionsHandler := handlers.NewExpressionsHandler(logger)
  175. mux.Handle("GET /expressions/get", authMiddleware.AuthenticateFlexible(
  176. authMiddleware.CORSMiddleware(
  177. http.HandlerFunc(expressionsHandler.Get))))
  178. // Phase 5: Chat room endpoints
  179. // All chat endpoints use aimsid for authentication
  180. mux.Handle("GET /chat/createAndJoinChat", authMiddleware.AuthenticateFlexible(
  181. authMiddleware.CORSMiddleware(
  182. http.HandlerFunc(chatHandler.CreateAndJoinChat))))
  183. mux.Handle("GET /chat/sendMessage", authMiddleware.AuthenticateFlexible(
  184. authMiddleware.CORSMiddleware(
  185. http.HandlerFunc(chatHandler.SendMessage))))
  186. mux.Handle("GET /chat/setTyping", authMiddleware.AuthenticateFlexible(
  187. authMiddleware.CORSMiddleware(
  188. http.HandlerFunc(chatHandler.SetTyping))))
  189. mux.Handle("GET /chat/leaveChat", authMiddleware.AuthenticateFlexible(
  190. authMiddleware.CORSMiddleware(
  191. http.HandlerFunc(chatHandler.LeaveChat))))
  192. servers = append(servers, &http.Server{
  193. Addr: l,
  194. Handler: mux,
  195. })
  196. }
  197. return &Server{
  198. servers: servers,
  199. logger: logger,
  200. }
  201. }
  202. // Server hosts an HTTP endpoint capable of handling AIM-style Kerberos
  203. // authentication. The messages are structured as SNACs transmitted over HTTP.
  204. type Server struct {
  205. servers []*http.Server
  206. logger *slog.Logger
  207. }
  208. func (s *Server) ListenAndServe() error {
  209. if len(s.servers) == 0 {
  210. s.logger.Debug("no webapi listeners defined")
  211. return nil
  212. }
  213. ctx, cancel := context.WithCancel(context.Background())
  214. defer cancel()
  215. g, ctx := errgroup.WithContext(ctx)
  216. for _, server := range s.servers {
  217. g.Go(func() error {
  218. s.logger.Info("starting server", "addr", server.Addr)
  219. if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
  220. cancel()
  221. return fmt.Errorf("unable to start webapi server: %w", err)
  222. }
  223. return nil
  224. })
  225. }
  226. return g.Wait()
  227. }
  228. func (s *Server) Shutdown(ctx context.Context) error {
  229. if len(s.servers) > 0 {
  230. for _, srv := range s.servers {
  231. _ = srv.Shutdown(ctx)
  232. }
  233. s.logger.Info("shutdown complete")
  234. }
  235. return nil
  236. }