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)
}

View File

@ -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()

View File

@ -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%;}

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

@ -7,21 +7,26 @@ 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"));
@ -31,14 +36,13 @@ function edit() {
}
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";

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");
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 = '<a target="_blank" href="' + resp.url + '">' + resp.url + '</a>';
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 <a target="_blank" href="' + resp.url + '">' + resp.url + '</a>';
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,

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% 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 %}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,7 @@
{% extends "base.html" %}
{% block main %}
<object data="/selif/{{ filename }}"
type="application/pdf"
width=910
height=800>
<object class="display-pdf" data="/selif/{{ filename }}" type="application/pdf">
<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>

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
<form id="reply" action='/upload' method='post'>
<div id="main">
<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">
<select id="expiry" name="expires">
@ -27,7 +27,7 @@
</div>
<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>