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. 72
      static/css/linx.css
  6. 16
      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. 4
      templates/index.html
  17. 2
      templates/oops.html
  18. 4
      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) http.ServeFile(w, r, filePath)
} }

34
server.go

@ -19,15 +19,18 @@ import (
) )
var Config struct { 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) 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()

72
static/css/linx.css

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

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

@ -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"); var upload = document.createElement("div");
upload.className = "upload"; 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; 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.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 = 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) {
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); }.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.rightLeftElement.innerHTML = "";
file.rightRightElement.innerHTML = "";
file.fileActions.removeChild(file.cancelActionElement);
file.fileActions.removeChild(file.progressElement);
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,

2
templates/404.html

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

6
templates/display/audio.html

@ -1,9 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% 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> </audio>
{% endblock %} {% endblock %}

36
templates/display/bin.html

@ -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 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 %} {% 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>
</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> </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>
<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> </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>
hljs.tabReplace = ' ';
hljs.initHighlightingOnLoad();
</script>
<script src="/static/js/bin_hljs.js"></script>
{% endif %} {% endif %}
<script type="text/javascript" src="/static/js/bin.js"></script>
<script src="/static/js/bin.js"></script>
{% endblock %} {% endblock %}

4
templates/display/file.html

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% 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> </div>
{% endblock %} {% endblock %}

2
templates/display/image.html

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

5
templates/display/pdf.html

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

10
templates/display/video.html

@ -1,10 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block main %} {% 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 %} {% endblock %}

4
templates/index.html

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

2
templates/oops.html

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

4
templates/paste.html

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

Loading…
Cancel
Save