server.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. package webapi
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "net/http"
  9. "strings"
  10. "time"
  11. "github.com/rs/cors"
  12. "golang.org/x/sync/errgroup"
  13. "github.com/mk6i/open-oscar-server/config"
  14. "github.com/mk6i/open-oscar-server/state"
  15. "github.com/mk6i/open-oscar-server/wire"
  16. )
  17. // allowAnyOrigin allows every origin through AllowOriginFunc, which echoes the
  18. // request origin back. Leaving AllowedOrigins empty would also allow everything,
  19. // but the library answers that with a literal "*".
  20. func allowAnyOrigin(opts cors.Options) cors.Options {
  21. opts.AllowOriginFunc = func(string) bool { return true }
  22. return opts
  23. }
  24. func NewServer(listeners []string, logger *slog.Logger, handler Handler, sessionManager *SessionManager) *Server {
  25. servers := make([]*http.Server, 0, len(listeners))
  26. authMiddleware := NewAuthMiddleware(logger)
  27. rateLimiter := NewRateLimitMiddleware(handler.SNACRateLimits, logger)
  28. authHandler := &AuthHandler{
  29. AuthService: handler.AuthService,
  30. Logger: logger,
  31. }
  32. aimHandler := &AimHandler{
  33. SessionManager: sessionManager,
  34. AuthService: handler.AuthService,
  35. FeedbagService: handler.FeedbagService,
  36. ICBMService: handler.ICBMService,
  37. LocateService: handler.LocateService,
  38. OServiceService: handler.OServiceService,
  39. BuddyListManager: handler.BuddyListManager,
  40. BuddyService: handler.BuddyService,
  41. IconSource: handler.IconSource,
  42. BOSListener: handler.BOSListener,
  43. SNACRateLimits: handler.SNACRateLimits,
  44. Logger: logger,
  45. }
  46. presenceHandler := &PresenceHandler{
  47. SessionManager: sessionManager,
  48. FeedbagService: handler.FeedbagService,
  49. BuddyBroadcaster: handler.BuddyBroadcaster,
  50. LocateService: handler.LocateService,
  51. IconSource: handler.IconSource,
  52. Logger: logger,
  53. }
  54. buddyListHandler := &BuddyListHandler{
  55. BuddyListManager: handler.BuddyListManager,
  56. Logger: logger,
  57. FeedbagService: handler.FeedbagService,
  58. }
  59. messagingHandler := &MessagingHandler{
  60. ICBMService: handler.ICBMService,
  61. LocateService: handler.LocateService,
  62. FeedbagService: handler.FeedbagService,
  63. Logger: logger,
  64. }
  65. preferenceHandler := &PreferenceHandler{
  66. SessionManager: sessionManager,
  67. FeedbagService: handler.FeedbagService,
  68. Logger: logger,
  69. }
  70. memberDirHandler := &MemberDirHandler{
  71. DirSearchService: handler.DirSearchService,
  72. LocateService: handler.LocateService,
  73. Logger: logger,
  74. }
  75. expressionsHandler := NewExpressionsHandler(
  76. handler.IconSource, handler.BARTService, handler.FeedbagService, logger)
  77. crossDomainHandler := &CrossDomainPolicyHandler{Logger: logger}
  78. conversationStub := &ConversationStubHandler{Logger: logger}
  79. lifestreamStub := &UserInfoStubHandler{Logger: logger}
  80. serviceStub := &ServiceStubHandler{Logger: logger}
  81. oscarRoute := func(foodGroup uint16, subGroup uint16, h SessionHandlerFunc) http.Handler {
  82. return authMiddleware.RequireSession(sessionManager,
  83. rateLimiter.OSCAR(foodGroup, subGroup)(h))
  84. }
  85. sessionRoute := func(h SessionHandlerFunc) http.Handler {
  86. return authMiddleware.RequireSession(sessionManager, h)
  87. }
  88. loginPSP := http.HandlerFunc(authHandler.LoginPSP)
  89. startSession := http.HandlerFunc(aimHandler.StartSession)
  90. sendIM := oscarRoute(wire.ICBM, wire.ICBMChannelMsgToHost, messagingHandler.SendIM)
  91. memberDirUpdate := oscarRoute(wire.Locate, wire.LocateSetDirInfo, memberDirHandler.Update)
  92. corsHandler := cors.New(corsOptions(logger, handler.AllowedOrigins))
  93. shutdownCtx, shutdownCancel := context.WithCancel(context.Background())
  94. for _, l := range listeners {
  95. mux := http.NewServeMux()
  96. mux.HandleFunc("GET /{$}", handler.GetHelloWorldHandler)
  97. mux.Handle("GET /crossdomain.xml", crossDomainHandler)
  98. mux.HandleFunc("POST /auth/clientLogin", authHandler.ClientLogin)
  99. mux.HandleFunc("GET /auth/getToken", authHandler.GetToken)
  100. mux.HandleFunc("GET /auth/getInfo", authHandler.GetInfo)
  101. mux.HandleFunc("POST /auth/getInfo", authHandler.GetInfo)
  102. mux.HandleFunc("GET /auth/logout", authHandler.Logout)
  103. mux.Handle("GET /_cqr/login/login.psp", loginPSP)
  104. mux.Handle("POST /_cqr/login/login.psp", loginPSP)
  105. mux.Handle("GET /aim/startSession", startSession)
  106. mux.Handle("POST /aim/startSession", startSession)
  107. mux.Handle("GET /aim/endSession", sessionRoute(aimHandler.EndSession))
  108. mux.Handle("GET /aim/fetchEvents", sessionRoute(aimHandler.FetchEvents))
  109. mux.HandleFunc("GET /aim/startOSCARSession", aimHandler.StartOSCARSession)
  110. mux.Handle("GET /presence/get", oscarRoute(wire.Feedbag, wire.FeedbagQuery, presenceHandler.GetPresence))
  111. mux.Handle("GET /presence/setState", oscarRoute(wire.OService, wire.OServiceSetUserInfoFields, presenceHandler.SetState))
  112. mux.Handle("GET /presence/setStatus", oscarRoute(wire.OService, wire.OServiceSetUserInfoFields, presenceHandler.SetStatus))
  113. mux.Handle("GET /presence/getProfile", oscarRoute(wire.Locate, wire.LocateUserInfoQuery, presenceHandler.GetProfile))
  114. mux.Handle("GET /presence/setProfile", oscarRoute(wire.Locate, wire.LocateSetInfo, presenceHandler.SetProfile))
  115. mux.HandleFunc("GET /presence/icon", presenceHandler.Icon)
  116. mux.Handle("GET /buddylist/addBuddy", oscarRoute(wire.Feedbag, wire.FeedbagInsertItem, buddyListHandler.AddBuddy))
  117. mux.Handle("GET /buddylist/addGroup", oscarRoute(wire.Feedbag, wire.FeedbagInsertItem, buddyListHandler.AddGroup))
  118. mux.Handle("GET /buddylist/removeBuddy", oscarRoute(wire.Feedbag, wire.FeedbagDeleteItem, buddyListHandler.RemoveBuddy))
  119. mux.Handle("GET /buddylist/removeGroup", oscarRoute(wire.Feedbag, wire.FeedbagDeleteItem, buddyListHandler.RemoveGroup))
  120. mux.Handle("GET /buddylist/moveBuddy", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, buddyListHandler.MoveBuddy))
  121. mux.Handle("GET /buddylist/renameGroup", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, buddyListHandler.RenameGroup))
  122. mux.Handle("GET /buddylist/setBuddyAttribute", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, buddyListHandler.SetBuddyAttribute))
  123. mux.Handle("GET /buddylist/setGroupAttribute", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, buddyListHandler.SetGroupAttribute))
  124. mux.Handle("GET /aim/addTempBuddy", oscarRoute(wire.Buddy, wire.BuddyAddTempBuddies, aimHandler.AddTempBuddy))
  125. mux.Handle("GET /aim/removeTempBuddy", oscarRoute(wire.Buddy, wire.BuddyDelTempBuddies, aimHandler.RemoveTempBuddy))
  126. // The Web AIM client POSTs the message body (non-IE browsers); IE uses GET.
  127. mux.Handle("GET /im/sendIM", sendIM)
  128. mux.Handle("POST /im/sendIM", sendIM)
  129. mux.Handle("GET /im/setTyping", oscarRoute(wire.ICBM, wire.ICBMClientEvent, messagingHandler.SetTyping))
  130. mux.Handle("GET /imlog/fetchStoredIMs", sessionRoute(conversationStub.FetchStoredIMs))
  131. mux.HandleFunc("GET /imlog/markRead", conversationStub.MarkRead)
  132. mux.HandleFunc("GET /conversation/update", conversationStub.Update)
  133. mux.HandleFunc("GET /conversation/close", conversationStub.Close)
  134. mux.Handle("GET /memberDir/search", oscarRoute(wire.ODir, wire.ODirInfoQuery, memberDirHandler.Search))
  135. mux.Handle("GET /memberDir/get", oscarRoute(wire.Locate, wire.LocateGetDirInfo, memberDirHandler.Get))
  136. mux.Handle("GET /memberDir/update", memberDirUpdate)
  137. mux.Handle("POST /memberDir/update", memberDirUpdate)
  138. mux.Handle("GET /preference/get", oscarRoute(wire.Feedbag, wire.FeedbagQuery, preferenceHandler.GetPreferences))
  139. mux.Handle("GET /preference/set", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, preferenceHandler.SetPreferences))
  140. mux.Handle("GET /preference/getPermitDeny", oscarRoute(wire.Feedbag, wire.FeedbagQuery, preferenceHandler.GetPermitDeny))
  141. mux.Handle("GET /preference/setPermitDeny", oscarRoute(wire.Feedbag, wire.FeedbagUpdateItem, preferenceHandler.SetPermitDeny))
  142. mux.HandleFunc("GET /expressions/get", expressionsHandler.Get)
  143. mux.Handle("POST /expressions/upload",
  144. WithBinaryBody(oscarRoute(wire.BART, wire.BARTUploadQuery, expressionsHandler.Upload)))
  145. mux.HandleFunc("GET /aim/setForwardDomain", aimHandler.SetForwardDomain)
  146. mux.HandleFunc("GET /aim/getData", aimHandler.GetData)
  147. mux.HandleFunc("GET /aim/reportAction", aimHandler.ReportAction)
  148. mux.HandleFunc("GET /lifestream/getUserDetails", lifestreamStub.GetUserDetails)
  149. mux.HandleFunc("GET /lifestream/getServices", lifestreamStub.GetServices)
  150. mux.HandleFunc("GET /lifestream/heyGetNotifications", lifestreamStub.HeyGetNotifications)
  151. mux.HandleFunc("GET /lifestream/", lifestreamStub.EmptyOK)
  152. mux.HandleFunc("GET /service/getAttributes", serviceStub.GetAttributes)
  153. mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  154. logger.Debug("webapi 404", "method", r.Method, "path", r.URL.Path)
  155. SendError(w, r, http.StatusNotFound, "not found")
  156. })
  157. servers = append(servers, &http.Server{
  158. Addr: l,
  159. Handler: RequestLogger(logger, corsHandler.Handler(mux)),
  160. })
  161. }
  162. aimHandler.FnSessCfg = func(sess *state.Session) {
  163. sess.OnSessionClose(func() {
  164. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
  165. defer cancel()
  166. if !shuttingDown(shutdownCtx) {
  167. if err := handler.BuddyBroadcaster.BroadcastBuddyDeparted(ctx, sess.IdentScreenName()); err != nil {
  168. logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
  169. }
  170. }
  171. // buddy list must be cleared before session is closed, otherwise
  172. // there will be a race condition that could cause the buddy list
  173. // be prematurely deleted.
  174. if err := handler.BuddyListRegistry.UnregisterBuddyList(ctx, sess.IdentScreenName()); err != nil {
  175. logger.ErrorContext(ctx, "error removing buddy list entry", "err", err.Error())
  176. }
  177. handler.ChatSessionManager.RemoveUserFromAllChats(sess.IdentScreenName())
  178. handler.AuthService.Signout(ctx, sess)
  179. })
  180. }
  181. aimHandler.FnSessInit = func(instance *state.SessionInstance) func() error {
  182. return func() error {
  183. // make buddy list visible to other users
  184. if err := handler.BuddyListRegistry.RegisterBuddyList(shutdownCtx, instance.IdentScreenName()); err != nil {
  185. return fmt.Errorf("unable to init buddy list: %w", err)
  186. }
  187. // restore warning level from last session
  188. if err := handler.RecalcWarning(shutdownCtx, instance); err != nil {
  189. return fmt.Errorf("failed to recalculate warning level: %w", err)
  190. }
  191. // periodically decay warning level
  192. go handler.LowerWarnLevel(shutdownCtx, instance)
  193. // broadcast rate limit transitions to every instance on the account
  194. go handler.OServiceService.MonitorRateLimits(shutdownCtx, instance.Session())
  195. return nil
  196. }
  197. }
  198. aimHandler.FnInstanceClose = func(instance *state.SessionInstance) func() {
  199. return func() {
  200. if shuttingDown(shutdownCtx) {
  201. return
  202. }
  203. if instance.Session().Invisible() {
  204. if err := handler.BuddyBroadcaster.BroadcastBuddyDeparted(shutdownCtx, instance.IdentScreenName()); err != nil {
  205. logger.ErrorContext(shutdownCtx, "error sending buddy departure notifications", "err", err.Error())
  206. }
  207. } else {
  208. if err := handler.BuddyBroadcaster.BroadcastBuddyArrived(shutdownCtx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  209. logger.ErrorContext(shutdownCtx, "error sending buddy arrival notifications", "err", err.Error())
  210. }
  211. }
  212. }
  213. }
  214. return &Server{
  215. servers: servers,
  216. logger: logger,
  217. sessionManager: sessionManager,
  218. shutdownCtx: shutdownCtx,
  219. shutdownCancel: shutdownCancel,
  220. }
  221. }
  222. // Server hosts an HTTP endpoint capable of handling AIM-style Kerberos
  223. // authentication. The messages are structured as SNACs transmitted over HTTP.
  224. //
  225. // shutdownCtx bounds the lifetime of the background session reaper: ListenAndServe
  226. // drives it, and Shutdown (or a failed listener) calls shutdownCancel to unwind.
  227. type Server struct {
  228. servers []*http.Server
  229. logger *slog.Logger
  230. sessionManager *SessionManager
  231. shutdownCtx context.Context
  232. shutdownCancel context.CancelFunc
  233. }
  234. func (s *Server) ListenAndServe() error {
  235. if len(s.servers) == 0 {
  236. s.logger.Debug("no webapi listeners defined")
  237. return nil
  238. }
  239. g, ctx := errgroup.WithContext(s.shutdownCtx)
  240. g.Go(func() error {
  241. s.sessionManager.Run(ctx)
  242. return nil
  243. })
  244. for _, server := range s.servers {
  245. g.Go(func() error {
  246. s.logger.Info("starting server", "addr", server.Addr)
  247. if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
  248. s.shutdownCancel()
  249. return fmt.Errorf("unable to start webapi server: %w", err)
  250. }
  251. return nil
  252. })
  253. }
  254. return g.Wait()
  255. }
  256. func (s *Server) Shutdown(ctx context.Context) error {
  257. s.logger.Debug("Initiating graceful shutdown...")
  258. s.shutdownCancel() // stop the session reaper so ListenAndServe's errgroup can drain
  259. var errs []error
  260. if err := s.sessionManager.Shutdown(ctx); err != nil {
  261. errs = append(errs, fmt.Errorf("draining webapi sessions: %w", err))
  262. }
  263. for _, srv := range s.servers {
  264. if err := srv.Shutdown(ctx); err != nil {
  265. errs = append(errs, fmt.Errorf("stopping webapi listener %s: %w", srv.Addr, err))
  266. }
  267. }
  268. if err := errors.Join(errs...); err != nil {
  269. s.logger.Error("shutdown incomplete", "err", err.Error())
  270. return err
  271. }
  272. s.logger.Info("shutdown complete")
  273. return nil
  274. }
  275. func shuttingDown(ctx context.Context) bool {
  276. select {
  277. case <-ctx.Done():
  278. // server is shutting down, don't send buddy notifications
  279. return true
  280. default:
  281. }
  282. return false
  283. }
  284. type Handler struct {
  285. AuthService AuthService
  286. BuddyListRegistry BuddyListRegistry
  287. ICBMService ICBMService
  288. LocateService LocateService
  289. Logger *slog.Logger
  290. OServiceService OServiceService
  291. BuddyBroadcaster BuddyBroadcaster
  292. BuddyService BuddyService
  293. BOSListener config.ListenerGroup
  294. AllowedOrigins []string
  295. BuddyListManager *BuddyListManager
  296. RecalcWarning func(ctx context.Context, instance *state.SessionInstance) error
  297. LowerWarnLevel func(ctx context.Context, instance *state.SessionInstance)
  298. ChatSessionManager ChatSessionManager
  299. FeedbagService FeedbagService
  300. DirSearchService DirSearchService
  301. IconSource BuddyIconSource
  302. BARTService BARTService
  303. SNACRateLimits wire.SNACRateLimits
  304. }
  305. func (h Handler) GetHelloWorldHandler(w http.ResponseWriter, r *http.Request) {
  306. _, _ = fmt.Fprintf(w, "WebAPI Server Running\n")
  307. // Must return the same JSON envelope as other Web AIM APIs.
  308. h.Logger.Info("webapi root GET", "remote", r.RemoteAddr, "host", r.Host, "path", r.URL.Path)
  309. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  310. resp := map[string]any{
  311. "response": map[string]any{
  312. "statusCode": 200,
  313. "statusText": "Ok",
  314. "data": map[string]any{},
  315. },
  316. }
  317. _ = json.NewEncoder(w).Encode(resp)
  318. }
  319. // corsOptions maps the configured origin allowlist onto rs/cors.
  320. func corsOptions(logger *slog.Logger, allowedOrigins []string) cors.Options {
  321. opts := cors.Options{
  322. AllowCredentials: false,
  323. AllowedMethods: []string{http.MethodGet, http.MethodPost},
  324. AllowedHeaders: []string{"Content-Type", "Authorization"},
  325. MaxAge: 3600,
  326. }
  327. origins := make([]string, 0, len(allowedOrigins))
  328. for _, origin := range allowedOrigins {
  329. origin = strings.TrimSpace(origin)
  330. if origin == "" {
  331. continue
  332. }
  333. if origin == "*" {
  334. logger.Info("WEBAPI_ALLOWED_ORIGINS is *, allowing browser calls from any origin")
  335. return allowAnyOrigin(opts)
  336. }
  337. origins = append(origins, origin)
  338. }
  339. if len(origins) == 0 {
  340. logger.Info("WEBAPI_ALLOWED_ORIGINS is not set, allowing browser calls from any origin")
  341. return allowAnyOrigin(opts)
  342. }
  343. opts.AllowedOrigins = origins
  344. return opts
  345. }