mgmt_api.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. package http
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "log/slog"
  9. "net"
  10. "net/http"
  11. "os"
  12. "strings"
  13. "time"
  14. "github.com/google/uuid"
  15. "github.com/mk6i/retro-aim-server/config"
  16. "github.com/mk6i/retro-aim-server/state"
  17. "github.com/mk6i/retro-aim-server/wire"
  18. )
  19. func StartManagementAPI(
  20. cfg config.Config,
  21. userManager UserManager,
  22. sessionRetriever SessionRetriever,
  23. chatRoomRetriever ChatRoomRetriever,
  24. chatRoomCreator ChatRoomCreator,
  25. chatSessionRetriever ChatSessionRetriever,
  26. messageRelayer MessageRelayer,
  27. bartRetriever BARTRetriever,
  28. feedbagRetriever FeedBagRetriever,
  29. accountRetriever AccountRetriever,
  30. profileRetriever ProfileRetriever,
  31. logger *slog.Logger,
  32. ) {
  33. mux := http.NewServeMux()
  34. // Handlers for '/user' route
  35. mux.HandleFunc("DELETE /user", func(w http.ResponseWriter, r *http.Request) {
  36. deleteUserHandler(w, r, userManager, logger)
  37. })
  38. mux.HandleFunc("GET /user", func(w http.ResponseWriter, r *http.Request) {
  39. getUserHandler(w, userManager, logger)
  40. })
  41. mux.HandleFunc("POST /user", func(w http.ResponseWriter, r *http.Request) {
  42. postUserHandler(w, r, userManager, uuid.New, logger)
  43. })
  44. // Handlers for '/user/password' route
  45. mux.HandleFunc("PUT /user/password", func(w http.ResponseWriter, r *http.Request) {
  46. putUserPasswordHandler(w, r, userManager, logger)
  47. })
  48. // Handlers for '/user/login' route
  49. mux.HandleFunc("GET /user/login", func(w http.ResponseWriter, r *http.Request) {
  50. getUserLoginHandler(w, r, userManager, logger)
  51. })
  52. // Handlers for '/user/{screenname}/account' route
  53. mux.HandleFunc("GET /user/{screenname}/account", func(w http.ResponseWriter, r *http.Request) {
  54. getUserAccountHandler(w, r, userManager, accountRetriever, profileRetriever, logger)
  55. })
  56. // Handlers for '/user/{screenname}/icon' route
  57. mux.HandleFunc("GET /user/{screenname}/icon", func(w http.ResponseWriter, r *http.Request) {
  58. getUserBuddyIconHandler(w, r, userManager, feedbagRetriever, bartRetriever, logger)
  59. })
  60. // Handlers for '/session' route
  61. mux.HandleFunc("GET /session", func(w http.ResponseWriter, r *http.Request) {
  62. getSessionHandler(w, r, sessionRetriever, time.Since)
  63. })
  64. // Handlers for '/session/{screenname}' route
  65. mux.HandleFunc("GET /session/{screenname}", func(w http.ResponseWriter, r *http.Request) {
  66. getSessionHandler(w, r, sessionRetriever, time.Since)
  67. })
  68. // Handlers for '/chat/room/public' route
  69. mux.HandleFunc("GET /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  70. getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  71. })
  72. mux.HandleFunc("POST /chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  73. postPublicChatHandler(w, r, chatRoomCreator, logger)
  74. })
  75. // Handlers for '/chat/room/private' route
  76. mux.HandleFunc("GET /chat/room/private", func(w http.ResponseWriter, r *http.Request) {
  77. getPrivateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  78. })
  79. // Handlers for '/instant-message' route
  80. mux.HandleFunc("POST /instant-message", func(w http.ResponseWriter, r *http.Request) {
  81. postInstantMessageHandler(w, r, messageRelayer, logger)
  82. })
  83. addr := net.JoinHostPort(cfg.ApiHost, cfg.ApiPort)
  84. logger.Info("starting management API server", "addr", addr)
  85. if err := http.ListenAndServe(addr, mux); err != nil {
  86. logger.Error("unable to bind management API address address", "err", err.Error())
  87. os.Exit(1)
  88. }
  89. }
  90. // deleteUserHandler handles the DELETE /user endpoint.
  91. func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) {
  92. user, err := userFromBody(r)
  93. if err != nil {
  94. http.Error(w, err.Error(), http.StatusBadRequest)
  95. return
  96. }
  97. err = manager.DeleteUser(state.NewIdentScreenName(user.ScreenName))
  98. switch {
  99. case errors.Is(err, state.ErrNoUser):
  100. http.Error(w, "user does not exist", http.StatusNotFound)
  101. return
  102. case err != nil:
  103. logger.Error("error deleting user DELETE /user", "err", err.Error())
  104. http.Error(w, "internal server error", http.StatusInternalServerError)
  105. return
  106. }
  107. w.WriteHeader(http.StatusNoContent)
  108. _, _ = fmt.Fprintln(w, "User account successfully deleted.")
  109. }
  110. // putUserPasswordHandler handles the PUT /user/password endpoint.
  111. func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  112. input, err := userFromBody(r)
  113. if err != nil {
  114. http.Error(w, err.Error(), http.StatusBadRequest)
  115. return
  116. }
  117. sn := state.NewIdentScreenName(input.ScreenName)
  118. if err := userManager.SetUserPassword(sn, input.Password); err != nil {
  119. switch {
  120. case errors.Is(err, state.ErrNoUser):
  121. http.Error(w, "user does not exist", http.StatusNotFound)
  122. return
  123. case errors.Is(err, state.ErrPasswordInvalid):
  124. http.Error(w, err.Error(), http.StatusBadRequest)
  125. return
  126. default:
  127. logger.Error("error updating user password PUT /user/password", "err", err.Error())
  128. http.Error(w, "internal server error", http.StatusInternalServerError)
  129. return
  130. }
  131. }
  132. w.WriteHeader(http.StatusNoContent)
  133. _, _ = fmt.Fprintln(w, "Password successfully reset.")
  134. }
  135. // getSessionHandler handles GET /session
  136. func getSessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever, funcTimeSince func(t time.Time) time.Duration) {
  137. w.Header().Set("Content-Type", "application/json")
  138. var allUsers []*state.Session
  139. if screenName := r.PathValue("screenname"); screenName != "" {
  140. session := sessionRetriever.RetrieveByScreenName(state.NewIdentScreenName(screenName))
  141. if session == nil {
  142. http.Error(w, "session not found", http.StatusNotFound)
  143. return
  144. }
  145. allUsers = append(allUsers, session)
  146. } else {
  147. allUsers = sessionRetriever.AllSessions()
  148. }
  149. ou := onlineUsers{
  150. Count: len(allUsers),
  151. Sessions: make([]sessionHandle, len(allUsers)),
  152. }
  153. for i, s := range allUsers {
  154. // report 0 if the user is not idle
  155. idleSeconds := funcTimeSince(s.IdleTime()).Seconds()
  156. if !s.Idle() {
  157. idleSeconds = 0
  158. }
  159. onlineSeconds := funcTimeSince(s.SignonTime()).Seconds()
  160. ou.Sessions[i] = sessionHandle{
  161. ID: s.IdentScreenName().String(),
  162. ScreenName: s.DisplayScreenName().String(),
  163. OnlineSeconds: onlineSeconds,
  164. AwayMessage: s.AwayMessage(),
  165. IdleSeconds: idleSeconds,
  166. IsICQ: s.UIN() > 0,
  167. }
  168. }
  169. if err := json.NewEncoder(w).Encode(ou); err != nil {
  170. http.Error(w, err.Error(), http.StatusInternalServerError)
  171. return
  172. }
  173. }
  174. // getUserHandler handles the GET /user endpoint.
  175. func getUserHandler(w http.ResponseWriter, userManager UserManager, logger *slog.Logger) {
  176. w.Header().Set("Content-Type", "application/json")
  177. users, err := userManager.AllUsers()
  178. if err != nil {
  179. logger.Error("error in GET /user", "err", err.Error())
  180. http.Error(w, "internal server error", http.StatusInternalServerError)
  181. return
  182. }
  183. out := make([]userHandle, len(users))
  184. for i, u := range users {
  185. out[i] = userHandle{
  186. ID: u.IdentScreenName.String(),
  187. ScreenName: u.DisplayScreenName.String(),
  188. IsICQ: u.IsICQ,
  189. }
  190. }
  191. if err := json.NewEncoder(w).Encode(out); err != nil {
  192. http.Error(w, err.Error(), http.StatusInternalServerError)
  193. return
  194. }
  195. }
  196. // postUserHandler handles the POST /user endpoint.
  197. func postUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  198. input, err := userFromBody(r)
  199. if err != nil {
  200. http.Error(w, err.Error(), http.StatusBadRequest)
  201. return
  202. }
  203. sn := state.DisplayScreenName(input.ScreenName)
  204. if sn.IsUIN() {
  205. if err := sn.ValidateUIN(); err != nil {
  206. http.Error(w, fmt.Sprintf("invalid uin: %s", err), http.StatusBadRequest)
  207. return
  208. }
  209. } else {
  210. if err := sn.ValidateAIMHandle(); err != nil {
  211. http.Error(w, fmt.Sprintf("invalid screen name: %s", err), http.StatusBadRequest)
  212. return
  213. }
  214. }
  215. user := state.User{
  216. AuthKey: newUUID().String(),
  217. DisplayScreenName: sn,
  218. IdentScreenName: sn.IdentScreenName(),
  219. IsICQ: sn.IsUIN(),
  220. }
  221. if err := user.HashPassword(input.Password); err != nil {
  222. http.Error(w, fmt.Sprintf("invalid password: %s", err), http.StatusBadRequest)
  223. return
  224. }
  225. err = userManager.InsertUser(user)
  226. switch {
  227. case errors.Is(err, state.ErrDupUser):
  228. http.Error(w, "user already exists", http.StatusConflict)
  229. return
  230. case err != nil:
  231. logger.Error("error inserting user POST /user", "err", err.Error())
  232. http.Error(w, "internal server error", http.StatusInternalServerError)
  233. return
  234. }
  235. w.WriteHeader(http.StatusCreated)
  236. _, _ = fmt.Fprintln(w, "User account created successfully.")
  237. }
  238. func userFromBody(r *http.Request) (userWithPassword, error) {
  239. user := userWithPassword{}
  240. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  241. return userWithPassword{}, errors.New("malformed input")
  242. }
  243. return user, nil
  244. }
  245. // getUserLoginHandler is a temporary endpoint for validating user credentials for
  246. // chivanet. do not rely on this endpoint, as it will be eventually removed.
  247. func getUserLoginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  248. authHeader := r.Header.Get("Authorization")
  249. if authHeader == "" {
  250. // No authentication header found
  251. w.WriteHeader(http.StatusUnauthorized)
  252. w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
  253. _, _ = w.Write([]byte("401 Unauthorized\n"))
  254. return
  255. }
  256. auth := strings.SplitN(authHeader, " ", 2)
  257. if len(auth) != 2 || auth[0] != "Basic" {
  258. w.WriteHeader(http.StatusUnauthorized)
  259. _, _ = w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
  260. return
  261. }
  262. payload, err := base64.StdEncoding.DecodeString(auth[1])
  263. if err != nil {
  264. w.WriteHeader(http.StatusUnauthorized)
  265. _, _ = w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
  266. return
  267. }
  268. pair := strings.SplitN(string(payload), ":", 2)
  269. if len(pair) != 2 {
  270. w.WriteHeader(http.StatusUnauthorized)
  271. _, _ = w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
  272. return
  273. }
  274. username, password := state.NewIdentScreenName(pair[0]), pair[1]
  275. user, err := userManager.User(username)
  276. if err != nil {
  277. w.WriteHeader(http.StatusInternalServerError)
  278. _, _ = w.Write([]byte("500 InternalServerError\n"))
  279. logger.Error("error getting user", "err", err.Error())
  280. return
  281. }
  282. if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
  283. w.WriteHeader(http.StatusUnauthorized)
  284. _, _ = w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
  285. return
  286. }
  287. // Successfully authenticated
  288. w.WriteHeader(http.StatusOK)
  289. _, _ = w.Write([]byte("200 OK: Successfully Authenticated\n"))
  290. }
  291. // getPublicChatHandler handles the GET /chat/room/public endpoint.
  292. func getPublicChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  293. w.Header().Set("Content-Type", "application/json")
  294. rooms, err := chatRoomRetriever.AllChatRooms(state.PublicExchange)
  295. if err != nil {
  296. logger.Error("error in GET /chat/rooms/public", "err", err.Error())
  297. http.Error(w, "internal server error", http.StatusInternalServerError)
  298. return
  299. }
  300. out := make([]chatRoom, len(rooms))
  301. for i, room := range rooms {
  302. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  303. cr := chatRoom{
  304. CreateTime: room.CreateTime(),
  305. Name: room.Name(),
  306. Participants: make([]aimChatUserHandle, len(sessions)),
  307. URL: room.URL().String(),
  308. }
  309. for j, sess := range sessions {
  310. cr.Participants[j] = aimChatUserHandle{
  311. ID: sess.IdentScreenName().String(),
  312. ScreenName: sess.DisplayScreenName().String(),
  313. }
  314. }
  315. out[i] = cr
  316. }
  317. if err := json.NewEncoder(w).Encode(out); err != nil {
  318. http.Error(w, err.Error(), http.StatusInternalServerError)
  319. return
  320. }
  321. }
  322. // postPublicChatHandler handles the POST /chat/room/public endpoint.
  323. func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreator ChatRoomCreator, logger *slog.Logger) {
  324. input := chatRoomCreate{}
  325. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  326. http.Error(w, "invalid input", http.StatusBadRequest)
  327. return
  328. }
  329. input.Name = strings.TrimSpace(input.Name)
  330. if input.Name == "" || len(input.Name) > 50 {
  331. http.Error(w, "chat room name must be between 1 and 50 characters", http.StatusBadRequest)
  332. return
  333. }
  334. cr := state.NewChatRoom(input.Name, state.NewIdentScreenName("system"), state.PublicExchange)
  335. err := chatRoomCreator.CreateChatRoom(&cr)
  336. switch {
  337. case errors.Is(err, state.ErrDupChatRoom):
  338. http.Error(w, "Chat room already exists.", http.StatusConflict)
  339. return
  340. case err != nil:
  341. logger.Error("error inserting chat room POST /chat/room/public", "err", err.Error())
  342. http.Error(w, "internal server error", http.StatusInternalServerError)
  343. return
  344. }
  345. w.WriteHeader(http.StatusCreated)
  346. _, _ = fmt.Fprintln(w, "Chat room created successfully.")
  347. }
  348. // getPrivateChatHandler handles the GET /chat/room/private endpoint.
  349. func getPrivateChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  350. w.Header().Set("Content-Type", "application/json")
  351. rooms, err := chatRoomRetriever.AllChatRooms(state.PrivateExchange)
  352. if err != nil {
  353. logger.Error("error in GET /chat/rooms/private", "err", err.Error())
  354. http.Error(w, "internal server error", http.StatusInternalServerError)
  355. return
  356. }
  357. out := make([]chatRoom, len(rooms))
  358. for i, room := range rooms {
  359. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  360. cr := chatRoom{
  361. CreateTime: room.CreateTime(),
  362. CreatorID: room.Creator().String(),
  363. Name: room.Name(),
  364. Participants: make([]aimChatUserHandle, len(sessions)),
  365. URL: room.URL().String(),
  366. }
  367. for j, sess := range sessions {
  368. cr.Participants[j] = aimChatUserHandle{
  369. ID: sess.IdentScreenName().String(),
  370. ScreenName: sess.DisplayScreenName().String(),
  371. }
  372. }
  373. out[i] = cr
  374. }
  375. if err := json.NewEncoder(w).Encode(out); err != nil {
  376. http.Error(w, err.Error(), http.StatusInternalServerError)
  377. return
  378. }
  379. }
  380. // postIMHandler handles the POST /instant-message endpoint.
  381. func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) {
  382. input := instantMessage{}
  383. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  384. http.Error(w, "malformed input", http.StatusBadRequest)
  385. return
  386. }
  387. tlv, err := wire.ICBMFragmentList(input.Text)
  388. if err != nil {
  389. logger.Error("error sending message POST /instant-message", "err", err.Error())
  390. http.Error(w, "internal server error", http.StatusInternalServerError)
  391. return
  392. }
  393. msg := wire.SNACMessage{
  394. Frame: wire.SNACFrame{
  395. FoodGroup: wire.ICBM,
  396. SubGroup: wire.ICBMChannelMsgToClient,
  397. },
  398. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  399. ChannelID: 1,
  400. TLVUserInfo: wire.TLVUserInfo{
  401. ScreenName: input.From,
  402. },
  403. TLVRestBlock: wire.TLVRestBlock{
  404. TLVList: wire.TLVList{
  405. wire.NewTLV(wire.ICBMTLVAOLIMData, tlv),
  406. },
  407. },
  408. },
  409. }
  410. messageRelayer.RelayToScreenName(context.Background(), state.NewIdentScreenName(input.To), msg)
  411. w.WriteHeader(http.StatusOK)
  412. _, _ = fmt.Fprintln(w, "Message sent successfully.")
  413. }
  414. // getUserBuddyIconHandler handles the GET /user/{screenname}/icon endpoint.
  415. func getUserBuddyIconHandler(w http.ResponseWriter, r *http.Request, u UserManager, f FeedBagRetriever, b BARTRetriever, logger *slog.Logger) {
  416. w.Header().Set("Content-Type", "image/gif")
  417. screenName := state.NewIdentScreenName(r.PathValue("screenname"))
  418. user, err := u.User(screenName)
  419. if err != nil {
  420. logger.Error("error retrieving user", "err", err.Error())
  421. http.Error(w, "internal server error", http.StatusInternalServerError)
  422. return
  423. }
  424. if user == nil {
  425. http.Error(w, "user not found", http.StatusNotFound)
  426. return
  427. }
  428. iconRef, err := f.BuddyIconRefByName(screenName)
  429. if err != nil {
  430. logger.Error("error retrieving buddy icon ref", "err", err.Error())
  431. http.Error(w, "internal server error", http.StatusInternalServerError)
  432. return
  433. }
  434. if iconRef == nil || iconRef.HasClearIconHash() {
  435. http.Error(w, "icon not found", http.StatusNotFound)
  436. return
  437. }
  438. icon, err := b.BARTRetrieve(iconRef.Hash)
  439. if err != nil {
  440. logger.Error("error retrieving buddy icon bart item", "err", err.Error())
  441. http.Error(w, "internal server error", http.StatusInternalServerError)
  442. return
  443. }
  444. w.Write(icon)
  445. }
  446. // getUserAccountHandler handles the GET /user/{screenname}/account endpoint.
  447. func getUserAccountHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, a AccountRetriever, p ProfileRetriever, logger *slog.Logger) {
  448. w.Header().Set("Content-Type", "application/json")
  449. screenName := r.PathValue("screenname")
  450. user, err := userManager.User(state.NewIdentScreenName(screenName))
  451. if err != nil {
  452. logger.Error("error in GET /user/{screenname}/account", "err", err.Error())
  453. http.Error(w, "internal server error", http.StatusInternalServerError)
  454. return
  455. }
  456. if user == nil {
  457. http.Error(w, "user not found", http.StatusNotFound)
  458. return
  459. }
  460. emailAddress := ""
  461. email, err := a.EmailAddressByName(user.IdentScreenName)
  462. if err != nil {
  463. emailAddress = ""
  464. } else {
  465. emailAddress = email.String()
  466. }
  467. regStatus, err := a.RegStatusByName(user.IdentScreenName)
  468. if err != nil {
  469. logger.Error("error in GET /user/*/account RegStatus", "err", err.Error())
  470. http.Error(w, "internal server error", http.StatusInternalServerError)
  471. return
  472. }
  473. confirmStatus, err := a.ConfirmStatusByName(user.IdentScreenName)
  474. if err != nil {
  475. logger.Error("error in GET /user/*/account ConfirmStatus", "err", err.Error())
  476. http.Error(w, "internal server error", http.StatusInternalServerError)
  477. return
  478. }
  479. profile, err := p.Profile(user.IdentScreenName)
  480. if err != nil {
  481. logger.Error("error in GET /user/*/account Profile", "err", err.Error())
  482. http.Error(w, "internal server error", http.StatusInternalServerError)
  483. return
  484. }
  485. out := userAccountHandle{
  486. ID: user.IdentScreenName.String(),
  487. ScreenName: user.DisplayScreenName.String(),
  488. EmailAddress: emailAddress,
  489. RegStatus: regStatus,
  490. Confirmed: confirmStatus,
  491. Profile: profile,
  492. IsICQ: user.IsICQ,
  493. }
  494. if err := json.NewEncoder(w).Encode(out); err != nil {
  495. http.Error(w, err.Error(), http.StatusInternalServerError)
  496. return
  497. }
  498. }