http.go 13 KB

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