58 Commits

Author SHA1 Message Date
dbb6a954e1 Don't capture cursor in screenshot.
Adds `-u` to maim to not capture the cursor.

However, maim still captures the cursor sometimes even with -u,
so this also switches the order to prefer `import` over `maim`,
which does not do this.
2020-05-15 13:08:18 +02:00
Daan Sprenkels
2c889e0808 Prevent directory traversal in file upload
Fixes #53
2020-05-12 20:01:03 +02:00
Daan Sprenkels
737a26fee3 test: Look for StatusFound instead of StatusOk; NFC 2020-05-12 19:09:43 +02:00
Daan Sprenkels
016ffa8949 meta: Do 410 Gone if paste deleted 2020-05-11 22:45:56 +02:00
Daan Sprenkels
b9119a0df5 meta: Fix CanDelete string 2020-05-11 22:32:35 +02:00
Daan Sprenkels
a4cec1e4b0 meta: Add a delete button 2020-05-11 22:26:45 +02:00
Daan Sprenkels
b7ea5dfa4f Redirect to /xd42/meta after upload
Fixes #51
2020-05-11 22:22:23 +02:00
Daan Sprenkels
a0c8383555 template: Fix <pre> nesting 2020-05-11 20:58:50 +02:00
bbe787da5d Merge pull request 'Make rushlink script more portable.' (#48) from mara/rushlink:master into master 2020-05-04 12:12:46 +02:00
28ddaee9d9 Make rushlink script more portable.
It didn't work on mac (and bsd, probably). Now it does.
2020-05-04 12:06:06 +02:00
Daan Sprenkels
01adfa8f2f Merge pull request 'Add a test for issue #45; NFC' (#46) from test-issue-45 into master 2020-04-27 13:00:31 +02:00
Daan Sprenkels
c57e719e15 Add a test for issue #45; NFC
Fixes #45
2020-04-27 13:00:46 +02:00
Daan Sprenkels
c0e4ac2c40 mod: Bump dependencies 2020-04-22 19:30:20 +02:00
Daan Sprenkels
b73317c249 Add generated bindata.go file to repo 2020-04-22 19:05:56 +02:00
Daan Sprenkels
8e89955ce9 Remove debug logging statement; NFC 2020-04-22 18:25:55 +02:00
Daan Sprenkels
e476797da0 db: Prevent infinite recursion when closing 2020-04-22 18:25:27 +02:00
Daan Sprenkels
728d3833c3 Change redirect status code to Temporary Redirect 2020-04-22 18:24:46 +02:00
Daan Sprenkels
42ccc18002 User url.Parse instead of url.ParseRequestURI
url.ParseRequestURI assumes the URL does not contain a fragment
identifier.  However, this is not disallowed. So we should use
url.Parse instead.

Related issue: #45
2020-04-22 16:11:32 +02:00
Daan Sprenkels
63a588ba59 db: Add docstrings to FileUpload; NFC 2020-04-22 16:00:36 +02:00
Daan Sprenkels
3da165a57b Merge pull request 'Add copy-to-clipboard button to meta page' (#44) from issue-42 into master 2020-04-14 16:08:07 +02:00
Daan Sprenkels
09481f47e6 Do not show button if clipboard not available 2020-04-05 18:46:40 +02:00
Daan Sprenkels
1dd0d17ba5 Add copy-to-clipboard button to meta page
Fixes #42.
2020-04-05 18:16:41 +02:00
Daan Sprenkels
7c0bfaee76 .gitignore: Ignore .vscode dir; NFC 2020-04-05 18:15:41 +02:00
Daan Sprenkels
a766d5d596 Update .gitignore; NFC 2020-04-05 18:12:16 +02:00
Daan Sprenkels
c28dfd0cb4 Merge pull request 'Add rushlink --screenshot.' (#41) from mara/rushlink:master into master 2020-03-26 09:37:25 +01:00
732b1fc2a6 Add rushlink --screenshot. 2020-03-24 17:25:51 +01:00
8403ad2258 Make rushlink --delete work with urls that have a file extension. 2020-03-24 14:42:54 +01:00
Daan Sprenkels
ad1ce67495 Merge pull request 'Add command line tool to submit and delete files and links.' (#39) from mara/rushlink:master into master 2020-03-23 17:00:45 +01:00
11ea63e1fc Add command line tool to submit and delete files and links.
Fixes #11.
2020-03-23 17:00:24 +01:00
Daan Sprenkels
0a35cb2508 Merge branch 'master' of gitea.hashru.nl:dsprenkels/rushlink 2020-01-05 00:15:23 +01:00
Daan Sprenkels
de19234108 return after serving meta page 2020-01-05 00:14:53 +01:00
9a690e2b8b README: restructure to accentuate building/deploying 2019-12-28 19:24:59 +01:00
ac2c62f9e6 README updated with -root_url and sample systemd unit file 2019-12-28 18:47:20 +01:00
Daan Sprenkels
095348d614 web: Add an example w/ upload from process output
Fixes #35
2019-12-19 23:29:09 +01:00
Daan Sprenkels
245dd64f82 Directly serve files instead of redirect
Fixes #28
2019-12-19 23:17:37 +01:00
Daan Sprenkels
5e6ce9c2be Replace io.Copy w/ http.ServeContent for download 2019-12-19 20:09:55 +04:00
Daan Sprenkels
8dce4e8483 web: Indent <pre> with padding-left (no spaces)
Fixes #34
2019-12-17 23:11:43 +05:30
Daan Sprenkels
ffeb9a3362 Rename --host => --root-url 2019-12-17 15:43:32 +05:30
Daan Sprenkels
3d07acb222 Show meta page immediately after create 2019-12-17 15:33:29 +05:30
Daan Sprenkels
c46a26f8a2 web: Implement drag-and-drop upload 2019-12-16 16:21:41 +05:30
Daan Sprenkels
087b9920e6 [refactor] Add !=nil check in renderCreateSuccess 2019-12-16 11:27:09 +05:30
Daan Sprenkels
ca859adab1 Redirect to /meta after upload/shorten 2019-12-16 10:51:21 +05:30
Daan Sprenkels
d34ac11d5e Update metadata info view 2019-12-16 10:19:17 +05:30
Daan Sprenkels
824c6f41e2 Enable submit using web interface 2019-12-16 09:53:36 +05:30
Daan Sprenkels
f32e47b4c8 Add deprecation warning for omitting --host 2019-12-15 17:08:32 +05:30
Daan Sprenkels
8cbe984ba4 Default {{.Host}} to https:// 2019-12-15 17:06:48 +05:30
Daan Sprenkels
0bffde1dc1 Implement --host flag to override {{.Host}}
Fixes #15
2019-12-15 16:48:50 +05:30
Daan Sprenkels
41f4de43ac Return both URLs after upload
Fixes #25
2019-12-15 12:42:10 +05:30
Daan Sprenkels
728b5d9d4b Add extension to FileUpload request url
Fixes #27
2019-12-15 12:21:54 +05:30
Daan Sprenkels
40a32fa535 Respond to HEAD requests
Fixes #21
2019-12-15 11:44:27 +05:30
Daan Sprenkels
ba08aca622 db: Refactor paste decoding into new func 2019-12-10 12:24:58 +01:00
Daan Sprenkels
76cf92e22d db: Add missing docs to public symbols 2019-12-10 12:08:27 +01:00
Daan Sprenkels
62e82d831e db: Migrate FileUpload.ContentTypes to auto-detect 2019-12-10 11:59:02 +01:00
Daan Sprenkels
eec5e4def4 Detect file types instead of trusting clients 2019-12-10 11:16:18 +01:00
Daan Sprenkels
f9c74a83f0 Add test for #17
Closes #17.
2019-12-08 22:49:42 +01:00
Daan Sprenkels
da5806a6f7 metrict: Commit change that was forgotten in cf95650 2019-12-08 22:02:30 +01:00
Daan Sprenkels
f1fe160655 go mod tidy 2019-12-08 21:56:30 +01:00
Daan Sprenkels
cf956501ac metrics: Add http_requests metric 2019-12-08 21:56:02 +01:00
30 changed files with 1634 additions and 259 deletions

10
.gitignore vendored
View File

@@ -14,8 +14,12 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# Generated code from assets using bindata
bindata.go
# Output binary # Output binary
/rushlink /rushlink
# Any kind of backup files
*~
*.bak
# Visual Code local config directory
.vscode

View File

@@ -2,12 +2,53 @@
A URL shortener and (maybe) a pastebin server for our #ru community. A URL shortener and (maybe) a pastebin server for our #ru community.
## Build instructions ## Building
- `go get -u github.com/go-bindata/go-bindata/...` - `go get -u github.com/go-bindata/go-bindata/...`
- `go generate ./...` - `go generate ./...`
- `go build ./cmd/rushlink` - `go build ./cmd/rushlink`
## Deploying
We recommend running `rushlink` behind a reverse proxy suitable for processing
HTTP requests, such as `nginx`, or `haproxy`.
## Sample `nginx` config
```
server {
location / {
root /var/www/rushlink;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host rushlink.local;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
}
}
```
`rushlink` automatically detects whether `http` or `https` is used when
`X-Forwarded-Proto` is correctly set. Otherwise, pass `-root_url
https://rushlink.local` to the binary (e.g. in the `systemd` unit file).
## Sample `systemd` unit file
```
[Install]
WantedBy=nginx.service
[Service]
Type=simple
User=rushlink
Group=nogroup
ExecStart=/var/lib/rushlink/rushlink -database /var/lib/rushlink/db -file-store /var/lib/rushlink/filestore -root_url https://rushlink.local
```
---
# Background
## Libraries ## Libraries
Use standard-Go-libraries if the job can be done with those. As of now, these Use standard-Go-libraries if the job can be done with those. As of now, these
@@ -24,7 +65,7 @@ are the exceptions:
## Database ## Database
We will be using [`go.etcd.io/bbolt`]. This file should be the *only* file We use [`go.etcd.io/bbolt`]. This file should be the *only* file
apart from our monolithic binary. All settings and keys should go in here. apart from our monolithic binary. All settings and keys should go in here.
Any read-only data resides in the binary file (possibly compressed). Any read-only data resides in the binary file (possibly compressed).
@@ -105,17 +146,3 @@ header that the client sends. We can still wrap the plain-text page in a single
We will try as hard as possible to not store any data about our users, and will We will try as hard as possible to not store any data about our users, and will
only provide any data when we have the legal obligation to do so. only provide any data when we have the legal obligation to do so.
## Sample `nginx` config
```
server {
location / {
root /var/www/rushlink;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host rushlink.local;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_http_version 1.1;
}
}
```

34
assets/css/main.css Normal file
View File

@@ -0,0 +1,34 @@
body {
font-family: monospace;
}
#dropZone {
background-color: rgba(0, 0, 0.2, 0.75);
color: white;
font-size: 60px;
font-weight: bold;
height: 100%;
left: 0;
padding: 1em;
position: fixed;
top: 0;
transition: visibility 175ms, opacity 175ms;
width: 100%;
z-index: 999;
}
pre {
padding-left: 4ex; /* approx 4 monospaced spaces */
}
.success {
color: #008000;
}
.fail {
color: #800000;
}
.hidden {
display: none;
}

View File

@@ -0,0 +1,51 @@
"use strict";
const COPY_TO_CLIPBOARD_CONTAINER_ID = "copyToClipboardContainer";
const COPY_TO_CLIPBOARD_STATUS_ID = "copyToClipboardStatus";
let copyToClipboardCtr = 0;
function copyToClipboardSuccess() {
let statusElem = document.getElementById(COPY_TO_CLIPBOARD_STATUS_ID);
statusElem.innerText = "URL copied!";
statusElem.classList.remove("fail");
statusElem.classList.add("success");
}
function copyToClipboardFail(cause) {
let statusElem = document.getElementById(COPY_TO_CLIPBOARD_STATUS_ID);
if (!cause) cause = "unknown error";
let msg = "copy failed: " + cause;
console.log(msg);
statusElem.innerText = msg;
statusElem.classList.remove("success");
statusElem.classList.add("fail");
}
function copyToClipboard(url) {
let copyEventIdx = ++copyToClipboardCtr;
if (!window.navigator.clipboard) {
copyToClipboardFail("could not access clipboard");
return;
}
window.navigator.clipboard.writeText(url).then(() => {
if (copyToClipboardCtr !== copyEventIdx) return;
copyToClipboardSuccess();
}, () => {
if (copyToClipboardCtr !== copyEventIdx) return;
copyToClipboardFail();
});
}
(function () {
if (!window.navigator.clipboard) {
let msg = "cannot access clipboard";
if (window.location.protocol !== "https:") {
msg += ": website not using https"
}
console.error(msg);
} else {
let container = document.getElementById(COPY_TO_CLIPBOARD_CONTAINER_ID);
container.classList.remove("hidden");
}
})();

48
assets/js/dragdrop.js vendored Normal file
View File

@@ -0,0 +1,48 @@
"use strict";
window.addEventListener('DOMContentLoaded', (event) => {
// The implementation of this drag-drop logic is largely based on the
// answer described in <https://stackoverflow.com/a/28226022/5207081>.
//
// The gist is this: every time the drag enters a new DOM object, it will
// fire a new 'dragenter' event on that element. That is a pain, because
// we do not know which element will be the last to receive a dragleave
// event, in case the user aborts the process.
//
// Therefore, we use an Ugly Hack™. There will be an element with id
// 'dropZone', which blocks the complete page. When a 'dragenter' event
// is received on any DOM element, we will unhide this modal page-blocker.
// This will guarantee us that the next 'enter' event will be received on
// this page-block element. This event we shall ignore.
//
// Then, whenever the user stop their drag action, we will receive the
// 'dragleave' on the '#dropZone' element. Now we know when exactly
// a drag action is cancelled (and we will hide the modal element
// accordingly).
let dropZone = document.getElementById("dropZone");
window.addEventListener("dragenter", (event) => {
// User starts a drag action
dropZone.style.visibility = "";
dropZone.style.opacity = 1;
});
dropZone.addEventListener("drop", (event) => {
// User drops a file
event.preventDefault();
document.getElementById("fileUploadField").files = event.dataTransfer.files;
document.getElementById("fileUploadForm").submit();
});
dropZone.addEventListener("dragleave", (event) => {
// User cancels drag action
dropZone.style.visibility = "hidden";
dropZone.style.opacity = 0;
});
window.addEventListener("dragover", (event) => {
// Prevent the browser from opening the file, because of a drop event
// on the window. (<https://stackoverflow.com/a/6756680/5207081>)
event.preventDefault();
});
});

View File

@@ -2,7 +2,9 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{block "title" .}}rushlink{{end}}</title>{{block "head-append" .}}{{end}} <title>{{block "title" .}}rushlink{{end}}</title>
<link rel="stylesheet" type="text/css" href="/css/main.css" />
{{block "head-append" .}}{{end -}}
</head> </head>
<body> <body>
{{block "body" .}} {{block "body" .}}

View File

@@ -1,3 +0,0 @@
{{define "title"}}
Success - rushlink
{{end}}

View File

@@ -1,20 +1,47 @@
{{define "body"}} {{define "head-append"}}
<pre> <script type="text/javascript" src="/js/dragdrop.js" defer></script>
#RU URL SHORTENER {{end}}
=================
Based on https://0x0.st/, this site allows you to easily shorten URLs using {{define "body"}}
<div style="visibility:hidden; opacity:0" id="dropZone">File incoming! :D</div>
<h1>#RU paste-dump</h1>
Based on https://0x0.st/, this site allows you to easily upload files and shorten URLs using
the command line. the command line.
## USAGE <h2>Web-API</h2>
<section>
<form id="fileUploadForm" action="/" method="post" enctype="multipart/form-data">
<label class="formLabel" for="fileUploadField">Upload a file:</label>
<input id="fileUploadField" class="formMain" name="file" type="file" />
<input id="fileUploadField" class="formSubmit" type="submit" value="upload!" />
</form>
</section>
<section>
<form id="shortenURLForm" action="/" method="post" enctype="multipart/form-data">
<label class="formLabel" for="shortenURLField">Upload a URL:</label>
<input id="shortenURLField" class="formMain" name="shorten" type="url" />
<input id="shortenURLSubmit" class="formSubmit" type="submit" value="shorten!" />
</form>
</section>
<h2>Command line API</h2>
<pre>
# Upload a file # Upload a file
curl -F'file=@yourfile.png' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a> curl -F'file=@yourfile.png' <a href="{{.RootURL}}">{{.RootURL}}</a>
# Shorten a URL # Shorten a URL
curl -F'shorten=http://example.com/some/long/url' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a> curl -F'shorten=http://example.com/some/long/url' <a href="{{.RootURL}}">{{.RootURL}}</a>
# Shorten a URL with a token to delete it later # The first line of the result will contain the shortened URL.
curl -F'shorten=http://example.com/some/long/url' -F'deleteToken=' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a> #
# In the other lines, you will find other information, including
# information on how to delete the shortened object.
# Upload output from another process
figlet Rushlink | curl -F'file=@-' <a href="{{.RootURL}}">{{.RootURL}}</a>
# To upload a file and only extract the shortened URL (i.e. throw away the rest)
curl -F'file=@yourfile.png' <a href="{{.RootURL}}">{{.RootURL}}</a> | head -n 1
</pre> </pre>
{{end}} {{end}}

View File

@@ -1,3 +0,0 @@
{{define "title"}}
Success - rushlink
{{end}}

View File

@@ -1,3 +0,0 @@
{{define "title"}}
Success - rushlink
{{end}}

View File

@@ -1,3 +1,37 @@
{{define "title"}} {{define "title"}}
'{{.Paste.Key}}' meta info - rushlink '{{.Paste.Key}}{{.FileExt}}' metadata - rushlink
{{end}}
{{define "head-append"}}
<script type="text/javascript" src="/js/copyclipboard.js" defer></script>
{{end}}
{{define "body"}}
<pre id="copyToClipboardContainer" class="hidden">
<button onclick="copyToClipboard('{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}')">Copy URL to clipboard</button> <span id="copyToClipboardStatus"></span>
</pre>
<pre>
<a href="{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}">{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}</a>
---
{{if and (ne .Paste.State.String "deleted") .CanDeleteBool}}
with delete token: <a href="{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken}}">{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken}}</a>
{{else -}}
with delete token: &lt;unknown&gt;
{{end -}}
type: {{.Paste.Type}}
state: {{.Paste.State}}
{{if .Paste.TimeCreated.IsZero -}}
created: unknown
{{else -}}
created: {{.Paste.TimeCreated}}
{{end -}}
delete token: {{.CanDeleteString}}
<form action="delete?deleteToken={{.Paste.DeleteToken}}" method="post">
<input type="submit" name="delete" value="Delete paste"
{{- if or (eq .Paste.State.String "deleted") (not .CanDeleteBool) -}}
disabled
{{- end -}}
>
</form>
</pre>
{{end}} {{end}}

View File

@@ -1 +0,0 @@
<{{.Request.Host}}/{{.Paste.Key}}> was succesfully deleted

View File

@@ -7,10 +7,18 @@ the command line.
## USAGE ## USAGE
# Upload a file # Upload a file
curl -F'file=@yourfile.png' https://{{.Request.Host}} curl -F'file=@yourfile.png' {{.RootURL}}
# Shorten a URL # Shorten a URL
curl -F'shorten=http://example.com/some/long/url' https://{{.Request.Host}} curl -F'shorten=http://example.com/some/long/url' {{.RootURL}}
# Shorten a URL with a token to delete it later # The first line of the result will contain the shortened URL.
curl -F'shorten=http://example.com/some/long/url' -F'deleteToken=' https://{{.Request.Host}} #
# In the other lines, you will find other information, including
# information on how to delete the shortened object.
# Upload output from another process
figlet Rushlink | curl -F'file=@-' {{.RootURL}}
# To upload a file and only extract the shortened URL (i.e. throw away the rest)
curl -F'file=@yourfile.png' {{.RootURL}} | head -n 1

View File

@@ -1,5 +0,0 @@
{{if .Request.PostForm.deleteToken -}}
https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}
{{else -}}
https://{{.Request.Host}}/{{.Paste.Key}}
{{end -}}

View File

@@ -1,5 +0,0 @@
{{if .Request.PostForm.deleteToken -}}
https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}
{{else -}}
https://{{.Request.Host}}/{{.Paste.Key}}
{{end -}}

