Merge pull request #36 from mutantmonkey/csp

Add support for Content-Security-Policy and X-Frame-Options
This commit is contained in:
Andrei Marcu 2015-10-04 18:22:52 -04:00
commit 7152adb902
18 changed files with 277 additions and 99 deletions

40
csp.go Normal file
View File

@ -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:

38
csp_test.go Normal file
View File

@ -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:

View File

@ -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) http.ServeFile(w, r, filePath)
} }

View File

@ -19,15 +19,18 @@ import (
) )
var Config struct { var Config struct {
bind string bind string
filesDir string filesDir string
metaDir string metaDir string
noLogs bool noLogs bool
allowHotlink bool allowHotlink bool
siteName string siteName string
siteURL string siteURL string
fastcgi bool fastcgi bool
remoteUploads bool remoteUploads bool
contentSecurityPolicy string
fileContentSecurityPolicy string
xFrameOptions string
} }
var Templates = make(map[string]*pongo2.Template) var Templates = make(map[string]*pongo2.Template)
@ -37,6 +40,11 @@ var timeStarted time.Time
var timeStartedStr string var timeStartedStr string
func setup() { func setup() {
goji.Use(ContentSecurityPolicy(CSPOptions{
policy: Config.contentSecurityPolicy,
frame: Config.xFrameOptions,
}))
if Config.noLogs { if Config.noLogs {
goji.Abandon(middleware.Logger) goji.Abandon(middleware.Logger)
} }
@ -126,6 +134,14 @@ func main() {
"serve through fastcgi") "serve through fastcgi")
flag.BoolVar(&Config.remoteUploads, "remoteuploads", false, flag.BoolVar(&Config.remoteUploads, "remoteuploads", false,
"enable remote uploads") "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() flag.Parse()
setup() setup()

View File

@ -80,6 +80,16 @@ body {
padding: 5px 5px 5px 5px; padding: 5px 5px 5px 5px;
} }
#info #filename,
#editform #filename {
width: 232px;
}
#info #extension,
#editform #extension {
width: 40px;
}
#info .float-left { #info .float-left {
margin-top: 2px; margin-top: 2px;
margin-right: 20px; margin-right: 20px;
@ -181,7 +191,7 @@ body {
} }
.clear { .clear {
clear:both; clear: both;
} }
#upload_header { #upload_header {
@ -248,6 +258,66 @@ body {
padding-top: 1px; 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 */ /* cat.js */
.qq-uploader { position:relative; width: 100%;} .qq-uploader { position:relative; width: 100%;}

16
static/js/bin.js Executable file → Normal file
View File

@ -7,21 +7,26 @@ function init() {
var editA = document.createElement('a'); var editA = document.createElement('a');
editA.setAttribute("href", "#"); editA.setAttribute("href", "#");
editA.setAttribute("onclick", "edit();return false;"); editA.addEventListener('click', function(ev) {
edit(ev);
return false;
});
editA.innerHTML = "edit"; editA.innerHTML = "edit";
var separator = document.createTextNode(" | "); var separator = document.createTextNode(" | ");
navlist.insertBefore(editA, navlist.firstChild); navlist.insertBefore(editA, navlist.firstChild);
navlist.insertBefore(separator, navlist.children[1]); 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(); navlist.remove();
document.getElementById("filename").remove(); document.getElementById("filename").remove();
document.getElementById("foarm").style.display = "block"; document.getElementById("editform").style.display = "block";
var normalcontent = document.getElementById("normal-content"); var normalcontent = document.getElementById("normal-content");
normalcontent.removeChild(document.getElementById("normal-code")); normalcontent.removeChild(document.getElementById("normal-code"));
@ -31,14 +36,13 @@ function edit() {
} }
function paste() { function paste(ev) {
var editordiv = document.getElementById("editor"); var editordiv = document.getElementById("editor");
document.getElementById("newcontent").value = editordiv.value; document.getElementById("newcontent").value = editordiv.value;
document.forms["reply"].submit(); document.forms["reply"].submit();
} }
function wrap() { function wrap(ev) {
if (document.getElementById("wordwrap").checked) { if (document.getElementById("wordwrap").checked) {
document.getElementById("codeb").style.wordWrap = "break-word"; document.getElementById("codeb").style.wordWrap = "break-word";
document.getElementById("codeb").style.whiteSpace = "pre-wrap"; document.getElementById("codeb").style.whiteSpace = "pre-wrap";

2
static/js/bin_hljs.js Normal file
View File

@ -0,0 +1,2 @@
hljs.tabReplace = ' ';
hljs.initHighlightingOnLoad();

View File

@ -5,33 +5,36 @@ Dropzone.options.dropzone = {
var upload = document.createElement("div"); var upload = document.createElement("div");
upload.className = "upload"; upload.className = "upload";
var left = document.createElement("span"); var fileLabel = document.createElement("span");
left.innerHTML = file.name; fileLabel.innerHTML = file.name;
file.leftElement = left; file.fileLabel = fileLabel;
upload.appendChild(left); upload.appendChild(fileLabel);
var right = document.createElement("div"); var fileActions = document.createElement("div");
right.className = "right"; fileActions.className = "right";
var rightleft = document.createElement("span"); file.fileActions = fileActions;
rightleft.className = "cancel"; upload.appendChild(fileActions);
rightleft.innerHTML = "Cancel";
rightleft.onclick = function(ev) { var cancelAction = document.createElement("span");
this.removeFile(file); cancelAction.className = "cancel";
}.bind(this); 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; file.uploadElement = upload;
document.getElementById("uploads").appendChild(upload); document.getElementById("uploads").appendChild(upload);
}, },
uploadprogress: function(file, p, bytesSent) { uploadprogress: function(file, p, bytesSent) {
p = parseInt(p); 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 + '%)'); 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) { sending: function(file, xhr, formData) {
@ -39,36 +42,48 @@ Dropzone.options.dropzone = {
formData.append("expires", document.getElementById("expires").selectedOptions[0].value); formData.append("expires", document.getElementById("expires").selectedOptions[0].value);
}, },
success: function(file, resp) { success: function(file, resp) {
file.rightLeftElement.innerHTML = ""; file.fileActions.removeChild(file.progressElement);
file.leftElement.innerHTML = '<a target="_blank" href="' + resp.url + '">' + resp.url + '</a>';
file.rightRightElement.innerHTML = "Delete"; var fileLabelLink = document.createElement("a");
file.rightRightElement.className = "cancel"; fileLabelLink.href = resp.url;
file.rightRightElement.onclick = function(ev) { 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 = new XMLHttpRequest();
xhr.open("DELETE", resp.url, true); xhr.open("DELETE", resp.url, true);
xhr.setRequestHeader("X-Delete-Key", resp.delete_key); xhr.setRequestHeader("X-Delete-Key", resp.delete_key);
xhr.onreadystatechange = function(file) { xhr.onreadystatechange = function(file) {
if (xhr.status === 200) { if (xhr.readyState == 4 && xhr.status === 200) {
file.leftElement.innerHTML = 'Deleted <a target="_blank" href="' + resp.url + '">' + resp.url + '</a>'; var text = document.createTextNode("Deleted ");
file.leftElement.className = "deleted"; file.fileLabel.insertBefore(text, file.fileLabelLink);
file.rightRightElement.onclick = null; file.fileLabel.className = "deleted";
file.rightRightElement.innerHTML = ""; file.fileActions.removeChild(file.cancelActionElement);
} }
}.bind(this, file); }.bind(this, file);
xhr.send(); xhr.send();
}.bind(this); });
file.fileActions.removeChild(file.cancelActionElement);
file.cancelActionElement = deleteAction;
file.fileActions.appendChild(deleteAction);
}, },
error: function(file, resp, xhrO) { error: function(file, resp, xhrO) {
file.rightLeftElement.onclick = null; file.fileActions.removeChild(file.cancelActionElement);
file.rightLeftElement.innerHTML = ""; file.fileActions.removeChild(file.progressElement);
file.rightRightElement.innerHTML = "";
if (file.status === "canceled") { if (file.status === "canceled") {
file.leftElement.innerHTML = file.name + ": Canceled "; file.fileLabel.innerHTML = file.name + ": Canceled ";
} }
else { 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, maxFilesize: 4096,

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<a href="/"><img style="border:0;" src='/static/images/404.jpg' width='400'></a> <a href="/"><img src='/static/images/404.jpg'></a>
{% endblock %} {% endblock %}

View File

@ -1,9 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% block main %}
<audio controls style='width: 500px;' preload='auto'> <audio class="display-audio" controls preload='auto'>
<source src='/selif/{{ filename }}'> <source src='/selif/{{ filename }}'>
<a href='/selif/{{ filename }}'>Download it instead</a> <a href='/selif/{{ filename }}'>Download it instead</a>
</audio> </audio>
{% endblock %} {% endblock %}

View File

@ -2,20 +2,20 @@
{% block head %} {% block head %}
{% if extra.extension == "story" %} {% if extra.extension == "story" %}
<link href="/static/css/highlight/story.css" rel="stylesheet" type="text/css" /> <link href="/static/css/highlight/story.css" rel="stylesheet" type="text/css">
{% else %} {% else %}
<link href="/static/css/highlight/tomorrow.css" rel="stylesheet" type="text/css" /> <link href="/static/css/highlight/tomorrow.css" rel="stylesheet" type="text/css">
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block innercontentmore %} style="overflow: auto;" {% endblock %} {% block innercontentmore %} class="scrollable"{% endblock %}
{% block mainmore %} {% if extra.extension == "story" %} style="background-color: #f0e0d6;"{% endif %} {% endblock %} {% block mainmore %} {% if extra.extension == "story" %} class="storycontent"{% endif %} {% endblock %}
{% block infoleft %} {% block infoleft %}
<div id="foarm" style="display: none;"> <div id="editform">
<form id="reply" action='/upload' method='post' > <form id="reply" action='/upload' method='post' >
<div class="right"> <div class="right">
<select id="expiry" name="expires"> <select id="expiry" name="expires">
<option disabled=disabled>Expires:</option> <option disabled=disabled>Expires:</option>
<option value="0">never</option> <option value="0">never</option>
<option value="60">a minute</option> <option value="60">a minute</option>
@ -25,36 +25,32 @@
<option value="604800">a week</option> <option value="604800">a week</option>
<option value="2419200">a month</option> <option value="2419200">a month</option>
<option value="29030400">a year</option> <option value="29030400">a year</option>
</select> </select>
<button id="save" onclick="paste()">save</button> <button id="save">save</button>
</div> </div>
<input style ="width:232px;" class="codebox" name='filename' id="filename" type='text' value="" placeholder="filename (empty for random filename)" />.<input id="extension" class="codebox" style="width:30px;" name='extension' type='text' value="{{ extra.extension }}" placeholder="txt" /> <input class="codebox" name='filename' id="filename" type='text' value="" placeholder="filename (empty for random filename)">.<input id="extension" class="codebox" name='extension' type='text' value="{{ extra.extension }}" placeholder="txt">
<textarea name='content' id="newcontent" class="editor" style="display: none;"></textarea> <textarea name='content' id="newcontent" class="editor"></textarea>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}
{%block infomore %} {%block infomore %}
<label>wrap <input id="wordwrap" type="checkbox" onclick="wrap()" checked /></label> | <label>wrap <input id="wordwrap" type="checkbox" checked></label> |
{% endblock %} {% endblock %}
{% block main %} {% block main %}
<div id="normal-content" class="normal {% if extra.lang_hl != "story" %}fixed{% endif %}"> <div id="normal-content" class="normal {% if extra.lang_hl != "story" %}fixed{% endif %}">
<pre id="normal-code"><code id="codeb" style="white-space: pre-wrap;" class="{{ extra.lang_hl }}">{{ extra.contents }}</pre></code> <pre id="normal-code"><code id="codeb" class="{{ extra.lang_hl }}">{{ extra.contents }}</code></pre>
<textarea id="editor" style="display: none; height: 800px; font-size: 11px;">{{ extra.contents }}</textarea> <textarea id="editor">{{ extra.contents }}</textarea>
</div> </div>
{% if extra.lang_hl != "text" %} {% if extra.lang_hl != "text" %}
<script src="/static/js/highlight/highlight.pack.js"></script> <script src="/static/js/highlight/highlight.pack.js"></script>
<script> <script src="/static/js/bin_hljs.js"></script>
hljs.tabReplace = ' ';
hljs.initHighlightingOnLoad();
</script>
{% endif %} {% endif %}
<script type="text/javascript" src="/static/js/bin.js"></script> <script src="/static/js/bin.js"></script>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% block main %}
<div class="normal" style="width: 500px;"> <div class="normal display-file">
<p class="center">You are requesting <a href="/selif/{{ filename }}">{{ filename }}</a>, <a href="/selif/{{ filename }}">click here</a> to download.</p> <p class="center">You are requesting <a href="/selif/{{ filename }}">{{ filename }}</a>, <a href="/selif/{{ filename }}">click here</a> to download.</p>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,6 +2,6 @@
{% block main %} {% block main %}
<a href="/selif/{{ filename }}"> <a href="/selif/{{ filename }}">
<img style="margin-bottom: -6px; max-width: 800px;" src="/selif/{{ filename }}" /> <img class="display-image" src="/selif/{{ filename }}" />
</a> </a>
{% endblock %} {% endblock %}

View File

@ -1,10 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% block main %}
<object data="/selif/{{ filename }}" <object class="display-pdf" data="/selif/{{ filename }}" type="application/pdf">
type="application/pdf"
width=910
height=800>
<p>It appears your Web browser is not configured to display PDF files. <p>It appears your Web browser is not configured to display PDF files.
No worries, just <a href="/selif/{{ filename }}">click here to download the PDF file.</a></p> No worries, just <a href="/selif/{{ filename }}">click here to download the PDF file.</a></p>

View File

@ -1,10 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% block main %}
<div id='video'> <video class="display-video" controls autoplay>
<video controls autoplay width="800"> <source src="/selif/{{ filename }}"/>
<source src="/selif/{{ filename }}"/> <a href='/selif/{{ filename }}'>Download it instead</a>
</video> </video>
</div>
{% endblock %} {% endblock %}

