mgmt_api.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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 string) error
  31. InsertUser(u state.User) error
  32. SetUserPassword(u state.User) error
  33. User(screenName string) (*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. newUser := func() state.User {
  41. return state.User{AuthKey: uuid.New().String()}
  42. }
  43. mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
  44. userHandler(w, r, userManager, newUser, logger)
  45. })
  46. mux.HandleFunc("/user/password", func(w http.ResponseWriter, r *http.Request) {
  47. userPasswordHandler(w, r, userManager, newUser, logger)
  48. })
  49. mux.HandleFunc("/user/login", func(w http.ResponseWriter, r *http.Request) {
  50. loginHandler(w, r, userManager, logger)
  51. })
  52. mux.HandleFunc("/session", func(w http.ResponseWriter, r *http.Request) {
  53. sessionHandler(w, r, sessionRetriever)
  54. })
  55. addr := net.JoinHostPort(cfg.ApiHost, cfg.ApiPort)
  56. logger.Info("starting management API server", "addr", addr)
  57. if err := http.ListenAndServe(addr, mux); err != nil {
  58. logger.Error("unable to bind management API address address", "err", err.Error())
  59. os.Exit(1)
  60. }
  61. }
  62. func userHandler(
  63. w http.ResponseWriter,
  64. r *http.Request,
  65. userManager UserManager,
  66. newUser func() state.User,
  67. logger *slog.Logger,
  68. ) {
  69. switch r.Method {
  70. case http.MethodDelete:
  71. deleteUserHandler(w, r, userManager, logger)
  72. case http.MethodGet:
  73. getUserHandler(w, r, userManager, logger)
  74. case http.MethodPost:
  75. postUserHandler(w, r, userManager, newUser, logger)
  76. default:
  77. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  78. }
  79. }
  80. func deleteUserHandler(w http.ResponseWriter, r *http.Request, manager UserManager, logger *slog.Logger) {
  81. user := state.User{}
  82. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  83. http.Error(w, "malformed input", http.StatusBadRequest)
  84. return
  85. }
  86. err := manager.DeleteUser(user.ScreenName)
  87. switch {
  88. case errors.Is(err, state.ErrNoUser):
  89. http.Error(w, "user does not exist", http.StatusNotFound)
  90. return
  91. case err != nil:
  92. logger.Error("error deleting user DELETE /user", "err", err.Error())
  93. http.Error(w, "internal server error", http.StatusInternalServerError)
  94. return
  95. }
  96. w.WriteHeader(http.StatusNoContent)
  97. fmt.Fprintln(w, "User account successfully deleted.")
  98. }
  99. func userPasswordHandler(
  100. w http.ResponseWriter,
  101. r *http.Request,
  102. userManager UserManager,
  103. userFactory func() state.User,
  104. logger *slog.Logger,
  105. ) {
  106. switch r.Method {
  107. case http.MethodPut:
  108. putUserPasswordHandler(w, r, userManager, userFactory, logger)
  109. default:
  110. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  111. }
  112. }
  113. // putUserPasswordHandler handles the PUT /user/password endpoint.
  114. func putUserPasswordHandler(
  115. w http.ResponseWriter,
  116. r *http.Request,
  117. userManager UserManager,
  118. newUser func() state.User,
  119. logger *slog.Logger,
  120. ) {
  121. user := userWithPassword{
  122. User: newUser(),
  123. }
  124. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  125. http.Error(w, "malformed input", http.StatusBadRequest)
  126. return
  127. }
  128. if err := user.HashPassword(user.Password); err != nil {
  129. logger.Error("error hashing user password in PUT /user/password", "err", err.Error())
  130. http.Error(w, "internal server error", http.StatusInternalServerError)
  131. return
  132. }
  133. if err := userManager.SetUserPassword(user.User); err != nil {
  134. switch {
  135. case errors.Is(err, state.ErrNoUser):
  136. http.Error(w, "user does not exist", http.StatusNotFound)
  137. return
  138. case err != nil:
  139. logger.Error("error updating user password PUT /user/password", "err", err.Error())
  140. http.Error(w, "internal server error", http.StatusInternalServerError)
  141. return
  142. }
  143. }
  144. w.WriteHeader(http.StatusNoContent)
  145. }
  146. // sessionHandler handles GET /session
  147. func sessionHandler(w http.ResponseWriter, r *http.Request, sessionRetriever SessionRetriever) {
  148. w.Header().Set("Content-Type", "application/json")
  149. if r.Method != http.MethodGet {
  150. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  151. return
  152. }
  153. allUsers := sessionRetriever.AllSessions()
  154. ou := onlineUsers{
  155. Count: len(allUsers),
  156. Sessions: make([]userSession, 0),
  157. }
  158. for _, s := range allUsers {
  159. ou.Sessions = append(ou.Sessions, userSession{
  160. ScreenName: s.ScreenName(),
  161. })
  162. }
  163. if err := json.NewEncoder(w).Encode(ou); err != nil {
  164. http.Error(w, err.Error(), http.StatusInternalServerError)
  165. return
  166. }
  167. }
  168. // getUserHandler handles the GET /user endpoint.
  169. func getUserHandler(w http.ResponseWriter, _ *http.Request, userManager UserManager, logger *slog.Logger) {
  170. w.Header().Set("Content-Type", "application/json")
  171. users, err := userManager.AllUsers()
  172. if err != nil {
  173. logger.Error("error in GET /user", "err", err.Error())
  174. http.Error(w, "internal server error", http.StatusInternalServerError)
  175. return
  176. }
  177. if err := json.NewEncoder(w).Encode(users); err != nil {
  178. http.Error(w, err.Error(), http.StatusInternalServerError)
  179. return
  180. }
  181. }
  182. // postUserHandler handles the POST /user endpoint.
  183. func postUserHandler(
  184. w http.ResponseWriter,
  185. r *http.Request,
  186. userManager UserManager,
  187. newUser func() state.User,
  188. logger *slog.Logger,
  189. ) {
  190. user := userWithPassword{
  191. User: newUser(),
  192. }
  193. if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
  194. http.Error(w, "malformed input", http.StatusBadRequest)
  195. return
  196. }
  197. if err := user.HashPassword(user.Password); err != nil {
  198. logger.Error("error hashing user password in POST /user", "err", err.Error())
  199. http.Error(w, "internal server error", http.StatusInternalServerError)
  200. return
  201. }
  202. err := userManager.InsertUser(user.User)
  203. switch {
  204. case errors.Is(err, state.ErrDupUser):
  205. http.Error(w, "user already exists", http.StatusConflict)
  206. return
  207. case err != nil:
  208. logger.Error("error inserting user POST /user", "err", err.Error())
  209. http.Error(w, "internal server error", http.StatusInternalServerError)
  210. return
  211. }
  212. w.WriteHeader(http.StatusCreated)
  213. fmt.Fprintln(w, "User account created successfully.")
  214. }
  215. // loginHandler is a temporary endpoint for validating user credentials for
  216. // chivanet. do not rely on this endpoint, as it will be eventually removed.
  217. func loginHandler(w http.ResponseWriter, r *http.Request, userManager UserManager, logger *slog.Logger) {
  218. authHeader := r.Header.Get("Authorization")
  219. if authHeader == "" {
  220. // No authentication header found
  221. w.WriteHeader(http.StatusUnauthorized)
  222. w.Header().Set("WWW-Authenticate", `Basic realm="User Login"`)
  223. w.Write([]byte("401 Unauthorized\n"))
  224. return
  225. }
  226. auth := strings.SplitN(authHeader, " ", 2)
  227. if len(auth) != 2 || auth[0] != "Basic" {
  228. w.WriteHeader(http.StatusUnauthorized)
  229. w.Write([]byte("401 Unauthorized: Missing Basic prefix\n"))
  230. return
  231. }
  232. payload, err := base64.StdEncoding.DecodeString(auth[1])
  233. if err != nil {
  234. w.WriteHeader(http.StatusUnauthorized)
  235. w.Write([]byte("401 Unauthorized: Invalid Base64 Encoding\n"))
  236. return
  237. }
  238. pair := strings.SplitN(string(payload), ":", 2)
  239. if len(pair) != 2 {
  240. w.WriteHeader(http.StatusUnauthorized)
  241. w.Write([]byte("401 Unauthorized: Invalid Authentication Token\n"))
  242. return
  243. }
  244. username, password := pair[0], pair[1]
  245. user, err := userManager.User(username)
  246. if err != nil {
  247. w.WriteHeader(http.StatusInternalServerError)
  248. w.Write([]byte("500 InternalServerError\n"))
  249. logger.Error("error getting user", "err", err.Error())
  250. return
  251. }
  252. if user == nil || !user.ValidateHash(wire.StrongMD5PasswordHash(password, user.AuthKey)) {
  253. w.WriteHeader(http.StatusUnauthorized)
  254. w.Write([]byte("401 Unauthorized: Invalid Credentials\n"))
  255. return
  256. }
  257. // Successfully authenticated
  258. w.WriteHeader(http.StatusOK)
  259. w.Write([]byte("200 OK: Successfully Authenticated\n"))
  260. }