View File

@@ -1,17 +1,21 @@
METADATA on <{{.Request.Host}}/{{.Paste.Key}}>: {{.RootURL}}/{{.Paste.Key}}{{.FileExt}}
---
TYPE: {{.Paste.Type}} {{if and (ne .Paste.State.String "deleted") .CanDeleteBool}}
STATE: {{.Paste.State}} with delete token: {{.RootURL}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken}}
{{if .Paste.TimeCreated.IsZero -}}
CREATED: undefined
{{else -}} {{else -}}
CREATED: {{.Paste.TimeCreated}} with delete token: <unknown>
{{end -}}type: {{.Paste.Type}}
state: {{.Paste.State}}
{{if .Paste.TimeCreated.IsZero -}}
created: unknown
{{else -}}
created: {{.Paste.TimeCreated}}
{{end -}} {{end -}}
DELETE TOKEN: {{.CanDelete.String}} delete token: {{.CanDeleteString}}
{{if and (ne .Paste.State.String "deleted") .CanDelete.Bool}} {{if and (ne .Paste.State.String "deleted") .CanDeleteBool}}
``` ```
# To delete this {{.Paste.Type}}, execute: # To delete this {{.Paste.Type}}, execute:
curl --request "DELETE" "{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Request.URL.Query.Get "deleteToken"}}" curl --request "DELETE" "{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken}}"
``` ```
{{end}} {{end}}

484
bindata.go Normal file
View File

