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. 16
      server.go
  5. 70
      static/css/linx.css
  6. 16
      static/js/bin.js
  7. 2
      static/js/bin_hljs.js
  8. 87
      static/js/upload.js
  9. 2
      templates/404.html
  10. 2
      templates/display/audio.html
  11. 30
      templates/display/bin.html
  12. 2
      templates/display/file.html
  13. 2
      templates/display/image.html
  14. 5
      templates/display/pdf.html
  15. 6
      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)
} }

16
server.go

@ -28,6 +28,9 @@ var Config struct {
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()

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

87
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) {
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); this.removeFile(file);
}.bind(this);
}.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 %}

2
templates/display/audio.html

@ -1,7 +1,7 @@
{% 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>

30
templates/display/bin.html

@ -2,17 +2,17 @@
{% 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">
@ -27,34 +27,30 @@
<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" />
<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 %}

2
templates/display/file.html

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

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>

6
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">
<video class="display-video" controls autoplay>
<source src="/selif/{{ filename }}"/> <source src="/selif/{{ filename }}"/>
<a href='/selif/{{ filename }}'>Download it instead</a>
</video> </video>
</div>
{% 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