package common import ( "crypto/subtle" "html" "io" "net" "net/http" "os" "regexp" "strconv" "strings" "time" "git.tuxpa.in/a/gosora/common/phrases" "git.tuxpa.in/a/gosora/uutils" ) // nolint var PreRoute func(http.ResponseWriter, *http.Request) (User, bool) = preRoute // TODO: Come up with a better middleware solution // nolint We need these types so people can tell what they are without scrolling to the bottom of the file var PanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*Header, PanelStats, RouteError) = panelUserCheck var SimplePanelUserCheck func(http.ResponseWriter, *http.Request, *User) (*HeaderLite, RouteError) = simplePanelUserCheck var SimpleForumUserCheck func(w http.ResponseWriter, r *http.Request, u *User, fid int) (headerLite *HeaderLite, err RouteError) = simpleForumUserCheck var ForumUserCheck func(h *Header, w http.ResponseWriter, r *http.Request, u *User, fid int) (err RouteError) = forumUserCheck var SimpleUserCheck func(w http.ResponseWriter, r *http.Request, u *User) (headerLite *HeaderLite, err RouteError) = simpleUserCheck var UserCheck func(w http.ResponseWriter, r *http.Request, u *User) (h *Header, err RouteError) = userCheck var UserCheckNano func(w http.ResponseWriter, r *http.Request, u *User, nano int64) (h *Header, err RouteError) = userCheck2 func simpleForumUserCheck(w http.ResponseWriter, r *http.Request, u *User, fid int) (h *HeaderLite, rerr RouteError) { h, rerr = SimpleUserCheck(w, r, u) if rerr != nil { return h, rerr } if !Forums.Exists(fid) { return nil, PreError("The target forum doesn't exist.", w, r) } // Is there a better way of doing the skip AND the success flag on this hook like multiple returns? /*skip, rerr := h.Hooks.VhookSkippable("simple_forum_check_pre_perms", w, r, u, &fid, h) if skip || rerr != nil { return h, rerr }*/ skip, rerr := H_simple_forum_check_pre_perms_hook(h.Hooks, w, r, u, &fid, h) if skip || rerr != nil { return h, rerr } fp, err := FPStore.Get(fid, u.Group) if err == ErrNoRows { fp = BlankForumPerms() } else if err != nil { return h, InternalError(err, w, r) } cascadeForumPerms(fp, u) return h, nil } func forumUserCheck(h *Header, w http.ResponseWriter, r *http.Request, u *User, fid int) (rerr RouteError) { if !Forums.Exists(fid) { return NotFound(w, r, h) } /*skip, rerr := h.Hooks.VhookSkippable("forum_check_pre_perms", w, r, u, &fid, h) if skip || rerr != nil { return rerr }*/ /*skip, rerr := VhookSkippableTest(h.Hooks, "forum_check_pre_perms", w, r, u, &fid, h) if skip || rerr != nil { return rerr }*/ skip, rerr := H_forum_check_pre_perms_hook(h.Hooks, w, r, u, &fid, h) if skip || rerr != nil { return rerr } fp, err := FPStore.Get(fid, u.Group) if err == ErrNoRows { fp = BlankForumPerms() } else if err != nil { return InternalError(err, w, r) } cascadeForumPerms(fp, u) h.CurrentUser = u // TODO: Use a pointer instead for CurrentUser, so we don't have to do this return rerr } // 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? func cascadeForumPerms(fp *ForumPerms, u *User) { if fp.Overrides && !u.IsSuperAdmin { u.Perms.ViewTopic = fp.ViewTopic u.Perms.LikeItem = fp.LikeItem u.Perms.CreateTopic = fp.CreateTopic u.Perms.EditTopic = fp.EditTopic u.Perms.DeleteTopic = fp.DeleteTopic u.Perms.CreateReply = fp.CreateReply u.Perms.EditReply = fp.EditReply u.Perms.DeleteReply = fp.DeleteReply u.Perms.PinTopic = fp.PinTopic u.Perms.CloseTopic = fp.CloseTopic u.Perms.MoveTopic = fp.MoveTopic if len(fp.ExtData) != 0 { for name, perm := range fp.ExtData { u.PluginPerms[name] = perm } } } } // 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 // TODO: Do a panel specific theme? func panelUserCheck(w http.ResponseWriter, r *http.Request, u *User) (h *Header, stats PanelStats, rerr RouteError) { theme := GetThemeByReq(r) h = &Header{ Site: Site, Settings: SettingBox.Load().(SettingMap), //Themes: Themes, ThemesSlice: ThemesSlice, Theme: theme, CurrentUser: u, Hooks: GetHookTable(), Zone: "panel", Writer: w, IsoCode: phrases.GetLangPack().IsoCode, //StartedAt: time.Now(), StartedAt: uutils.Nanotime(), } // TODO: We should probably initialise header.ExtData // ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well //if user.IsAdmin { //h.StartedAt = time.Now() //} h.AddSheet(theme.Name + "/main.css") h.AddSheet(theme.Name + "/panel.css") if len(theme.Resources) > 0 { rlist := theme.Resources for _, res := range rlist { if res.LocID == LocGlobal || res.LocID == LocPanel { if res.Type == ResTypeSheet { h.AddSheet(res.Name) } else if res.Type == ResTypeScript { if res.Async { h.AddScriptAsync(res.Name) } else { h.AddScript(res.Name) } } } } } //h := w.Header() //h.Set("Content-Security-Policy", "default-src 'self'") // TODO: GDPR. Add a global control panel notice warning the admins of staff members who don't have 2FA enabled stats.Users = Users.Count() stats.Groups = Groups.Count() stats.Forums = Forums.Count() stats.Pages = Pages.Count() stats.Settings = len(h.Settings) stats.WordFilters = WordFilters.EstCount() stats.Themes = len(Themes) stats.Reports = 0 // TODO: Do the report count. Only show open threads? addPreScript := func(name string, i int) { // TODO: Optimise this by removing a superfluous string alloc if theme.OverridenMap != nil { //fmt.Printf("name %+v\n", name) //fmt.Printf("theme.OverridenMap %+v\n", theme.OverridenMap) if _, ok := theme.OverridenMap[name]; ok { tname := "_" + theme.Name //fmt.Printf("tname %+v\n", tname) h.AddPreScriptAsync("tmpl_" + name + tname + ".js") return } } //fmt.Printf("tname %+v\n", tname) h.AddPreScriptAsync(ucstrs[i]) } addPreScript("alert", 3) addPreScript("notice", 4) return h, stats, nil } func simplePanelUserCheck(w http.ResponseWriter, r *http.Request, u *User) (lite *HeaderLite, rerr RouteError) { return SimpleUserCheck(w, r, u) } // SimpleUserCheck is back from the grave, yay :D func simpleUserCheck(w http.ResponseWriter, r *http.Request, u *User) (lite *HeaderLite, rerr RouteError) { return &HeaderLite{ Site: Site, Settings: SettingBox.Load().(SettingMap), Hooks: GetHookTable(), }, nil } func GetThemeByReq(r *http.Request) *Theme { theme := &Theme{Name: ""} cookie, e := r.Cookie("current_theme") if e == nil { inTheme, ok := Themes[html.EscapeString(cookie.Value)] if ok && !theme.HideFromThemes { theme = inTheme } } if theme.Name == "" { theme = Themes[DefaultThemeBox.Load().(string)] } return theme } func userCheck(w http.ResponseWriter, r *http.Request, u *User) (h *Header, rerr RouteError) { return userCheck2(w, r, u, uutils.Nanotime()) } // TODO: Add the ability for admins to restrict certain themes to certain groups? // ! Be careful about firing errors off here as CustomError uses this func userCheck2(w http.ResponseWriter, r *http.Request, u *User, nano int64) (h *Header, rerr RouteError) { theme := GetThemeByReq(r) h = &Header{ Site: Site, Settings: SettingBox.Load().(SettingMap), //Themes: Themes, ThemesSlice: ThemesSlice, Theme: theme, CurrentUser: u, // ! Some things rely on this being a pointer downstream from this function Hooks: GetHookTable(), Zone: ucstrs[0], Writer: w, IsoCode: phrases.GetLangPack().IsoCode, StartedAt: nano, } // TODO: Optimise this by avoiding accessing a map string index if !u.Loggedin { h.GoogSiteVerify = h.Settings["google_site_verify"].(string) } if u.IsBanned { h.AddNotice("account_banned") } if u.Loggedin && !u.Active { h.AddNotice("account_inactive") } /*h.Scripts, _ = StrSlicePool.Get().([]string) if h.Scripts != nil { h.Scripts = h.Scripts[:0] } h.PreScriptsAsync, _ = StrSlicePool.Get().([]string) if h.PreScriptsAsync != nil { h.PreScriptsAsync = h.PreScriptsAsync[:0] }*/ // An optimisation so we don't populate StartedAt for users who shouldn't see the stat anyway // ? - Should we only show this in debug mode? It might be useful for detecting issues in production, if we show it there as-well //if u.IsAdmin { //h.StartedAt = time.Now() //} //PrepResources(u,h,theme) return h, nil } func PrepResources(u *User, h *Header, theme *Theme) { h.AddSheet(theme.Name + "/main.css") if len(theme.Resources) > 0 { rlist := theme.Resources for _, res := range rlist { if res.Loggedin && !u.Loggedin { continue } if res.LocID == LocGlobal || res.LocID == LocFront { if res.Type == ResTypeSheet { h.AddSheet(res.Name) } else if res.Type == ResTypeScript { if res.Async { h.AddScriptAsync(res.Name) } else { h.AddScript(res.Name) } } } } } addPreScript := func(name string, i int) { // TODO: Optimise this by removing a superfluous string alloc if theme.OverridenMap != nil { //fmt.Printf("name %+v\n", name) //fmt.Printf("theme.OverridenMap %+v\n", theme.OverridenMap) if _, ok := theme.OverridenMap[name]; ok { tname := "_" + theme.Name //fmt.Printf("tname %+v\n", tname) h.AddPreScriptAsync("tmpl_" + name + tname + ".js") return } } //fmt.Printf("tname %+v\n", tname) h.AddPreScriptAsync(ucstrs[i]) } addPreScript("topics_topic", 1) addPreScript("paginator", 2) addPreScript("alert", 3) addPreScript("notice", 4) if u.Loggedin { addPreScript("topic_c_edit_post", 5) addPreScript("topic_c_attach_item", 6) addPreScript("topic_c_poll_input", 7) } } func pstr(name string) string { return "tmpl_" + name + ".js" } var ucstrs = [...]string{ "frontend", pstr("topics_topic"), pstr("paginator"), pstr("alert"), pstr("notice"), pstr("topic_c_edit_post"), pstr("topic_c_attach_item"), pstr("topic_c_poll_input"), } func preRoute(w http.ResponseWriter, r *http.Request) (User, bool) { userptr, halt := Auth.SessionCheck(w, r) if halt { return *userptr, false } var usercpy *User = BlankUser() *usercpy = *userptr usercpy.Init() // TODO: Can we reduce the amount of work we do here? // TODO: Add a config setting to disable this header // TODO: Have this header cover more things if Config.SslSchema { w.Header().Set("Content-Security-Policy", "upgrade-insecure-requests") } // TODO: WIP. Refactor this to eliminate the unnecessary query // TODO: Better take proxies into consideration if !Config.DisableIP { var host string // TODO: Prefer Cf-Connecting-Ip header, fewer shenanigans if Site.HasProxy { // TODO: Check the right-most IP, might get tricky with multiple proxies, maybe have a setting for the number of hops we jump through xForwardedFor := r.Header.Get("X-Forwarded-For") if xForwardedFor != "" { forwardedFor := strings.Split(xForwardedFor, ",") // TODO: Check if this is a valid IP Address, reject if not host = forwardedFor[len(forwardedFor)-1] } } if host == "" { var e error host, _, e = net.SplitHostPort(r.RemoteAddr) if e != nil { _ = PreError("Bad IP", w, r) return *usercpy, false } } if !Config.DisableLastIP && usercpy.Loggedin && host != usercpy.GetIP() { mon := time.Now().Month() e := usercpy.UpdateIP(strconv.Itoa(int(mon)) + "-" + host) if e != nil { _ = InternalError(e, w, r) return *usercpy, false } } usercpy.LastIP = host } return *usercpy, true } func UploadAvatar(w http.ResponseWriter, r *http.Request, u *User, tuid int) (ext string, ferr RouteError) { // We don't want multiple files // TODO: Are we doing this correctly? filenameMap := make(map[string]bool) for _, fheaders := range r.MultipartForm.File { for _, hdr := range fheaders { if hdr.Filename == "" { continue } filenameMap[hdr.Filename] = true } } if len(filenameMap) > 1 { return "", LocalError("You may only upload one avatar", w, r, u) } for _, fheaders := range r.MultipartForm.File { for _, hdr := range fheaders { if hdr.Filename == "" { continue } inFile, err := hdr.Open() if err != nil { return "", LocalError("Upload failed", w, r, u) } defer inFile.Close() if ext == "" { extarr := strings.Split(hdr.Filename, ".") if len(extarr) < 2 { return "", LocalError("Bad file", w, r, u) } ext = extarr[len(extarr)-1] // TODO: Can we do this without a regex? reg, err := regexp.Compile("[^A-Za-z0-9]+") if err != nil { return "", LocalError("Bad file extension", w, r, u) } ext = reg.ReplaceAllString(ext, "") ext = strings.ToLower(ext) if !ImageFileExts.Contains(ext) { return "", LocalError("You can only use an image for your avatar", w, r, u) } } // TODO: Centralise this string, so we don't have to change it in two different places when it changes outFile, err := os.Create("./uploads/avatar_" + strconv.Itoa(tuid) + "." + ext) if err != nil { return "", LocalError("Upload failed [File Creation Failed]", w, r, u) } defer outFile.Close() _, err = io.Copy(outFile, inFile) if err != nil { return "", LocalError("Upload failed [Copy Failed]", w, r, u) } } } if ext == "" { return "", LocalError("No file", w, r, u) } return ext, nil } func ChangeAvatar(path string, w http.ResponseWriter, r *http.Request, u *User) RouteError { e := u.ChangeAvatar(path) if e != nil { return InternalError(e, w, r) } // Clean up the old avatar data, so we don't end up with too many dead files in /uploads/ if len(u.RawAvatar) > 2 { if u.RawAvatar[0] == '.' && u.RawAvatar[1] == '.' { e := os.Remove("./uploads/avatar_" + strconv.Itoa(u.ID) + "_tmp" + u.RawAvatar[1:]) if e != nil && !os.IsNotExist(e) { LogWarning(e) return LocalError("Something went wrong", w, r, u) } e = os.Remove("./uploads/avatar_" + strconv.Itoa(u.ID) + "_w48" + u.RawAvatar[1:]) if e != nil && !os.IsNotExist(e) { LogWarning(e) return LocalError("Something went wrong", w, r, u) } } } return nil } // SuperAdminOnly makes sure that only super admin can access certain critical panel routes func SuperAdminOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError { if !u.IsSuperAdmin { return NoPermissions(w, r, u) } return nil } // AdminOnly makes sure that only admins can access certain panel routes func AdminOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError { if !u.IsAdmin { return NoPermissions(w, r, u) } return nil } // SuperModeOnly makes sure that only super mods or higher can access the panel routes func SuperModOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError { if !u.IsSuperMod { return NoPermissions(w, r, u) } return nil } // MemberOnly makes sure that only logged in users can access this route func MemberOnly(w http.ResponseWriter, r *http.Request, u *User) RouteError { if !u.Loggedin { return LoginRequired(w, r, u) } return nil } // NoBanned stops any banned users from accessing this route func NoBanned(w http.ResponseWriter, r *http.Request, u *User) RouteError { if u.IsBanned { return Banned(w, r, u) } return nil } func ParseForm(w http.ResponseWriter, r *http.Request, u *User) RouteError { if e := r.ParseForm(); e != nil { return LocalError("Bad Form", w, r, u) } return nil } func NoSessionMismatch(w http.ResponseWriter, r *http.Request, u *User) RouteError { if e := r.ParseForm(); e != nil { return LocalError("Bad Form", w, r, u) } if len(u.Session) == 0 { return SecurityError(w, r, u) } // TODO: Try to eliminate some of these allocations sess := []byte(u.Session) if subtle.ConstantTimeCompare([]byte(r.FormValue("session")), sess) != 1 && subtle.ConstantTimeCompare([]byte(r.FormValue("s")), sess) != 1 { return SecurityError(w, r, u) } return nil } func ReqIsJson(r *http.Request) bool { return r.Header.Get("Content-type") == "application/json" } func HandleUploadRoute(w http.ResponseWriter, r *http.Request, u *User, maxFileSize int) RouteError { // TODO: Reuse this code more if r.ContentLength > int64(maxFileSize) { size, unit := ConvertByteUnit(float64(maxFileSize)) 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) } r.Body = http.MaxBytesReader(w, r.Body, r.ContentLength) e := r.ParseMultipartForm(int64(Megabyte)) if e != nil { return LocalError("Bad Form", w, r, u) } return nil } func NoUploadSessionMismatch(w http.ResponseWriter, r *http.Request, u *User) RouteError { if len(u.Session) == 0 { return SecurityError(w, r, u) } // TODO: Try to eliminate some of these allocations sess := []byte(u.Session) if subtle.ConstantTimeCompare([]byte(r.FormValue("session")), sess) != 1 && subtle.ConstantTimeCompare([]byte(r.FormValue("s")), sess) != 1 { return SecurityError(w, r, u) } return nil }