github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/routes/api.go (about) 1 package routes 2 3 import ( 4 "encoding/json" 5 "errors" 6 "net/http" 7 "strconv" 8 "strings" 9 10 c "github.com/Azareal/Gosora/common" 11 ) 12 13 // TODO: Make this a static file somehow? Is it possible for us to put this file somewhere else? 14 // TODO: Add an API so that plugins can register disallowed areas. E.g. /guilds/join for plugin_guilds 15 func RobotsTxt(w http.ResponseWriter, r *http.Request) c.RouteError { 16 // TODO: Do we have to put * or something at the end of the paths? 17 _, _ = w.Write([]byte(`User-agent: * 18 Disallow: /panel/* 19 Disallow: /topics/create/ 20 Disallow: /user/edit/* 21 Disallow: /accounts/* 22 Disallow: /report/* 23 `)) 24 return nil 25 } 26 27 var sitemapPageCap = 40000 // 40k, bump it up to 50k once we gzip this? Does brotli work on sitemaps? 28 29 func writeXMLHeader(w http.ResponseWriter, r *http.Request) { 30 w.Header().Set("Content-Type", "application/xml") 31 w.Write([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")) 32 } 33 34 // TODO: Keep track of when a sitemap was last modifed and add a lastmod element for it 35 func SitemapXml(w http.ResponseWriter, r *http.Request) c.RouteError { 36 var s string 37 if c.Config.SslSchema { 38 s = "s" 39 } 40 sitemapItem := func(path string) { 41 w.Write([]byte(`<sitemap> 42 <loc>http` + s + `://` + c.Site.URL + "/" + path + `</loc> 43 </sitemap> 44 `)) 45 } 46 writeXMLHeader(w, r) 47 w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n")) 48 sitemapItem("sitemaps/topics.xml") 49 //sitemapItem("sitemaps/forums.xml") 50 //sitemapItem("sitemaps/users.xml") 51 w.Write([]byte("</sitemapindex>")) 52 53 return nil 54 } 55 56 type FuzzyRoute struct { 57 Path string 58 Handle func(http.ResponseWriter, *http.Request, int) c.RouteError 59 } 60 61 // TODO: Add a sitemap API and clean things up 62 // TODO: ^-- Make sure that the API is concurrent 63 // TODO: Add a social group sitemap 64 var sitemapRoutes = map[string]func(http.ResponseWriter, *http.Request) c.RouteError{ 65 "forums.xml": SitemapForums, 66 "topics.xml": SitemapTopics, 67 } 68 69 // TODO: Use a router capable of parsing this rather than hard-coding the logic in 70 var fuzzySitemapRoutes = map[string]FuzzyRoute{ 71 "topics_page_": {"topics_page_(%d).xml", SitemapTopic}, 72 } 73 74 func sitemapSwitch(w http.ResponseWriter, r *http.Request) c.RouteError { 75 path := r.URL.Path[len("/sitemaps/"):] 76 for name, fuzzy := range fuzzySitemapRoutes { 77 if strings.HasPrefix(path, name) && strings.HasSuffix(path, ".xml") { 78 spath := strings.TrimPrefix(path, name) 79 spath = strings.TrimSuffix(spath, ".xml") 80 page, err := strconv.Atoi(spath) 81 if err != nil { 82 // ? What's this? Do we need it? Was it just a quick trace? 83 c.DebugLogf("Unable to convert string '%s' to integer in fuzzy route", spath) 84 return c.NotFound(w, r, nil) 85 } 86 return fuzzy.Handle(w, r, page) 87 } 88 } 89 90 route, ok := sitemapRoutes[path] 91 if !ok { 92 return c.NotFound(w, r, nil) 93 } 94 return route(w, r) 95 } 96 97 func SitemapForums(w http.ResponseWriter, r *http.Request) c.RouteError { 98 var s string 99 if c.Config.SslSchema { 100 s = "s" 101 } 102 sitemapItem := func(path string) { 103 w.Write([]byte(`<url> 104 <loc>http` + s + `://` + c.Site.URL + path + `</loc> 105 </url> 106 `)) 107 } 108 109 group, err := c.Groups.Get(c.GuestUser.Group) 110 if err != nil { 111 return c.SilentInternalErrorXML(errors.New("The guest group doesn't exist for some reason"), w, r) 112 } 113 114 writeXMLHeader(w, r) 115 w.Write([]byte("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n")) 116 117 for _, fid := range group.CanSee { 118 // Avoid data races by copying the struct into something we can freely mold without worrying about breaking something somewhere else 119 f := c.Forums.DirtyGet(fid).Copy() 120 if f.ParentID == 0 && f.Name != "" && f.Active { 121 sitemapItem(c.BuildForumURL(c.NameToSlug(f.Name), f.ID)) 122 } 123 } 124 125 w.Write([]byte("</urlset>")) 126 return nil 127 } 128 129 // TODO: Add a global ratelimit. 10 50MB files (smaller if compressed better) per minute? 130 // ? We might have problems with banned users, if they have fewer ViewTopic permissions than guests as they'll be able to see this list. Then again, a banned user could just logout to see it 131 func SitemapTopics(w http.ResponseWriter, r *http.Request) c.RouteError { 132 var s string 133 if c.Config.SslSchema { 134 s = "s" 135 } 136 sitemapItem := func(path string) { 137 w.Write([]byte(`<sitemap> 138 <loc>http` + s + `://` + c.Site.URL + "/" + path + `</loc> 139 </sitemap> 140 `)) 141 } 142 143 group, err := c.Groups.Get(c.GuestUser.Group) 144 if err != nil { 145 return c.SilentInternalErrorXML(errors.New("The guest group doesn't exist for some reason"), w, r) 146 } 147 148 var visibleForums []c.Forum 149 for _, fid := range group.CanSee { 150 forum := c.Forums.DirtyGet(fid) 151 if forum.Name != "" && forum.Active { 152 visibleForums = append(visibleForums, forum.Copy()) 153 } 154 } 155 156 topicCount, err := c.TopicCountInForums(visibleForums) 157 if err != nil { 158 return c.InternalErrorXML(err, w, r) 159 } 160 161 pageCount := topicCount / sitemapPageCap 162 //log.Print("topicCount", topicCount) 163 //log.Print("pageCount", pageCount) 164 writeXMLHeader(w, r) 165 w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n")) 166 for i := 0; i <= pageCount; i++ { 167 sitemapItem("sitemaps/topics_page_" + strconv.Itoa(i) + ".xml") 168 } 169 w.Write([]byte("</sitemapindex>")) 170 return nil 171 } 172 173 func SitemapTopic(w http.ResponseWriter, r *http.Request, page int) c.RouteError { 174 /*var s string 175 if c.Config.SslSchema { 176 s = "s" 177 } 178 var sitemapItem = func(path string) { 179 w.Write([]byte(`<url> 180 <loc>http` + s + `://` + c.Site.URL + "/" + path + `</loc> 181 </url> 182 `)) 183 }*/ 184 185 group, err := c.Groups.Get(c.GuestUser.Group) 186 if err != nil { 187 return c.SilentInternalErrorXML(errors.New("The guest group doesn't exist for some reason"), w, r) 188 } 189 190 var visibleForums []c.Forum 191 for _, fid := range group.CanSee { 192 forum := c.Forums.DirtyGet(fid) 193 if forum.Name != "" && forum.Active { 194 visibleForums = append(visibleForums, forum.Copy()) 195 } 196 } 197 198 argList, qlist := c.ForumListToArgQ(visibleForums) 199 topicCount, err := c.ArgQToTopicCount(argList, qlist) 200 if err != nil { 201 return c.InternalErrorXML(err, w, r) 202 } 203 204 pageCount := topicCount / sitemapPageCap 205 //log.Print("topicCount", topicCount) 206 //log.Print("pageCount", pageCount) 207 //log.Print("page",page) 208 if page > pageCount { 209 page = pageCount 210 } 211 212 writeXMLHeader(w, r) 213 w.Write([]byte("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n")) 214 215 w.Write([]byte("</urlset>")) 216 return nil 217 } 218 219 func SitemapUsers(w http.ResponseWriter, r *http.Request) c.RouteError { 220 writeXMLHeader(w, r) 221 w.Write([]byte("<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n")) 222 return nil 223 } 224 225 type JsonMe struct { 226 User *c.MeUser 227 Site MeSite 228 } 229 230 // We don't want to expose too much information about the site, so we'll make this a small subset of c.site 231 type MeSite struct { 232 MaxReqSize int 233 StaticPrefix string 234 } 235 236 // APIMe returns information about the current logged-in user 237 // TODO: Find some way to stop intermediaries from doing compression to avoid the BREACH attack 238 // TODO: Decouple site settings into a different API? I'd like to avoid having too many requests, if possible, maybe we can use a different name for this? 239 func APIMe(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError { 240 // TODO: Don't make this too JSON dependent so that we can swap in newer more efficient formats 241 h := w.Header() 242 h.Set("Content-Type", "application/json") 243 // We don't want an intermediary accidentally caching this 244 // TODO: Use this header anywhere with a user check? 245 h.Set("Cache-Control", "private") 246 247 me := JsonMe{u.Me(), MeSite{c.Site.MaxRequestSize, c.StaticFiles.Prefix}} 248 jsonBytes, err := json.Marshal(me) 249 if err != nil { 250 return c.InternalErrorJS(err, w, r) 251 } 252 w.Write(jsonBytes) 253 254 return nil 255 } 256 257 func OpenSearchXml(w http.ResponseWriter, r *http.Request) c.RouteError { 258 w.Header().Set("Content-Type", "application/xml") 259 furl := "http" 260 if c.Config.SslSchema { 261 furl += "s" 262 } 263 furl += "://" + c.Site.URL 264 w.Write([]byte(`<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/"> 265 <ShortName>` + c.Site.Name + `</ShortName> 266 <InputEncoding>UTF-8</InputEncoding> 267 <Url type="text/html" template="` + furl + `/topics/?q={searchTerms}"/> 268 <Url type="application/opensearchdescription+xml" rel="self" template="` + furl + `/opensearch.xml"/> 269 <moz:SearchForm>` + furl + `</moz:SearchForm> 270 </OpenSearchDescription>`)) 271 return nil 272 }