4
0

mgmt_api.go 18 KB

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