server.go 22 KB

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