auth.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. package oscar
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "log/slog"
  9. "net"
  10. "sync"
  11. "time"
  12. "github.com/mk6i/retro-aim-server/config"
  13. "github.com/mk6i/retro-aim-server/state"
  14. "github.com/mk6i/retro-aim-server/wire"
  15. "github.com/google/uuid"
  16. "github.com/patrickmn/go-cache"
  17. "golang.org/x/time/rate"
  18. )
  19. type AuthService interface {
  20. BUCPChallenge(ctx context.Context, bodyIn wire.SNAC_0x17_0x06_BUCPChallengeRequest, newUUID func() uuid.UUID) (wire.SNACMessage, error)
  21. BUCPLogin(ctx context.Context, bodyIn wire.SNAC_0x17_0x02_BUCPLoginRequest, newUserFn func(screenName state.DisplayScreenName) (state.User, error)) (wire.SNACMessage, error)
  22. FLAPLogin(ctx context.Context, frame wire.FLAPSignonFrame, newUserFn func(screenName state.DisplayScreenName) (state.User, error)) (wire.TLVRestBlock, error)
  23. KerberosLogin(ctx context.Context, inBody wire.SNAC_0x050C_0x0002_KerberosLoginRequest, newUserFn func(screenName state.DisplayScreenName) (state.User, error)) (wire.SNACMessage, error)
  24. RegisterBOSSession(ctx context.Context, authCookie []byte) (*state.Session, error)
  25. RegisterChatSession(ctx context.Context, authCookie []byte) (*state.Session, error)
  26. RetrieveBOSSession(ctx context.Context, authCookie []byte) (*state.Session, error)
  27. Signout(ctx context.Context, sess *state.Session)
  28. SignoutChat(ctx context.Context, sess *state.Session)
  29. }
  30. // IPRateLimiter enforces a per-IP rate limit using a token bucket algorithm.
  31. // It caches individual rate limiters by IP address and supports tagging requests
  32. // as originating from the BUCP or FLAP auth.
  33. //
  34. // The limiter uses an in-memory cache with TTL expiration, so rate limits reset
  35. // after the TTL if no activity is observed for a given IP.
  36. type IPRateLimiter struct {
  37. cache *cache.Cache // In-memory cache mapping IPs to rate limiters with optional BUCP tag
  38. rate rate.Limit // Requests allowed per second
  39. burst int // Maximum burst size allowed
  40. }
  41. type rateLimitEntry struct {
  42. isBUCP bool
  43. limiter *rate.Limiter
  44. }
  45. // NewIPRateLimiter initializes a new IPRateLimiter with the specified rate,
  46. // burst size, and TTL for each IP's limiter. Entries expire after 2×TTL.
  47. func NewIPRateLimiter(rate rate.Limit, burst int, ttl time.Duration) *IPRateLimiter {
  48. return &IPRateLimiter{
  49. cache: cache.New(ttl, 2*ttl),
  50. rate: rate,
  51. burst: burst,
  52. }
  53. }
  54. // SetBUCP marks the rate limiter for the given IP as originating from BUCP auth
  55. // (default FLAP auth).
  56. func (l *IPRateLimiter) SetBUCP(ip string) {
  57. limiter, found := l.cache.Get(ip)
  58. if !found {
  59. limiter = &rateLimitEntry{
  60. isBUCP: true,
  61. limiter: rate.NewLimiter(l.rate, l.burst),
  62. }
  63. l.cache.Set(ip, limiter, cache.DefaultExpiration)
  64. }
  65. limiter.(*rateLimitEntry).isBUCP = true
  66. }
  67. // Allow checks if a request from the given IP is allowed under its rate limit.
  68. // It returns whether the request is allowed and whether the connection uses
  69. // BUCP auth.
  70. func (l *IPRateLimiter) Allow(ip string) (allowed bool, isBUCP bool) {
  71. limiter, found := l.cache.Get(ip)
  72. if !found {
  73. limiter = &rateLimitEntry{
  74. limiter: rate.NewLimiter(l.rate, l.burst),
  75. }
  76. l.cache.Set(ip, limiter, cache.DefaultExpiration)
  77. }
  78. entry := limiter.(*rateLimitEntry)
  79. return entry.limiter.Allow(), entry.isBUCP
  80. }
  81. // AuthServer is an authentication server for both FLAP (AIM v1.0-3.0) and BUCP
  82. // (AIM v3.5-5.9) authentication flows.
  83. type AuthServer struct {
  84. AuthService
  85. config.Config
  86. Logger *slog.Logger
  87. *IPRateLimiter
  88. }
  89. // Start starts the authentication server and listens for new connections.
  90. func (rt AuthServer) Start(ctx context.Context) error {
  91. addr := net.JoinHostPort("", rt.Config.AuthPort)
  92. listener, err := net.Listen("tcp", addr)
  93. if err != nil {
  94. return fmt.Errorf("unable to start auth server: %w", err)
  95. }
  96. go func() {
  97. <-ctx.Done()
  98. listener.Close()
  99. }()
  100. rt.Logger.Info("starting server", "listen_host", addr, "oscar_host", rt.Config.OSCARHost)
  101. wg := sync.WaitGroup{}
  102. for {
  103. conn, err := listener.Accept()
  104. if err != nil {
  105. if errors.Is(err, net.ErrClosed) {
  106. break
  107. }
  108. rt.Logger.Error("accept failed", "err", err.Error())
  109. continue
  110. }
  111. wg.Add(1)
  112. go func() {
  113. defer wg.Done()
  114. connCtx := context.WithValue(ctx, "ip", conn.RemoteAddr().String())
  115. rt.Logger.DebugContext(connCtx, "accepted connection")
  116. if err := rt.handleNewConnection(connCtx, conn); err != nil {
  117. rt.Logger.Info("user session failed", "err", err.Error())
  118. }
  119. }()
  120. }
  121. if !waitForShutdown(&wg) {
  122. rt.Logger.Error("shutdown complete, but connections didn't close cleanly")
  123. } else {
  124. rt.Logger.Info("shutdown complete")
  125. }
  126. return nil
  127. }
  128. func (rt AuthServer) handleNewConnection(ctx context.Context, conn net.Conn) error {
  129. defer conn.Close()
  130. flapc := wire.NewFlapClient(100, conn, conn)
  131. ip, _, err := net.SplitHostPort(conn.RemoteAddr().String())
  132. if err != nil {
  133. rt.Logger.Error("failed to parse remote address", "err", err.Error())
  134. return err
  135. }
  136. if ok, isBUCP := rt.Allow(ip); !ok {
  137. rt.Logger.Error("user rate limited at login", "remote", ip)
  138. tlv := wire.TLVRestBlock{
  139. TLVList: []wire.TLV{
  140. wire.NewTLVBE(wire.LoginTLVTagsErrorSubcode, wire.LoginErrRateLimitExceeded),
  141. },
  142. }
  143. // gives wrong response if you quickly switch between BUCP/FLAP clients
  144. if isBUCP {
  145. return flapc.SendSNAC(
  146. wire.SNACFrame{
  147. FoodGroup: wire.BUCP,
  148. SubGroup: wire.BUCPLoginResponse,
  149. },
  150. wire.SNAC_0x17_0x03_BUCPLoginResponse{
  151. TLVRestBlock: tlv,
  152. },
  153. )
  154. } else {
  155. return flapc.SendSignoffFrame(tlv)
  156. }
  157. }
  158. // auth must complete within the next 30 seconds
  159. if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
  160. return fmt.Errorf("failed to set deadline: %w", err)
  161. }
  162. if err := flapc.SendSignonFrame(nil); err != nil {
  163. return err
  164. }
  165. signonFrame, err := flapc.ReceiveSignonFrame()
  166. if err != nil {
  167. return err
  168. }
  169. // decide whether the client is using BUCP or FLAP authentication based on
  170. // the presence of the screen name TLV. this block used to check for the
  171. // presence of the roasted password TLV, however that proved an unreliable
  172. // indicator of FLAP-auth because older ICQ clients appear to omit the
  173. // roasted password TLV when the password is not stored client-side.
  174. if _, hasScreenName := signonFrame.Uint16BE(wire.LoginTLVTagsScreenName); hasScreenName {
  175. return rt.processFLAPAuth(ctx, signonFrame, flapc)
  176. }
  177. rt.SetBUCP(ip)
  178. return rt.processBUCPAuth(ctx, flapc)
  179. }
  180. func (rt AuthServer) processFLAPAuth(ctx context.Context, signonFrame wire.FLAPSignonFrame, flapc *wire.FlapClient) error {
  181. tlv, err := rt.AuthService.FLAPLogin(ctx, signonFrame, state.NewStubUser)
  182. if err != nil {
  183. return err
  184. }
  185. return flapc.SendSignoffFrame(tlv)
  186. }
  187. func (rt AuthServer) processBUCPAuth(ctx context.Context, flapc *wire.FlapClient) error {
  188. frames := 0
  189. for {
  190. frame, err := flapc.ReceiveFLAP()
  191. if err != nil {
  192. return err
  193. }
  194. if frames > 10 {
  195. // a lot of frames received, the client is misbehaving
  196. return fmt.Errorf("too many auth flap packets received")
  197. }
  198. frames++
  199. switch frame.FrameType {
  200. case wire.FLAPFrameSignoff:
  201. rt.Logger.Debug("signed off mid-login")
  202. return io.EOF // client disconnected
  203. case wire.FLAPFrameKeepAlive:
  204. rt.Logger.Debug("received flap keepalive frame")
  205. case wire.FLAPFrameData:
  206. buf := bytes.NewReader(frame.Payload)
  207. fr := wire.SNACFrame{}
  208. if err := wire.UnmarshalBE(&fr, buf); err != nil {
  209. return err
  210. }
  211. switch {
  212. case fr.FoodGroup == wire.BUCP && fr.SubGroup == wire.BUCPChallengeRequest:
  213. challengeRequest := wire.SNAC_0x17_0x06_BUCPChallengeRequest{}
  214. if err := wire.UnmarshalBE(&challengeRequest, buf); err != nil {
  215. return err
  216. }
  217. outSNAC, err := rt.BUCPChallenge(ctx, challengeRequest, uuid.New)
  218. if err != nil {
  219. return err
  220. }
  221. if err := flapc.SendSNAC(outSNAC.Frame, outSNAC.Body); err != nil {
  222. return err
  223. }
  224. if outSNAC.Frame.SubGroup == wire.BUCPLoginResponse {
  225. screenName, _ := challengeRequest.String(wire.LoginTLVTagsScreenName)
  226. rt.Logger.Debug("failed BUCP challenge: user does not exist", "screen_name", screenName)
  227. return nil // account does not exist
  228. }
  229. case fr.FoodGroup == wire.BUCP && fr.SubGroup == wire.BUCPLoginRequest:
  230. loginRequest := wire.SNAC_0x17_0x02_BUCPLoginRequest{}
  231. if err := wire.UnmarshalBE(&loginRequest, buf); err != nil {
  232. return err
  233. }
  234. outSNAC, err := rt.BUCPLogin(ctx, loginRequest, state.NewStubUser)
  235. if err != nil {
  236. return err
  237. }
  238. return flapc.SendSNAC(outSNAC.Frame, outSNAC.Body)
  239. default:
  240. rt.Logger.Debug("unexpected SNAC received during login",
  241. "foodgroup", wire.FoodGroupName(fr.FoodGroup),
  242. "subgroup", wire.SubGroupName(fr.FoodGroup, fr.SubGroup))
  243. return io.EOF
  244. }
  245. default:
  246. rt.Logger.Debug("unexpected frame type received during login", "type", frame.FrameType)
  247. return io.EOF
  248. }
  249. }
  250. }