server.go 15 KB

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