From 43d72e6f3bb74da51d0b032bdcf6971f12bd60d1 Mon Sep 17 00:00:00 2001 From: Azareal Date: Thu, 30 Jul 2020 18:10:29 +1000 Subject: [PATCH] wip allow for more cdns add res template function add ExtraCSPOrigins config setting add StaticResBase config setting skip flush directives --- common/files.go | 24 ++++++++----- common/site.go | 22 +++++++++++- common/template_init.go | 11 ++++++ common/templates/templates.go | 64 ++++++++++++++++++++++++---------- common/theme.go | 2 +- docs/configuration.md | 4 +++ routes/common.go | 8 ++--- templates/header.html | 12 +++---- templates/topic_alt_inner.html | 54 ++++++++++++++-------------- 9 files changed, 136 insertions(+), 65 deletions(-) diff --git a/common/files.go b/common/files.go index 59735109..4b73823c 100644 --- a/common/files.go +++ b/common/files.go @@ -10,6 +10,7 @@ import ( "io/ioutil" "mime" "net/http" + "net/url" "os" "path/filepath" "strconv" @@ -23,14 +24,16 @@ import ( //type SFileList map[string]*SFile //type SFileListShort map[string]*SFile -var StaticFiles = SFileList{make(map[string]*SFile),make(map[string]*SFile)} +var StaticFiles = SFileList{"/s/", make(map[string]*SFile), make(map[string]*SFile)} + //var StaticFilesShort SFileList = make(map[string]*SFile) var staticFileMutex sync.RWMutex // ? Is it efficient to have two maps for this? type SFileList struct { - Long map[string]*SFile - Short map[string]*SFile + Prefix string + Long map[string]*SFile + Short map[string]*SFile } type SFile struct { @@ -305,7 +308,7 @@ func (l SFileList) JSTmplInit() error { hasher.Write(data) checksum := hex.EncodeToString(hasher.Sum(nil)) - l.Set("/s/"+path, &SFile{data, gzipData, brData, checksum, path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)}) + l.Set(l.Prefix+path, &SFile{data, gzipData, brData, checksum, l.Prefix + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)}) DebugLogf("Added the '%s' static file.", path) return nil @@ -367,7 +370,7 @@ func (l SFileList) Init() error { } } - l.Set("/s/"+path, &SFile{data, gzipData, brData, checksum, path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mimetype, f, f.ModTime().UTC().Format(http.TimeFormat)}) + l.Set(l.Prefix+path, &SFile{data, gzipData, brData, checksum, l.Prefix + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mimetype, f, f.ModTime().UTC().Format(http.TimeFormat)}) DebugLogf("Added the '%s' static file.", path) return nil @@ -424,7 +427,7 @@ func (l SFileList) Add(path, prefix string) error { hasher.Write(data) checksum := hex.EncodeToString(hasher.Sum(nil)) - l.Set("/s/"+path, &SFile{data, gzipData, brData, checksum, path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)}) + l.Set(l.Prefix+path, &SFile{data, gzipData, brData, checksum, l.Prefix + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)}) DebugLogf("Added the '%s' static file", path) return nil @@ -448,8 +451,13 @@ func (l SFileList) GetShort(name string) (file *SFile, exists bool) { func (l SFileList) Set(name string, data *SFile) { staticFileMutex.Lock() defer staticFileMutex.Unlock() - l.Long[name] = data - l.Short[strings.TrimPrefix("/s/",name)] = data + // TODO: Propagate errors back up + uurl, err := url.Parse(name) + if err != nil { + return + } + l.Long[uurl.Path] = data + l.Short[strings.TrimPrefix(strings.TrimPrefix(name, l.Prefix), "/")] = data } var gzipBestCompress sync.Pool diff --git a/common/site.go b/common/site.go index a5d9da5f..2dc49301 100644 --- a/common/site.go +++ b/common/site.go @@ -2,11 +2,13 @@ package common import ( "encoding/json" - "errors" "io/ioutil" "log" + "net/url" "strconv" "strings" + + "github.com/pkg/errors" ) // Site holds the basic settings which should be tweaked when setting up a site, we might move them to the settings table at some point @@ -119,6 +121,9 @@ type config struct { RefNoRef bool NoEmbed bool + ExtraCSPOrigins string + StaticResBase string // /s/ + Noavatar string // ? - Move this into the settings table? ItemsPerPage int // ? - Move this into the settings table? MaxTopicTitleLength int @@ -238,6 +243,21 @@ func ProcessConfig() (err error) { } Site.MaxRequestSize = Config.MaxRequestSize + local := func(h string) bool { + return h == "localhost" || h == "127.0.0.1" || h == "::1" || h == Site.URL + } + uurl, err := url.Parse(Config.StaticResBase) + if err != nil { + return errors.Wrap(err, "failed to parse Config.StaticResBase: ") + } + host := uurl.Hostname() + if !local(host) { + Config.ExtraCSPOrigins += " " + host + } + if Config.StaticResBase != "" { + StaticFiles.Prefix = Config.StaticResBase + } + if Config.PostIPCutoff == 0 { Config.PostIPCutoff = 120 // Default cutoff } diff --git a/common/template_init.go b/common/template_init.go index 7e56e8c9..a7235dca 100644 --- a/common/template_init.go +++ b/common/template_init.go @@ -878,6 +878,17 @@ func initDefaultTmplFuncMap() { return nil } + fmap["res"] = func(nameInt interface{}) interface{} { + n := nameInt.(string) + if n[0] == '/' && n[1] == '/' { + } else { + if f, ok := StaticFiles.GetShort(n); ok { + n = f.OName + } + } + return n + } + DefaultTemplateFuncMap = fmap } diff --git a/common/templates/templates.go b/common/templates/templates.go index 3348ce40..69ad0ab5 100644 --- a/common/templates/templates.go +++ b/common/templates/templates.go @@ -119,6 +119,7 @@ func NewCTemplateSet(in string) *CTemplateSet { "js": true, "index": true, "flush": true, + "res": true, }, logger: log.New(f, "", log.LstdFlags), loggerf: f, @@ -683,8 +684,9 @@ func (c *CTemplateSet) compileSwitch(con CContext, node parse.Node) { for _, cmd := range node.Pipe.Cmds { c.detail("If Node Bit:", cmd) c.detail("Bit Type:", reflect.ValueOf(cmd).Type().Name()) - expr += c.compileExprSwitch(con, cmd) - c.detail("Expression Step:", c.compileExprSwitch(con, cmd)) + exprStep := c.compileExprSwitch(con, cmd) + expr += exprStep + c.detail("Expression Step:", exprStep) } c.detail("Expression:", expr) @@ -1031,8 +1033,7 @@ func (c *CTemplateSet) compileExprSwitch(con CContext, node *parse.CommandNode) case *parse.NilNode: panic("Nil is not a command x.x") case *parse.PipeNode: - c.detail("Pipe Node!") - c.detail(n) + c.detail("Pipe Node:", n) c.detail("Node Args:", node.Args) out += c.compileIdentSwitchN(con, node) default: @@ -1043,9 +1044,9 @@ func (c *CTemplateSet) compileExprSwitch(con CContext, node *parse.CommandNode) } func (c *CTemplateSet) unknownNode(n parse.Node) { - elem := reflect.ValueOf(n).Elem() - c.logger.Println("Unknown Kind:", elem.Kind()) - c.logger.Println("Unknown Type:", elem.Type().Name()) + el := reflect.ValueOf(n).Elem() + c.logger.Println("Unknown Kind:", el.Kind()) + c.logger.Println("Unknown Type:", el.Type().Name()) panic("I don't know what node this is! Grr...") } @@ -1114,7 +1115,7 @@ func (c *CTemplateSet) compareJoin(con CContext, pos int, node *parse.CommandNod return pos, out } -func (c *CTemplateSet) compileIdentSwitch(con CContext, node *parse.CommandNode) (out string, val reflect.Value, literal, notident bool) { +func (c *CTemplateSet) compileIdentSwitch(con CContext, node *parse.CommandNode) (out string, val reflect.Value, literal, notIdent bool) { c.dumpCall("compileIdentSwitch", con, node) litString := func(inner string, bytes bool) { if !bytes { @@ -1135,9 +1136,16 @@ ArgLoop: var rout string pos, rout = c.compareJoin(con, pos, node, c.funcMap[id.String()].(string)) // TODO: Test this out += rout - case "le", "lt", "gt", "ge", "eq", "ne": + case "le", "lt", "gt", "ge": out += c.compareFunc(con, pos, node, c.funcMap[id.String()].(string)) break ArgLoop + case "eq", "ne": + o := c.compareFunc(con, pos, node, c.funcMap[id.String()].(string)) + if out == "!" { + o = "(" + o + ")" + } + out += o + break ArgLoop case "add", "subtract", "divide", "multiply": rout, rval := c.simpleMath(con, pos, node, c.funcMap[id.String()].(string)) out += rout @@ -1236,7 +1244,7 @@ ArgLoop: // ! Slightly crude but it does the job leftParam := strings.Replace(leftOp, "\"", "", -1) c.langIndexToName = append(c.langIndexToName, leftParam) - notident = true + notIdent = true con.PushPhrase(len(c.langIndexToName) - 1) } else { leftParam := leftOp @@ -1386,17 +1394,37 @@ ArgLoop: // TODO: Refactor this // TODO: Call the template function directly rather than going through RunThemeTemplate to eliminate a round of indirection? - out = "{\nerr := " + headParam + ".Theme.RunTmpl(" + nameParam + "," + pageParam + ",w)\n" - out += "if err != nil {\nreturn err\n}\n}\n" + out = "{\ne := " + headParam + ".Theme.RunTmpl(" + nameParam + "," + pageParam + ",w)\n" + out += "if e != nil {\nreturn e\n}\n}\n" literal = true break ArgLoop case "flush": - if c.lang == "js" { - continue - } - out = "if fl, ok := iw.(http.Flusher); ok {\nfl.Flush()\n}\n" literal = true - c.importMap["net/http"] = "net/http" + break ArgLoop + /*if c.lang == "js" { + continue + } + out = "if fl, ok := iw.(http.Flusher); ok {\nfl.Flush()\n}\n" + literal = true + c.importMap["net/http"] = "net/http" + break ArgLoop*/ + // TODO: Test this + case "res": + leftOp := node.Args[pos+1].String() + if len(leftOp) == 0 { + panic("The leftoperand for function res cannot be left blank") + } + leftParam, _ := c.compileIfVarSub(con, leftOp) + literal = true + if leftParam[0] == '"' { + if leftParam[1] == '/' && leftParam[2] == '/' { + litString(leftParam, false) + break ArgLoop + } + out = "{n := " + leftParam + "\nif f, ok := c.StaticFiles.GetShort(n); ok {\nw.Write(StringToBytes(f.OName))\n} else {\nw.Write(StringToBytes(n))\n}}\n" + break ArgLoop + } + out = "{n := " + leftParam + "\nif n[0] == '/' && n[1] == '/' {\n} else {\nif f, ok := c.StaticFiles.GetShort(n); ok {\nn = f.OName\n}\nw.Write(StringToBytes(n))\n}\n" break ArgLoop default: c.detail("Variable!") @@ -1410,7 +1438,7 @@ ArgLoop: } } c.retCall("compileIdentSwitch", out, val, literal) - return out, val, literal, notident + return out, val, literal, notIdent } func (c *CTemplateSet) compileReflectSwitch(con CContext, node *parse.CommandNode) (out string, outVal reflect.Value) { diff --git a/common/theme.go b/common/theme.go index a2a92f43..59d4acd9 100644 --- a/common/theme.go +++ b/common/theme.go @@ -284,7 +284,7 @@ func (t *Theme) AddThemeStaticFiles() error { hasher.Write(data) checksum := hex.EncodeToString(hasher.Sum(nil)) - StaticFiles.Set("/s/"+t.Name+path, &SFile{data, gzipData, brData, checksum, t.Name + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)}) + StaticFiles.Set(StaticFiles.Prefix+t.Name+path, &SFile{data, gzipData, brData, checksum, StaticFiles.Prefix + t.Name + path + "?h=" + checksum, 0, int64(len(data)), strconv.Itoa(len(data)), int64(len(gzipData)), strconv.Itoa(len(gzipData)), int64(len(brData)), strconv.Itoa(len(brData)), mime.TypeByExtension(ext), f, f.ModTime().UTC().Format(http.TimeFormat)}) DebugLog("Added the '/" + t.Name + path + "' static file for theme " + t.Name + ".") return nil diff --git a/docs/configuration.md b/docs/configuration.md index 59e8a059..b3397b2d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -126,6 +126,10 @@ RefNoRef - This switch makes it so that if a user clicks on a link, then the inc NoEmbed - Don't expand links into videos or images. Default: false +ExtraCSPOrigins - Extra origins which may want whitelisted in the default Content Security Policy. + +StaticResBase - The default prefix for static resource files. May be a path or an external domain like a CDN domain. Default: /s/ + NoAvatar - The default avatar to use for users when they don't have their own. The default for this may change in the near future to better utilise HTTP/2. Example: https://api.adorable.io/avatars/{width}/{id}.png ItemsPerPage - The number of posts, topics, etc. you want on each page. diff --git a/routes/common.go b/routes/common.go index 4fae6ebe..d37ff535 100644 --- a/routes/common.go +++ b/routes/common.go @@ -139,15 +139,15 @@ func FootHeaders(w http.ResponseWriter, h *c.Header) { he := w.Header() if c.Config.SslSchema { if h.ExternalMedia { - he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self' www.youtube-nocookie.com embed.nicovideo.jp;upgrade-insecure-requests") + he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'"+c.Config.ExtraCSPOrigins+"; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self' www.youtube-nocookie.com embed.nicovideo.jp;upgrade-insecure-requests") } else { - he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self';upgrade-insecure-requests") + he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'"+c.Config.ExtraCSPOrigins+"; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self';upgrade-insecure-requests") } } else { if h.ExternalMedia { - he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self' www.youtube-nocookie.com embed.nicovideo.jp") + he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'"+c.Config.ExtraCSPOrigins+"; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self' www.youtube-nocookie.com embed.nicovideo.jp") } else { - he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self'") + he.Set("Content-Security-Policy", "default-src 'self' 'unsafe-eval'"+c.Config.ExtraCSPOrigins+"; style-src 'self' 'unsafe-eval' 'unsafe-inline'; img-src * data: 'unsafe-eval' 'unsafe-inline'; connect-src * 'unsafe-eval' 'unsafe-inline'; frame-src 'self'") } } } diff --git a/templates/header.html b/templates/header.html index e5be2f43..c22209cb 100644 --- a/templates/header.html +++ b/templates/header.html @@ -3,16 +3,16 @@ {{.Title}} | {{.Header.Site.Name}} {{range .Header.Stylesheets}} - {{end}} + {{end}} {{range .Header.PreScriptsAsync}} - {{end}} + {{end}} {{if .CurrentUser.Loggedin}}{{end}} - + {{range .Header.ScriptsAsync}} - {{end}} - + {{end}} + {{range .Header.Scripts}} - {{end}} + {{end}} {{if .Header.MetaDesc}}{{end}} {{/** TODO: Have page / forum / topic level tags and descriptions below as-well **/}} diff --git a/templates/topic_alt_inner.html b/templates/topic_alt_inner.html index a3c563b3..b0b51589 100644 --- a/templates/topic_alt_inner.html +++ b/templates/topic_alt_inner.html @@ -1,37 +1,37 @@
-{{if gt .Page 1}}{{end}} -{{if ne .LastPage .Page}}{{end}} -{{if not .CurrentUser.Loggedin}}{{end}} +{{if gt .Page 1}}{{end}} +{{if ne .LastPage .Page}}{{end}} +{{if not .CurrentUser.Loggedin}}{{end}} -
+
-

{{.Topic.Title}}

+

{{.Topic.Title}}

- - {{.Forum.Name}} + {{.Forum.Name}} {{/** TODO: Does this need to be guarded by a permission? It's only visible in edit mode anyway, which can't be triggered, if they don't have the permission **/}} {{if .CurrentUser.Loggedin}} {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} {{if .CurrentUser.Perms.EditTopic}} -
- - +
+ + {{end}} {{end}} {{end}} {{.Topic.ViewCount}} {{/** TODO: Inline this CSS **/}} - {{if .Topic.IsClosed}}🔒︎{{end}} + {{if .Topic.IsClosed}}🔒︎{{end}}
{{if .Poll}}{{template "topic_alt_poll.html" . }}{{end}} -
+
{{template "topic_alt_userinfo.html" .Topic }}
-
{{.Topic.ContentHTML}}
- {{if .CurrentUser.Loggedin}} +
{{.Topic.ContentHTML}}
+ {{if .CurrentUser.Loggedin}} {{if .CurrentUser.Perms.EditTopic}}
@@ -45,8 +45,8 @@ {{end}}
{{if .CurrentUser.Perms.UploadFiles}} - - {{end}} + + {{end}}
@@ -56,26 +56,26 @@
{{if .CurrentUser.Loggedin}} {{if .CurrentUser.Perms.LikeItem}}{{if ne .CurrentUser.ID .Topic.CreatedBy}} - {{if .Topic.Liked}}{{else}}{{end}} + {{if .Topic.Liked}}{{else}}{{end}} {{end}}{{end}} - + {{if not .Topic.IsClosed or .CurrentUser.Perms.CloseTopic}} - {{if .CurrentUser.Perms.EditTopic}}{{end}} + {{if .CurrentUser.Perms.EditTopic}}{{end}} {{end}} - {{if .Topic.Deletable}}{{end}} + {{if .Topic.Deletable}}{{end}} {{if .CurrentUser.Perms.CloseTopic}} - {{if .Topic.IsClosed}}{{else}}{{end}}{{end}} + {{if .Topic.IsClosed}}{{else}}{{end}}{{end}} {{if .CurrentUser.Perms.PinTopic}} - {{if .Topic.Sticky}}{{else}}{{end}}{{end}} - {{if .CurrentUser.Perms.ViewIPs}}{{end}} - - + {{if .Topic.Sticky}}{{else}}{{end}}{{end}} + {{if .CurrentUser.Perms.ViewIPs}}{{end}} + + {{end}}
- - {{reltime .Topic.CreatedAt}} - {{if .CurrentUser.Perms.ViewIPs}}{{end}} + + {{reltime .Topic.CreatedAt}} + {{if .CurrentUser.Perms.ViewIPs}}{{end}}