@@ -0,0 +1,484 @@
// Code generated for package rushlink by go-bindata DO NOT EDIT. (@generated)
// sources:
// assets/css/main.css
// assets/js/copyclipboard.js
// assets/js/dragdrop.js
// assets/templates/html/base.html.tmpl
// assets/templates/html/error.html.tmpl
// assets/templates/html/index.html.tmpl
// assets/templates/html/pasteMeta.html.tmpl
// assets/templates/txt/base.txt.tmpl
// assets/templates/txt/error.txt.tmpl
// assets/templates/txt/index.txt.tmpl
// assets/templates/txt/pasteMeta.txt.tmpl
package rushlink
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info os.FileInfo
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
// Name return file name
func (fi bindataFileInfo) Name() string {
return fi.name
}
// Size return file size
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
// Mode return file mode
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
// Mode return file modify time
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
// IsDir return file whether a directory
func (fi bindataFileInfo) IsDir() bool {
return fi.mode&os.ModeDir != 0
}
// Sys return file is sys mode
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var _cssMainCss = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\x51\xd1\x6e\xc3\x20\x0c\x7c\xcf\x57\x58\xaa\x26\x6d\x55\xd3\xd2\xa9\x5d\x57\xf2\x27\x7b\x23\xc1\x49\xac\x11\x8c\x80\xae\x49\xa7\xfe\xfb\x94\x84\xac\xd3\x10\x02\x7c\x77\x36\x27\xbb\x64\x3d\xc0\x77\x06\x00\x50\xb3\x8d\x79\xad\x3a\x32\x83\x84\x8e\x2d\x07\xa7\x2a\x2c\xb2\x7b\x96\xad\xb4\x67\xf7\xc1\x16\x93\xb4\x54\xd5\x67\xe3\xf9\x62\x75\x5e\xb1\x61\x2f\xc1\x37\xa5\x7a\x16\x1b\x18\xf7\xf6\x75\x3c\x4e\xc7\x97\x62\x12\x27\xc5\xb5\xa5\x88\xc5\xe3\xa7\x40\x37\x94\xf0\x26\x5c\xff\x07\xbc\x22\x35\x6d\x94\x50\xb2\xd1\x33\xdc\x26\x64\x2f\xc4\x53\x01\xcb\x9a\x28\x83\x75\x94\x20\x8a\x39\x72\x4a\x6b\xb2\x8d\x84\x3d\x76\x73\xaa\xe3\x40\x91\xd8\x4a\xa8\xa9\xc7\x54\x2f\xb2\x1b\x73\xe6\xb7\x57\x76\x91\x7c\x51\xa0\x92\x0c\xc5\x01\xf6\xa7\x63\x17\x36\xc0\x4e\x55\xbf\xe1\x9c\x70\x25\x1d\xdb\xe4\x65\x02\x6e\x39\x59\x8d\xbd\x84\xf3\xf9\xfc\x70\x97\x3c\xde\xb3\xcc\xf9\xa5\x65\xc9\x5e\x3e\x9b\x3e\x60\x5f\xc0\x6e\x0d\xca\x39\xcf\x3d\x1c\x1e\xfd\xd6\x30\x5d\x01\xd6\xbb\xb1\xc0\x36\x5c\xaa\x0a\x43\x48\x55\x52\x2f\x57\x42\xbc\x0b\x21\xa6\xd9\x6c\x6b\x45\xe6\x1f\x3d\x92\x0b\xdd\x92\xd6\x68\x93\x40\x53\x70\x46\x0d\x12\x2c\xdb\x69\xb4\x3f\x01\x00\x00\xff\xff\x22\xc0\x9d\x3a\x00\x02\x00\x00")
func cssMainCssBytes() ([]byte, error) {
return bindataRead(
_cssMainCss,
"css/main.css",
)
}
func cssMainCss() (*asset, error) {
bytes, err := cssMainCssBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "css/main.css", size: 512, mode: os.FileMode(420), modTime: time.Unix(1587564030, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _jsCopyclipboardJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x54\x4f\x6b\xe3\x3e\x10\xbd\xfb\x53\x4c\x75\xb2\xc9\x0f\xf3\x3b\xd7\x78\xa1\x4d\xbb\x10\x28\x4d\x69\xdc\xc3\x9e\x82\x2a\x4d\x1c\xb1\xb2\x14\xa4\x71\xd2\xb2\xf8\xbb\x2f\xb2\xe3\xc4\xf9\xcb\xf6\x50\x1f\x12\x12\x8f\xde\xcc\x7b\x6f\x9e\x58\xed\x11\x3c\x39\x25\x88\x65\x51\x24\xac\xf1\x04\xe3\xe9\xcb\xaf\x79\x31\x9d\x8f\x9f\x26\x2f\xf7\xd3\xbb\xd7\x87\xf9\x78\xfa\x5c\xdc\x4d\x9e\x1f\x5f\xe7\x93\x07\xc8\x81\x09\xbb\xfa\x2c\xec\x58\xab\xd5\xbb\xe5\x4e\x8e\xad\x21\xae\x0c\x3a\x96\x5d\x84\x98\x15\x77\xc5\xdb\xec\xfc\xf9\x19\x71\xaa\x3d\xcb\x22\x8d\x04\xc7\xd8\xe4\x20\x87\xff\xb3\x28\x5a\xd4\x46\x90\xb2\xe6\xb8\x62\x56\x0b\x81\xde\xc7\x09\xfc\x89\x00\x00\x02\x88\x6f\x11\x1f\x35\x56\x90\x83\xb4\xa2\xae\xd0\x50\x5a\x22\x85\xbf\xd0\xd0\xfd\xe7\x44\xc6\x57\x86\x4c\xb2\x16\x6a\x0f\x93\x2a\x63\xd0\x15\xf8\x41\x61\xfe\xb7\xd7\xa7\x30\x85\x42\x79\xc3\x4e\x2a\x85\xe6\xde\x3f\x29\x4f\xa9\xc3\xca\xae\x31\x66\x0b\xae\x34\x3b\x85\xdc\x17\x72\x29\x63\xe6\x3b\x1e\xa1\xb0\xb9\xcc\xf6\x27\x57\x3a\x16\xbc\xf6\xf8\x0d\x7c\xd5\x02\xe2\x9b\x2d\x78\xfb\x15\xc8\xd6\xe6\xb7\xb1\x1b\x03\xe8\x9c\x75\x5b\xba\xa1\x67\xe5\xcb\xde\x4b\x08\x0c\x51\xde\x02\x83\x51\x77\xb0\x2b\x0b\xcb\x60\x35\xa6\xda\x96\x71\xe5\xcb\xeb\xaa\x56\xbe\xfc\x07\x2d\x07\x2a\x5d\x97\xb3\x17\xfd\x8a\x96\x71\xed\xf4\x50\xc5\xf0\xfa\x71\x8d\x86\x26\xf2\x03\x72\x18\x8d\x4e\x77\x31\x8b\xf6\x42\x6d\x94\x91\x76\x93\x1a\xbe\x56\x25\x27\xeb\x52\xd1\x17\xf6\xa0\x9d\x06\xa7\xfe\x31\x61\x6b\x2d\xc1\x58\x02\xde\xd2\x81\xdd\xd1\x9e\x58\x78\x1c\x52\xed\x4c\xf7\xbb\x69\x3f\x2f\xb7\x4c\x37\x4e\x11\x06\x29\x5b\x56\x29\x2d\xd1\xc4\x71\x02\xf9\x8f\xc1\x2c\x61\xec\x33\xf9\xba\xc9\xf3\x03\xee\xc9\x41\xe7\x33\x24\x76\x91\xdb\xce\xf6\x1f\x7c\x57\xa7\x56\xae\xbe\x4d\xe7\x66\xbc\xb3\x73\x17\xf9\x2f\xd8\x31\xdc\x5c\x6e\xce\x3a\x90\x1d\xb0\xd8\xc2\x6a\x2b\x78\x68\x9a\xae\x9c\x25\x2b\xac\x6e\xb9\xb0\x25\xd1\xca\xdf\xb2\x61\x87\xf0\x84\x0e\xa3\x1c\xd8\x2d\x6c\xf0\xdd\x2b\xc2\xd6\xeb\xda\x2b\x53\x42\x7b\x84\xed\xca\x9b\x01\xf5\x2e\x2d\x6d\xd0\x06\x79\x69\x00\xb5\xc7\x23\x0e\xa2\xbf\x73\xbf\x14\xf8\xe1\x45\x9e\x0c\x45\xdf\x82\x9d\x09\xdc\x52\x49\x89\xa6\x5f\xcb\x26\x6a\x92\xe0\xc7\xdf\x00\x00\x00\xff\xff\x70\xbd\x7a\x61\x39\x06\x00\x00")
func jsCopyclipboardJsBytes() ([]byte, error) {
return bindataRead(
_jsCopyclipboardJs,
"js/copyclipboard.js",
)
}
func jsCopyclipboardJs() (*asset, error) {
bytes, err := jsCopyclipboardJsBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "js/copyclipboard.js", size: 1593, mode: os.FileMode(420), modTime: time.Unix(1587564030, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _jsDragdropJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x95\xcd\x6e\xdb\x46\x10\xc7\xef\x7e\x8a\x01\x7b\xa0\x08\x28\x94\x22\x20\x8e\x11\xd5\x39\xb4\x4e\xd1\x02\x4d\xd3\x83\x8d\x02\xbd\x0d\x77\x47\xe4\xd4\xab\x5d\x62\x77\x24\x86\x28\x72\xeb\x93\xf4\xd1\xfa\x24\xc5\x2e\x3f\x2c\xb7\xb6\x63\x9f\x04\x91\xf3\xf1\x9b\xff\xfc\x97\x9b\x1d\x02\x41\x10\xcf\x4a\xb2\xed\xd9\x59\xc7\x56\xbb\xae\x44\xad\x3f\x1c\xc9\xca\xcf\x1c\x84\x2c\xf9\x45\x7e\xf5\xe9\xe3\xf7\xce\x4a\x7c\xe6\x50\x93\xce\x97\xb0\xa0\x18\x52\xc0\xe5\x7b\xf8\xf3\x0c\x00\x60\xb5\x82\xeb\x86\x80\xf7\xad\xa1\x3d\x59\x41\x61\x67\xc1\xed\x40\x1a\x0e\xa0\x3d\xd6\xaf\xb4\x77\x2d\x18\x57\xb3\x02\x0e\x60\xd0\xd7\x64\x7a\xa8\x30\x90\x06\x67\x41\x1a\x9a\x2a\xa1\x0d\x1d\x79\xd0\x14\x94\xe7\x8a\x34\xb0\x85\x6f\x1b\x91\x36\xbc\x5b\xad\x82\xa0\xba\x75\x47\xf2\x3b\xe3\xba\x52\xb9\xfd\x0a\x57\x9b\x8b\xcd\xe6\x7c\xbd\xd9\xac\xde\x6c\xd6\x6f\xd7\x17\xaf\xdf\x97\x63\xa9\x53\xb6\x9a\x83\xc4\xce\x91\xe8\x1d\xd0\x91\x7c\x0f\xc2\x7b\x8a\x9d\x13\x21\x90\x15\xf2\x01\x10\x2c\x75\x70\xf5\xe9\x23\xb8\xea\x0f\x52\xb2\x04\x16\xe8\xd8\x98\xa9\xda\x8e\x3d\x8d\x51\x79\x4c\x4c\x79\x39\x24\x4d\x86\x51\x50\x80\x06\x21\x4a\x80\xeb\xf8\x97\x63\xdd\x16\xd9\x2e\xa1\x22\x85\x87\x30\x4f\xdb\x11\x68\x07\xd6\x09\xdc\x5a\xd7\x41\xd7\xb0\x6a\xa6\xec\xd4\x16\xaa\x81\xd1\x60\x10\x10\x07\x9e\x14\xf1\x31\x12\xc4\xe6\x86\xf0\x38\xd7\x4a\x08\xcb\xa8\x97\xc2\x30\x64\x1d\x02\x79\xc0\xca\x79\x09\xe9\x7f\xeb\x9d\xa2\x10\x1e\x50\xc8\xd3\xce\x79\x5a\x46\xa0\xe8\x0c\xb4\x70\x53\x9b\x1e\x7e\x44\x75\xfb\xcf\x5f\x7f\xa7\x41\xc8\xd3\x8c\x84\xf6\x84\x52\x1a\x60\x3d\xd5\xca\xe3\xaa\x7f\x77\x96\xf2\xe5\x38\x4e\x65\x9c\xba\x1d\xfa\x2b\x17\x3d\x22\x04\x2d\xd6\x54\x02\xfc\xd6\x90\x05\xfc\xbf\x90\x53\x31\x0e\xd3\xc0\xc9\x26\x68\xfb\xb4\x9a\xb1\x75\xa2\x4d\x44\x07\xdb\xb0\xa6\xc1\x6f\x7b\xa7\xd1\xa4\x06\xaf\x52\x67\xf2\xe5\xdd\x9c\x1c\x86\x84\xfa\x80\x1e\xad\x50\x9c\x76\x58\x59\xc4\xb3\xf4\x59\x20\xbf\xb7\xd1\x69\xe0\x13\x8c\xa9\x5a\xea\x76\xd7\xe7\xde\xd2\x39\x4c\xf9\x04\xa1\x41\x63\x80\x6b\xeb\x3c\x3d\xa0\xbc\x8d\x3a\x91\x8d\x9e\xbc\xdb\x59\x10\xd7\xc6\x7f\xec\x07\x77\xa2\x8a\x67\xea\x6e\xe0\xc9\x06\x27\x07\x27\x9f\x0d\x91\x8f\x47\x0a\xf2\x6f\xe6\x65\x9c\xd0\xfd\x12\x8d\x46\x93\xe1\xc8\x02\x7d\x46\x25\xa6\x9f\x4f\xe0\x69\xcb\xb8\x02\x85\x56\x91\x31\xa4\x61\x81\x56\xcf\x0c\xa3\xe4\x34\x2a\x3e\x36\x98\xab\x28\xe5\xbc\x66\x5b\x9b\xbe\x28\xcf\xd2\x53\x43\x02\x13\x10\x5c\x82\x76\xea\x90\x90\x6a\x92\x0f\x43\xf2\x77\xfd\x4f\x7a\x91\x4d\x31\x59\xb1\x4d\x79\x8f\x7d\x9c\xb2\xd9\x36\xd9\x03\x5f\xa5\x91\xe3\x66\x90\x13\xe3\x11\xb8\x37\xd9\x1c\x34\xb5\x2b\x83\xf4\x86\xca\x23\x07\xae\xd8\xb0\xf4\x70\x09\x59\xb6\x7d\x2c\xce\xb5\xa8\x86\xa0\xd7\x43\xcc\x97\x62\x3b\xcc\x39\x07\x3e\x44\xec\xda\xaf\xc1\xc6\x98\xc8\xba\x63\x43\xf3\xcb\x94\x50\xb6\x3e\xfd\x5e\xd1\x0e\x0f\x46\x16\xc5\x09\xdc\x63\x62\xc6\x2a\x37\xad\x71\xa8\x7f\x60\x32\x3a\x2b\xca\xf8\x24\xc0\xe5\x58\x52\xa3\xe0\xb5\x47\x1b\x76\xe4\x87\x57\x2f\x2b\xea\xfc\x3e\x2b\xca\x70\xa8\xf6\x3c\x03\x3d\x4f\x89\xd1\xad\x5f\x93\x63\x70\x5f\x78\xf9\xea\x1a\xd6\x9a\xec\x73\x16\xb8\xfe\x0f\xf6\x93\x86\x8b\xd7\xcf\x13\xcc\xbf\x0e\x2b\x4a\x07\xa3\xf2\xae\x8b\x23\xec\xbc\xdb\x83\x6b\xc9\xb2\xad\xd3\x8b\xa8\xdf\x7c\x17\xc4\x9b\x12\x13\xdc\xc9\xc7\x6f\xac\x36\x1e\xe4\x11\x08\x60\xf1\xe4\x55\x78\xfe\xf6\xcd\xf9\xf9\xc5\x7a\xbe\x09\x8b\x67\xd9\x27\x8e\xfd\xa5\xd8\xfe\x1b\x00\x00\xff\xff\xdf\xfa\x50\xb3\x12\x08\x00\x00")
func jsDragdropJsBytes() ([]byte, error) {
return bindataRead(
_jsDragdropJs,
"js/dragdrop.js",
)
}
func jsDragdropJs() (*asset, error) {
bytes, err := jsDragdropJsBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "js/dragdrop.js", size: 2066, mode: os.FileMode(420), modTime: time.Unix(1576493365, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesHtmlBaseHtmlTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x6c\x90\x31\x6f\x84\x30\x0c\x85\xf7\xfb\x15\xae\x77\xc8\xda\x21\xb0\xb4\x9d\x7b\x43\x97\x8e\x39\xf0\x29\xe8\x0c\x87\x12\x9f\x54\x14\xe5\xbf\x57\x38\x55\xc3\x70\x13\xb6\x1f\x7e\x9f\x5f\xec\xcb\xfb\xe7\xdb\xd7\xf7\xf9\x03\xbc\xcc\xdc\x9f\x6c\xf9\x00\x00\x58\x4f\x6e\x2c\xa5\xb6\x33\x89\x83\xc1\xbb\x10\x49\x3a\x7c\xc8\xb5\x79\xc5\x83\x2c\x93\x30\xf5\x29\x5d\xf8\x3e\xdc\x00\xb5\x45\x68\x73\x0e\x8f\xe8\x79\x5a\x6e\x29\xd1\x32\xe6\x6c\x4d\xf9\xb3\x6e\xee\x22\x04\xe2\x0e\xa3\x6c\x4c\xd1\x13\x09\x82\x6c\x2b\x75\x28\xf4\x23\x66\x88\x11\xc1\x07\xba\x76\xb8\xd7\x66\x76\xd3\xd2\xea\xd0\x54\x9b\x7f\xf2\x7e\x76\xe3\xd6\x95\x96\x51\xf9\xca\x85\x26\xe7\x92\xca\xd4\x58\xf6\x72\x1f\xb7\x27\x0e\xfb\x58\x57\xeb\x8d\x6b\xa0\xfe\x94\xd2\xe0\x98\xa1\x3d\x07\x3a\x6a\x46\xc5\xea\xa2\x31\xff\x60\x85\x60\x8d\x3e\xeb\x6f\x00\x00\x00\xff\xff\x1e\x3c\x4d\x87\x6d\x01\x00\x00")
func templatesHtmlBaseHtmlTmplBytes() ([]byte, error) {
return bindataRead(
_templatesHtmlBaseHtmlTmpl,
"templates/html/base.html.tmpl",
)
}
func templatesHtmlBaseHtmlTmpl() (*asset, error) {
bytes, err := templatesHtmlBaseHtmlTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "templates/html/base.html.tmpl", size: 365, mode: os.FileMode(420), modTime: time.Unix(1576604408, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesHtmlErrorHtmlTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xae\x4e\x49\x4d\xcb\xcc\x4b\x55\x50\x2a\xc9\x2c\xc9\x49\x55\xaa\xad\xe5\x72\x2d\x2a\xca\x2f\x52\xd0\x55\x28\x2a\x2d\xce\xc8\xc9\xcc\xcb\xe6\xaa\xae\x4e\xcd\x4b\xa9\xad\x05\x04\x00\x00\xff\xff\x73\xdb\x0d\xaf\x2b\x00\x00\x00")
func templatesHtmlErrorHtmlTmplBytes() ([]byte, error) {
return bindataRead(
_templatesHtmlErrorHtmlTmpl,
"templates/html/error.html.tmpl",
)
}
func templatesHtmlErrorHtmlTmpl() (*asset, error) {
bytes, err := templatesHtmlErrorHtmlTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "templates/html/error.html.tmpl", size: 43, mode: os.FileMode(420), modTime: time.Unix(1568580372, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesHtmlIndexHtmlTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x95\x4d\x6f\xe3\x36\x13\xc7\xef\xfc\x14\xb3\xf4\x61\x9f\x07\x88\xc5\x64\x8f\xae\x24\xf4\x0d\x01\x16\x70\x81\xc2\x5b\xa3\x40\x6f\xb4\x38\xb2\x98\x52\x1c\x81\x1c\xd9\x31\xbc\xfe\xee\x05\x29\x3b\x71\x52\x64\xd1\x00\x45\x6f\x1a\x72\xe6\x3f\xe4\x6f\x86\xa3\xe3\xd1\x60\x6b\x3d\x82\xec\x50\x9b\xb9\x1e\x06\xf4\x46\x9e\x4e\xa2\x8c\x4d\xb0\x03\x03\x1f\x06\xac\x24\xe3\x23\xab\x07\xbd\xd3\xd3\xaa\x84\x18\x9a\x4a\xaa\x87\xa8\x4c\xd0\x5b\x13\x68\x28\x1e\xa2\x04\x83\x2d\x86\xba\x54\x93\x57\x2d\x8e\x47\xf4\xe6\x74\x12\xe2\x39\xcd\x86\xcc\x21\xeb\x1b\xbb\x83\xc8\x07\x87\x95\xdc\xd9\x68\x37\xd6\x59\x3e\x2c\x3a\x6b\x0c\xfa\xef\x80\x06\xdd\x24\xfb\x56\x82\x35\x95\x4c\x19\xfe\x20\x8f\xb2\xbe\xb7\x0e\xc1\xfa\x86\x7a\xeb\xb7\x1f\x60\xf1\x73\xa9\x8c\xdd\xd5\xa2\xec\xee\xea\xd9\x6a\x0d\x83\x8e\x8c\x73\x33\xf6\x43\xa9\xba\xbb\x5a\x88\x1f\x75\x44\x03\xe4\xa1\x63\x1e\xe2\x42\xa9\xdb\xc7\xdb\x22\xb2\xba\x01\xee\x6c\x84\x68\x19\x41\x3b\x47\xfb\x08\x07\x1a\x81\x09\x50\x47\xeb\x0e\x30\x0e\x8e\xb4\x81\xd6\x3a\x8c\xa0\xbd\x81\xd8\x51\x60\xf4\xb0\x5e\x2d\x23\x8c\xd1\xfa\xad\xe0\x0e\xa1\xa1\xbe\x4f\xdb\xce\x7a\x2c\x84\x28\xbb\x4f\xf5\xef\xb8\x99\xff\xf0\xeb\xe7\x52\x75\x9f\x6a\x51\x46\x6c\xd8\x92\xaf\x05\x00\x40\xd9\x52\xe8\xf3\x9d\x92\xf0\x3a\xe7\xb8\xa7\xd0\x4b\xd0\xd9\xab\x92\x4a\x42\x8f\xdc\x91\xa9\xe4\x40\x91\x25\xa0\x6f\xa6\x2a\xf4\xa3\x63\x3b\xe8\xc0\x2a\x89\xcc\x8d\x66\x2d\x27\xd5\xac\xec\xf4\x06\x1d\x34\x4e\xc7\x58\xc9\xe4\xb1\x4c\x0b\x12\x5a\x0a\x2f\xb2\x59\x74\x46\xd6\x93\x01\x3a\x5f\x70\x51\xaa\x1c\x7d\xa5\x66\xfd\x30\xf2\xeb\x83\xe6\xd0\xeb\x14\xbf\x68\xeb\x25\x78\xdd\xe3\xe4\x27\xcf\x0d\x33\x7d\xab\x77\xeb\x7d\x19\x37\xbd\xe5\x8b\x4a\x3c\x5b\x3b\xed\x46\xac\xe4\x54\x91\x0f\x4f\xba\x65\xe6\x50\x8b\x52\x3d\x21\x7e\x0b\xf6\xb9\x76\xeb\xd5\xf2\xbf\x80\x7d\x95\xed\x15\xec\xf5\x6a\xf9\x4d\xd6\xaf\x23\xdf\x62\x7d\xf6\xbb\x80\x1a\x83\x7b\x8b\xf6\xb3\xe2\x85\xed\x3f\xc5\x7d\x8e\xfc\x26\xef\xdc\xee\x3f\x5d\xbd\x00\x78\xee\xfb\x21\x60\x2d\x66\xf0\xa2\xd1\x44\x33\x06\x07\xf3\xfb\x8f\xc9\xa8\xbe\x3f\xd0\x18\xd2\x57\x31\xf8\xed\x47\x28\x35\x74\x01\xdb\x4a\x1e\x8f\xc5\x8a\x88\xd7\xab\xe5\xe9\x24\xeb\x6b\xab\x54\xba\x16\x62\x06\x5f\xce\x6f\x31\x13\x7d\x12\x3d\x9f\xb8\x4a\x4f\x7d\xa1\x14\x3e\xea\x7e\x70\x58\x34\xd4\xab\x48\x3d\x2a\x47\x7e\xab\xc6\xe0\xde\x95\xea\xb7\x0e\xa1\xb5\x21\xf2\x74\x3f\x6a\x21\xbd\xfa\x80\x71\x74\x0c\x7b\xeb\x1c\x34\xe4\x59\x5b\x9f\xd7\xcf\x47\x40\x93\x0e\x56\x88\x99\x98\xc1\xe7\x69\x87\xb8\xc3\x90\x35\xe2\x4d\x1e\x35\x39\xb6\xb5\xde\x9c\xb7\xac\x4f\x78\x75\x02\x7b\x93\x46\x9c\x1b\x4d\x9a\x31\xb3\xeb\x8d\x3c\xc8\x68\x9f\xc6\x94\x41\x87\x8c\xaf\x92\xd2\xe6\x01\x1b\x2e\xc4\x33\x77\x1a\x39\xb5\x42\x1b\xa8\x07\xed\xa7\x4c\x43\xa0\x06\x63\x14\xad\xdd\x3a\x64\x58\x8d\xb1\x73\xd6\xff\x09\x5f\xe1\x65\x79\xe6\xef\x03\x45\x97\x99\x39\xd5\x3a\x0f\x4d\xf2\xee\x00\xf8\xc8\x41\x37\xfc\x77\x40\xf0\x3f\x5b\x60\x01\xdc\x05\xda\x83\xde\xeb\xc3\x85\x2d\xff\xff\xdf\xe8\x14\xf8\x0a\xe9\xb7\x06\x73\x0f\x77\xa2\x54\xb9\x21\x2f\xff\xa4\xbf\x02\x00\x00\xff\xff\x55\x95\x0e\x65\xfa\x06\x00\x00")
func templatesHtmlIndexHtmlTmplBytes() ([]byte, error) {
return bindataRead(
_templatesHtmlIndexHtmlTmpl,
"templates/html/index.html.tmpl",
)
}
func templatesHtmlIndexHtmlTmpl() (*asset, error) {
bytes, err := templatesHtmlIndexHtmlTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "templates/html/index.html.tmpl", size: 1786, mode: os.FileMode(420), modTime: time.Unix(1576794526, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesHtmlPastemetaHtmlTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x94\xc1\x6e\xdb\x30\x0c\x86\xef\x7a\x0a\x42\x87\x36\x3d\x38\xbe\xb7\xb6\x07\x2c\xdd\x80\x61\x3d\x0c\x5d\x7b\xd9\x4d\xb1\x98\x5a\x8d\x22\x69\x12\xdd\xd6\x10\xf4\xee\x83\xec\x3a\xe9\xb6\x60\x4b\x2f\x01\x42\x51\x1f\xff\x9f\x22\x1d\xa3\xc4\x8d\x32\x08\x9c\x14\x69\xe4\x29\xb1\xf3\x18\x97\xdf\x44\x20\x5c\x7e\xc5\x21\xa5\x18\x97\x9f\x95\xc6\x4f\x2f\x94\xd2\x39\xec\x90\x84\x14\x24\xa0\x00\xdf\x87\x4e\x2b\xb3\x65\x31\xa2\x91\x29\x31\x76\x80\x75\x28\x64\x21\x9c\x43\x23\x33\xb2\x0a\xad\x57\x8e\x80\x06\x87\x35\x27\x7c\xa1\xf2\x51\x3c\x89\x29\xca\x21\xf8\xb6\xe6\xe5\x63\x28\x5b\xeb\x86\x56\x2b\xb7\xb6\xc2\xcb\xe5\x63\xe0\x20\x71\x83\xbe\xa9\xca\x29\xb5\x39\x56\x6b\x6d\xe5\x30\x16\x71\x1e\x41\xc9\x9a\x67\xca\x9d\x5d\xcd\x9c\x95\x35\x24\x94\x41\xcf\xa1\xd5\x22\x84\x9a\x77\x4a\x4a\x34\xbc\x61\xd5\xba\x27\xb2\x06\xac\x69\xb5\x6a\xb7\x7f\x5d\x5d\xe4\x5e\xdc\x5a\x4b\xf7\xb7\x37\x29\x95\xff\x68\xcc\x05\x6f\x56\xd6\x0d\x70\x7f\x7b\x03\x64\x61\x6f\xa2\x2a\xa7\x12\x0d\x54\xc1\x09\x73\x4c\xdf\x77\x12\xd4\x07\x9e\x4d\x3a\x61\x1a\x56\x95\xce\x63\x33\xda\x69\x58\x25\xa0\xf3\xb8\xa9\xf9\x89\x42\x78\x73\x62\x62\x55\x8a\x86\x15\x45\xc1\x62\x54\x1b\x10\x46\xc2\xc2\x20\xbc\x66\x67\x49\xf9\xd7\x2b\xf3\x00\x5c\xa2\x46\x42\xc9\x2f\x60\xb9\x12\xe6\x7a\xfc\xf7\xd1\x5a\x9d\x12\x7b\x56\xd4\xc1\x74\x0e\x64\xb7\x68\x2e\xe1\xbd\x8a\x3f\x4c\xd7\xef\xf2\xed\x7a\x9f\x76\x7d\x08\xbe\xc3\xd4\x09\xac\xd1\x77\x8c\xa8\x03\x42\x71\xdc\xc1\x99\xa6\xab\xde\x6c\x8d\x7d\x36\x67\x0f\x74\x35\xcd\xdc\x98\x9c\xe7\xf7\x12\xf6\xe0\xbb\xc1\x61\x4a\x2c\xe4\x6e\xbd\x09\x8f\xdd\x4b\x69\xea\xec\x9c\xaa\x76\xb8\xf2\x28\x08\xe5\xf2\x4b\xf8\x81\xde\x8e\xc0\x76\x0a\x5d\xc2\x6b\xbd\xb7\xca\xf6\x67\x87\x7a\x07\xc8\x88\x9f\x65\xfd\x2e\x3f\xc6\xc3\x33\x4d\x4f\x98\x97\x63\x63\xfd\x0e\x44\x4b\xca\x9a\xfa\xf5\x45\x4f\xe9\x7c\x5e\xf8\xce\xca\x9a\x3b\x1b\x28\x6f\x8c\x32\xae\x9f\x17\x39\xf4\xeb\x9d\x22\x0e\x46\xec\x70\x86\x72\x78\x12\xba\xc7\x9a\x4f\x1c\x70\x19\xca\x59\x8c\x05\xa8\x0d\x58\x0f\x0b\xfc\xf9\xbf\x31\x5b\x18\x4b\x7f\xcc\xda\xc5\x64\x54\x05\xb1\xd6\x28\x47\xdc\xec\x3e\x6f\x4c\x76\xb7\xdf\x9c\xf9\x1b\xf1\x2b\x00\x00\xff\xff\xf7\x78\x59\x94\xdc\x04\x00\x00")
func templatesHtmlPastemetaHtmlTmplBytes() ([]byte, error) {
return bindataRead(
_templatesHtmlPastemetaHtmlTmpl,
"templates/html/pasteMeta.html.tmpl",
)
}
func templatesHtmlPastemetaHtmlTmpl() (*asset, error) {
bytes, err := templatesHtmlPastemetaHtmlTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "templates/html/pasteMeta.html.tmpl", size: 1244, mode: os.FileMode(420), modTime: time.Unix(1589228935, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesTxtBaseTxtTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
func templatesTxtBaseTxtTmplBytes() ([]byte, error) {
return bindataRead(
_templatesTxtBaseTxtTmpl,
"templates/txt/base.txt.tmpl",
)
}
func templatesTxtBaseTxtTmpl() (*asset, error) {
bytes, err := templatesTxtBaseTxtTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "templates/txt/base.txt.tmpl", size: 0, mode: os.FileMode(420), modTime: time.Unix(1568569370, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesTxtErrorTxtTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xae\xd6\xf3\x4d\x2d\x2e\x4e\x4c\x4f\xad\xad\x05\x04\x00\x00\xff\xff\xa1\x68\xe0\xbd\x0c\x00\x00\x00")
func templatesTxtErrorTxtTmplBytes() ([]byte, error) {
return bindataRead(
_templatesTxtErrorTxtTmpl,
"templates/txt/error.txt.tmpl",
)
}
func templatesTxtErrorTxtTmpl() (*asset, error) {
bytes, err := templatesTxtErrorTxtTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "templates/txt/error.txt.tmpl", size: 12, mode: os.FileMode(420), modTime: time.Unix(1568575116, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesTxtIndexTxtTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x91\xcf\x6a\xdc\x30\x10\xc6\xef\x7a\x8a\x0f\x7c\x48\x0b\xbb\x76\x7a\x0d\x2c\xb4\x85\xed\x1f\x08\x2d\x78\xe3\x07\x50\xed\xf1\x5a\xed\x58\x63\xa4\x11\x5e\x93\xe4\xdd\x8b\x9c\x6d\xd9\x26\x3d\x44\x27\x49\xc3\xcc\xef\xe3\x37\x45\xdd\xa0\xa9\x6f\x71\xf8\xf2\xbd\xbe\xdb\x7f\xdb\xd7\x66\xf7\xfc\x18\xf3\xd1\x46\xea\x20\x1e\x83\xea\x14\x6f\xaa\xea\xfa\x74\x5d\x46\xad\x36\xd0\xc1\x45\x44\xa7\x04\xcb\x2c\x73\xc4\x22\x09\x2a\x20\x1b\x1d\x2f\x88\x83\x04\x25\x9f\x09\x11\x29\x3a\x7f\x34\x3a\x10\x5a\x19\x47\xeb\x3b\xb0\xf3\x54\x1a\x53\x14\x68\x0e\x1f\x3e\xef\x8d\x29\xd0\x4c\x2c\xb6\x83\x45\xef\x98\x4c\x9b\x02\x63\xfb\xe9\x2a\x3f\x76\xef\x17\x49\x21\xdf\xca\xc9\x1f\xaf\x70\x7f\x5f\xd6\x22\xda\xd4\xb7\x8f\x8f\xb9\xf3\x70\x66\xd9\x4c\xfb\xdb\x79\x4e\xb0\xcb\xc9\x6f\xaa\x8a\x4e\x76\x9c\x98\xca\x56\xc6\x2a\xca\x48\x15\x8b\x3f\x56\x29\xf0\xcb\x79\x77\x03\xa1\x77\x21\xea\x1a\x13\xd2\x23\x47\x0f\x14\x13\x2b\x66\xc7\x8c\x56\xbc\x5a\xe7\xd7\xff\x33\x87\xba\x4c\x2f\x4d\x61\x0a\x7c\x7d\xaa\x88\x0e\x14\xd6\x19\x71\xb3\xea\x59\x7b\x7b\xe7\xbb\x73\xc9\xf9\x5e\xc2\x68\xd5\x89\xdf\xc0\xf9\x96\x53\x97\x45\x15\x97\x85\x55\xbe\xcc\x59\x6d\x47\x4c\x4a\xcf\xa0\xf2\xe3\x27\xb5\x5a\x5e\x18\x94\xa4\x53\x52\xf4\x41\x46\x58\xff\x44\x9a\x82\xb4\x14\xa3\xe9\xdd\x91\x49\x51\xa7\x38\xb0\xf3\xbf\xf0\x80\x7f\x45\x6f\xff\x63\x43\x90\x2e\x57\x83\xbc\x3f\xf1\xbc\x80\x4e\x1a\x6c\xab\x2f\x2d\xe0\x8d\x2b\xa9\x84\x0e\x41\x66\xd8\xd9\x2e\x7f\x04\xea\xdb\x57\x2f\x16\x0f\x18\xc8\x76\xd8\x7a\xbc\x33\xbf\x03\x00\x00\xff\xff\x54\x6e\xbb\xfa\xac\x02\x00\x00")
func templatesTxtIndexTxtTmplBytes() ([]byte, error) {
return bindataRead(
_templatesTxtIndexTxtTmpl,
"templates/txt/index.txt.tmpl",
)
}
func templatesTxtIndexTxtTmpl() (*asset, error) {
bytes, err := templatesTxtIndexTxtTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "templates/txt/index.txt.tmpl", size: 684, mode: os.FileMode(420), modTime: time.Unix(1576794536, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _templatesTxtPastemetaTxtTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x51\xcd\x4a\x03\x31\x10\xbe\xe7\x29\x86\x78\x51\x30\xf1\x5e\xfc\x01\xdb\x15\xc4\x1e\xa4\xae\x17\x4f\x5d\x76\xa7\x36\x74\x4d\xea\x66\x96\x76\x09\xf3\xee\xb2\x49\xdd\x2d\xea\x41\xd0\x4b\x20\x33\xdf\xcc\xf7\x33\x21\xe8\x85\x73\xf4\xbc\x98\x33\x5f\x84\xa0\x1f\x0b\x4f\xa8\x1f\xb0\x63\x0e\x41\xdf\x99\x1a\xb3\x3d\x31\x0b\xa5\x94\x08\xc1\xac\xa0\xb0\x15\x9c\x5a\x84\x03\xf2\x89\x8a\xf8\x36\xc6\xbe\x82\xac\xb0\x46\xc2\x4a\x9e\x81\x9e\x16\x76\x16\x7f\xb7\xce\xd5\xcc\x62\x67\x68\x0d\xa9\x0f\xe4\x36\x68\x27\xf0\x4b\xee\x9b\x34\x95\xf7\x43\x57\x03\x6c\x36\x16\x99\x45\x08\x58\x7b\x04\xf5\x33\xd1\x65\x6b\x37\xd6\xed\xec\x75\x8f\xb3\x55\x0f\xa3\x6e\x8b\x51\x41\xda\x96\x77\x5b\x64\x16\xbe\x77\x73\x54\x8e\xee\xe2\x7a\xb3\xfa\x74\x9c\x9b\x37\x9c\x36\x58\x10\x56\xfa\xde\xbf\x60\xe3\x22\x6d\x99\x4a\x13\x38\x70\x1d\x4b\x1a\x7a\x23\xdf\xb8\x24\xa9\x4f\xaa\xc4\xb7\x80\x86\x18\x53\xc4\xcc\xe2\x6f\x67\x58\x2e\x97\xe2\x04\x72\x37\x24\xb4\x36\xfe\x6b\x0c\xe7\x80\x7b\x2c\x5b\xc2\x89\x28\xdb\xa6\x06\xa5\x1a\x7c\x6f\xd1\x13\xc8\x59\x36\xcf\xf2\x4c\x82\xfc\xbf\xdb\xc9\xa8\x29\x46\xc0\x2c\x3e\x02\x00\x00\xff\xff\x34\xf2\xf4\x10\x90\x02\x00\x00")
func templatesTxtPastemetaTxtTmplBytes() ([]byte, error) {
return bindataRead(
_templatesTxtPastemetaTxtTmpl,
"templates/txt/pasteMeta.txt.tmpl",
)
}
func templatesTxtPastemetaTxtTmpl() (*asset, error) {
bytes, err := templatesTxtPastemetaTxtTmplBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "templates/txt/pasteMeta.txt.tmpl", size: 656, mode: os.FileMode(420), modTime: time.Unix(1589229127, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"css/main.css": cssMainCss,
"js/copyclipboard.js": jsCopyclipboardJs,
"js/dragdrop.js": jsDragdropJs,
"templates/html/base.html.tmpl": templatesHtmlBaseHtmlTmpl,
"templates/html/error.html.tmpl": templatesHtmlErrorHtmlTmpl,
"templates/html/index.html.tmpl": templatesHtmlIndexHtmlTmpl,
"templates/html/pasteMeta.html.tmpl": templatesHtmlPastemetaHtmlTmpl,
"templates/txt/base.txt.tmpl": templatesTxtBaseTxtTmpl,
"templates/txt/error.txt.tmpl": templatesTxtErrorTxtTmpl,
"templates/txt/index.txt.tmpl": templatesTxtIndexTxtTmpl,
"templates/txt/pasteMeta.txt.tmpl": templatesTxtPastemetaTxtTmpl,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"css": &bintree{nil, map[string]*bintree{
"main.css": &bintree{cssMainCss, map[string]*bintree{}},
}},
"js": &bintree{nil, map[string]*bintree{
"copyclipboard.js": &bintree{jsCopyclipboardJs, map[string]*bintree{}},
"dragdrop.js": &bintree{jsDragdropJs, map[string]*bintree{}},
}},
"templates": &bintree{nil, map[string]*bintree{
"html": &bintree{nil, map[string]*bintree{
"base.html.tmpl": &bintree{templatesHtmlBaseHtmlTmpl, map[string]*bintree{}},
"error.html.tmpl": &bintree{templatesHtmlErrorHtmlTmpl, map[string]*bintree{}},
"index.html.tmpl": &bintree{templatesHtmlIndexHtmlTmpl, map[string]*bintree{}},
"pasteMeta.html.tmpl": &bintree{templatesHtmlPastemetaHtmlTmpl, map[string]*bintree{}},
}},
"txt": &bintree{nil, map[string]*bintree{
"base.txt.tmpl": &bintree{templatesTxtBaseTxtTmpl, map[string]*bintree{}},
"error.txt.tmpl": &bintree{templatesTxtErrorTxtTmpl, map[string]*bintree{}},
"index.txt.tmpl": &bintree{templatesTxtIndexTxtTmpl, map[string]*bintree{}},
"pasteMeta.txt.tmpl": &bintree{templatesTxtPastemetaTxtTmpl, map[string]*bintree{}},
}},
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}

View File

@@ -13,21 +13,22 @@ var (
fileStorePath = flag.String("file-store", "", "path to the directory where uploaded files will be stored") fileStorePath = flag.String("file-store", "", "path to the directory where uploaded files will be stored")
httpListen = flag.String("listen", "127.0.0.1:8000", "listen address (host:port)") httpListen = flag.String("listen", "127.0.0.1:8000", "listen address (host:port)")
metricsListen = flag.String("metrics_listen", "127.0.0.1:58614", "listen address for metrics (host:port)") metricsListen = flag.String("metrics_listen", "127.0.0.1:58614", "listen address for metrics (host:port)")
rootURL = flag.String("root_url", "", "host root (example: 'https://example.com', uses an educated guess if omitted)")
) )
func main() { func main() {
flag.Parse() flag.Parse()
database, err := db.OpenDB(*databasePath)
if err != nil {
log.Fatalln(err)
}
defer database.Close()
filestore, err := db.OpenFileStore(*fileStorePath) filestore, err := db.OpenFileStore(*fileStorePath)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
database, err := db.OpenDB(*databasePath, filestore)
go rushlink.StartMetricsServer(*metricsListen, database) if err != nil {
rushlink.StartMainServer(*httpListen, database, filestore) log.Fatalln(err)
}
defer database.Close()
go rushlink.StartMetricsServer(*metricsListen, database, filestore)
rushlink.StartMainServer(*httpListen, database, filestore, *rootURL)
} }

122
contrib/rushlink Executable file
View File

@@ -0,0 +1,122 @@
#!/usr/bin/env sh
set -e
LINK=https://hashru.link/
DELFILE="${XDG_DATA_HOME:-$HOME/.local/share}/rushlink/delete"
link() {
case "$1" in
https://*|http://*)
printf '%s' "$1" | curl -sS -F'shorten=<-' "$LINK"
;;
*)
curl -sS -Ffile=@- "$LINK" < "$1"
;;
esac | awk 'NR == 1 { print } $NF ~ /deleteToken=/ { print $NF; exit }' | (
IFS= read -r link
IFS= read -r deletelink
echo "$link"
mkdir -p "$(dirname "$DELFILE")"
echo "$deletelink" >> "$DELFILE"
)
}
del() {
case "$1" in
https://*|http://*)
URL="$1"
;;
*)
URL="$LINK$1"
;;
esac
# Remove file extension, if any.
NAME="${URL##*/}"
URL="${URL%/*}/${NAME%%.*}"
if DELURL=$(grep -s -m1 "$URL?deleteToken=" "$DELFILE"); then
echo "Deleting $URL..." >&2
curl -sS -X DELETE "$DELURL" | grep deleted
else
echo "Delete token for $URL is not known." >&2
exit 1
fi
}
screenshot() {
if command -v import >/dev/null 2>&1; then
CMD="import png:-"
elif command -v maim >/dev/null 2>&1; then
CMD="maim -uks"
else
echo "Neither import (imagemagick) nor maim were found. One of these is needed to make screenshots." >&2
exit 1
fi
FILE=$(mktemp)
if $CMD > "$FILE"; then
LINK=$(link "$FILE")
rm -f "$FILE"
echo "$LINK.png"
if command -v xdg-open >/dev/null 2>&1; then
xdg-open "$LINK.png"
fi
else
rm -f "$FILE"
exit $?
fi
}
if [ $# -gt 0 ]; then
case "$1" in
--help|-h)
CMD=$(basename "$0")
echo "$CMD - Command line tool for $LINK"
echo
echo "Usage:"
echo " $CMD https://example.com/"
echo " Shorten link."
echo " $CMD file.txt"
echo " $CMD < file.txt"
echo " Upload file."
echo " $CMD"
echo " echo hi | $CMD"
echo " Upload file from standard input."
echo " $CMD (--screenshot|-s)"
echo " Select a window or an area of your screen, and upload it as png."
echo " $CMD (--delete|-d) ${LINK}xd42"
echo " $CMD (--delete|-d) xd42"
echo " Delete file or shortened link."
echo
echo "Delete tokens are stored in $DELFILE"
exit 0
;;
--delete|-d)
shift
for url in "$@"; do
del "$url"
done
exit 0
;;
--screenshot|-s)
shift
if [ $# -gt 0 ]; then
echo "No arguments expected to --screenshot" >&2
exit 1
fi
screenshot
exit 0
;;
*)
for url in "$@"; do
link "$url"
done
exit 0
esac
fi
if [ -t 0 ]; then
echo "Sending standard input to $LINK" >&2
echo "^C to cancel, ^D to send." >&2
fi
link /dev/stdin

8
go.mod
View File

@@ -4,8 +4,8 @@ go 1.12
require ( require (
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.4
github.com/pkg/errors v0.8.1 github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.1.0 github.com/prometheus/client_golang v1.5.1
go.etcd.io/bbolt v1.3.3 go.etcd.io/bbolt v1.3.4
) )

37
go.sum
View File

@@ -1,12 +1,17 @@
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -16,16 +21,24 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -36,40 +49,64 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8= github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo= github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE= github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -3,12 +3,12 @@ package rushlink
import ( import (
"crypto/subtle" "crypto/subtle"
"fmt" "fmt"
"io"
"log" "log"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strings"
"time" "time"
"gitea.hashru.nl/dsprenkels/rushlink/internal/db" "gitea.hashru.nl/dsprenkels/rushlink/internal/db"
@@ -28,65 +28,55 @@ const (
const cookieDeleteToken = "owner_token" const cookieDeleteToken = "owner_token"
type canDelete uint
const (
canDeleteUndef canDelete = iota
canDeleteYes
canDeleteNo
)
func (cd *canDelete) Bool() bool {
return *cd == canDeleteYes
}
func (cd *canDelete) String() string {
switch *cd {
case canDeleteUndef:
return "undefined"
case canDeleteYes:
return "correct"
case canDeleteNo:
return "invalid"
default:
panic("unreachable")
}
}
func (rl *rushlink) staticGetHandler(w http.ResponseWriter, r *http.Request) {
rl.renderStatic(w, r, mux.Vars(r)["path"])
}
func (rl *rushlink) indexGetHandler(w http.ResponseWriter, r *http.Request) { func (rl *rushlink) indexGetHandler(w http.ResponseWriter, r *http.Request) {
render(w, r, "index", map[string]interface{}{}) rl.render(w, r, http.StatusOK, "index", map[string]interface{}{})
}
func (rl *rushlink) uploadFileGetHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
var fu *db.FileUpload
var badID bool
if err := rl.db.Bolt.View(func(tx *bolt.Tx) error {
fuID, err := uuid.Parse(id)
if err != nil {
badID = true
return err
}
fu, err = db.GetFileUpload(tx, fuID)
return err
}); err != nil {
if badID {
renderError(w, r, http.StatusNotFound, "malformed file id")
return
} else {
panic(err)
}
}
filePath := rl.fs.FilePath(fu.ID, fu.FileName)
file, err := os.Open(filePath)
if err != nil {
if os.IsNotExist(err) {
log.Printf("error: %v should exist according to the database, but it doesn't", filePath)
renderError(w, r, http.StatusNotFound, "file not found")
return
} else {
panic(err)
}
}
w.Header().Set("Content-Type", fu.ContentType)
io.Copy(w, file)
} }
func (rl *rushlink) viewPasteHandler(w http.ResponseWriter, r *http.Request) { func (rl *rushlink) viewPasteHandler(w http.ResponseWriter, r *http.Request) {
rl.viewPasteHandlerInner(w, r, 0) rl.viewPasteHandlerFlags(w, r, 0)
} }
func (rl *rushlink) viewPasteHandlerNoRedirect(w http.ResponseWriter, r *http.Request) { func (rl *rushlink) viewPasteHandlerNoRedirect(w http.ResponseWriter, r *http.Request) {
rl.viewPasteHandlerInner(w, r, viewNoRedirect) rl.viewPasteHandlerFlags(w, r, viewNoRedirect)
} }
func (rl *rushlink) viewPasteHandlerMeta(w http.ResponseWriter, r *http.Request) { func (rl *rushlink) viewPasteHandlerMeta(w http.ResponseWriter, r *http.Request) {
rl.viewPasteHandlerInner(w, r, viewShowMeta) rl.viewPasteHandlerFlags(w, r, viewShowMeta)
} }
func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPaste) { func (rl *rushlink) viewPasteHandlerFlags(w http.ResponseWriter, r *http.Request, flags viewPaste) {
vars := mux.Vars(r) vars := mux.Vars(r)
key := vars["key"] key := vars["key"]
var p *db.Paste var p *db.Paste
var fuID *uuid.UUID
var fu *db.FileUpload var fu *db.FileUpload
if err := rl.db.Bolt.View(func(tx *bolt.Tx) error { if err := rl.db.Bolt.View(func(tx *bolt.Tx) error {
var err error var err error
@@ -97,7 +87,6 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request
if p != nil && p.Type == db.PasteTypeFileUpload { if p != nil && p.Type == db.PasteTypeFileUpload {
var id uuid.UUID var id uuid.UUID
copy(id[:], p.Content) copy(id[:], p.Content)
fuID = &id
fu, err = db.GetFileUpload(tx, id) fu, err = db.GetFileUpload(tx, id)
if err != nil { if err != nil {
return err return err
@@ -109,62 +98,130 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request
} }
if p == nil { if p == nil {
renderError(w, r, http.StatusNotFound, "url key not found in the database") rl.renderError(w, r, http.StatusNotFound, "url key not found in the database")
return return
} }
if flags&viewShowMeta != 0 { rl.viewPasteHandlerInner(w, r, flags, p, fu)
canDelete := struct {
Bool bool
String string
}{Bool: false}
deleteToken := getDeleteTokenFromRequest(r)
if deleteToken == "" {
canDelete.String = "undefined"
} else {
if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 {
canDelete.Bool = true
canDelete.String = "correct"
} else {
canDelete.String = "invalid"
}
} }
data := map[string]interface{}{ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPaste, p *db.Paste, fu *db.FileUpload) {
"Paste": p, if flags&viewShowMeta != 0 {
"CanDelete": canDelete, rl.viewPasteHandlerInnerMeta(w, r, p, fu)
}
render(w, r, "pasteMeta", data)
return return
} }
switch p.State { switch p.State {
case db.PasteStatePresent: case db.PasteStatePresent:
var location string
switch p.Type { switch p.Type {
case db.PasteTypeFileUpload: case db.PasteTypeFileUpload:
if fu == nil { if fu == nil {
panic(fmt.Sprintf("file for id %v does not exist in database\n", fuID)) panic(fmt.Sprintf("file for id %v does not exist in database\n", string(p.Content)))
} }
location = fu.URL().String() rl.viewFileUploadHandler(w, r, fu)
break return
case db.PasteTypeRedirect: case db.PasteTypeRedirect:
location = p.RedirectURL().String() if flags&viewNoRedirect == 0 {
break http.Redirect(w, r, p.RedirectURL().String(), http.StatusTemporaryRedirect)
}
return
default: default:
panic("paste type unsupported") panic("paste type unsupported")
} }
if flags&viewNoRedirect == 0 {
http.Redirect(w, r, location, http.StatusSeeOther)
}
fmt.Fprint(w, location)
case db.PasteStateDeleted: case db.PasteStateDeleted:
renderError(w, r, http.StatusGone, "paste has been deleted\n") rl.renderError(w, r, http.StatusGone, "paste has been deleted\n")
return
default: default:
panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key)) panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key))
} }
} }
func (rl *rushlink) viewFileUploadHandler(w http.ResponseWriter, r *http.Request, fu *db.FileUpload) {
filePath := fu.Path(rl.fs)
file, err := os.Open(filePath)
if err != nil {
if os.IsNotExist(err) {
log.Printf("error: '%v' should exist according to the database, but it doesn't", filePath)
rl.renderError(w, r, http.StatusNotFound, "file not found")
return
}
// unexpected error
panic(err)
}
var modtime time.Time
info, err := file.Stat()
if err != nil {
log.Printf("error: %v", errors.Wrapf(err, "could not stat file '%v'", filePath))
} else {
modtime = info.ModTime()
}
// Provide the real filename to the client (to be used in Ctrl+S etc.)
quotedName := strings.ReplaceAll(fu.FileName, "\"", "\\\"")
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", quotedName))
// We use http.ServeContent (instead of http.ServeFile) because we cannot
// use http.ServeFile together with the assertion that the file exists,
// without introducing a TOCTOU flaw.
http.ServeContent(w, r, fu.FileName, modtime, file)
}
func (rl *rushlink) viewPasteHandlerInnerMeta(w http.ResponseWriter, r *http.Request, p *db.Paste, fu *db.FileUpload) {
var cd canDelete
deleteToken := getDeleteTokenFromRequest(r)
if deleteToken != "" {
if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 {
cd = canDeleteYes
} else {
cd = canDeleteNo
}
}
var fileExt string
if fu != nil {
fileExt = fu.Ext()
}
data := map[string]interface{}{
"Paste": p,
"FileExt": fileExt,
"CanDeleteString": cd.String(),
"CanDeleteBool": cd.Bool(),
}
var status int
if p.State == db.PasteStateDeleted {
status = http.StatusGone
} else {
status = http.StatusOK
}
rl.render(w, r, status, "pasteMeta", data)
return
}
func (rl *rushlink) viewActionSuccess(w http.ResponseWriter, r *http.Request, p *db.Paste, fu *db.FileUpload) {
var fileExt string
if fu != nil {
fileExt = fu.Ext()
}
// Redirect to the new paste.
pasteURL := url.URL{
Path: fmt.Sprintf("/%s%s/meta", p.Key, fileExt),
RawQuery: fmt.Sprintf("deleteToken=%s", url.QueryEscape(p.DeleteToken)),
}
http.Redirect(w, r, pasteURL.String(), http.StatusFound)
// But still render the page for CURL-like clients.
cd := canDeleteYes
data := map[string]interface{}{
"Paste": p,
"FileExt": fileExt,
"CanDeleteString": cd.String(),
"CanDeleteBool": cd.Bool(),
}
rl.render(w, r, 0, "pasteMeta", data)
return
}
func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) { func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
file, fileHeader, err := r.FormFile("file") file, fileHeader, err := r.FormFile("file")
if err == nil { if err == nil {
@@ -174,7 +231,7 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
// Fallthrough // Fallthrough
} else { } else {
msg := fmt.Sprintf("could not parse form: %v\n", err) msg := fmt.Sprintf("could not parse form: %v\n", err)
renderError(w, r, http.StatusBadRequest, msg) rl.renderError(w, r, http.StatusBadRequest, msg)
return return
} }
@@ -184,7 +241,7 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
renderError(w, r, http.StatusBadRequest, "no 'file' and no 'shorten' fields given in form\n") rl.renderError(w, r, http.StatusBadRequest, "no 'file' and no 'shorten' fields given in form\n")
} }
func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) { func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) {
@@ -192,8 +249,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req
var paste *db.Paste var paste *db.Paste
if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error { if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error {
var err error var err error
// Create the fileUpload in the database fu, err = db.NewFileUpload(rl.fs, file, header.Filename)
fu, err = db.NewFileUpload(rl.fs, file, header.Filename, header.Header.Get("Content-Type"))
if err != nil { if err != nil {
panic(errors.Wrap(err, "creating fileUpload")) panic(errors.Wrap(err, "creating fileUpload"))
} }
@@ -206,8 +262,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req
}); err != nil { }); err != nil {
panic(err) panic(err)
} }
data := map[string]interface{}{"Paste": paste} rl.viewActionSuccess(w, r, paste, fu)
render(w, r, "newFileUploadPasteSuccess", data)
} }
func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
@@ -217,25 +272,25 @@ func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Req
} }
shorten := r.PostFormValue("shorten") shorten := r.PostFormValue("shorten")
if shorten == "" { if shorten == "" {
renderError(w, r, http.StatusBadRequest, "no 'shorten' param given\n") rl.renderError(w, r, http.StatusBadRequest, "no 'shorten' param given\n")
return return
} }
rl.newRedirectPasteHandler(w, r, shorten) rl.newRedirectPasteHandler(w, r, shorten)
} }
func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, rawurl string) { func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, rawurl string) {
userURL, err := url.ParseRequestURI(rawurl) userURL, err := url.Parse(rawurl)
if err != nil { if err != nil {
msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl) msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl)
renderError(w, r, http.StatusBadRequest, msg) rl.renderError(w, r, http.StatusBadRequest, msg)
return return
} }
if userURL.Scheme == "" { if userURL.Scheme == "" {
renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)\n") rl.renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)\n")
return return
} }
if userURL.Host == "" { if userURL.Host == "" {
renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)\n") rl.renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)\n")
return return
} }
@@ -247,8 +302,7 @@ func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Reque
}); err != nil { }); err != nil {
panic(err) panic(err)
} }
data := map[string]interface{}{"Paste": paste} rl.viewActionSuccess(w, r, paste, nil)
render(w, r, "newRedirectPasteSuccess", data)
} }
// Delete a URL from the database // Delete a URL from the database
@@ -258,40 +312,38 @@ func (rl *rushlink) deletePasteHandler(w http.ResponseWriter, r *http.Request) {
deleteToken := getDeleteTokenFromRequest(r) deleteToken := getDeleteTokenFromRequest(r)
if deleteToken == "" { if deleteToken == "" {
renderError(w, r, http.StatusBadRequest, "no delete token provided\n") rl.renderError(w, r, http.StatusBadRequest, "no delete token provided\n")
return return
} }
var errorCode int var errorCode int
var paste db.Paste var paste *db.Paste
if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error { if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error {
p, err := db.GetPaste(tx, key) var err error
paste, err = db.GetPaste(tx, key)
if err != nil { if err != nil {
errorCode = http.StatusNotFound errorCode = http.StatusNotFound
return err return err
} }
if p.State == db.PasteStateDeleted { if paste.State == db.PasteStateDeleted {
errorCode = http.StatusGone errorCode = http.StatusGone
return errors.New("already deleted") return errors.New("already deleted")
} }
if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 0 { if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(paste.DeleteToken)) == 0 {
errorCode = http.StatusForbidden errorCode = http.StatusForbidden
return errors.New("invalid delete token") return errors.New("invalid delete token")
} }
if err := p.Delete(tx, rl.fs); err != nil { if err := paste.Delete(tx, rl.fs); err != nil {
errorCode = http.StatusInternalServerError errorCode = http.StatusInternalServerError
return err return err
} }
paste = *p
return nil return nil
}); err != nil { }); err != nil {
log.Printf("error: %v\n", err) log.Printf("error: %v\n", err)
renderError(w, r, errorCode, fmt.Sprintf("error: %v\n", err)) rl.renderError(w, r, errorCode, fmt.Sprintf("error: %v\n", err))
return return
} }
rl.viewActionSuccess(w, r, paste, nil)
data := map[string]interface{}{"Paste": paste}
render(w, r, "deletePasteSuccess", data)
} }
// Add a new fileUpload redirect to the database // Add a new fileUpload redirect to the database

