Browse Source

Merge pull request #36 from mutantmonkey/csp

Add support for Content-Security-Policy and X-Frame-Options
pull/40/head
Andrei Marcu 9 years ago
parent
commit
7152adb902
  1. 40
      csp.go
  2. 38
      csp_test.go
  3. 2
      fileserve.go
  4. 34
      server.go
  5. 74
      static/css/linx.css
  6. 18
      static/js/bin.js
  7. 2
      static/js/bin_hljs.js
  8. 89
      static/js/upload.js
  9. 2
      templates/404.html
  10. 6
      templates/display/audio.html
  11. 36
      templates/display/bin.html
  12. 4
      templates/display/file.html
  13. 2
      templates/display/image.html
  14. 5
      templates/display/pdf.html
  15. 10
      templates/display/video.html
  16. 6
      templates/index.html
  17. 2
      templates/oops.html
  18. 6
      templates/paste.html

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

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

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

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

74
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); }
transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s cubic-bezier(0.71, 1.7, 0.77, 1.24); }

18
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";

2
static/js/bin_hljs.js

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

89
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 = '<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,

2
templates/404.html

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

6
templates/display/audio.html

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

36
templates/display/bin.html

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

4
templates/display/file.html

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

2
templates/display/image.html

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

5
templates/display/pdf.html

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

10
templates/display/video.html

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

6
templates/index.html

@ -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,15 +29,16 @@
<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>
<script src="/static/js/upload.js"></script>
{% endblock %}
{% endblock %}

2
templates/oops.html

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

6
templates/paste.html

@ -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>
@ -37,4 +37,4 @@
</form>
{% endblock %}
{% endblock %}
Loading…
Cancel
Save