connection.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. package oscar
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "log/slog"
  9. "time"
  10. "github.com/mk6i/retro-aim-server/server/oscar/middleware"
  11. "github.com/mk6i/retro-aim-server/state"
  12. "github.com/mk6i/retro-aim-server/wire"
  13. )
  14. func sendInvalidSNACErr(frameIn wire.SNACFrame, rw ResponseWriter) error {
  15. frameOut := wire.SNACFrame{
  16. FoodGroup: frameIn.FoodGroup,
  17. SubGroup: 0x01, // error subgroup for all SNACs
  18. RequestID: frameIn.RequestID,
  19. }
  20. bodyOut := wire.SNACError{
  21. Code: wire.ErrorCodeInvalidSnac,
  22. }
  23. return rw.SendSNAC(frameOut, bodyOut)
  24. }
  25. // dispatchIncomingMessages receives incoming messages and sends them to the
  26. // appropriate message handler. Messages from the client are sent to the
  27. // router. Messages relayed from the user session are forwarded to the client.
  28. // This function ensures that the same sequence number is incremented for both
  29. // types of messages. The function terminates upon receiving a connection error
  30. // or when the session closes.
  31. //
  32. // todo: this method has too many params and should be folded into a new type
  33. func dispatchIncomingMessages(ctx context.Context, sess *state.Session, flapc *wire.FlapClient, r io.Reader, logger *slog.Logger, router Handler, rateLimitUpdater RateLimitUpdater, snacRateLimits wire.SNACRateLimits) error {
  34. defer func() {
  35. logger.InfoContext(ctx, "user disconnected")
  36. }()
  37. // buffered so that the go routine has room to exit
  38. msgCh := make(chan wire.FLAPFrame, 1)
  39. errCh := make(chan error, 1)
  40. // consume flap frames
  41. go func() {
  42. defer close(msgCh)
  43. defer close(errCh)
  44. for {
  45. frame := wire.FLAPFrame{}
  46. if err := wire.UnmarshalBE(&frame, r); err != nil {
  47. errCh <- err
  48. return
  49. }
  50. msgCh <- frame
  51. }
  52. }()
  53. for {
  54. select {
  55. case flap, ok := <-msgCh:
  56. if !ok {
  57. return nil
  58. }
  59. switch flap.FrameType {
  60. case wire.FLAPFrameData:
  61. flapBuf := bytes.NewBuffer(flap.Payload)
  62. inFrame := wire.SNACFrame{}
  63. if err := wire.UnmarshalBE(&inFrame, flapBuf); err != nil {
  64. return err
  65. }
  66. rateClassID, ok := snacRateLimits.RateClassLookup(inFrame.FoodGroup, inFrame.SubGroup)
  67. if ok {
  68. if status := sess.EvaluateRateLimit(time.Now(), rateClassID); status == wire.RateLimitStatusLimited {
  69. logger.DebugContext(ctx, "rate limit exceeded, dropping SNAC",
  70. "foodgroup", wire.FoodGroupName(inFrame.FoodGroup),
  71. "subgroup", wire.SubGroupName(inFrame.FoodGroup, inFrame.SubGroup))
  72. break
  73. }
  74. } else {
  75. logger.ErrorContext(ctx, "rate limit not found, allowing request through")
  76. }
  77. // route a client request to the appropriate service handler. the
  78. // handler may write a response to the client connection.
  79. if err := router.Handle(ctx, sess, inFrame, flapBuf, flapc); err != nil {
  80. middleware.LogRequestError(ctx, logger, inFrame, err)
  81. if errors.Is(err, ErrRouteNotFound) {
  82. if err1 := sendInvalidSNACErr(inFrame, flapc); err1 != nil {
  83. return errors.Join(err1, err)
  84. }
  85. break
  86. }
  87. return err
  88. }
  89. case wire.FLAPFrameSignon:
  90. return fmt.Errorf("shouldn't get FLAPFrameSignon. flap: %v", flap)
  91. case wire.FLAPFrameError:
  92. return fmt.Errorf("got FLAPFrameError. flap: %v", flap)
  93. case wire.FLAPFrameSignoff:
  94. logger.InfoContext(ctx, "got FLAPFrameSignoff", "flap", flap)
  95. return nil
  96. case wire.FLAPFrameKeepAlive:
  97. logger.DebugContext(ctx, "keepalive heartbeat")
  98. default:
  99. return fmt.Errorf("got unknown FLAP frame type. flap: %v", flap)
  100. }
  101. case m := <-sess.ReceiveMessage():
  102. // forward a notification sent from another client to this client
  103. if err := flapc.SendSNAC(m.Frame, m.Body); err != nil {
  104. middleware.LogRequestError(ctx, logger, m.Frame, err)
  105. return err
  106. }
  107. middleware.LogRequest(ctx, logger, m.Frame, m.Body)
  108. case <-time.After(1 * time.Second):
  109. msgs := rateLimitUpdater.RateLimitUpdates(ctx, sess, time.Now())
  110. for _, rate := range msgs {
  111. if err := flapc.SendSNAC(rate.Frame, rate.Body); err != nil {
  112. middleware.LogRequestError(ctx, logger, rate.Frame, err)
  113. return err
  114. }
  115. }
  116. case <-sess.Closed():
  117. block := wire.TLVRestBlock{}
  118. // error code indicating user signed in a different location
  119. block.Append(wire.NewTLVBE(0x0009, wire.OServiceDiscErrNewLogin))
  120. // "more info" button
  121. block.Append(wire.NewTLVBE(0x000b, "https://github.com/mk6i/retro-aim-server"))
  122. if err := flapc.SendSignoffFrame(block); err != nil {
  123. return fmt.Errorf("unable to gracefully disconnect user. %w", err)
  124. }
  125. return nil
  126. case <-ctx.Done():
  127. // application is shutting down
  128. if err := flapc.Disconnect(); err != nil {
  129. return fmt.Errorf("unable to gracefully disconnect user. %w", err)
  130. }
  131. return nil
  132. case err := <-errCh:
  133. if !errors.Is(io.EOF, err) {
  134. logger.ErrorContext(ctx, "client disconnected with error", "err", err)
  135. }
  136. return nil
  137. }
  138. }
  139. }