server.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. package oscar
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "log/slog"
  9. "net"
  10. "net/netip"
  11. "sync"
  12. "time"
  13. "github.com/google/uuid"
  14. "github.com/patrickmn/go-cache"
  15. "golang.org/x/time/rate"
  16. "github.com/mk6i/open-oscar-server/config"
  17. "github.com/mk6i/open-oscar-server/server/oscar/middleware"
  18. "github.com/mk6i/open-oscar-server/state"
  19. "github.com/mk6i/open-oscar-server/wire"
  20. )
  21. func NewServer(
  22. authService AuthService,
  23. buddyListRegistry BuddyListRegistry,
  24. chatSessionManager *state.InMemoryChatSessionManager,
  25. departureNotifier DepartureNotifier,
  26. logger *slog.Logger,
  27. onlineNotifier OnlineNotifier,
  28. SNACHandler func(ctx context.Context, serverType uint16, instance *state.SessionInstance, inFrame wire.SNACFrame, r io.Reader, rw ResponseWriter, endpointCfg config.Endpoint) error,
  29. rateLimitUpdater RateLimitUpdater,
  30. limits wire.SNACRateLimits,
  31. limiter *IPRateLimiter,
  32. listenerGroups []config.ListenerGroup,
  33. recalcWarning func(ctx context.Context, instance *state.SessionInstance) error,
  34. lowerWarnLevel func(ctx context.Context, instance *state.SessionInstance),
  35. ) *Server {
  36. oscarSvc := oscarServer{
  37. authService: authService,
  38. buddyListRegistry: buddyListRegistry,
  39. chatSessionManager: chatSessionManager,
  40. departureNotifier: departureNotifier,
  41. logger: logger,
  42. onlineNotifier: onlineNotifier,
  43. snacHandler: SNACHandler,
  44. rateLimitUpdater: rateLimitUpdater,
  45. rateLimits: limits,
  46. ipRateLimiter: limiter,
  47. recalcWarning: recalcWarning,
  48. lowerWarnLevel: lowerWarnLevel,
  49. }
  50. ctx, cancel := context.WithCancel(context.Background())
  51. return &Server{
  52. closed: make(chan struct{}),
  53. conns: make(map[net.Conn]struct{}),
  54. handler: oscarSvc.routeConnection,
  55. listenerGroups: listenerGroups,
  56. logger: logger,
  57. shutdownCancel: cancel,
  58. shutdownCtx: ctx,
  59. }
  60. }
  61. type Server struct {
  62. logger *slog.Logger
  63. listenerGroups []config.ListenerGroup
  64. listeners []net.Listener
  65. connMu sync.Mutex
  66. conns map[net.Conn]struct{}
  67. connWg sync.WaitGroup
  68. listenWg sync.WaitGroup
  69. shutdownCtx context.Context
  70. shutdownCancel context.CancelFunc
  71. closed chan struct{}
  72. handler func(ctx context.Context, conn net.Conn, endpointCfg config.Endpoint) error
  73. }
  74. func (s *Server) ListenAndServe() error {
  75. for _, group := range s.listenerGroups {
  76. for _, endpoint := range group.Endpoints() {
  77. ln, err := net.Listen("tcp", endpoint.ListenAddress)
  78. if err != nil {
  79. s.cleanupListeners()
  80. s.shutdownCancel()
  81. return fmt.Errorf("failed to listen on %s: %w", endpoint.ListenAddress, err)
  82. }
  83. s.logger.Info("starting server",
  84. "listener", group.Name,
  85. "listen_address", endpoint.ListenAddress,
  86. "advertised_host", endpoint.AdvertisedHost(),
  87. "ssl", endpoint.IsSSL)
  88. s.listeners = append(s.listeners, ln)
  89. s.listenWg.Add(1)
  90. go s.acceptLoop(ln, endpoint)
  91. }
  92. }
  93. <-s.closed // block until Shutdown is called
  94. return nil
  95. }
  96. func (s *Server) Shutdown(ctx context.Context) error {
  97. s.logger.Debug("Initiating graceful shutdown...")
  98. // connections are tracked under connMu, so none can start after this
  99. s.connMu.Lock()
  100. s.shutdownCancel()
  101. s.connMu.Unlock()
  102. s.cleanupListeners()
  103. // Wait for handlers to complete
  104. done := make(chan struct{})
  105. go func() {
  106. s.connWg.Wait()
  107. s.listenWg.Wait()
  108. close(done)
  109. }()
  110. select {
  111. case <-done:
  112. s.logger.Info("shutdown complete")
  113. case <-ctx.Done():
  114. s.logger.Info("shutdown complete, but connections didn't close cleanly")
  115. }
  116. close(s.closed)
  117. return nil
  118. }
  119. func (s *Server) acceptLoop(ln net.Listener, endpointCfg config.Endpoint) {
  120. defer s.listenWg.Done()
  121. for {
  122. conn, err := ln.Accept()
  123. if err != nil {
  124. if errors.Is(err, net.ErrClosed) {
  125. return
  126. }
  127. s.logger.Error("accept error", "err", err.Error())
  128. continue
  129. }
  130. if !s.trackConn(conn) {
  131. _ = conn.Close()
  132. continue
  133. }
  134. go s.handleConnection(s.shutdownCtx, conn, endpointCfg)
  135. }
  136. }
  137. // ServeConn serves a connection that didn't come from one of the server's
  138. // listeners, such as an in-memory connection opened by the TOC server, as if
  139. // it had arrived on the listener at endpointCfg. It returns when the
  140. // connection ends.
  141. func (s *Server) ServeConn(conn net.Conn, endpointCfg config.Endpoint) {
  142. if !s.trackConn(conn) {
  143. _ = conn.Close()
  144. return
  145. }
  146. s.handleConnection(s.shutdownCtx, conn, endpointCfg)
  147. }
  148. // trackConn registers conn so that Shutdown waits for it. It returns false
  149. // if the server is shutting down.
  150. func (s *Server) trackConn(conn net.Conn) bool {
  151. s.connMu.Lock()
  152. defer s.connMu.Unlock()
  153. if s.shutdownCtx.Err() != nil {
  154. return false
  155. }
  156. s.conns[conn] = struct{}{}
  157. s.connWg.Add(1)
  158. return true
  159. }
  160. func (s *Server) handleConnection(ctx context.Context, conn net.Conn, endpointCfg config.Endpoint) {
  161. defer func() {
  162. // untrack connections
  163. s.connMu.Lock()
  164. delete(s.conns, conn)
  165. s.connMu.Unlock()
  166. _ = conn.Close()
  167. s.connWg.Done()
  168. }()
  169. ctx = middleware.WithIP(ctx, conn.RemoteAddr().String())
  170. if err := s.handler(ctx, conn, endpointCfg); err != nil {
  171. s.logger.InfoContext(ctx, "user session failed", "err", err.Error())
  172. }
  173. }
  174. func (s *Server) cleanupListeners() {
  175. for _, ln := range s.listeners {
  176. _ = ln.Close()
  177. }
  178. s.listeners = nil
  179. }
  180. type oscarServer struct {
  181. authService AuthService
  182. buddyListRegistry BuddyListRegistry
  183. chatSessionManager ChatSessionManager
  184. departureNotifier DepartureNotifier
  185. logger *slog.Logger
  186. onlineNotifier OnlineNotifier
  187. snacHandler func(ctx context.Context, serverType uint16, instance *state.SessionInstance, inFrame wire.SNACFrame, r io.Reader, rw ResponseWriter, endpointCfg config.Endpoint) error
  188. rateLimitUpdater RateLimitUpdater
  189. rateLimits wire.SNACRateLimits
  190. ipRateLimiter *IPRateLimiter
  191. recalcWarning func(ctx context.Context, instance *state.SessionInstance) error
  192. lowerWarnLevel func(ctx context.Context, instance *state.SessionInstance)
  193. }
  194. func (s oscarServer) routeConnection(ctx context.Context, conn net.Conn, endpointCfg config.Endpoint) error {
  195. ip, _, err := net.SplitHostPort(conn.RemoteAddr().String())
  196. if err != nil {
  197. s.logger.Error("failed to parse remote address", "err", err.Error())
  198. return err
  199. }
  200. flapc := wire.NewFlapClient(100, conn, conn)
  201. // send flap signon with server capabilities
  202. if err := flapc.SendSignonFrame(nil); err != nil {
  203. return err
  204. }
  205. flap, err := flapc.ReceiveSignonFrame()
  206. if err != nil {
  207. return err
  208. }
  209. if flap.HasTag(wire.OServiceTLVTagsLoginCookie) {
  210. return s.connectToOSCARService(ctx, flap, flapc, conn, endpointCfg)
  211. }
  212. return s.authenticate(ctx, flap, ip, conn, flapc, endpointCfg)
  213. }
  214. func (s oscarServer) connectToOSCARService(
  215. ctx context.Context,
  216. flap wire.FLAPSignonFrame,
  217. flapc *wire.FlapClient,
  218. conn net.Conn,
  219. endpointCfg config.Endpoint,
  220. ) error {
  221. authCookie, ok := flap.Bytes(wire.OServiceTLVTagsLoginCookie)
  222. if !ok {
  223. return errors.New("unable to get session id from payload")
  224. }
  225. cookie, _, err := s.authService.CrackCookie(authCookie)
  226. if err != nil {
  227. return err
  228. }
  229. s.logger.Debug("connecting to service", "service", wire.FoodGroupName(cookie.Service))
  230. var instance *state.SessionInstance
  231. switch cookie.Service {
  232. case wire.BOS:
  233. sessCfg := func(sess *state.Session) {
  234. sess.OnSessionClose(func() {
  235. if !shuttingDown(ctx) {
  236. if err := s.departureNotifier.BroadcastBuddyDeparted(ctx, sess.IdentScreenName()); err != nil {
  237. s.logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
  238. }
  239. }
  240. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
  241. defer cancel()
  242. // buddy list must be cleared before session is closed, otherwise
  243. // there will be a race condition that could cause the buddy list
  244. // be prematurely deleted.
  245. if err := s.buddyListRegistry.UnregisterBuddyList(ctx, sess.IdentScreenName()); err != nil {
  246. s.logger.ErrorContext(ctx, "error removing buddy list entry", "err", err.Error())
  247. }
  248. s.chatSessionManager.RemoveUserFromAllChats(sess.IdentScreenName())
  249. s.authService.Signout(ctx, sess)
  250. })
  251. }
  252. instance, err = s.authService.RegisterBOSSession(ctx, cookie, sessCfg)
  253. if err != nil {
  254. if errors.Is(err, state.ErrMaxConcurrentSessionsReached) {
  255. s.logger.Debug("session registration failed", "err", err.Error())
  256. block := wire.TLVRestBlock{}
  257. // error code indicating the signon is blocked. i can't find a
  258. // more appropriate error code to indicate the maximum session limit is reached
  259. block.Append(wire.NewTLVBE(0x0008, uint8(0x18)))
  260. if err := flapc.NewSignoff(block); err != nil {
  261. return fmt.Errorf("unable to gracefully disconnect user. %w", err)
  262. }
  263. return nil
  264. }
  265. return err
  266. }
  267. if instance == nil {
  268. return errors.New("session not found")
  269. }
  270. defer func() {
  271. instance.CloseInstance()
  272. }()
  273. if err = instance.Session().RunOnce(func() error {
  274. // make buddy list visible to other users
  275. if err := s.buddyListRegistry.RegisterBuddyList(ctx, instance.IdentScreenName()); err != nil {
  276. return fmt.Errorf("unable to init buddy list: %w", err)
  277. }
  278. // restore warning level from last session
  279. if err := s.recalcWarning(ctx, instance); err != nil {
  280. return fmt.Errorf("failed to recalculate warning level: %w", err)
  281. }
  282. // periodically decay warning level
  283. go s.lowerWarnLevel(ctx, instance)
  284. // broadcast rate limit transitions to every instance on the account
  285. go s.rateLimitUpdater.MonitorRateLimits(ctx, instance.Session())
  286. return nil
  287. }); err != nil {
  288. return err
  289. }
  290. // Update user visibility when an instance closes, as the user's overall status may change.
  291. // Example: With 1 away and 1 non-away instance, the user appears available. If the non-away
  292. // instance closes, the user should appear away.
  293. instance.OnClose(func() {
  294. if shuttingDown(ctx) {
  295. return
  296. }
  297. if instance.Session().Invisible() {
  298. if err := s.departureNotifier.BroadcastBuddyDeparted(ctx, instance.IdentScreenName()); err != nil {
  299. s.logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
  300. }
  301. } else {
  302. if err := s.departureNotifier.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  303. s.logger.ErrorContext(ctx, "error sending buddy arrival notifications", "err", err.Error())
  304. }
  305. }
  306. })
  307. if remoteAddr, ok := middleware.IPFromContext(ctx); ok {
  308. ip, err := netip.ParseAddrPort(remoteAddr)
  309. if err != nil {
  310. return errors.New("unable to parse ip addr")
  311. }
  312. instance.SetRemoteAddr(&ip)
  313. }
  314. go s.receiveSessMessages(ctx, instance, flapc)
  315. case wire.Chat:
  316. sessCfg := func(sess *state.Session) {
  317. sess.OnSessionClose(func() {
  318. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
  319. defer cancel()
  320. s.authService.SignoutChat(ctx, sess)
  321. })
  322. }
  323. instance, err = s.authService.RegisterChatSession(ctx, cookie, sessCfg)
  324. if err != nil {
  325. return err
  326. }
  327. if instance == nil {
  328. return errors.New("session not found")
  329. }
  330. defer func() {
  331. instance.CloseInstance()
  332. }()
  333. // A chat session is a Session of its own, with its own rate limit states
  334. // and subscriptions, so it needs its own monitor — the BOS session's
  335. // cannot see these states.
  336. if err := instance.Session().RunOnce(func() error {
  337. go s.rateLimitUpdater.MonitorRateLimits(ctx, instance.Session())
  338. return nil
  339. }); err != nil {
  340. return err
  341. }
  342. go s.receiveSessMessages(ctx, instance, flapc)
  343. default:
  344. instance, err = s.authService.RetrieveBOSSession(ctx, cookie)
  345. if err != nil {
  346. return err
  347. }
  348. if instance == nil {
  349. return errors.New("session not found")
  350. }
  351. }
  352. ctx = middleware.WithScreenName(ctx, instance.IdentScreenName())
  353. msg := s.onlineNotifier.HostOnline(cookie.Service)
  354. if err := flapc.SendSNAC(msg.Frame, msg.Body); err != nil {
  355. return err
  356. }
  357. return s.dispatchIncomingMessages(ctx, cookie.Service, instance, flapc, conn, endpointCfg)
  358. }
  359. func shuttingDown(ctx context.Context) bool {
  360. select {
  361. case <-ctx.Done():
  362. // server is shutting down, don't send buddy notifications
  363. return true
  364. default:
  365. }
  366. return false
  367. }
  368. func (s oscarServer) receiveSessMessages(ctx context.Context, instance *state.SessionInstance, flapc *wire.FlapClient) {
  369. for {
  370. select {
  371. case <-instance.Closed():
  372. return
  373. case <-ctx.Done():
  374. return
  375. case m := <-instance.ReceiveMessage():
  376. // forward a notification sent from another client to this client
  377. if err := flapc.SendSNAC(m.Frame, m.Body); err != nil {
  378. middleware.LogRequestError(ctx, s.logger, m.Frame, err)
  379. } else {
  380. middleware.LogRequest(ctx, s.logger, m.Frame, m.Body)
  381. }
  382. }
  383. }
  384. }
  385. func (s oscarServer) authenticate(ctx context.Context, flap wire.FLAPSignonFrame, ip string, conn net.Conn, flapc *wire.FlapClient, endpointCfg config.Endpoint) error {
  386. if ok, isBUCP := s.ipRateLimiter.Allow(ip); !ok {
  387. s.logger.InfoContext(ctx, "user rate limited at login, dropping connection")
  388. tlv := wire.TLVRestBlock{
  389. TLVList: []wire.TLV{
  390. wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrRateLimitExceeded),
  391. },
  392. }
  393. // gives wrong response if you quickly switch between BUCP/FLAP clients
  394. if isBUCP {
  395. return flapc.SendSNAC(
  396. wire.SNACFrame{
  397. FoodGroup: wire.BUCP,
  398. SubGroup: wire.BUCPLoginResponse,
  399. },
  400. wire.SNAC_0x17_0x03_BUCPLoginResponse{
  401. TLVRestBlock: tlv,
  402. },
  403. )
  404. } else {
  405. return flapc.NewSignoff(tlv)
  406. }
  407. }
  408. // auth must complete within the next 30 seconds
  409. if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
  410. return fmt.Errorf("failed to set deadline: %w", err)
  411. }
  412. // decide whether the client is using BUCP or FLAP authentication based on
  413. // the presence of the screen name TLV. this block used to check for the
  414. // presence of the roasted password TLV, however that proved an unreliable
  415. // indicator of FLAP-auth because older ICQ clients appear to omit the
  416. // roasted password TLV when the password is not stored client-side.
  417. if _, hasScreenName := flap.Uint16BE(wire.LoginTLVTagsScreenName); hasScreenName {
  418. return s.processFLAPAuth(ctx, flap, flapc, endpointCfg)
  419. }
  420. s.ipRateLimiter.SetBUCP(ip)
  421. return s.processBUCPAuth(ctx, flapc, endpointCfg)
  422. }
  423. func (s oscarServer) processFLAPAuth(ctx context.Context, signonFrame wire.FLAPSignonFrame, flapc *wire.FlapClient, endpointCfg config.Endpoint) error {
  424. tlv, err := s.authService.FLAPLogin(ctx, signonFrame, endpointCfg)
  425. if err != nil {
  426. return err
  427. }
  428. return flapc.NewSignoff(tlv)
  429. }
  430. func (s oscarServer) processBUCPAuth(ctx context.Context, flapc *wire.FlapClient, endpointCfg config.Endpoint) error {
  431. frames := 0
  432. for {
  433. frame, err := flapc.ReceiveFLAP()
  434. if err != nil {
  435. return err
  436. }
  437. if frames > 10 {
  438. // a lot of frames received, the client is misbehaving
  439. return fmt.Errorf("too many auth flap packets received")
  440. }
  441. frames++
  442. switch frame.FrameType {
  443. case wire.FLAPFrameSignoff:
  444. s.logger.Debug("signed off mid-login")
  445. return io.EOF // client disconnected
  446. case wire.FLAPFrameKeepAlive:
  447. s.logger.Debug("received flap keepalive frame")
  448. case wire.FLAPFrameData:
  449. buf := bytes.NewReader(frame.Payload)
  450. fr := wire.SNACFrame{}
  451. if err := wire.UnmarshalBE(&fr, buf); err != nil {
  452. return err
  453. }
  454. switch {
  455. case fr.FoodGroup == wire.BUCP && fr.SubGroup == wire.BUCPChallengeRequest:
  456. challengeRequest := wire.SNAC_0x17_0x06_BUCPChallengeRequest{}
  457. if err := wire.UnmarshalBE(&challengeRequest, buf); err != nil {
  458. return err
  459. }
  460. outSNAC, err := s.authService.BUCPChallenge(ctx, challengeRequest, uuid.New)
  461. if err != nil {
  462. return err
  463. }
  464. outSNAC.Frame.RequestID = fr.RequestID
  465. if err := flapc.SendSNAC(outSNAC.Frame, outSNAC.Body); err != nil {
  466. return err
  467. }
  468. if outSNAC.Frame.SubGroup == wire.BUCPLoginResponse {
  469. screenName, _ := challengeRequest.String(wire.LoginTLVTagsScreenName)
  470. s.logger.Debug("failed BUCP challenge: user does not exist", "screen_name", screenName)
  471. return nil // account does not exist
  472. }
  473. case fr.FoodGroup == wire.BUCP && fr.SubGroup == wire.BUCPLoginRequest:
  474. loginRequest := wire.SNAC_0x17_0x02_BUCPLoginRequest{}
  475. if err := wire.UnmarshalBE(&loginRequest, buf); err != nil {
  476. return err
  477. }
  478. outSNAC, err := s.authService.BUCPLogin(ctx, loginRequest, endpointCfg)
  479. if err != nil {
  480. return err
  481. }
  482. outSNAC.Frame.RequestID = fr.RequestID
  483. // Clients expect login response as SNAC on FLAP
  484. // channel 2 followed by a FLAP signoff frame to properly close the auth
  485. // connection
  486. if err := flapc.SendSNAC(outSNAC.Frame, outSNAC.Body); err != nil {
  487. return err
  488. }
  489. return flapc.NewSignoff(wire.TLVRestBlock{})
  490. default:
  491. s.logger.Debug("unexpected SNAC received during login",
  492. "foodgroup", wire.FoodGroupName(fr.FoodGroup),
  493. "subgroup", wire.SubGroupName(fr.FoodGroup, fr.SubGroup))
  494. return io.EOF
  495. }
  496. default:
  497. s.logger.Debug("unexpected frame type received during login", "type", frame.FrameType)
  498. return io.EOF
  499. }
  500. }
  501. }
  502. func sendInvalidSNACErr(frameIn wire.SNACFrame, rw ResponseWriter) error {
  503. frameOut := wire.SNACFrame{
  504. FoodGroup: frameIn.FoodGroup,
  505. SubGroup: 0x01, // error subgroup for all SNACs
  506. RequestID: frameIn.RequestID,
  507. }
  508. bodyOut := wire.SNACError{
  509. Code: wire.ErrorCodeInvalidSnac,
  510. }
  511. return rw.SendSNAC(frameOut, bodyOut)
  512. }
  513. // dispatchIncomingMessages receives incoming messages and sends them to the
  514. // appropriate message handler. Messages from the client are sent to the
  515. // router. Messages relayed from the user session are forwarded to the client.
  516. // This function ensures that the same sequence number is incremented for both
  517. // types of messages. The function terminates upon receiving a connection error
  518. // or when the session closes.
  519. func (s oscarServer) dispatchIncomingMessages(
  520. ctx context.Context,
  521. fg uint16,
  522. instance *state.SessionInstance,
  523. flapc *wire.FlapClient,
  524. r io.ReadCloser,
  525. endpointCfg config.Endpoint,
  526. ) error {
  527. defer func() {
  528. s.logger.InfoContext(ctx, "user disconnected")
  529. }()
  530. // buffered so that the go routine has room to exit
  531. msgCh := make(chan wire.FLAPFrame, 1)
  532. errCh := make(chan error, 1)
  533. // consume flap frames
  534. go func() {
  535. defer close(msgCh)
  536. defer close(errCh)
  537. for {
  538. frame := wire.FLAPFrame{}
  539. if err := wire.UnmarshalBE(&frame, r); err != nil {
  540. errCh <- err
  541. return
  542. }
  543. msgCh <- frame
  544. }
  545. }()
  546. for {
  547. select {
  548. case flap, ok := <-msgCh:
  549. if !ok {
  550. return nil
  551. }
  552. switch flap.FrameType {
  553. case wire.FLAPFrameData:
  554. flapBuf := bytes.NewBuffer(flap.Payload)
  555. inFrame := wire.SNACFrame{}
  556. if err := wire.UnmarshalBE(&inFrame, flapBuf); err != nil {
  557. return err
  558. }
  559. rateClassID, ok := s.rateLimits.RateClassLookup(inFrame.FoodGroup, inFrame.SubGroup)
  560. if ok {
  561. if status := instance.Session().EvaluateRateLimit(time.Now(), rateClassID); status == wire.RateLimitStatusLimited {
  562. s.logger.DebugContext(ctx, "rate limit exceeded, dropping SNAC",
  563. "foodgroup", wire.FoodGroupName(inFrame.FoodGroup),
  564. "subgroup", wire.SubGroupName(inFrame.FoodGroup, inFrame.SubGroup))
  565. break
  566. }
  567. } else {
  568. s.logger.ErrorContext(ctx, "rate limit not found, allowing request through")
  569. }
  570. // route a client request to the appropriate service handler. the
  571. // handler may write a response to the client connection.
  572. if err := s.snacHandler(ctx, fg, instance, inFrame, flapBuf, flapc, endpointCfg); err != nil {
  573. middleware.LogRequestError(ctx, s.logger, inFrame, err)
  574. if errors.Is(err, ErrRouteNotFound) {
  575. if err1 := sendInvalidSNACErr(inFrame, flapc); err1 != nil {
  576. return errors.Join(err1, err)
  577. }
  578. break
  579. }
  580. return err
  581. }
  582. case wire.FLAPFrameSignon:
  583. return fmt.Errorf("shouldn't get FLAPFrameSignon. flap: %v", flap)
  584. case wire.FLAPFrameError:
  585. return fmt.Errorf("got FLAPFrameError. flap: %v", flap)
  586. case wire.FLAPFrameSignoff:
  587. s.logger.InfoContext(ctx, "got FLAPFrameSignoff", "flap", flap)
  588. return nil
  589. case wire.FLAPFrameKeepAlive:
  590. s.logger.DebugContext(ctx, "keepalive heartbeat")
  591. default:
  592. return fmt.Errorf("got unknown FLAP frame type. flap: %v", flap)
  593. }
  594. case <-instance.Closed():
  595. // add logoff reason to clients that support multi-conn
  596. if instance.MultiConnFlag() == wire.MultiConnFlagsOldClient {
  597. if err := flapc.OldSignoff(); err != nil {
  598. return fmt.Errorf("unable to gracefully disconnect user. %w", err)
  599. }
  600. } else {
  601. block := wire.TLVRestBlock{}
  602. // error code indicating user signed in a different location
  603. block.Append(wire.NewTLVBE(0x0009, wire.OServiceDiscErrNewLogin))
  604. // "more info" button
  605. block.Append(wire.NewTLVBE(0x000b, "https://github.com/mk6i/open-oscar-server"))
  606. if err := flapc.NewSignoff(block); err != nil {
  607. return fmt.Errorf("unable to gracefully disconnect user. %w", err)
  608. }
  609. }
  610. return nil
  611. case <-ctx.Done():
  612. if instance.MultiConnFlag() == wire.MultiConnFlagsOldClient {
  613. if err := flapc.OldSignoff(); err != nil {
  614. return fmt.Errorf("unable to gracefully disconnect user. %w", err)
  615. }
  616. } else {
  617. if err := flapc.NewSignoff(wire.TLVRestBlock{}); err != nil {
  618. return fmt.Errorf("unable to gracefully disconnect user. %w", err)
  619. }
  620. }
  621. return nil
  622. case err := <-errCh:
  623. if !errors.Is(err, io.EOF) {
  624. s.logger.ErrorContext(ctx, "client disconnected with error", "err", err)
  625. }
  626. return nil
  627. }
  628. }
  629. }
  630. // IPRateLimiter enforces a per-IP rate limit using a token bucket algorithm.
  631. // It caches individual rate limiters by IP address and supports tagging requests
  632. // as originating from the BUCP or FLAP auth.
  633. //
  634. // The limiter uses an in-memory cache with TTL expiration, so rate limits reset
  635. // after the TTL if no activity is observed for a given IP.
  636. type IPRateLimiter struct {
  637. cache *cache.Cache // In-memory cache mapping IPs to rate limiters with optional BUCP tag
  638. rate rate.Limit // Requests allowed per second
  639. burst int // Maximum burst size allowed
  640. }
  641. type rateLimitEntry struct {
  642. isBUCP bool
  643. limiter *rate.Limiter
  644. }
  645. // NewIPRateLimiter initializes a new IPRateLimiter with the specified rate,
  646. // burst size, and TTL for each IP's limiter. Entries expire after 2×TTL.
  647. func NewIPRateLimiter(rate rate.Limit, burst int, ttl time.Duration) *IPRateLimiter {
  648. return &IPRateLimiter{
  649. cache: cache.New(ttl, 2*ttl),
  650. rate: rate,
  651. burst: burst,
  652. }
  653. }
  654. // SetBUCP marks the rate limiter for the given IP as originating from BUCP auth
  655. // (default FLAP auth).
  656. func (l *IPRateLimiter) SetBUCP(ip string) {
  657. limiter, found := l.cache.Get(ip)
  658. if !found {
  659. limiter = &rateLimitEntry{
  660. isBUCP: true,
  661. limiter: rate.NewLimiter(l.rate, l.burst),
  662. }
  663. l.cache.Set(ip, limiter, cache.DefaultExpiration)
  664. }
  665. limiter.(*rateLimitEntry).isBUCP = true
  666. }
  667. // Allow checks if a request from the given IP is allowed under its rate limit.
  668. // It returns whether the request is allowed and whether the connection uses
  669. // BUCP auth.
  670. func (l *IPRateLimiter) Allow(ip string) (allowed bool, isBUCP bool) {
  671. limiter, found := l.cache.Get(ip)
  672. if !found {
  673. limiter = &rateLimitEntry{
  674. limiter: rate.NewLimiter(l.rate, l.burst),
  675. }
  676. l.cache.Set(ip, limiter, cache.DefaultExpiration)
  677. }
  678. entry := limiter.(*rateLimitEntry)
  679. return entry.limiter.Allow(), entry.isBUCP
  680. }