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