github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/routes_common.go (about) 1 package common 2 3 import ( 4 "crypto/subtle" 5 "html" 6 "io" 7 "net" 8 "net/http" 9 "os" 10 "regexp" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/Azareal/Gosora/common/phrases" 16 "github.com/Azareal/Gosora/uutils" 17 ) 18 19 // nolint 20 var PreRoute func(http.ResponseWriter, *http.Request) (User, bool) = preRoute 21 22 // TODO: Come up with a better middleware solution 23 // nolint We need these types so people can tell what they are without scrolling to the bottom of the file 24 var PanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*Header, PanelStats, RouteError) = panelUserCheck 25 var SimplePanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*HeaderLite, RouteError) = simplePanelUserCheck 26 var SimpleForumUserCheck func(w http.ResponseWriter, r *http.Request, u *User, fid int) (headerLite *HeaderLite, err RouteError) = simpleForumUserCheck 27 var ForumUserCheck func(h *Header, w http.ResponseWriter, r *http.Request, u *User, fid int) (err RouteError) = forumUserCheck 28 var SimpleUserCheck func(w http.ResponseWriter, r *http.Request, u *User) (headerLite *HeaderLite, err RouteError) = simpleUserCheck 29 var UserCheck func(w http.ResponseWriter, r *http.Request, u *User) (h *Header, err RouteError) = userCheck 30 var UserCheckNano func(w http.ResponseWriter, r *http.Request, u *User, nano int64) (h *Header, err RouteError) = userCheck2 31 32 func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, u *User, fid int) (h *HeaderLite, rerr RouteError) { 33 h, rerr = SimpleUserCheck(w, r, u) 34 if rerr != nil { 35 return h, rerr 36 } 37 if !Forums.Exists(fid) { 38 return nil, PreError("The target forum doesn't exist.", w, r) 39 } 40 41 // Is there a better way of doing the skip AND the success flag on this hook like multiple returns? 42 /*skip, rerr := h.Hooks.VhookSkippable("simple_forum_check_pre_perms", w, r, u, &fid, h) 43 if skip || rerr != nil { 44 return h, rerr 45 }*/ 46 skip, rerr := H_simple_forum_check_pre_perms_hook(h.Hooks, w, r, u, &fid, h) 47 if skip || rerr != nil { 48 return h, rerr 49 } 50 51 fp, err := FPStore.Get(fid, u.Group) 52 if err == ErrNoRows { 53 fp = BlankForumPerms() 54 } else if err != nil { 55 return h, InternalError(err, w, r) 56 } 57 cascadeForumPerms(fp, u) 58 return h, nil 59 } 60 61 func forumUserCheck(h *Header, w http.ResponseWriter, r *http.Request, u *User, fid int) (rerr RouteError) { 62 if !Forums.Exists(fid) { 63 return NotFound(w, r, h) 64 } 65 66 /*skip, rerr := h.Hooks.VhookSkippable("forum_check_pre_perms", w, r, u, &fid, h) 67 if skip || rerr != nil { 68 return rerr 69 }*/ 70 /*skip, rerr := VhookSkippableTest(h.Hooks, "forum_check_pre_perms", w, r, u, &fid, h) 71 if skip || rerr != nil { 72 return rerr 73 }*/ 74 skip, rerr := H_forum_check_pre_perms_hook(h.Hooks, w, r, u, &fid, h) 75 if skip || rerr != nil { 76 return rerr 77 } 78 79 fp, err := FPStore.Get(fid, u.Group) 80 if err == ErrNoRows { 81 fp = BlankForumPerms() 82 } else if err != nil { 83 return InternalError(err, w, r) 84 } 85 cascadeForumPerms(fp, u) 86 h.CurrentUser = u // TODO: Use a pointer instead for CurrentUser, so we don't have to do this 87 return rerr 88 } 89 90 // TODO: Put this on the user instance? Do we really want forum specific logic in there? Maybe, a method which spits a new pointer with the same contents as user? 91 func cascadeForumPerms(fp *ForumPerms, u *User) { 92 if fp.Overrides && !u.IsSuperAdmin { 93 u.Perms.ViewTopic = fp.ViewTopic 94 u.Perms.LikeItem = fp.LikeItem 95 u.Perms.CreateTopic = fp.CreateTopic 96 u.Perms.EditTopic = fp.EditTopic 97 u.Perms.DeleteTopic = fp.DeleteTopic 98 u.Perms.CreateReply = fp.CreateReply 99 u.Perms.EditReply = fp.EditReply 100 u.Perms.DeleteReply = fp.DeleteReply 101 u.Perms.PinTopic = fp.PinTopic 102 u.Perms.CloseTopic = fp.CloseTopic 103 u.Perms.MoveTopic = fp.MoveTopic 104 105 if len(fp.ExtData) != 0 { 106 for name, perm := range fp.ExtData { 107 u.PluginPerms[name] = perm 108 } 109 } 110 } 111 } 112 113 // Even if they have the right permissions, the control panel is only open to supermods+. There are many areas without subpermissions which assume that the current user is a supermod+ and admins are extremely unlikely to give these permissions to someone who isn't at-least a supermod to begin with 114 // TODO: Do a panel specific theme? 115 func panelUserCheck(w http.ResponseWriter, r *http.Request, u *User) (h *Header, stats PanelStats, rerr RouteError) { 116 theme := GetThemeByReq(r) 117 h = &Header{ 118 Site: Site, 119 Settings: SettingBox.Load().(SettingMap), 120 //Themes: Themes, 121 ThemesSlice: ThemesSlice, 122 Theme: theme, 123 CurrentUser: u, 124 Hooks: GetHookTable(), 125 Zone: "panel", 126 Writer: w, 127 IsoCode: phrases.GetLangPack().IsoCode, 128 //StartedAt: time.Now(), 129 StartedAt: uutils.Nanotime(), 130 } 131 // TODO: We should probably initialise header.ExtData 132 // ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well 133 //if user.IsAdmin { 134 //h.StartedAt = time.Now() 135 //} 136 137 h.AddSheet(theme.Name + "/main.css") 138 h.AddSheet(theme.Name + "/panel.css") 139 if len(theme.Resources) > 0 { 140 rlist := theme.Resources 141 for _, res := range rlist { 142 if res.LocID == LocGlobal || res.LocID == LocPanel { 143 if res.Type == ResTypeSheet { 144 h.AddSheet(res.Name) 145 } else if res.Type == ResTypeScript { 146 if res.Async { 147 h.AddScriptAsync(res.Name) 148 } else { 149 h.AddScript(res.Name) 150 } 151 } 152 } 153 } 154 } 155 156 //h := w.Header() 157 //h.Set("Content-Security-Policy", "default-src 'self'") 158 159 // TODO: GDPR. Add a global control panel notice warning the admins of staff members who don't have 2FA enabled 160 stats.Users = Users.Count() 161 stats.Groups = Groups.Count() 162 stats.Forums = Forums.Count() 163 stats.Pages = Pages.Count() 164 stats.Settings = len(h.Settings) 165 stats.WordFilters = WordFilters.EstCount() 166 stats.Themes = len(Themes) 167 stats.Reports = 0 // TODO: Do the report count. Only show open threads? 168 169 addPreScript := func(name string, i int) { 170 // TODO: Optimise this by removing a superfluous string alloc 171 if theme.OverridenMap != nil { 172 //fmt.Printf("name %+v\n", name) 173 //fmt.Printf("theme.OverridenMap %+v\n", theme.OverridenMap) 174 if _, ok := theme.OverridenMap[name]; ok { 175 tname := "_" + theme.Name 176 //fmt.Printf("tname %+v\n", tname) 177 h.AddPreScriptAsync("tmpl_" + name + tname + ".js") 178 return 179 } 180 } 181 //fmt.Printf("tname %+v\n", tname) 182 h.AddPreScriptAsync(ucstrs[i]) 183 } 184 addPreScript("alert", 3) 185 addPreScript("notice", 4) 186 187 return h, stats, nil 188 } 189 190 func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, u *User) (lite *HeaderLite, rerr RouteError) { 191 return SimpleUserCheck(w, r, u) 192 } 193 194 // SimpleUserCheck is back from the grave, yay :D 195 func simpleUserCheck(w http.ResponseWriter, r *http.Request, u *User) (lite *HeaderLite, rerr RouteError) { 196 return &HeaderLite{ 197 Site: Site, 198 Settings: SettingBox.Load().(SettingMap), 199 Hooks: GetHookTable(), 200 }, nil 201 } 202 203 func GetThemeByReq(r *http.Request) *Theme { 204 theme := &Theme{Name: ""} 205 cookie, e := r.Cookie("current_theme") 206 if e == nil { 207 inTheme, ok := Themes[html.EscapeString(cookie.Value)] 208 if ok && !theme.HideFromThemes { 209 theme = inTheme 210 } 211 } 212 if theme.Name == "" { 213 theme = Themes[DefaultThemeBox.Load().(string)] 214 } 215 return theme 216 } 217 218 func userCheck(w http.ResponseWriter, r *http.Request, u *User) (h *Header, rerr RouteError) { 219 return userCheck2(w, r, u, uutils.Nanotime()) 220 } 221 222 // TODO: Add the ability for admins to restrict certain themes to certain groups? 223 // ! Be careful about firing errors off here as CustomError uses this 224 func userCheck2(w http.ResponseWriter, r *http.Request, u *User, nano int64) (h *Header, rerr RouteError) { 225 theme := GetThemeByReq(r) 226 h = &Header{ 227 Site: Site, 228 Settings: SettingBox.Load().(SettingMap), 229 //Themes: Themes, 230 ThemesSlice: ThemesSlice, 231 Theme: theme, 232 CurrentUser: u, // ! Some things rely on this being a pointer downstream from this function 233 Hooks: GetHookTable(), 234 Zone: ucstrs[0], 235 Writer: w, 236 IsoCode: phrases.GetLangPack().IsoCode, 237 StartedAt: nano, 238 } 239 // TODO: Optimise this by avoiding accessing a map string index 240 if !u.Loggedin { 241 h.GoogSiteVerify = h.Settings["google_site_verify"].(string) 242 } 243 244 if u.IsBanned { 245 h.AddNotice("account_banned") 246 } 247 if u.Loggedin && !u.Active { 248 h.AddNotice("account_inactive") 249 } 250 /*h.Scripts, _ = StrSlicePool.Get().([]string) 251 if h.Scripts != nil { 252 h.Scripts = h.Scripts[:0] 253 } 254 h.PreScriptsAsync, _ = StrSlicePool.Get().([]string) 255 if h.PreScriptsAsync != nil { 256 h.PreScriptsAsync = h.PreScriptsAsync[:0] 257 }*/ 258 259 // An optimisation so we don't populate StartedAt for users who shouldn't see the stat anyway 260 // ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well 261 //if u.IsAdmin { 262 //h.StartedAt = time.Now() 263 //} 264 265 //PrepResources(u,h,theme) 266 return h, nil 267 } 268 269 func PrepResources(u *User, h *Header, theme *Theme) { 270 h.AddSheet(theme.Name + "/main.css") 271 272 if len(theme.Resources) > 0 { 273 rlist := theme.Resources 274 for _, res := range rlist { 275 if res.Loggedin && !u.Loggedin { 276 continue 277 } 278 if res.LocID == LocGlobal || res.LocID == LocFront { 279 if res.Type == ResTypeSheet { 280 h.AddSheet(res.Name) 281 } else if res.Type == ResTypeScript { 282 if res.Async { 283 h.AddScriptAsync(res.Name) 284 } else { 285 h.AddScript(res.Name) 286 } 287 } 288 } 289 } 290 } 291 292 addPreScript := func(name string, i int) { 293 // TODO: Optimise this by removing a superfluous string alloc 294 if theme.OverridenMap != nil { 295 //fmt.Printf("name %+v\n", name) 296 //fmt.Printf("theme.OverridenMap %+v\n", theme.OverridenMap) 297 if _, ok := theme.OverridenMap[name]; ok { 298 tname := "_" + theme.Name 299 //fmt.Printf("tname %+v\n", tname) 300 h.AddPreScriptAsync("tmpl_" + name + tname + ".js") 301 return 302 } 303 } 304 //fmt.Printf("tname %+v\n", tname) 305 h.AddPreScriptAsync(ucstrs[i]) 306 } 307 addPreScript("topics_topic", 1) 308 addPreScript("paginator", 2) 309 addPreScript("alert", 3) 310 addPreScript("notice", 4) 311 if u.Loggedin { 312 addPreScript("topic_c_edit_post", 5) 313 addPreScript("topic_c_attach_item", 6) 314 addPreScript("topic_c_poll_input", 7) 315 } 316 } 317 318 func pstr(name string) string { 319 return "tmpl_" + name + ".js" 320 } 321 322 var ucstrs = [...]string{ 323 "frontend", 324 325 pstr("topics_topic"), 326 pstr("paginator"), 327 pstr("alert"), 328 pstr("notice"), 329 330 pstr("topic_c_edit_post"), 331 pstr("topic_c_attach_item"), 332 pstr("topic_c_poll_input"), 333 } 334 335 func preRoute(w http.ResponseWriter, r *http.Request) (User, bool) { 336 userptr, halt := Auth.SessionCheck(w, r) 337 if halt { 338 return *userptr, false 339 } 340 var usercpy *User = BlankUser() 341 *usercpy = *userptr 342 usercpy.Init() // TODO: Can we reduce the amount of work we do here? 343 344 // TODO: Add a config setting to disable this header 345 // TODO: Have this header cover more things 346 if Config.SslSchema { 347 w.Header().Set("Content-Security-Policy", "upgrade-insecure-requests") 348 } 349 350 // TODO: WIP. Refactor this to eliminate the unnecessary query 351 // TODO: Better take proxies into consideration 352 if !Config.DisableIP { 353 var host string 354 // TODO: Prefer Cf-Connecting-Ip header, fewer shenanigans 355 if Site.HasProxy { 356 // TODO: Check the right-most IP, might get tricky with multiple proxies, maybe have a setting for the number of hops we jump through 357 xForwardedFor := r.Header.Get("X-Forwarded-For") 358 if xForwardedFor != "" { 359 forwardedFor := strings.Split(xForwardedFor, ",") 360 // TODO: Check if this is a valid IP Address, reject if not 361 host = forwardedFor[len(forwardedFor)-1] 362 } 363 } 364 365 if host == "" { 366 var e error 367 host, _, e = net.SplitHostPort(r.RemoteAddr) 368 if e != nil { 369 _ = PreError("Bad IP", w, r) 370 return *usercpy, false 371 } 372 } 373 374 if !Config.DisableLastIP && usercpy.Loggedin && host != usercpy.GetIP() { 375 mon := time.Now().Month() 376 e := usercpy.UpdateIP(strconv.Itoa(int(mon)) + "-" + host) 377 if e != nil { 378 _ = InternalError(e, w, r) 379 return *usercpy, false 380 } 381 } 382 usercpy.LastIP = host 383 } 384 385 return *usercpy, true 386 } 387 388 func UploadAvatar(w http.ResponseWriter, r *http.Request, u *User, tuid int) (ext string, ferr RouteError) { 389 // We don't want multiple files 390 // TODO: Are we doing this correctly? 391 filenameMap := make(map[string]bool) 392 for _, fheaders := range r.MultipartForm.File { 393 for _, hdr := range fheaders { 394 if hdr.Filename == "" { 395 continue 396 } 397 filenameMap[hdr.Filename] = true 398 } 399 } 400 if len(filenameMap) > 1 { 401 return "", LocalError("You may only upload one avatar", w, r, u) 402 } 403 404 for _, fheaders := range r.MultipartForm.File { 405 for _, hdr := range fheaders { 406 if hdr.Filename == "" { 407 continue 408 } 409 inFile, err := hdr.Open() 410 if err != nil { 411 return "", LocalError("Upload failed", w, r, u) 412 } 413 defer inFile.Close() 414 415 if ext == "" { 416 extarr := strings.Split(hdr.Filename, ".") 417 if len(extarr) < 2 { 418 return "", LocalError("Bad file", w, r, u) 419 } 420 ext = extarr[len(extarr)-1] 421 422 // TODO: Can we do this without a regex? 423 reg, err := regexp.Compile("[^A-Za-z0-9]+") 424 if err != nil { 425 return "", LocalError("Bad file extension", w, r, u) 426 } 427 ext = reg.ReplaceAllString(ext, "") 428 ext = strings.ToLower(ext) 429 430 if !ImageFileExts.Contains(ext) { 431 return "", LocalError("You can only use an image for your avatar", w, r, u) 432 } 433 } 434 435 // TODO: Centralise this string, so we don't have to change it in two different places when it changes 436 outFile, err := os.Create("./uploads/avatar_" + strconv.Itoa(tuid) + "." + ext) 437 if err != nil { 438 return "", LocalError("Upload failed [File Creation Failed]", w, r, u) 439 } 440 defer outFile.Close() 441 442 _, err = io.Copy(outFile, inFile) 443 if err != nil { 444 return "", LocalError("Upload failed [Copy Failed]", w, r, u) 445 } 446 } 447 } 448 if ext == "" { 449 return "", LocalError("No file", w, r, u) 450 } 451 return ext, nil 452 } 453 454 func ChangeAvatar(path string, w http.ResponseWriter, r *http.Request, u *User) RouteError { 455 e := u.ChangeAvatar(path) 456 if e != nil { 457 return InternalError(e, w, r) 458 } 459 460 // Clean up the old avatar data, so we don't end up with too many dead files in /uploads/ 461 if len(u.RawAvatar) > 2 { 462 if u.RawAvatar[0] == '.' && u.RawAvatar[1] == '.' { 463 e := os.Remove("./uploads/avatar_" + strconv.Itoa(u.ID) + "_tmp" + u.RawAvatar[1:]) 464 if e != nil && !os.IsNotExist(e) { 465 LogWarning(e) 466 return LocalError("Something went wrong", w, r, u) 467 } 468 e = os.Remove("./uploads/avatar_" + strconv.Itoa(u.ID) + "_w48" + u.RawAvatar[1:]) 469 if e != nil && !os.IsNotExist(e) { 470 LogWarning(e) 471 return LocalError("Something went wrong", w, r, u) 472 } 473 } 474 } 475 476 return nil 477 } 478 479 // SuperAdminOnly makes sure that only super admin can access certain critical panel routes 480 func SuperAdminOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError { 481 if !u.IsSuperAdmin { 482 return NoPermissions(w, r, u) 483 } 484 return nil 485 } 486 487 // AdminOnly makes sure that only admins can access certain panel routes 488 func AdminOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError { 489 if !u.IsAdmin { 490 return NoPermissions(w, r, u) 491 } 492 return nil 493 } 494 495 // SuperModeOnly makes sure that only super mods or higher can access the panel routes 496 func SuperModOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError { 497 if !u.IsSuperMod { 498 return NoPermissions(w, r, u) 499 } 500 return nil 501 } 502 503 // MemberOnly makes sure that only logged in users can access this route 504 func MemberOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError { 505 if !u.Loggedin { 506 return LoginRequired(w, r, u) 507 } 508 return nil 509 } 510 511 // NoBanned stops any banned users from accessing this route 512 func NoBanned(w http.ResponseWriter, r *http.Request, u *User) RouteError { 513 if u.IsBanned { 514 return Banned(w, r, u) 515 } 516 return nil 517 } 518 519 func ParseForm(w http.ResponseWriter, r *http.Request, u *User) RouteError { 520 if e := r.ParseForm(); e != nil { 521 return LocalError("Bad Form", w, r, u) 522 } 523 return nil 524 } 525 526 func NoSessionMismatch(w http.ResponseWriter, r *http.Request, u *User) RouteError { 527 if e := r.ParseForm(); e != nil { 528 return LocalError("Bad Form", w, r, u) 529 } 530 if len(u.Session) == 0 { 531 return SecurityError(w, r, u) 532 } 533 // TODO: Try to eliminate some of these allocations 534 sess := []byte(u.Session) 535 if subtle.ConstantTimeCompare([]byte(r.FormValue("session")), sess) != 1 && subtle.ConstantTimeCompare([]byte(r.FormValue("s")), sess) != 1 { 536 return SecurityError(w, r, u) 537 } 538 return nil 539 } 540 541 func ReqIsJson(r *http.Request) bool { 542 return r.Header.Get("Content-type") == "application/json" 543 } 544 545 func HandleUploadRoute(w http.ResponseWriter, r *http.Request, u *User, maxFileSize int) RouteError { 546 // TODO: Reuse this code more 547 if r.ContentLength > int64(maxFileSize) { 548 size, unit := ConvertByteUnit(float64(maxFileSize)) 549 return CustomError("Your upload is too big. Your files need to be smaller than "+strconv.Itoa(int(size))+unit+".", http.StatusExpectationFailed, "Error", w, r, nil, u) 550 } 551 r.Body = http.MaxBytesReader(w, r.Body, r.ContentLength) 552 553 e := r.ParseMultipartForm(int64(Megabyte)) 554 if e != nil { 555 return LocalError("Bad Form", w, r, u) 556 } 557 return nil 558 } 559 560 func NoUploadSessionMismatch(w http.ResponseWriter, r *http.Request, u *User) RouteError { 561 if len(u.Session) == 0 { 562 return SecurityError(w, r, u) 563 } 564 // TODO: Try to eliminate some of these allocations 565 sess := []byte(u.Session) 566 if subtle.ConstantTimeCompare([]byte(r.FormValue("session")), sess) != 1 && subtle.ConstantTimeCompare([]byte(r.FormValue("s")), sess) != 1 { 567 return SecurityError(w, r, u) 568 } 569 return nil 570 }