diff --git a/csp.go b/csp.go new file mode 100644 index 0000000..ac68d1a --- /dev/null +++ b/csp.go @@ -0,0 +1,40 @@ +package main + +import ( + "net/http" +) + +const ( + cspHeader = "Content-Security-Policy" + frameOptionsHeader = "X-Frame-Options" +) + +type csp struct { + h http.Handler + opts CSPOptions +} + +type CSPOptions struct { + policy string + frame string +} + +func (c csp) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // only add a CSP if one is not already set + if existing := w.Header().Get(cspHeader); existing == "" { + w.Header().Add(cspHeader, c.opts.policy) + } + + w.Header().Set(frameOptionsHeader, c.opts.frame) + + c.h.ServeHTTP(w, r) +} + +func ContentSecurityPolicy(o CSPOptions) func(http.Handler) http.Handler { + fn := func(h http.Handler) http.Handler { + return csp{h, o} + } + return fn +} + +// vim:set ts=8 sw=8 noet: diff --git a/csp_test.go b/csp_test.go new file mode 100644 index 0000000..ae4c6db --- /dev/null +++ b/csp_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/zenazn/goji" +) + +var testCSPHeaders = map[string]string{ + "Content-Security-Policy": "default-src 'none'; style-src 'self';", + "X-Frame-Options": "SAMEORIGIN", +} + +func TestContentSecurityPolicy(t *testing.T) { + w := httptest.NewRecorder() + + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatal(err) + } + + goji.Use(ContentSecurityPolicy(CSPOptions{ + policy: testCSPHeaders["Content-Security-Policy"], + frame: testCSPHeaders["X-Frame-Options"], + })) + + goji.DefaultMux.ServeHTTP(w, req) + + for k, v := range testCSPHeaders { + if w.HeaderMap[k][0] != v { + t.Fatalf("%s header did not match expected value set by middleware", k) + } + } +} + +// vim:set ts=8 sw=8 noet: diff --git a/fileserve.go b/fileserve.go index e1d2e16..e3fd5f0 100644 --- a/fileserve.go +++ b/fileserve.go @@ -26,6 +26,8 @@ func fileServeHandler(c web.C, w http.ResponseWriter, r *http.Request) { } } + w.Header().Set("Content-Security-Policy", Config.fileContentSecurityPolicy) + http.ServeFile(w, r, filePath) } diff --git a/server.go b/server.go index 2f314ec..cf40fd2 100644 --- a/server.go +++ b/server.go @@ -19,15 +19,18 @@ import ( ) var Config struct { - bind string - filesDir string - metaDir string - noLogs bool - allowHotlink bool - siteName string - siteURL string - fastcgi bool - remoteUploads bool + bind string + filesDir string + metaDir string + noLogs bool + allowHotlink bool + siteName string + siteURL string + fastcgi bool + remoteUploads bool + contentSecurityPolicy string + fileContentSecurityPolicy string + xFrameOptions string } var Templates = make(map[string]*pongo2.Template) @@ -37,6 +40,11 @@ var timeStarted time.Time var timeStartedStr string func setup() { + goji.Use(ContentSecurityPolicy(CSPOptions{ + policy: Config.contentSecurityPolicy, + frame: Config.xFrameOptions, + })) + if Config.noLogs { goji.Abandon(middleware.Logger) } @@ -126,6 +134,14 @@ func main() { "serve through fastcgi") flag.BoolVar(&Config.remoteUploads, "remoteuploads", false, "enable remote uploads") + flag.StringVar(&Config.contentSecurityPolicy, "contentSecurityPolicy", + "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; referrer none;", + "value of default Content-Security-Policy header") + flag.StringVar(&Config.fileContentSecurityPolicy, "fileContentSecurityPolicy", + "default-src 'none'; img-src 'self'; object-src 'self'; media-src 'self'; sandbox; referrer none;", + "value of Content-Security-Policy header for file access") + flag.StringVar(&Config.xFrameOptions, "xFrameOptions", "SAMEORIGIN", + "value of X-Frame-Options header") flag.Parse() setup() diff --git a/static/css/linx.css b/static/css/linx.css index cf83032..524f998 100644 --- a/static/css/linx.css +++ b/static/css/linx.css @@ -80,6 +80,16 @@ body { padding: 5px 5px 5px 5px; } +#info #filename, +#editform #filename { + width: 232px; +} + +#info #extension, +#editform #extension { + width: 40px; +} + #info .float-left { margin-top: 2px; margin-right: 20px; @@ -181,7 +191,7 @@ body { } .clear { - clear:both; + clear: both; } #upload_header { @@ -248,6 +258,66 @@ body { padding-top: 1px; } +.oopscontent { + width: 400px; +} + +.oopscontent img { + width: 400px; + border: 0; +} + +.editor { + width: 705px; + height: 450px; + border-color: #cccccc; +} + +/* Content display {{{ */ +.display-audio, +.display-file { + width: 500px; +} + +.display-image { + margin-bottom: -6px; + max-width: 800px; +} + +.display-pdf { + width: 910px; + height: 800px; +} + +.display-video { + width: 800px; +} + +.scrollable { + overflow: auto; +} + +.storycontent { + background-color: #f0e0d6; +} + +#editform, +#editform .editor { + display: none; +} + +#codeb { + white-space: pre-wrap; +} + +#editor { + display: none; + border: 0; + width: 794px; + height: 800px; +} +/* }}} */ + /* cat.js */ .qq-uploader { position:relative; width: 100%;} @@ -694,4 +764,4 @@ THE SOFTWARE. .hint--bounce:before, .hint--bounce:after { -webkit-transition: opacity 0.3s ease, visibility 0.3s ease, -webkit-transform 0.3s cubic-bezier(0.71, 1.7, 0.77, 1.24); -moz-transition: opacity 0.3s ease, visibility 0.3s ease, -moz-transform 0.3s cubic-bezier(0.71, 1.7, 0.77, 1.24); - transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s cubic-bezier(0.71, 1.7, 0.77, 1.24); } \ No newline at end of file + transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s cubic-bezier(0.71, 1.7, 0.77, 1.24); } diff --git a/static/js/bin.js b/static/js/bin.js old mode 100755 new mode 100644 index 850ef65..b2ad0a5 --- a/static/js/bin.js +++ b/static/js/bin.js @@ -7,38 +7,42 @@ function init() { var editA = document.createElement('a'); editA.setAttribute("href", "#"); - editA.setAttribute("onclick", "edit();return false;"); + editA.addEventListener('click', function(ev) { + edit(ev); + return false; + }); editA.innerHTML = "edit"; var separator = document.createTextNode(" | "); navlist.insertBefore(editA, navlist.firstChild); navlist.insertBefore(separator, navlist.children[1]); + document.getElementById('save').addEventListener('click', paste); + document.getElementById('wordwrap').addEventListener('click', wrap); } -function edit() { +function edit(ev) { navlist.remove(); document.getElementById("filename").remove(); - document.getElementById("foarm").style.display = "block"; + document.getElementById("editform").style.display = "block"; var normalcontent = document.getElementById("normal-content"); normalcontent.removeChild(document.getElementById("normal-code")); - + var editordiv = document.getElementById("editor"); editordiv.style.display = "block"; } -function paste() { +function paste(ev) { var editordiv = document.getElementById("editor"); document.getElementById("newcontent").value = editordiv.value; document.forms["reply"].submit(); - } -function wrap() { +function wrap(ev) { if (document.getElementById("wordwrap").checked) { document.getElementById("codeb").style.wordWrap = "break-word"; document.getElementById("codeb").style.whiteSpace = "pre-wrap"; diff --git a/static/js/bin_hljs.js b/static/js/bin_hljs.js new file mode 100644 index 0000000..ff8ef6e --- /dev/null +++ b/static/js/bin_hljs.js @@ -0,0 +1,2 @@ +hljs.tabReplace = ' '; +hljs.initHighlightingOnLoad(); diff --git a/static/js/upload.js b/static/js/upload.js index 6c6dc62..222332c 100644 --- a/static/js/upload.js +++ b/static/js/upload.js @@ -5,33 +5,36 @@ Dropzone.options.dropzone = { var upload = document.createElement("div"); upload.className = "upload"; - var left = document.createElement("span"); - left.innerHTML = file.name; - file.leftElement = left; - upload.appendChild(left); + var fileLabel = document.createElement("span"); + fileLabel.innerHTML = file.name; + file.fileLabel = fileLabel; + upload.appendChild(fileLabel); - var right = document.createElement("div"); - right.className = "right"; - var rightleft = document.createElement("span"); - rightleft.className = "cancel"; - rightleft.innerHTML = "Cancel"; - rightleft.onclick = function(ev) { - this.removeFile(file); - }.bind(this); + var fileActions = document.createElement("div"); + fileActions.className = "right"; + file.fileActions = fileActions; + upload.appendChild(fileActions); + + var cancelAction = document.createElement("span"); + cancelAction.className = "cancel"; + cancelAction.innerHTML = "Cancel"; + cancelAction.addEventListener('click', function(ev) { + this.removeFile(file); + }.bind(this)); + file.cancelActionElement = cancelAction; + fileActions.appendChild(cancelAction); + + var progress = document.createElement("span"); + file.progressElement = progress; + fileActions.appendChild(progress); - var rightright = document.createElement("span"); - right.appendChild(rightleft); - file.rightLeftElement = rightleft; - right.appendChild(rightright); - file.rightRightElement = rightright; - file.rightElement = right; - upload.appendChild(right); file.uploadElement = upload; + document.getElementById("uploads").appendChild(upload); }, uploadprogress: function(file, p, bytesSent) { p = parseInt(p); - file.rightRightElement.innerHTML = p + "%"; + file.progressElement.innerHTML = p + "%"; file.uploadElement.setAttribute("style", 'background-image: -webkit-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -moz-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -ms-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -o-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%)'); }, sending: function(file, xhr, formData) { @@ -39,36 +42,48 @@ Dropzone.options.dropzone = { formData.append("expires", document.getElementById("expires").selectedOptions[0].value); }, success: function(file, resp) { - file.rightLeftElement.innerHTML = ""; - file.leftElement.innerHTML = '' + resp.url + ''; - file.rightRightElement.innerHTML = "Delete"; - file.rightRightElement.className = "cancel"; - file.rightRightElement.onclick = function(ev) { + file.fileActions.removeChild(file.progressElement); + + var fileLabelLink = document.createElement("a"); + fileLabelLink.href = resp.url; + fileLabelLink.target = "_blank"; + fileLabelLink.innerHTML = resp.url; + file.fileLabel.innerHTML = ""; + file.fileLabelLink = fileLabelLink; + file.fileLabel.appendChild(fileLabelLink); + + var deleteAction = document.createElement("span"); + deleteAction.innerHTML = "Delete"; + deleteAction.className = "cancel"; + deleteAction.addEventListener('click', function(ev) { xhr = new XMLHttpRequest(); xhr.open("DELETE", resp.url, true); xhr.setRequestHeader("X-Delete-Key", resp.delete_key); xhr.onreadystatechange = function(file) { - if (xhr.status === 200) { - file.leftElement.innerHTML = 'Deleted ' + resp.url + ''; - file.leftElement.className = "deleted"; - file.rightRightElement.onclick = null; - file.rightRightElement.innerHTML = ""; + if (xhr.readyState == 4 && xhr.status === 200) { + var text = document.createTextNode("Deleted "); + file.fileLabel.insertBefore(text, file.fileLabelLink); + file.fileLabel.className = "deleted"; + file.fileActions.removeChild(file.cancelActionElement); } }.bind(this, file); xhr.send(); - }.bind(this); + }); + file.fileActions.removeChild(file.cancelActionElement); + file.cancelActionElement = deleteAction; + file.fileActions.appendChild(deleteAction); }, error: function(file, resp, xhrO) { - file.rightLeftElement.onclick = null; - file.rightLeftElement.innerHTML = ""; - file.rightRightElement.innerHTML = ""; + file.fileActions.removeChild(file.cancelActionElement); + file.fileActions.removeChild(file.progressElement); + if (file.status === "canceled") { - file.leftElement.innerHTML = file.name + ": Canceled "; + file.fileLabel.innerHTML = file.name + ": Canceled "; } else { - file.leftElement.innerHTML = file.name + ": " + resp.error; + file.fileLabel.innerHTML = file.name + ": " + resp.error; } - file.leftElement.className = "error"; + file.fileLabel.className = "error"; }, maxFilesize: 4096, diff --git a/templates/404.html b/templates/404.html index 0999393..e792bf3 100644 --- a/templates/404.html +++ b/templates/404.html @@ -1,5 +1,5 @@ {% extends "base.html" %} {% block content %} - + {% endblock %} diff --git a/templates/display/audio.html b/templates/display/audio.html index 66b36fa..a88319f 100644 --- a/templates/display/audio.html +++ b/templates/display/audio.html @@ -1,9 +1,9 @@ {% extends "base.html" %} {% block main %} -