mgmt_api.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  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. "github.com/google/uuid"
  14. "github.com/mk6i/retro-aim-server/config"
  15. "github.com/mk6i/retro-aim-server/state"
  16. "github.com/mk6i/retro-aim-server/wire"
  17. )
  18. func StartManagementAPI(
  19. cfg config.Config,
  20. userManager UserManager,
  21. sessionRetriever SessionRetriever,
  22. chatRoomRetriever ChatRoomRetriever,
  23. chatRoomCreator ChatRoomCreator,
  24. chatSessionRetriever ChatSessionRetriever,
  25. messageRelayer MessageRelayer,
  26. logger *slog.Logger,
  27. ) {
  28. mux := http.NewServeMux()
  29. mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
  30. userHandler(w, r, userManager, uuid.New, logger)
  31. })
  32. mux.HandleFunc("/user/password", func(w http.ResponseWriter, r *http.Request) {
  33. userPasswordHandler(w, r, userManager, uuid.New, logger)
  34. })
  35. mux.HandleFunc("/user/login", func(w http.ResponseWriter, r *http.Request) {
  36. loginHandler(w, r, userManager, logger)
  37. })
  38. mux.HandleFunc("/session", func(w http.ResponseWriter, r *http.Request) {
  39. sessionHandler(w, r, sessionRetriever)
  40. })
  41. mux.HandleFunc("/chat/room/public", func(w http.ResponseWriter, r *http.Request) {
  42. publicChatHandler(w, r, chatRoomRetriever, chatRoomCreator, chatSessionRetriever, logger)
  43. })
  44. mux.HandleFunc("/chat/room/private", func(w http.ResponseWriter, r *http.Request) {
  45. privateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  46. })
  47. mux.HandleFunc("/instant-message", func(w http.ResponseWriter, r *http.Request) {
  48. instantMessageHandler(w, r, messageRelayer, logger)
  49. })
  50. addr := net.JoinHostPort(cfg.ApiHost, cfg.ApiPort)
  51. logger.Info("starting management API server", "addr", addr)
  52. if err := http.ListenAndServe(addr, mux); err != nil {
  53. logger.Error("unable to bind management API address address", "err", err.Error())
  54. os.Exit(1)
  55. }
  56. }
  57. func userHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  58. switch r.Method {
  59. case http.MethodDelete:
  60. deleteUserHandler(w, r, userManager, logger)
  61. case http.MethodGet:
  62. getUserHandler(w, r, userManager, logger)
  63. case http.MethodPost:
  64. postUserHandler(w, r, userManager, newUUID, logger)
  65. default:
  66. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  67. }
  68. }
  69. func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) {
  70. user, err := userFromBody(r)
  71. if err != nil {
  72. http.Error(w, err.Error(), http.StatusBadRequest)
  73. return
  74. }
  75. err = manager.DeleteUser(state.NewIdentScreenName(user.ScreenName))
  76. switch {
  77. case errors.Is(err, state.ErrNoUser):
  78. http.Error(w, "user does not exist", http.StatusNotFound)
  79. return
  80. case err != nil:
  81. logger.Error("error deleting user DELETE /user", "err", err.Error())
  82. http.Error(w, "internal server error", http.StatusInternalServerError)
  83. return
  84. }
  85. w.WriteHeader(http.StatusNoContent)
  86. _, _ = fmt.Fprintln(w, "User account successfully deleted.")
  87. }
  88. func userPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  89. switch r.Method {
  90. case http.MethodPut:
  91. putUserPasswordHandler(w, r, userManager, newUUID, logger)
  92. default:
  93. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  94. }
  95. }
  96. // putUserPasswordHandler handles the PUT /user/password endpoint.
  97. func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  98. input, err := userFromBody(r)
  99. if err != nil {
  100. http.Error(w, err.Error(), http.StatusBadRequest)
  101. return
  102. }
  103. user := state.User{
  104. AuthKey: newUUID().String(),
  105. IdentScreenName: state.NewIdentScreenName(input.ScreenName),
  106. }
  107. if err := user.HashPassword(input.Password); err != nil {
  108. logger.Error("error hashing user password in PUT /user/password", "err", err.Error())
  109. http.Error(w, "internal server error", http.StatusInternalServerError)
  110. return
  111. }
  112. if err := userManager.SetUserPassword(user); err != nil {
  113. switch {
  114. case errors.Is(err, state.ErrNoUser):
  115. http.Error(w, "user does not exist", http.StatusNotFound)
  116. return
  117. case err != nil:
  118. logger.Error("error updating user password PUT /user/password", "err", err.Error())
  119. http.Error(w, "internal server error", http.StatusInternalServerError)
  120. return
  121. }
  122. }
  123. w.WriteHeader(http.StatusNoContent)
  124. }
  125. // sessionHandler handles GET /session
  126. func sessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever) {
  127. w.Header().Set("Content-Type", "application/json")
  128. if r.Method != http.MethodGet {
  129. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  130. return
  131. }
  132. allUsers := sessionRetriever.AllSessions()
  133. ou := onlineUsers{
  134. Count: len(allUsers),
  135. Sessions: make([]userHandle, len(allUsers)),
  136. }
  137. for i, s := range allUsers {
  138. ou.Sessions[i] = userHandle{
  139. ID: s.IdentScreenName().String(),
  140. ScreenName: s.DisplayScreenName().String(),
  141. }
  142. }
  143. if err := json.NewEncoder(w).Encode(ou); err != nil {
  144. http.Error(w, err.Error(), http.StatusInternalServerError)
  145. return
  146. }
  147. }
  148. // getUserHandler handles the GET /user endpoint.
  149. func getUserHandler(w http.ResponseWriter, _ *http.Request, userManager UserManager, logger *slog.Logger) {
  150. w.Header().Set("Content-Type", "application/json")
  151. users, err := userManager.AllUsers()
  152. if err != nil {
  153. logger.Error("error in GET /user", "err", err.Error())
  154. http.Error(w, "internal server error", http.StatusInternalServerError)
  155. return
  156. }
  157. out := make([]userHandle, len(users))
  158. for i, u := range users {
  159. out[i] = userHandle{
  160. ID: u.IdentScreenName.String(),
  161. ScreenName: u.DisplayScreenName.String(),
  162. }
  163. }
  164. if err := json.NewEncoder(w).Encode(out); err != nil {
  165. http.Error(w, err.Error(), http.StatusInternalServerError)
  166. return
  167. }
  168. }
  169. // postUserHandler handles the POST /user endpoint.
  170. func postUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  171. input, err := userFromBody(r)
  172. if err != nil {
  173. http.Error(w, err.Error(), http.StatusBadRequest)
  174. return
  175. }
  176. sn := state.DisplayScreenName(input.ScreenName)
  177. if err := sn.ValidateAIMHandle(); err != nil {
  178. http.Error(w, fmt.Sprintf("invalid screen name: %s", err), http.StatusBadRequest)
  179. return
  180. }
  181. user := state.User{
  182. AuthKey: newUUID().String(),
  183. DisplayScreenName: sn,
  184. IdentScreenName: sn.IdentScreenName(),
  185. }
  186. if err := user.HashPassword(input.Password); err != nil {
  187. http.Error(w, fmt.Sprintf("invalid password: %s", err), http.StatusBadRequest)
  188. return
  189. }
  190. err = userManager.InsertUser(user)
  191. switch {
  192. case errors.Is(err, state.ErrDupUser):
  193. http.Error(w, "user already exists", http.StatusConflict)
  194. return
  195. case err != nil:
  196. logger.Error("error inserting user POST /user", "err", err.Error())
  197. http.Error(w, "internal server error", http.StatusInternalServerError)
  198. return
  199. }
  200. w.WriteHeader(http.StatusCreated)
  201. _, _ = fmt.Fprintln(w, "User account created successfully.")
  202. }
  203. func userFromBody(r *http.Request) (userWithPassword, error) {
  204. user := userWithPassword{}
  205. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  206. return userWithPassword{}, errors.New("malformed input")
  207. }
  208. return user, nil
  209. }
  210. // loginHandler is a temporary endpoint for validating user credentials for
  211. // chivanet. do not rely on this endpoint, as it will be eventually removed.
  212. func loginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  213. authHeader := r.Header.Get("Authorization")
  214. if authHeader == "" {
  215. // No authentication header found
  216. w.WriteHeader(http.StatusUnauthorized)
  217. w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
  218. _, _ = w.Write([]byte("401 Unauthorized\n"))
  219. return
  220. }
  221. auth := strings.SplitN(authHeader, " ", 2)
  222. if len(auth) != 2 || auth[0] != "Basic" {
  223. w.WriteHeader(http.StatusUnauthorized)
  224. _, _ = w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
  225. return
  226. }
  227. payload, err := base64.StdEncoding.DecodeString(auth[1])
  228. if err != nil {
  229. w.WriteHeader(http.StatusUnauthorized)
  230. _, _ = w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
  231. return
  232. }
  233. pair := strings.SplitN(string(payload), ":", 2)
  234. if len(pair) != 2 {
  235. w.WriteHeader(http.StatusUnauthorized)
  236. _, _ = w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
  237. return
  238. }
  239. username, password := state.NewIdentScreenName(pair[0]), pair[1]
  240. user, err := userManager.User(username)
  241. if err != nil {
  242. w.WriteHeader(http.StatusInternalServerError)
  243. _, _ = w.Write([]byte("500 InternalServerError\n"))
  244. logger.Error("error getting user", "err", err.Error())
  245. return
  246. }
  247. if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
  248. w.WriteHeader(http.StatusUnauthorized)
  249. _, _ = w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
  250. return
  251. }
  252. // Successfully authenticated
  253. w.WriteHeader(http.StatusOK)
  254. _, _ = w.Write([]byte("200 OK: Successfully Authenticated\n"))
  255. }
  256. func publicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatRoomCreator ChatRoomCreator, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  257. switch r.Method {
  258. case http.MethodGet:
  259. getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  260. case http.MethodPost:
  261. postPublicChatHandler(w, r, chatRoomCreator, logger)
  262. default:
  263. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  264. }
  265. }
  266. func privateChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  267. switch r.Method {
  268. case http.MethodGet:
  269. getPrivateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  270. default:
  271. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  272. }
  273. }
  274. // getPublicChatHandler handles the GET /chat/room/public endpoint.
  275. func getPublicChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  276. w.Header().Set("Content-Type", "application/json")
  277. rooms, err := chatRoomRetriever.AllChatRooms(state.PublicExchange)
  278. if err != nil {
  279. logger.Error("error in GET /chat/rooms/public", "err", err.Error())
  280. http.Error(w, "internal server error", http.StatusInternalServerError)
  281. return
  282. }
  283. out := make([]chatRoom, len(rooms))
  284. for i, room := range rooms {
  285. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  286. cr := chatRoom{
  287. CreateTime: room.CreateTime(),
  288. Name: room.Name(),
  289. Participants: make([]userHandle, len(sessions)),
  290. URL: room.URL().String(),
  291. }
  292. for j, sess := range sessions {
  293. cr.Participants[j] = userHandle{
  294. ID: sess.IdentScreenName().String(),
  295. ScreenName: sess.DisplayScreenName().String(),
  296. }
  297. }
  298. out[i] = cr
  299. }
  300. if err := json.NewEncoder(w).Encode(out); err != nil {
  301. http.Error(w, err.Error(), http.StatusInternalServerError)
  302. return
  303. }
  304. }
  305. // postPublicChatHandler handles the POST /chat/room/public endpoint.
  306. func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreator ChatRoomCreator, logger *slog.Logger) {
  307. input := chatRoomCreate{}
  308. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  309. http.Error(w, "invalid input", http.StatusBadRequest)
  310. return
  311. }
  312. input.Name = strings.TrimSpace(input.Name)
  313. if input.Name == "" || len(input.Name) > 50 {
  314. http.Error(w, "chat room name must be between 1 and 50 characters", http.StatusBadRequest)
  315. return
  316. }
  317. cr := state.NewChatRoom(input.Name, state.NewIdentScreenName("system"), state.PublicExchange)
  318. err := chatRoomCreator.CreateChatRoom(&cr)
  319. switch {
  320. case errors.Is(err, state.ErrDupChatRoom):
  321. http.Error(w, "Chat room already exists.", http.StatusConflict)
  322. return
  323. case err != nil:
  324. logger.Error("error inserting chat room POST /chat/room/public", "err", err.Error())
  325. http.Error(w, "internal server error", http.StatusInternalServerError)
  326. return
  327. }
  328. w.WriteHeader(http.StatusCreated)
  329. _, _ = fmt.Fprintln(w, "Chat room created successfully.")
  330. }
  331. // getPrivateChatHandler handles the GET /chat/room/private endpoint.
  332. func getPrivateChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  333. w.Header().Set("Content-Type", "application/json")
  334. rooms, err := chatRoomRetriever.AllChatRooms(state.PrivateExchange)
  335. if err != nil {
  336. logger.Error("error in GET /chat/rooms/private", "err", err.Error())
  337. http.Error(w, "internal server error", http.StatusInternalServerError)
  338. return
  339. }
  340. out := make([]chatRoom, len(rooms))
  341. for i, room := range rooms {
  342. sessions := chatSessionRetriever.AllSessions(room.Cookie())
  343. cr := chatRoom{
  344. CreateTime: room.CreateTime(),
  345. CreatorID: room.Creator().String(),
  346. Name: room.Name(),
  347. Participants: make([]userHandle, len(sessions)),
  348. URL: room.URL().String(),
  349. }
  350. for j, sess := range sessions {
  351. cr.Participants[j] = userHandle{
  352. ID: sess.IdentScreenName().String(),
  353. ScreenName: sess.DisplayScreenName().String(),
  354. }
  355. }
  356. out[i] = cr
  357. }
  358. if err := json.NewEncoder(w).Encode(out); err != nil {
  359. http.Error(w, err.Error(), http.StatusInternalServerError)
  360. return
  361. }
  362. }
  363. func instantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) {
  364. switch r.Method {
  365. case http.MethodPost:
  366. postInstantMessageHandler(w, r, messageRelayer, logger)
  367. default:
  368. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  369. }
  370. }
  371. // postIMHandler handles the POST /instant-message endpoint.
  372. func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) {
  373. input := instantMessage{}
  374. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  375. http.Error(w, "malformed input", http.StatusBadRequest)
  376. return
  377. }
  378. tlv, err := wire.ICBMFragmentList(input.Text)
  379. if err != nil {
  380. logger.Error("error sending message POST /instant-message", "err", err.Error())
  381. http.Error(w, "internal server error", http.StatusInternalServerError)
  382. return
  383. }
  384. msg := wire.SNACMessage{
  385. Frame: wire.SNACFrame{
  386. FoodGroup: wire.ICBM,
  387. SubGroup: wire.ICBMChannelMsgToClient,
  388. },
  389. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  390. ChannelID: 1,
  391. TLVUserInfo: wire.TLVUserInfo{
  392. ScreenName: input.From,
  393. },
  394. TLVRestBlock: wire.TLVRestBlock{
  395. TLVList: wire.TLVList{
  396. wire.NewTLV(wire.ICBMTLVAOLIMData, tlv),
  397. },
  398. },
  399. },
  400. }
  401. messageRelayer.RelayToScreenName(context.Background(), state.NewIdentScreenName(input.To), msg)
  402. w.WriteHeader(http.StatusOK)
  403. _, _ = fmt.Fprintln(w, "Message sent successfully.")
  404. }