149
handlers_test.go Normal file
View File

@@ -0,0 +1,149 @@
package rushlink
import (
"bytes"
"fmt"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"gitea.hashru.nl/dsprenkels/rushlink/internal/db"
"github.com/gorilla/mux"
"go.etcd.io/bbolt"
)
// createTemporaryRouter initializes a rushlink instance, with temporary
// filestore and database.
//
// It will use testing.T.Cleanup to cleanup after itself.
func createTemporaryRouter(t *testing.T) (*mux.Router, *rushlink) {
tempDir, err := ioutil.TempDir("", "rushlink-tmp-*")
if err != nil {
t.Fatalf("creating temporary directory: %s\n", err)
}
t.Cleanup(func() {
os.RemoveAll(tempDir)
})
fileStore, err := db.OpenFileStore(filepath.Join(tempDir, "filestore"))
if err != nil {
t.Fatalf("opening temporary filestore: %s\n", err)
}
databasePath := filepath.Join(tempDir, "rushlink.db")
database, err := db.OpenDB(databasePath, fileStore)
if err != nil {
t.Fatalf("opening temporary database: %s\n", err)
}
t.Cleanup(func() {
if err := database.Close(); err != nil {
t.Errorf("closing database: %d\n", err)
}
})
// *.invalid. is guaranteed not to exist (RFC 6761).
rootURL, err := url.Parse("https://rushlink.invalid")
if err != nil {
t.Fatalf("parsing URL: %s\n", err)
}
rl := rushlink{
db: database,
fs: fileStore,
rootURL: rootURL,
}
return CreateMainRouter(&rl), &rl
}
// checkStatusCode checks whether the status code from a recorded response is equal
// to some response code.
func checkStatusCode(t *testing.T, rr *httptest.ResponseRecorder, code int) {
if actual := rr.Code; actual != code {
t.Logf("request body:\n%v\n", rr.Body.String())
t.Fatalf("handler returned wrong status code: got %v want %v\n",
actual, code)
}
}
// checkLocationHeader checks whether the status code from a recorded response is equal
// to some expected URL.
func checkLocationHeader(t *testing.T, rr *httptest.ResponseRecorder, expected string) {
location := rr.Header().Get("Location")
if location != expected {
t.Fatalf("handler returned bad redirect location: got %v want %v", location, expected)
}
}
func TestIssue43(t *testing.T) {
srv, _ := createTemporaryRouter(t)
// Put a URL with a fragment identifier into the database.
var body bytes.Buffer
form := multipart.NewWriter(&body)
form.WriteField("shorten", "https://example.com#fragment")
form.Close()
req, err := http.NewRequest("POST", "/", bytes.NewReader(body.Bytes()))
if err != nil {
t.Fatal(err)
}
req.Header.Add("Content-Type", form.FormDataContentType())
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
checkStatusCode(t, rr, http.StatusFound)
rawURL := strings.SplitN(rr.Body.String(), "\n", 2)[0]
pasteURL, err := url.Parse(rawURL)
if err != nil {
t.Fatal(err)
}
// Check if the URL was encoded correctly.
req, err = http.NewRequest("GET", pasteURL.Path, nil)
if err != nil {
t.Fatal(err)
}
req = mux.SetURLVars(req, map[string]string{"key": pasteURL.Path[1:]})
rr = httptest.NewRecorder()
srv.ServeHTTP(rr, req)
checkStatusCode(t, rr, http.StatusTemporaryRedirect)
checkLocationHeader(t, rr, "https://example.com#fragment")
}
func TestIssue53(t *testing.T) {
srv, rl := createTemporaryRouter(t)
// Put a URL with a fragment identifier into the database.
var body bytes.Buffer
form := multipart.NewWriter(&body)
if _, err := form.CreateFormFile("file", "../directory-traversal/file.txt"); err != nil {
t.Fatal(err)
}
form.Close()
req, err := http.NewRequest("POST", "/", bytes.NewReader(body.Bytes()))
if err != nil {
t.Fatal(err)
}
req.Header.Add("Content-Type", form.FormDataContentType())
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
checkStatusCode(t, rr, http.StatusFound)
// Check that any attempt to do directory traversal has failed.
rl.db.Bolt.View(func(tx *bbolt.Tx) error {
fus, err := db.AllFileUploads(tx)
if err != nil {
t.Fatal(err)
}
for _, fu := range fus {
if strings.ContainsAny(fu.FileName, "/\\") {
t.Fatalf(fmt.Sprintf("found a slash in file name: %v", fu.FileName))
}
}
return nil
})
}