View File

@ -20,7 +20,6 @@
<div id="expiry"> <div id="expiry">
<label>File expiry: <label>File expiry:
<select name="expires" id="expires"> <select name="expires" id="expires">
</label>
<option value="0">never</option> <option value="0">never</option>
<option value="60">a minute</option> <option value="60">a minute</option>
<option value="300">5 minutes</option> <option value="300">5 minutes</option>
@ -30,13 +29,14 @@
<option value="2419200">a month</option> <option value="2419200">a month</option>
<option value="29030400">a year</option> <option value="29030400">a year</option>
</select> </select>
</label>
</div> </div>
<label><input name="randomize" id="randomize" type="checkbox" checked /> Randomize filename</label> <label><input name="randomize" id="randomize" type="checkbox" checked /> Randomize filename</label>
</div> </div>
<div class="clear"></div> <div class="clear"></div>
</form> </form>
<div id="uploads"></div> <div id="uploads"></div>
<div style="clear:both;"></div> <div class="clear"></div>
</div> </div>
<script src="/static/js/dropzone.js"></script> <script src="/static/js/dropzone.js"></script>

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div id="main"> <div id="main">
<div id='inner_content' style='width: 400px'> <div id='inner_content' class='oopscontent'>
<p>{{ msg }}</p> <p>{{ msg }}</p>
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@
<form id="reply" action='/upload' method='post'> <form id="reply" action='/upload' method='post'>
<div id="main"> <div id="main">
<div id="info" class="ninfo"> <div id="info" class="ninfo">
<input style ="width:232px;" class="codebox" name='filename' id="filename" type='text' value="" placeholder="filename (empty for random filename)" />.<span class="hint--top hint--bounce" data-hint="Enable syntax highlighting by adding the extension"><input id="extension" class="codebox" style="width:40px;" name='extension' type='text' value="" placeholder="txt" /></span> <input class="codebox" name='filename' id="filename" type='text' value="" placeholder="filename (empty for random filename)" />.<span class="hint--top hint--bounce" data-hint="Enable syntax highlighting by adding the extension"><input id="extension" class="codebox" name='extension' type='text' value="" placeholder="txt" /></span>
<div class="right"> <div class="right">
<select id="expiry" name="expires"> <select id="expiry" name="expires">
@ -27,7 +27,7 @@
</div> </div>
<div id="inner_content"> <div id="inner_content">
<textarea name='content' id="content" class="editor" style="width: 705px; height: 450px; border-color: #cccccc;"></textarea> <textarea name='content' id="content" class="editor"></textarea>
</div> </div>
</div> </div>