mgmt_api.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  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, state.NewChatRoom, 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(user.DisplayScreenName.IdentScreenName())
  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. user, err := userFromBody(r)
  99. if err != nil {
  100. http.Error(w, err.Error(), http.StatusBadRequest)
  101. return
  102. }
  103. user.AuthKey = newUUID().String()
  104. user.IdentScreenName = user.DisplayScreenName.IdentScreenName()
  105. if err := user.HashPassword(user.Password); err != nil {
  106. logger.Error("error hashing user password in PUT /user/password", "err", err.Error())
  107. http.Error(w, "internal server error", http.StatusInternalServerError)
  108. return
  109. }
  110. if err := userManager.SetUserPassword(user.User); err != nil {
  111. switch {
  112. case errors.Is(err, state.ErrNoUser):
  113. http.Error(w, "user does not exist", http.StatusNotFound)
  114. return
  115. case err != nil:
  116. logger.Error("error updating user password PUT /user/password", "err", err.Error())
  117. http.Error(w, "internal server error", http.StatusInternalServerError)
  118. return
  119. }
  120. }
  121. w.WriteHeader(http.StatusNoContent)
  122. }
  123. // sessionHandler handles GET /session
  124. func sessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever) {
  125. w.Header().Set("Content-Type", "application/json")
  126. if r.Method != http.MethodGet {
  127. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  128. return
  129. }
  130. allUsers := sessionRetriever.AllSessions()
  131. ou := onlineUsers{
  132. Count: len(allUsers),
  133. Sessions: make([]userSession, 0),
  134. }
  135. for _, s := range allUsers {
  136. ou.Sessions = append(ou.Sessions, userSession{
  137. ScreenName: s.DisplayScreenName().String(),
  138. })
  139. }
  140. if err := json.NewEncoder(w).Encode(ou); err != nil {
  141. http.Error(w, err.Error(), http.StatusInternalServerError)
  142. return
  143. }
  144. }
  145. // getUserHandler handles the GET /user endpoint.
  146. func getUserHandler(w http.ResponseWriter, _ *http.Request, userManager UserManager, logger *slog.Logger) {
  147. w.Header().Set("Content-Type", "application/json")
  148. users, err := userManager.AllUsers()
  149. if err != nil {
  150. logger.Error("error in GET /user", "err", err.Error())
  151. http.Error(w, "internal server error", http.StatusInternalServerError)
  152. return
  153. }
  154. if err := json.NewEncoder(w).Encode(users); err != nil {
  155. http.Error(w, err.Error(), http.StatusInternalServerError)
  156. return
  157. }
  158. }
  159. // postUserHandler handles the POST /user endpoint.
  160. func postUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  161. user, err := userFromBody(r)
  162. if err != nil {
  163. http.Error(w, err.Error(), http.StatusBadRequest)
  164. return
  165. }
  166. user.AuthKey = newUUID().String()
  167. if err := user.HashPassword(user.Password); err != nil {
  168. logger.Error("error hashing user password in POST /user", "err", err.Error())
  169. http.Error(w, "internal server error", http.StatusInternalServerError)
  170. return
  171. }
  172. err = userManager.InsertUser(user.User)
  173. switch {
  174. case errors.Is(err, state.ErrDupUser):
  175. http.Error(w, "user already exists", http.StatusConflict)
  176. return
  177. case err != nil:
  178. logger.Error("error inserting user POST /user", "err", err.Error())
  179. http.Error(w, "internal server error", http.StatusInternalServerError)
  180. return
  181. }
  182. w.WriteHeader(http.StatusCreated)
  183. fmt.Fprintln(w, "User account created successfully.")
  184. }
  185. func userFromBody(r *http.Request) (userWithPassword, error) {
  186. user := userWithPassword{}
  187. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  188. return userWithPassword{}, errors.New("malformed input")
  189. }
  190. user.IdentScreenName = user.DisplayScreenName.IdentScreenName()
  191. return user, nil
  192. }
  193. // loginHandler is a temporary endpoint for validating user credentials for
  194. // chivanet. do not rely on this endpoint, as it will be eventually removed.
  195. func loginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  196. authHeader := r.Header.Get("Authorization")
  197. if authHeader == "" {
  198. // No authentication header found
  199. w.WriteHeader(http.StatusUnauthorized)
  200. w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
  201. w.Write([]byte("401 Unauthorized\n"))
  202. return
  203. }
  204. auth := strings.SplitN(authHeader, " ", 2)
  205. if len(auth) != 2 || auth[0] != "Basic" {
  206. w.WriteHeader(http.StatusUnauthorized)
  207. w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
  208. return
  209. }
  210. payload, err := base64.StdEncoding.DecodeString(auth[1])
  211. if err != nil {
  212. w.WriteHeader(http.StatusUnauthorized)
  213. w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
  214. return
  215. }
  216. pair := strings.SplitN(string(payload), ":", 2)
  217. if len(pair) != 2 {
  218. w.WriteHeader(http.StatusUnauthorized)
  219. w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
  220. return
  221. }
  222. username, password := state.NewIdentScreenName(pair[0]), pair[1]
  223. user, err := userManager.User(username)
  224. if err != nil {
  225. w.WriteHeader(http.StatusInternalServerError)
  226. w.Write([]byte("500 InternalServerError\n"))
  227. logger.Error("error getting user", "err", err.Error())
  228. return
  229. }
  230. if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
  231. w.WriteHeader(http.StatusUnauthorized)
  232. w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
  233. return
  234. }
  235. // Successfully authenticated
  236. w.WriteHeader(http.StatusOK)
  237. w.Write([]byte("200 OK: Successfully Authenticated\n"))
  238. }
  239. func publicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatRoomCreator ChatRoomCreator, chatSessionRetriever ChatSessionRetriever, newChatRoom func() state.ChatRoom, logger *slog.Logger) {
  240. switch r.Method {
  241. case http.MethodGet:
  242. getPublicChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  243. case http.MethodPost:
  244. postPublicChatHandler(w, r, chatRoomCreator, newChatRoom, logger)
  245. default:
  246. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  247. }
  248. }
  249. func privateChatHandler(w http.ResponseWriter, r *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  250. switch r.Method {
  251. case http.MethodGet:
  252. getPrivateChatHandler(w, r, chatRoomRetriever, chatSessionRetriever, logger)
  253. default:
  254. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  255. }
  256. }
  257. // getPublicChatHandler handles the GET /chat/room/public endpoint.
  258. func getPublicChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  259. w.Header().Set("Content-Type", "application/json")
  260. rooms, err := chatRoomRetriever.AllChatRooms(state.PublicExchange)
  261. if err != nil {
  262. logger.Error("error in GET /chat/rooms/public", "err", err.Error())
  263. http.Error(w, "internal server error", http.StatusInternalServerError)
  264. return
  265. }
  266. out := make([]chatRoom, len(rooms))
  267. for i, room := range rooms {
  268. sessions := chatSessionRetriever.AllSessions(room.Cookie)
  269. cr := chatRoom{
  270. CreateTime: room.CreateTime,
  271. Name: room.Name,
  272. Participants: make([]userHandle, 0, len(sessions)),
  273. URL: room.URL().String(),
  274. }
  275. for _, sess := range sessions {
  276. cr.Participants = append(cr.Participants, userHandle{
  277. ID: sess.IdentScreenName().String(),
  278. ScreenName: sess.DisplayScreenName().String(),
  279. })
  280. }
  281. out[i] = cr
  282. }
  283. if err := json.NewEncoder(w).Encode(out); err != nil {
  284. http.Error(w, err.Error(), http.StatusInternalServerError)
  285. return
  286. }
  287. }
  288. // postPublicChatHandler handles the POST /chat/room/public endpoint.
  289. func postPublicChatHandler(w http.ResponseWriter, r *http.Request, chatRoomCreator ChatRoomCreator, newChatRoom func() state.ChatRoom, logger *slog.Logger) {
  290. input := chatRoomCreate{}
  291. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  292. http.Error(w, "invalid input", http.StatusBadRequest)
  293. return
  294. }
  295. input.Name = strings.TrimSpace(input.Name)
  296. if input.Name == "" || len(input.Name) > 50 {
  297. http.Error(w, "chat room name must be between 1 and 50 characters", http.StatusBadRequest)
  298. return
  299. }
  300. cr := newChatRoom()
  301. cr.Name = input.Name
  302. cr.Exchange = state.PublicExchange
  303. err := chatRoomCreator.CreateChatRoom(cr)
  304. switch {
  305. case errors.Is(err, state.ErrDupChatRoom):
  306. http.Error(w, "Chat room already exists.", http.StatusConflict)
  307. return
  308. case err != nil:
  309. logger.Error("error inserting chat room POST /chat/room/public", "err", err.Error())
  310. http.Error(w, "internal server error", http.StatusInternalServerError)
  311. return
  312. }
  313. w.WriteHeader(http.StatusCreated)
  314. fmt.Fprintln(w, "Chat room created successfully.")
  315. }
  316. // getPrivateChatHandler handles the GET /chat/room/private endpoint.
  317. func getPrivateChatHandler(w http.ResponseWriter, _ *http.Request, chatRoomRetriever ChatRoomRetriever, chatSessionRetriever ChatSessionRetriever, logger *slog.Logger) {
  318. w.Header().Set("Content-Type", "application/json")
  319. rooms, err := chatRoomRetriever.AllChatRooms(state.PrivateExchange)
  320. if err != nil {
  321. logger.Error("error in GET /chat/rooms/private", "err", err.Error())
  322. http.Error(w, "internal server error", http.StatusInternalServerError)
  323. return
  324. }
  325. out := make([]chatRoom, len(rooms))
  326. for i, room := range rooms {
  327. sessions := chatSessionRetriever.AllSessions(room.Cookie)
  328. cr := chatRoom{
  329. CreateTime: room.CreateTime,
  330. CreatorID: room.Creator.String(),
  331. Name: room.Name,
  332. Participants: make([]userHandle, 0, len(sessions)),
  333. URL: room.URL().String(),
  334. }
  335. for _, sess := range sessions {
  336. cr.Participants = append(cr.Participants, userHandle{
  337. ID: sess.IdentScreenName().String(),
  338. ScreenName: sess.DisplayScreenName().String(),
  339. })
  340. }
  341. out[i] = cr
  342. }
  343. if err := json.NewEncoder(w).Encode(out); err != nil {
  344. http.Error(w, err.Error(), http.StatusInternalServerError)
  345. return
  346. }
  347. }
  348. func instantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) {
  349. switch r.Method {
  350. case http.MethodPost:
  351. postInstantMessageHandler(w, r, messageRelayer, logger)
  352. default:
  353. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  354. }
  355. }
  356. // postIMHandler handles the POST /instant-message endpoint.
  357. func postInstantMessageHandler(w http.ResponseWriter, r *http.Request, messageRelayer MessageRelayer, logger *slog.Logger) {
  358. input := instantMessage{}
  359. if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
  360. http.Error(w, "malformed input", http.StatusBadRequest)
  361. return
  362. }
  363. tlv, err := wire.ICBMFragmentList(input.Text)
  364. if err != nil {
  365. logger.Error("error sending message POST /instant-message", "err", err.Error())
  366. http.Error(w, "internal server error", http.StatusInternalServerError)
  367. return
  368. }
  369. msg := wire.SNACMessage{
  370. Frame: wire.SNACFrame{
  371. FoodGroup: wire.ICBM,
  372. SubGroup: wire.ICBMChannelMsgToClient,
  373. },
  374. Body: wire.SNAC_0x04_0x07_ICBMChannelMsgToClient{
  375. ChannelID: 1,
  376. TLVUserInfo: wire.TLVUserInfo{
  377. ScreenName: input.From,
  378. },
  379. TLVRestBlock: wire.TLVRestBlock{
  380. TLVList: wire.TLVList{
  381. wire.NewTLV(wire.ICBMTLVAOLIMData, tlv),
  382. },
  383. },
  384. },
  385. }
  386. messageRelayer.RelayToScreenName(context.Background(), state.NewIdentScreenName(input.To), msg)
  387. w.WriteHeader(http.StatusOK)
  388. fmt.Fprintln(w, "Message sent successfully.")
  389. }