http.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. package toc
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/hex"
  6. "errors"
  7. "fmt"
  8. "html/template"
  9. "io"
  10. "net/http"
  11. "golang.org/x/net/html"
  12. "github.com/mk6i/retro-aim-server/state"
  13. "github.com/mk6i/retro-aim-server/wire"
  14. )
  15. // profileTpl is the profile lookup response go template.
  16. const profileTpl = `
  17. <HTML><HEAD><TITLE>Profile Lookup</TITLE></HEAD><BODY>
  18. Username : <B>{{- .ScreenName -}}</B><BR><BR>
  19. {{ .Profile }}
  20. </BODY></HTML>`
  21. // directoryTpl is the directory search response go template.
  22. const directoryTpl = `
  23. <HTML><HEAD><TITLE>Retro AIM Server</TITLE></HEAD><BODY><H3>Dir Results</H3>
  24. {{- if .Results -}}
  25. <TABLE>
  26. {{- range .Results -}}
  27. <TR><TD>
  28. {{- if .FirstName}}<B>First Name:</B> {{.FirstName}}<BR>{{- end -}}
  29. {{- if .MiddleName}}<B>Middle Name:</B> {{.MiddleName}}<BR>{{- end -}}
  30. {{- if .LastName}}<B>Last Name:</B> {{.LastName}}<BR>{{- end -}}
  31. {{- if .MaidenName}}<B>Maiden Name:</B> {{.MaidenName}}<BR>{{- end -}}
  32. {{- if .Country}}<B>Country:</B> {{.Country}}<BR>{{- end -}}
  33. {{- if .State}}<B>State:</B> {{.State}}<BR>{{- end -}}
  34. {{- if .City}}<B>City:</B> {{.City}}<BR>{{- end -}}
  35. {{- if .NickName}}<B>Nick Name:</B> {{.NickName}}<BR>{{- end -}}
  36. {{- if .ZIP}}<B>ZIP Code:</B> {{.ZIP}}<BR>{{- end -}}
  37. {{- if .Address}}<B>Address :</B> {{.Address}}<BR>{{- end -}}
  38. </TD></TR>
  39. {{- end -}}
  40. </TABLE>
  41. {{- else -}}
  42. <BR>No results found.
  43. {{- end -}}
  44. </BODY></HTML>`
  45. // NewServeMux creates and returns an HTTP mux that serves all TOC routes.
  46. func (s OSCARProxy) NewServeMux() http.Handler {
  47. mux := http.NewServeMux()
  48. mux.Handle("GET /info", s.AuthMiddleware(http.HandlerFunc(s.ProfileHandler)))
  49. mux.Handle("GET /dir_info", s.AuthMiddleware(http.HandlerFunc(s.DirInfoHandler)))
  50. mux.Handle("GET /dir_search", s.AuthMiddleware(http.HandlerFunc(s.DirSearchHandler)))
  51. return mux
  52. }
  53. // AuthMiddleware is an HTTP middleware that enforces authentication using an
  54. // authorization cookie provided as a query parameter. It validates and decrypts
  55. // the cookie before allowing the request to proceed.
  56. //
  57. // If the `cookie` query parameter is missing or invalid, the middleware
  58. // responds with an appropriate HTTP error:
  59. // - 400 Bad Request if the `cookie` parameter is missing.
  60. // - 403 Forbidden if the cookie is invalid or cannot be decrypted.
  61. //
  62. // Requests with a valid cookie are passed to the next handler.
  63. func (s OSCARProxy) AuthMiddleware(next http.Handler) http.Handler {
  64. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  65. ctx := r.Context()
  66. cookie := r.URL.Query().Get("cookie")
  67. if cookie == "" {
  68. http.Error(w, "required `cookie` param is missing", http.StatusBadRequest)
  69. return
  70. }
  71. data, err := hex.DecodeString(cookie)
  72. if err != nil {
  73. s.Logger.DebugContext(ctx, "error decoding string", "err", err.Error())
  74. http.Error(w, "invalid auth cookie", http.StatusForbidden)
  75. return
  76. }
  77. if _, err = s.CookieBaker.Crack(data); err != nil {
  78. s.Logger.DebugContext(ctx, "error cracking auth cookie", "err", err.Error())
  79. http.Error(w, "invalid auth cookie", http.StatusForbidden)
  80. return
  81. }
  82. next.ServeHTTP(w, r)
  83. })
  84. }
  85. // ProfileHandler handles requests to retrieve a user's profile information.
  86. // It queries the LocateService to fetch profile data for the specified user.
  87. //
  88. // The request must include the following query parameters:
  89. // - `from`: The screen name of the user making the request.
  90. // - `user`: The screen name of the user whose profile is being requested.
  91. //
  92. // If any required parameter is missing, it responds with a 400 Bad Request.
  93. // If the requested user is unavailable, it responds with a 404 Not Found.
  94. func (s OSCARProxy) ProfileHandler(w http.ResponseWriter, r *http.Request) {
  95. from := r.URL.Query().Get("from")
  96. if from == "" {
  97. http.Error(w, "required `from` param is missing", http.StatusBadRequest)
  98. return
  99. }
  100. user := r.URL.Query().Get("user")
  101. if user == "" {
  102. http.Error(w, "required `user` param is missing", http.StatusBadRequest)
  103. return
  104. }
  105. sess := state.NewSession()
  106. sess.SetIdentScreenName(state.NewIdentScreenName(from))
  107. inBody := wire.SNAC_0x02_0x05_LocateUserInfoQuery{
  108. Type: uint16(wire.LocateTypeSig),
  109. ScreenName: user,
  110. }
  111. ctx := r.Context()
  112. info, err := s.LocateService.UserInfoQuery(ctx, sess, wire.SNACFrame{}, inBody)
  113. if err != nil {
  114. s.logAndReturn500(ctx, w, fmt.Errorf("LocateService.UserInfoQuery: %w", err))
  115. return
  116. }
  117. switch v := info.Body.(type) {
  118. case wire.SNACError:
  119. if v.Code == wire.ErrorCodeNotLoggedOn {
  120. http.Error(w, "user is unavailable", http.StatusNotFound)
  121. } else {
  122. s.logAndReturn500(ctx, w, fmt.Errorf("LocateService.UserInfoQuery error code: %d", v.Code))
  123. }
  124. case wire.SNAC_0x02_0x06_LocateUserInfoReply:
  125. profile, hasProf := v.LocateInfo.Bytes(wire.LocateTLVTagsInfoSigData)
  126. if !hasProf {
  127. s.logAndReturn500(ctx, w, errors.New("LocateInfo.Bytes: missing wire.LocateTLVTagsInfoSigData"))
  128. return
  129. }
  130. t, err := template.New("results").Parse(profileTpl)
  131. if err != nil {
  132. s.logAndReturn500(ctx, w, fmt.Errorf("template.New: %w", err))
  133. return
  134. }
  135. pd := struct {
  136. ScreenName string
  137. Profile string
  138. }{
  139. ScreenName: user,
  140. Profile: extractBodyContent(profile),
  141. }
  142. if err := t.Execute(w, pd); err != nil {
  143. s.logAndReturn500(ctx, w, fmt.Errorf("t.Execute: %w", err))
  144. }
  145. default:
  146. s.logAndReturn500(ctx, w, fmt.Errorf("unknown response type: %T", v))
  147. }
  148. }
  149. // DirInfoHandler handles requests to retrieve directory information for a user.
  150. // It queries the LocateService to fetch directory details associated with the
  151. // given screen name.
  152. //
  153. // The request must include the following query parameter:
  154. // - `user`: The screen name of the user whose directory info is being requested.
  155. //
  156. // If the `user` parameter is missing, it responds with a 400 Bad Request.
  157. // If no directory information is found, it responds with a 404 Not Found.
  158. func (s OSCARProxy) DirInfoHandler(w http.ResponseWriter, request *http.Request) {
  159. user := request.URL.Query().Get("user")
  160. if user == "" {
  161. http.Error(w, "required `user` param is missing", http.StatusBadRequest)
  162. return
  163. }
  164. inBody := wire.SNAC_0x02_0x0B_LocateGetDirInfo{
  165. ScreenName: user,
  166. }
  167. ctx := request.Context()
  168. info, err := s.LocateService.DirInfo(ctx, wire.SNACFrame{}, inBody)
  169. if err != nil {
  170. s.logAndReturn500(ctx, w, fmt.Errorf("LocateService.DirInfo: %w", err))
  171. return
  172. }
  173. switch v := info.Body.(type) {
  174. case wire.SNAC_0x02_0x0C_LocateGetDirReply:
  175. if len(v.TLVList) > 0 {
  176. s.outputSearchResults(ctx, w, v.TLVBlock)
  177. } else {
  178. http.Error(w, "no user directory info found", http.StatusNotFound)
  179. }
  180. default:
  181. s.logAndReturn500(ctx, w, fmt.Errorf("LocateService.DirInfo: unknown response type: %T", v))
  182. }
  183. }
  184. // DirSearchHandler handles requests to perform a directory search based on
  185. // various criteria. It queries the DirSearchService to find users matching the
  186. // specified parameters. There are 3 search modes: name, email, keyword.
  187. //
  188. // -Named-based search is toggled by the presence of either `first_name`
  189. // and/or `last_name` params. The following search params can be passed:
  190. // -`first_name`
  191. // -`middle_name`
  192. // -`last_name`
  193. // -`maiden_name`
  194. // -`city`
  195. // -`state`
  196. // -`country`
  197. // -Email-based search is triggered by the`email` param.
  198. // -Keyword-based search is triggered by the `keyword` param.
  199. //
  200. // If the search is missing required name parameters, it responds with a 400
  201. // Bad Request.
  202. func (s OSCARProxy) DirSearchHandler(w http.ResponseWriter, r *http.Request) {
  203. inBody := wire.SNAC_0x0F_0x02_InfoQuery{}
  204. q := r.URL.Query()
  205. switch {
  206. case q.Has("first_name") || q.Has("last_name"):
  207. if val := q.Get("first_name"); val != "" {
  208. inBody.Append(wire.NewTLVBE(wire.ODirTLVFirstName, val))
  209. }
  210. if val := q.Get("middle_name"); val != "" {
  211. inBody.Append(wire.NewTLVBE(wire.ODirTLVMiddleName, val))
  212. }
  213. if val := q.Get("last_name"); val != "" {
  214. inBody.Append(wire.NewTLVBE(wire.ODirTLVLastName, val))
  215. }
  216. if val := q.Get("maiden_name"); val != "" {
  217. inBody.Append(wire.NewTLVBE(wire.ODirTLVMaidenName, val))
  218. }
  219. if val := q.Get("city"); val != "" {
  220. inBody.Append(wire.NewTLVBE(wire.ODirTLVCity, val))
  221. }
  222. if val := q.Get("state"); val != "" {
  223. inBody.Append(wire.NewTLVBE(wire.ODirTLVState, val))
  224. }
  225. if val := q.Get("country"); val != "" {
  226. inBody.Append(wire.NewTLVBE(wire.ODirTLVCountry, val))
  227. }
  228. case q.Has("email"):
  229. inBody.Append(wire.NewTLVBE(wire.ODirTLVEmailAddress, q.Get("email")))
  230. case q.Has("keyword"):
  231. inBody.Append(wire.NewTLVBE(wire.ODirTLVInterest, q.Get("keyword")))
  232. }
  233. ctx := r.Context()
  234. info, err := s.DirSearchService.InfoQuery(ctx, wire.SNACFrame{}, inBody)
  235. if err != nil {
  236. s.logAndReturn500(ctx, w, fmt.Errorf("DirSearchService.InfoQuery: %w", err))
  237. return
  238. }
  239. switch v := info.Body.(type) {
  240. case wire.SNAC_0x0F_0x03_InfoReply:
  241. switch v.Status {
  242. case wire.ODirSearchResponseNameMissing:
  243. http.Error(w, "missing search parameters", http.StatusBadRequest)
  244. case wire.ODirSearchResponseOK:
  245. s.outputSearchResults(nil, w, v.Results.List...)
  246. default:
  247. s.logAndReturn500(ctx, w, fmt.Errorf("DirSearchService.InfoQuery unknown status: %d", v.Status))
  248. }
  249. default:
  250. s.logAndReturn500(ctx, w, fmt.Errorf("DirSearchService.InfoQuery: unknown response type: %T", v))
  251. }
  252. }
  253. func (s OSCARProxy) outputSearchResults(ctx context.Context, w http.ResponseWriter, users ...wire.TLVBlock) {
  254. type DirSearchResult struct {
  255. FirstName string
  256. MiddleName string
  257. LastName string
  258. MaidenName string
  259. Country string
  260. State string
  261. City string
  262. NickName string
  263. ZIP string
  264. Address string
  265. }
  266. type PageData struct {
  267. Results []DirSearchResult
  268. }
  269. results := make([]DirSearchResult, 0, len(users))
  270. for _, result := range users {
  271. rec := DirSearchResult{}
  272. rec.FirstName, _ = result.String(wire.ODirTLVFirstName)
  273. rec.MiddleName, _ = result.String(wire.ODirTLVMiddleName)
  274. rec.LastName, _ = result.String(wire.ODirTLVLastName)
  275. rec.MaidenName, _ = result.String(wire.ODirTLVMaidenName)
  276. rec.Country, _ = result.String(wire.ODirTLVCountry)
  277. rec.State, _ = result.String(wire.ODirTLVState)
  278. rec.City, _ = result.String(wire.ODirTLVCity)
  279. rec.NickName, _ = result.String(wire.ODirTLVNickName)
  280. rec.ZIP, _ = result.String(wire.ODirTLVZIP)
  281. rec.Address, _ = result.String(wire.ODirTLVAddress)
  282. results = append(results, rec)
  283. }
  284. t, err := template.New("results").Parse(directoryTpl)
  285. if err != nil {
  286. s.logAndReturn500(ctx, w, fmt.Errorf("template.New: %w", err))
  287. return
  288. }
  289. if err := t.Execute(w, PageData{Results: results}); err != nil {
  290. s.logAndReturn500(ctx, w, fmt.Errorf("t.Execute: %w", err))
  291. }
  292. }
  293. func (s OSCARProxy) logAndReturn500(ctx context.Context, w http.ResponseWriter, err error) {
  294. s.Logger.ErrorContext(ctx, "internal service error", "err", err.Error())
  295. http.Error(w, "internal server error", http.StatusInternalServerError)
  296. }
  297. // extractBodyContent parses an HTML string and extracts the content within <BODY>...</BODY> tags.
  298. func extractBodyContent(htmlContent []byte) string {
  299. tokenizer := html.NewTokenizer(bytes.NewReader(htmlContent))
  300. var bodyContent bytes.Buffer
  301. inBody := false
  302. for {
  303. switch tokenizer.Next() {
  304. case html.ErrorToken:
  305. if err := tokenizer.Err(); err != nil && err != io.EOF {
  306. return "unable to read profile"
  307. }
  308. return bodyContent.String()
  309. case html.StartTagToken:
  310. token := tokenizer.Token()
  311. if token.Data == "body" {
  312. inBody = true
  313. }
  314. case html.EndTagToken:
  315. token := tokenizer.Token()
  316. if token.Data == "body" {
  317. inBody = false
  318. }
  319. case html.TextToken:
  320. if inBody {
  321. bodyContent.Write(tokenizer.Text())
  322. }
  323. }
  324. }
  325. return ""
  326. }