mgmt_api.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. package http
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "net"
  9. "net/http"
  10. "os"
  11. "strings"
  12. "github.com/google/uuid"
  13. "github.com/mk6i/retro-aim-server/config"
  14. "github.com/mk6i/retro-aim-server/state"
  15. "github.com/mk6i/retro-aim-server/wire"
  16. )
  17. type userWithPassword struct {
  18. state.User
  19. Password string `json:"password,omitempty"`
  20. }
  21. type userSession struct {
  22. ScreenName string `json:"screen_name"`
  23. }
  24. type onlineUsers struct {
  25. Count int `json:"count"`
  26. Sessions []userSession `json:"sessions"`
  27. }
  28. type UserManager interface {
  29. AllUsers() ([]state.User, error)
  30. DeleteUser(screenName state.IdentScreenName) error
  31. InsertUser(u state.User) error
  32. SetUserPassword(u state.User) error
  33. User(screenName state.IdentScreenName) (*state.User, error)
  34. }
  35. type SessionRetriever interface {
  36. AllSessions() []*state.Session
  37. }
  38. func StartManagementAPI(cfg config.Config, userManager UserManager, sessionRetriever SessionRetriever, logger *slog.Logger) {
  39. mux := http.NewServeMux()
  40. mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
  41. userHandler(w, r, userManager, uuid.New, logger)
  42. })
  43. mux.HandleFunc("/user/password", func(w http.ResponseWriter, r *http.Request) {
  44. userPasswordHandler(w, r, userManager, uuid.New, logger)
  45. })
  46. mux.HandleFunc("/user/login", func(w http.ResponseWriter, r *http.Request) {
  47. loginHandler(w, r, userManager, logger)
  48. })
  49. mux.HandleFunc("/session", func(w http.ResponseWriter, r *http.Request) {
  50. sessionHandler(w, r, sessionRetriever)
  51. })
  52. addr := net.JoinHostPort(cfg.ApiHost, cfg.ApiPort)
  53. logger.Info("starting management API server", "addr", addr)
  54. if err := http.ListenAndServe(addr, mux); err != nil {
  55. logger.Error("unable to bind management API address address", "err", err.Error())
  56. os.Exit(1)
  57. }
  58. }
  59. func userHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  60. switch r.Method {
  61. case http.MethodDelete:
  62. deleteUserHandler(w, r, userManager, logger)
  63. case http.MethodGet:
  64. getUserHandler(w, r, userManager, logger)
  65. case http.MethodPost:
  66. postUserHandler(w, r, userManager, newUUID, logger)
  67. default:
  68. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  69. }
  70. }
  71. func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) {
  72. user, err := userFromBody(r)
  73. if err != nil {
  74. http.Error(w, err.Error(), http.StatusBadRequest)
  75. return
  76. }
  77. err = manager.DeleteUser(user.DisplayScreenName.IdentScreenName())
  78. switch {
  79. case errors.Is(err, state.ErrNoUser):
  80. http.Error(w, "user does not exist", http.StatusNotFound)
  81. return
  82. case err != nil:
  83. logger.Error("error deleting user DELETE /user", "err", err.Error())
  84. http.Error(w, "internal server error", http.StatusInternalServerError)
  85. return
  86. }
  87. w.WriteHeader(http.StatusNoContent)
  88. fmt.Fprintln(w, "User account successfully deleted.")
  89. }
  90. func userPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  91. switch r.Method {
  92. case http.MethodPut:
  93. putUserPasswordHandler(w, r, userManager, newUUID, logger)
  94. default:
  95. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  96. }
  97. }
  98. // putUserPasswordHandler handles the PUT /user/password endpoint.
  99. func putUserPasswordHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  100. user, err := userFromBody(r)
  101. if err != nil {
  102. http.Error(w, err.Error(), http.StatusBadRequest)
  103. return
  104. }
  105. user.AuthKey = newUUID().String()
  106. user.IdentScreenName = user.DisplayScreenName.IdentScreenName()
  107. if err := user.HashPassword(user.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.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([]userSession, 0),
  136. }
  137. for _, s := range allUsers {
  138. ou.Sessions = append(ou.Sessions, userSession{
  139. ScreenName: s.DisplayScreenName().String(),
  140. })
  141. }
  142. if err := json.NewEncoder(w).Encode(ou); err != nil {
  143. http.Error(w, err.Error(), http.StatusInternalServerError)
  144. return
  145. }
  146. }
  147. // getUserHandler handles the GET /user endpoint.
  148. func getUserHandler(w http.ResponseWriter, _ *http.Request, userManager UserManager, logger *slog.Logger) {
  149. w.Header().Set("Content-Type", "application/json")
  150. users, err := userManager.AllUsers()
  151. if err != nil {
  152. logger.Error("error in GET /user", "err", err.Error())
  153. http.Error(w, "internal server error", http.StatusInternalServerError)
  154. return
  155. }
  156. if err := json.NewEncoder(w).Encode(users); err != nil {
  157. http.Error(w, err.Error(), http.StatusInternalServerError)
  158. return
  159. }
  160. }
  161. // postUserHandler handles the POST /user endpoint.
  162. func postUserHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, newUUID func() uuid.UUID, logger *slog.Logger) {
  163. user, err := userFromBody(r)
  164. if err != nil {
  165. http.Error(w, err.Error(), http.StatusBadRequest)
  166. return
  167. }
  168. user.AuthKey = newUUID().String()
  169. if err := user.HashPassword(user.Password); err != nil {
  170. logger.Error("error hashing user password in POST /user", "err", err.Error())
  171. http.Error(w, "internal server error", http.StatusInternalServerError)
  172. return
  173. }
  174. err = userManager.InsertUser(user.User)
  175. switch {
  176. case errors.Is(err, state.ErrDupUser):
  177. http.Error(w, "user already exists", http.StatusConflict)
  178. return
  179. case err != nil:
  180. logger.Error("error inserting user POST /user", "err", err.Error())
  181. http.Error(w, "internal server error", http.StatusInternalServerError)
  182. return
  183. }
  184. w.WriteHeader(http.StatusCreated)
  185. fmt.Fprintln(w, "User account created successfully.")
  186. }
  187. func userFromBody(r *http.Request) (userWithPassword, error) {
  188. user := userWithPassword{}
  189. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  190. return userWithPassword{}, errors.New("malformed input")
  191. }
  192. user.IdentScreenName = user.DisplayScreenName.IdentScreenName()
  193. return user, nil
  194. }
  195. // loginHandler is a temporary endpoint for validating user credentials for
  196. // chivanet. do not rely on this endpoint, as it will be eventually removed.
  197. func loginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  198. authHeader := r.Header.Get("Authorization")
  199. if authHeader == "" {
  200. // No authentication header found
  201. w.WriteHeader(http.StatusUnauthorized)
  202. w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
  203. w.Write([]byte("401 Unauthorized\n"))
  204. return
  205. }
  206. auth := strings.SplitN(authHeader, " ", 2)
  207. if len(auth) != 2 || auth[0] != "Basic" {
  208. w.WriteHeader(http.StatusUnauthorized)
  209. w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
  210. return
  211. }
  212. payload, err := base64.StdEncoding.DecodeString(auth[1])
  213. if err != nil {
  214. w.WriteHeader(http.StatusUnauthorized)
  215. w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
  216. return
  217. }
  218. pair := strings.SplitN(string(payload), ":", 2)
  219. if len(pair) != 2 {
  220. w.WriteHeader(http.StatusUnauthorized)
  221. w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
  222. return
  223. }
  224. username, password := state.NewIdentScreenName(pair[0]), pair[1]
  225. user, err := userManager.User(username)
  226. if err != nil {
  227. w.WriteHeader(http.StatusInternalServerError)
  228. w.Write([]byte("500 InternalServerError\n"))
  229. logger.Error("error getting user", "err", err.Error())
  230. return
  231. }
  232. if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
  233. w.WriteHeader(http.StatusUnauthorized)
  234. w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
  235. return
  236. }
  237. // Successfully authenticated
  238. w.WriteHeader(http.StatusOK)
  239. w.Write([]byte("200 OK: Successfully Authenticated\n"))
  240. }