http.go 12 KB

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