View File

@@ -1,8 +1,12 @@
package db package db
import ( import (
"bytes"
"fmt" "fmt"
"io"
"log" "log"
"net/http"
"os"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -24,7 +28,7 @@ type Database struct {
// //
// If we alter the database format, we bump this number and write a new // If we alter the database format, we bump this number and write a new
// database migration in migrate(). // database migration in migrate().
const CurrentMigrateVersion = 2 const CurrentMigrateVersion = 3
// BucketConf holds the name for the "configuration" bucket. // BucketConf holds the name for the "configuration" bucket.
// //
@@ -42,7 +46,7 @@ const BucketFileUpload = "fileUpload"
const KeyMigrateVersion = "migrate_version" const KeyMigrateVersion = "migrate_version"
// OpenDB opens a database file located at path. // OpenDB opens a database file located at path.
func OpenDB(path string) (*Database, error) { func OpenDB(path string, fs *FileStore) (*Database, error) {
if path == "" { if path == "" {
return nil, errors.New("database not set") return nil, errors.New("database not set")
} }
@@ -51,7 +55,7 @@ func OpenDB(path string) (*Database, error) {
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "failed to open database at '%v'", path) return nil, errors.Wrapf(err, "failed to open database at '%v'", path)
} }
if err := db.Update(migrate); err != nil { if err := db.Update(func(tx *bolt.Tx) error { return migrate(tx, fs) }); err != nil {
return nil, err return nil, err
} }
return &Database{db}, nil return &Database{db}, nil
@@ -62,11 +66,16 @@ func (db *Database) Close() error {
if db == nil { if db == nil {
panic("no open database") panic("no open database")
} }
return db.Close() return db.Bolt.Close()
} }
// Initialize and migrate the database to the current version // Initialize and migrate the database to the current version
func migrate(tx *bolt.Tx) error { func migrate(tx *bolt.Tx, fs *FileStore) error {
// Guidelines for error handling:
// - Errors based on malformed *structure* should be fatal!
// - Errors based on malformed *data* should print a warning
// (and if possible try to fix the error).
dbVersion, err := dbVersion(tx) dbVersion, err := dbVersion(tx)
if err != nil { if err != nil {
return err return err
@@ -104,6 +113,54 @@ func migrate(tx *bolt.Tx) error {
} }
} }
if dbVersion < 3 {
log.Println("migrating database to version 3")
// In this version, we changed te way how Content-Types are being
// stored. Previously, we allowed clients to provide their own
// Content-Types for files, using the Content-Disposition header in
// multipart forms. The new way detects these types using
// http.DetectContentType.
//
// Scan through all the FileUploads and update their ContentTypes.
bucket := tx.Bucket([]byte(BucketFileUpload))
cursor := bucket.Cursor()
var id, storedBytes []byte
id, storedBytes = cursor.First()
for id != nil {
fu, err := decodeFileUpload(storedBytes)
if err != nil {
log.Print("error: ", errors.Wrapf(err, "corrupted FileUpload in database at '%v'", id))
id, storedBytes = cursor.Next()
continue
}
if fu.State != FileUploadStatePresent {
id, storedBytes = cursor.Next()
continue
}
filePath := fu.Path(fs)
file, err := os.Open(fu.Path(fs))
if err != nil {
log.Print("error: ", errors.Wrapf(err, "could not open file at '%v'", filePath))
id, storedBytes = cursor.Next()
continue
}
var buf bytes.Buffer
buf.Grow(512)
io.CopyN(&buf, file, 512)
contentType := http.DetectContentType(buf.Bytes())
if contentType != fu.ContentType {
fu.ContentType = contentType
fu.Save(tx)
cursor.Seek(id)
}
id, storedBytes = cursor.Next()
}
// Update the version number
if err := setDBVersion(tx, 3); err != nil {
return err
}
}
return nil return nil
} }

View File

@@ -1,12 +1,15 @@
package db package db
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"hash/crc32" "hash/crc32"
"io" "io"
"net/http"
"net/url" "net/url"
"os" "os"
"path" "path"
"path/filepath"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -28,10 +31,22 @@ type FileUploadState int
// FileUpload models an uploaded file. // FileUpload models an uploaded file.
type FileUpload struct { type FileUpload struct {
// State of the FileUpload (present/deleted/etc).
State FileUploadState State FileUploadState
// ID identifies this FileUpload.
ID uuid.UUID ID uuid.UUID
// FileName contains the original filename of this FileUpload.
FileName string FileName string
// Content type as determined by http.DetectContentType.
ContentType string ContentType string
// Checksum holds a crc32c checksum of the file.
//
// This checksum is only meant to allow for the detection of random
// database corruption.
Checksum uint32 Checksum uint32
} }
@@ -76,14 +91,43 @@ func OpenFileStore(path string) (*FileStore, error) {
return &FileStore{path[:]}, nil return &FileStore{path[:]}, nil
} }
// Path returns the path of the FileStore root.
func (fs *FileStore) Path() string {
return fs.path
}
// filePath resolves the path of a file in the FileStore given some id and filename.
func (fs *FileStore) filePath(id uuid.UUID, fileName string) string {
if fs.path == "" {
panic("fileStoreDir called while the file store path has not been set")
}
return path.Join(fs.path, hex.EncodeToString(id[:]), fileName)
}
// NewFileUpload creates a new FileUpload object. // NewFileUpload creates a new FileUpload object.
func NewFileUpload(fs *FileStore, r io.Reader, fileName string, contentType string) (*FileUpload, error) { //
// Internally, this function detects the type of the file stored in `r` using
// `http.DetectContentType`.
func NewFileUpload(fs *FileStore, r io.Reader, fileName string) (*FileUpload, error) {
// Generate a file ID
id, err := uuid.NewRandom() id, err := uuid.NewRandom()
if err != nil { if err != nil {
return nil, errors.Wrap(err, "generating UUID") return nil, errors.Wrap(err, "generating UUID")
} }
filePath := fs.FilePath(id, fileName) // Construct a checksum for this file
hash := crc32.New(checksumTable)
tee := io.TeeReader(r, hash)
// Detect the file type
var tmpBuf bytes.Buffer
tmpBuf.Grow(512)
io.CopyN(&tmpBuf, tee, 512)
contentType := http.DetectContentType(tmpBuf.Bytes())
// Open the file on disk for writing
baseName := filepath.Base(fileName)
filePath := fs.filePath(id, baseName)
if err := os.Mkdir(path.Dir(filePath), dirMode); err != nil { if err := os.Mkdir(path.Dir(filePath), dirMode); err != nil {
return nil, errors.Wrap(err, "creating file dir") return nil, errors.Wrap(err, "creating file dir")
} }
@@ -93,8 +137,11 @@ func NewFileUpload(fs *FileStore, r io.Reader, fileName string, contentType stri
} }
defer file.Close() defer file.Close()
hash := crc32.New(checksumTable) // Write the file to disk
tee := io.TeeReader(r, hash) _, err = io.Copy(file, &tmpBuf)
if err != nil {
return nil, errors.Wrap(err, "writing to file")
}
_, err = io.Copy(file, tee) _, err = io.Copy(file, tee)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "writing to file") return nil, errors.Wrap(err, "writing to file")
@@ -103,24 +150,14 @@ func NewFileUpload(fs *FileStore, r io.Reader, fileName string, contentType stri
fu := &FileUpload{ fu := &FileUpload{
State: FileUploadStatePresent, State: FileUploadStatePresent,
ID: id, ID: id,
FileName: fileName, FileName: baseName,
ContentType: contentType, ContentType: contentType,
Checksum: hash.Sum32(), Checksum: hash.Sum32(),
} }
return fu, nil return fu, nil
} }
func (fs *FileStore) Path() string { // GetFileUpload tries to retrieve a FileUpload object from the bolt database.
return fs.path
}
func (fs *FileStore) FilePath(id uuid.UUID, fileName string) string {
if fs.path == "" {
panic("fileStoreDir called while the file store path has not been set")
}
return path.Join(fs.path, hex.EncodeToString(id[:]), fileName)
}
func GetFileUpload(tx *bolt.Tx, id uuid.UUID) (*FileUpload, error) { func GetFileUpload(tx *bolt.Tx, id uuid.UUID) (*FileUpload, error) {
bucket := tx.Bucket([]byte(BucketFileUpload)) bucket := tx.Bucket([]byte(BucketFileUpload))
if bucket == nil { if bucket == nil {
@@ -130,6 +167,31 @@ func GetFileUpload(tx *bolt.Tx, id uuid.UUID) (*FileUpload, error) {
if storedBytes == nil { if storedBytes == nil {
return nil, nil return nil, nil
} }
return decodeFileUpload(storedBytes)
}
// AllFileUploads tries to retrieve all FileUpload objects from the bolt database.
func AllFileUploads(tx *bolt.Tx) ([]FileUpload, error) {
bucket := tx.Bucket([]byte(BucketFileUpload))
if bucket == nil {
return nil, errors.Errorf("bucket %v does not exist", BucketFileUpload)
}
var fus []FileUpload
err := bucket.ForEach(func(_, storedBytes []byte) error {
fu, err := decodeFileUpload(storedBytes)
if err != nil {
return err
}
fus = append(fus, *fu)
return nil
})
if err != nil {
return nil, err
}
return fus, nil
}
func decodeFileUpload(storedBytes []byte) (*FileUpload, error) {
fu := &FileUpload{} fu := &FileUpload{}
err := gobmarsh.Unmarshal(storedBytes, fu) err := gobmarsh.Unmarshal(storedBytes, fu)
return fu, err return fu, err
@@ -155,7 +217,7 @@ func (fu *FileUpload) Save(tx *bolt.Tx) error {
// Delete deletes a FileUpload from the database. // Delete deletes a FileUpload from the database.
func (fu *FileUpload) Delete(tx *bolt.Tx, fs *FileStore) error { func (fu *FileUpload) Delete(tx *bolt.Tx, fs *FileStore) error {
// Remove the file in the backend // Remove the file in the backend
filePath := fs.FilePath(fu.ID, fu.FileName) filePath := fu.Path(fs)
if err := os.Remove(filePath); err != nil { if err := os.Remove(filePath); err != nil {
return err return err
} }
@@ -173,6 +235,11 @@ func (fu *FileUpload) Delete(tx *bolt.Tx, fs *FileStore) error {
return errors.Wrap(os.Remove(path.Dir(filePath)), wrap) return errors.Wrap(os.Remove(path.Dir(filePath)), wrap)
} }
// Path returns the path to this FileUpload in the FileStore provided in fs.
func (fu *FileUpload) Path(fs *FileStore) string {
return fs.filePath(fu.ID, fu.FileName)
}
// URL returns the URL for the FileUpload. // URL returns the URL for the FileUpload.
func (fu *FileUpload) URL() *url.URL { func (fu *FileUpload) URL() *url.URL {
rawurl := "/uploads/" + hex.EncodeToString(fu.ID[:]) + "/" + fu.FileName rawurl := "/uploads/" + hex.EncodeToString(fu.ID[:]) + "/" + fu.FileName
@@ -182,3 +249,8 @@ func (fu *FileUpload) URL() *url.URL {
} }
return urlParse return urlParse
} }
// Ext returns the extension of the file attached to this FileUpload.
func (fu *FileUpload) Ext() string {
return filepath.Ext(fu.FileName)
}

View File

@@ -14,9 +14,13 @@ import (
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
// PasteType describes the type of Paste (i.e. file, redirect, [...]).
type PasteType int type PasteType int
// PasteState describes the state of a Paste (i.e. present, deleted, [...]).
type PasteState int type PasteState int
// Paste describes the main Paste model in the database.
type Paste struct { type Paste struct {
Type PasteType Type PasteType
State PasteState State PasteState
@@ -35,6 +39,8 @@ var ReservedPasteKeys = []string{"xd42", "example"}
// allowed at the bottom of this block, for the same reason. // allowed at the bottom of this block, for the same reason.
const ( const (
PasteTypeUndef PasteType = iota PasteTypeUndef PasteType = iota
// PasteTypePaste is as of yet unused. It is still unclear if this type
// will ever get a proper meaning.
PasteTypePaste PasteTypePaste
PasteTypeRedirect PasteTypeRedirect
PasteTypeFileUpload PasteTypeFileUpload
@@ -89,11 +95,16 @@ func GetPaste(tx *bolt.Tx, key string) (*Paste, error) {
if storedBytes == nil { if storedBytes == nil {
return nil, nil return nil, nil
} }
return decodePaste(storedBytes)
}
func decodePaste(storedBytes []byte) (*Paste, error) {
p := &Paste{} p := &Paste{}
err := gobmarsh.Unmarshal(storedBytes, p) err := gobmarsh.Unmarshal(storedBytes, p)
return p, err return p, err
} }
// Save saves this Paste to the database.
func (p *Paste) Save(tx *bolt.Tx) error { func (p *Paste) Save(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketPastes)) bucket := tx.Bucket([]byte(BucketPastes))
if bucket == nil { if bucket == nil {
@@ -110,6 +121,7 @@ func (p *Paste) Save(tx *bolt.Tx) error {
return nil return nil
} }
// Delete deletes this Paste from the database.
func (p *Paste) Delete(tx *bolt.Tx, fs *FileStore) error { func (p *Paste) Delete(tx *bolt.Tx, fs *FileStore) error {
// Remove the (maybe) attached file // Remove the (maybe) attached file
if p.Type == PasteTypeFileUpload { if p.Type == PasteTypeFileUpload {
@@ -226,6 +238,7 @@ func generatePasteKeyInner(epoch int) (string, error) {
return string(urlKey), nil return string(urlKey), nil
} }
// GenerateDeleteToken generates a new (random) delete token.
func GenerateDeleteToken() (string, error) { func GenerateDeleteToken() (string, error) {
var deleteToken [16]byte var deleteToken [16]byte
_, err := rand.Read(deleteToken[:]) _, err := rand.Read(deleteToken[:])

View File

@@ -10,21 +10,22 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
func StartMetricsServer(addr string, db *db.Database) { const metricNamespace = "rushlink"
var (
_ = promauto.NewGaugeFunc(prometheus.GaugeOpts{ var metricRequestsTotalCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "rushlink", Namespace: metricNamespace,
Subsystem: "pastes", Subsystem: "http",
Name: "urls_total", Name: "requests_total",
Help: "The current amount of pastes in the database.", Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
}, func() float64 { }, []string{"code", "method"})
func metricURLsTotal(database *db.Database) float64 {
var metric float64 var metric float64
if err := db.Bolt.View(func(tx *bolt.Tx) error { if err := database.Bolt.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte("pastes")) bucket := tx.Bucket([]byte("pastes"))
if bucket == nil { if bucket == nil {
return errors.New("bucket 'pastes' could not be found") return errors.New("bucket 'pastes' could not be found")
@@ -36,8 +37,18 @@ func StartMetricsServer(addr string, db *db.Database) {
return 0 return 0
} }
return metric return metric
}) }
)
// StartMetricsServer starts sering Prometheus metrics exports on addr
func StartMetricsServer(addr string, database *db.Database, fs *db.FileStore) {
prometheus.MustRegister(metricRequestsTotalCounter)
prometheus.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
Namespace: metricNamespace,
Subsystem: "pastes",
Name: "urls_total",
Help: "The current amount of pastes in the database.",
}, func() float64 { return metricURLsTotal(database) }))
router := mux.NewRouter() router := mux.NewRouter()
router.Handle("/metrics", promhttp.Handler()).Methods("GET") router.Handle("/metrics", promhttp.Handler()).Methods("GET")

View File

@@ -4,19 +4,31 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url"
"runtime/debug" "runtime/debug"
"strconv"
"time" "time"
"gitea.hashru.nl/dsprenkels/rushlink/internal/db" "gitea.hashru.nl/dsprenkels/rushlink/internal/db"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pkg/errors"
) )
const staticFilenameExpr = "[A-Za-z0-9-_.]+"
const urlKeyExpr = "{key:[A-Za-z0-9-_]{4,}}"
const urlKeyWithExtExpr = urlKeyExpr + "{ext:\\.[A-Za-z0-9-_]+}"
type rushlink struct { type rushlink struct {
db *db.Database db *db.Database
fs *db.FileStore fs *db.FileStore
rootURL *url.URL
} }
func recoveryMiddleware(next http.Handler) http.Handler { func (rl *rushlink) RootURL() *url.URL {
return rl.rootURL
}
func (rl *rushlink) recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() { defer func() {
defer func() { defer func() {
@@ -30,33 +42,84 @@ func recoveryMiddleware(next http.Handler) http.Handler {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Printf("error: %v\n", err) log.Printf("error: %v\n", err)
debug.PrintStack() debug.PrintStack()
renderInternalServerError(w, r, err) rl.renderInternalServerError(w, r, err)
} }
}() }()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
func StartMainServer(addr string, db *db.Database, fs *db.FileStore) { func (rl *rushlink) metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
srw := statusResponseWriter{Inner: w}
next.ServeHTTP(&srw, r)
status := strconv.Itoa(srw.StatusCode)
metricRequestsTotalCounter.WithLabelValues(status, r.Method).Inc()
})
}
type statusResponseWriter struct {
Inner http.ResponseWriter
StatusCode int
}
func (w *statusResponseWriter) Header() http.Header {
return w.Inner.Header()
}
func (w *statusResponseWriter) Write(buf []byte) (int, error) {
if w.StatusCode == 0 {
w.WriteHeader(http.StatusOK)
}
return w.Inner.Write(buf)
}
func (w *statusResponseWriter) WriteHeader(statusCode int) {
w.StatusCode = statusCode
w.Inner.WriteHeader(statusCode)
}
// CreateMainRouter creates the main Gorilla router for the application.
func CreateMainRouter(rl *rushlink) *mux.Router {
router := mux.NewRouter()
router.Use(rl.recoveryMiddleware)
router.Use(rl.metricsMiddleware)
router.HandleFunc("/{path:img/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")
router.HandleFunc("/{path:css/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")
router.HandleFunc("/{path:js/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")
router.HandleFunc("/", rl.indexGetHandler).Methods("GET", "HEAD")
router.HandleFunc("/", rl.newPasteHandler).Methods("POST")
router.HandleFunc("/"+urlKeyExpr, rl.viewPasteHandler).Methods("GET", "HEAD")
router.HandleFunc("/"+urlKeyWithExtExpr, rl.viewPasteHandler).Methods("GET", "HEAD")
router.HandleFunc("/"+urlKeyExpr+"/nr", rl.viewPasteHandlerNoRedirect).Methods("GET", "HEAD")
router.HandleFunc("/"+urlKeyWithExtExpr+"/nr", rl.viewPasteHandlerNoRedirect).Methods("GET", "HEAD")
router.HandleFunc("/"+urlKeyExpr+"/meta", rl.viewPasteHandlerMeta).Methods("GET", "HEAD")
router.HandleFunc("/"+urlKeyWithExtExpr+"/meta", rl.viewPasteHandlerMeta).Methods("GET", "HEAD")
router.HandleFunc("/"+urlKeyExpr, rl.deletePasteHandler).Methods("DELETE")
router.HandleFunc("/"+urlKeyWithExtExpr, rl.deletePasteHandler).Methods("DELETE")
router.HandleFunc("/"+urlKeyExpr+"/delete", rl.deletePasteHandler).Methods("POST")
router.HandleFunc("/"+urlKeyWithExtExpr+"/delete", rl.deletePasteHandler).Methods("POST")
return router
}
// StartMainServer starts the main http server listening on addr.
func StartMainServer(addr string, db *db.Database, fs *db.FileStore, rawRootURL string) {
var rootURL *url.URL
if rawRootURL != "" {
var err error
rootURL, err = url.Parse(rawRootURL)
if err != nil {
log.Fatalln(errors.Wrap(err, "could not parse rootURL flag"))
}
}
rl := rushlink{ rl := rushlink{
db: db, db: db,
fs: fs, fs: fs,
rootURL: rootURL,
} }
// Initialize Gorilla router
router := mux.NewRouter()
router.Use(recoveryMiddleware)
router.HandleFunc("/", rl.indexGetHandler).Methods("GET")
router.HandleFunc("/", rl.newPasteHandler).Methods("POST")
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}", rl.viewPasteHandler).Methods("GET")
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/nr", rl.viewPasteHandlerNoRedirect).Methods("GET")
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/meta", rl.viewPasteHandlerMeta).Methods("GET")
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}", rl.deletePasteHandler).Methods("DELETE")
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/delete", rl.deletePasteHandler).Methods("POST")
router.HandleFunc("/uploads/{id:[A-Za-z0-9-_]+}/{filename:.+}", rl.uploadFileGetHandler).Methods("GET")
srv := &http.Server{ srv := &http.Server{
Handler: router, Handler: CreateMainRouter(&rl),
Addr: addr, Addr: addr,
WriteTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second,

View File

@@ -16,10 +16,13 @@ import (
"strconv" "strconv"
"strings" "strings"
text "text/template" text "text/template"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const defaultScheme = "http"
// Plain text templates // Plain text templates
var textBaseTemplate *text.Template = text.Must(text.New("").Parse(string(MustAsset("templates/txt/base.txt.tmpl")))) var textBaseTemplate *text.Template = text.Must(text.New("").Parse(string(MustAsset("templates/txt/base.txt.tmpl"))))
var htmlBaseTemplate *html.Template = html.Must(html.New("").Parse(string(MustAsset("templates/html/base.html.tmpl")))) var htmlBaseTemplate *html.Template = html.Must(html.New("").Parse(string(MustAsset("templates/html/base.html.tmpl"))))
@@ -76,7 +79,27 @@ func parseFail(tmplName string, err error) {
panic(errors.Wrapf(err, "parsing of %v failed", tmplName)) panic(errors.Wrapf(err, "parsing of %v failed", tmplName))
} }
func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[string]interface{}) { func mapExtend(m map[string]interface{}, key string, value interface{}) {
if m[key] != nil {
return
}
m[key] = value
}
func (rl *rushlink) renderStatic(w http.ResponseWriter, r *http.Request, path string) {
var modTime time.Time
if info, err := AssetInfo(path); err != nil {
modTime = info.ModTime()
}
contents, err := Asset(path)
if err != nil {
rl.renderError(w, r, http.StatusNotFound, err.Error())
return
}
http.ServeContent(w, r, path, modTime, bytes.NewReader(contents))
}
func (rl *rushlink) render(w http.ResponseWriter, r *http.Request, status int, tmplName string, data map[string]interface{}) {
contentType, err := resolveResponseContentType(r, []string{"text/plain", "text/html"}) contentType, err := resolveResponseContentType(r, []string{"text/plain", "text/html"})
if err != nil { if err != nil {
w.WriteHeader(http.StatusNotAcceptable) w.WriteHeader(http.StatusNotAcceptable)
@@ -84,7 +107,8 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st
} }
// Add the request to the template data // Add the request to the template data
data["Request"] = r mapExtend(data, "RootURL", rl.resolveRootURL(r))
mapExtend(data, "Request", r)
switch contentType { switch contentType {
case "text/plain": case "text/plain":
@@ -115,7 +139,13 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st
} }
return buf.String() return buf.String()
} }
if status != 0 {
w.WriteHeader(status)
}
if r.Method != "HEAD" {
err = tmpl.Execute(w, data) err = tmpl.Execute(w, data)
}
default: default:
// Fall back to plain text without template // Fall back to plain text without template
w.WriteHeader(http.StatusNotAcceptable) w.WriteHeader(http.StatusNotAcceptable)
@@ -127,14 +157,46 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st
} }
} }
func renderError(w http.ResponseWriter, r *http.Request, status int, msg string) { func (rl *rushlink) renderError(w http.ResponseWriter, r *http.Request, status int, msg string) {
w.WriteHeader(status) rl.render(w, r, status, "error", map[string]interface{}{"Message": msg})
render(w, r, "error", map[string]interface{}{"Message": msg})
} }
func renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) { func (rl *rushlink) renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) {
msg := fmt.Sprintf("internal server error: %v", err) msg := fmt.Sprintf("internal server error: %v", err)
renderError(w, r, http.StatusInternalServerError, msg) rl.renderError(w, r, http.StatusInternalServerError, msg)
}
// resolveRootURL constructs the `scheme://host` part of rushlinks public API.
//
// If the `--root_url` flag is set, it will return that URL.
// Otherwise, this function will return 'https://{Host}', where `{Host}` is
// the value provided by the client in the HTTP `Host` header. This value may
// be invalid, but it is impossible to handle this error (because we *cannot*
// know the real host).
func (rl *rushlink) resolveRootURL(r *http.Request) string {
rlHost := rl.RootURL()
if rlHost != nil {
// Root URL overridden by command line arguments
return rlHost.String()
}
// Guess scheme
scheme := defaultScheme
forwardedScheme := r.Header.Get("X-Forwarded-Proto")
switch forwardedScheme {
case "http":
scheme = "http"
break
case "https":
scheme = "https"
break
}
// Guess host
host := r.Host
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
host = forwardedHost
}
return scheme + "://" + host
} }
// Try to resolve the preferred content-type for the response to this request. // Try to resolve the preferred content-type for the response to this request.

33
views_test.go Normal file
View File

@@ -0,0 +1,33 @@
package rushlink
import (
"net/http"
"testing"
)
func resolveResponseContentTypeSuccess(t *testing.T, expected string, types []string, acceptVal string) {
r, err := http.NewRequest("HEAD", "", nil)
if err != nil {
panic(err)
}
r.Header.Set("Accept", acceptVal)
got, err := resolveResponseContentType(r, types)
if err != nil {
t.Errorf("error: '%v'\n", err)
}
if got != expected {
t.Errorf("error: '%v' should be '%v'\n", got, expected)
}
}
func TestResolveResponseContentType(t *testing.T) {
resolveResponseContentTypeSuccess(t, "", []string{}, "text/html")
resolveResponseContentTypeSuccess(t, "text/html", []string{"text/html"}, "")
resolveResponseContentTypeSuccess(t, "text/html", []string{"text/txt", "text/html"}, "text/txt;q=0.5,text/html")
resolveResponseContentTypeSuccess(t, "text/html", []string{"text/txt", "text/html"}, "text/txt;q=0.5,text/html;q=0.9")
resolveResponseContentTypeSuccess(t, "", []string{"text"}, "text/html")
resolveResponseContentTypeSuccess(t, "", []string{"text/*"}, "image/*")
// Issue #17
resolveResponseContentTypeSuccess(t, "*/*", []string{"*/*"}, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
}