').prop('id', 'create_linenos').append(">"))
66 | .append(area)
67 |
68 | main.append(text)
69 |
70 |
71 | area.val(data).focus()[0].setSelectionRange(0, 0)
72 |
73 | area.scrollTop(0)
74 | }
75 | })
76 |
--------------------------------------------------------------------------------
/client/js/loadencryption.js:
--------------------------------------------------------------------------------
1 | window.crypt = {}
2 |
3 | var crypto = window.crypto || window.msCrypto;
4 |
5 | function getEntropy() {
6 | var entropy = new Uint32Array(256)
7 | crypto.getRandomValues(entropy)
8 | return entropy
9 | }
10 |
11 | function getSeed() {
12 | var seed = new Uint8Array(16)
13 | crypto.getRandomValues(seed)
14 | return seed
15 | }
16 |
17 | var worker = new Worker("./js/encryption.js")
18 |
19 |
20 | var promises = {}
21 |
22 | function str2ab(str) {
23 | var buf = new ArrayBuffer(str.length * 2);
24 | var bufView = new DataView(buf);
25 | for (var i = 0, strLen = str.length; i < strLen; i++) {
26 | bufView.setUint16(i * 2, str.charCodeAt(i), false)
27 | }
28 | return buf;
29 | }
30 |
31 | worker.onmessage = function (e) {
32 | if (e.data.type == 'progress') {
33 | promises[e.data.id].notify(e.data)
34 | } else {
35 | promises[e.data.id].resolve(e.data)
36 | delete promises[e.data.id]
37 | }
38 | }
39 |
40 | var counter = 0
41 |
42 | function getpromise() {
43 | var promise = $.Deferred()
44 | var promiseid = counter
45 | counter += 1
46 | promise.id = promiseid
47 | promises[promiseid] = promise;
48 | return promise
49 | }
50 |
51 | crypt.encrypt = function (file, name) {
52 |
53 | var extension = file.type.split('/')
54 |
55 | var header = JSON.stringify({
56 | 'mime': file.type,
57 | 'name': name ? name : (file.name ? file.name : ('Pasted ' + extension[0] + '.' + (extension[1] == 'plain' ? 'txt' : extension[1])))
58 | })
59 |
60 | var zero = new Uint8Array([0, 0]);
61 |
62 | var blob = new Blob([str2ab(header), zero, file])
63 |
64 | var promise = getpromise()
65 |
66 | var fr = new FileReader()
67 |
68 | fr.onload = function () {
69 | worker.postMessage({
70 | 'data': this.result,
71 | 'entropy': getEntropy(),
72 | 'seed': getSeed(),
73 | 'id': promise.id
74 | })
75 | }
76 |
77 | fr.readAsArrayBuffer(blob)
78 |
79 | return promise
80 | }
81 |
82 |
83 | crypt.ident = function (seed) {
84 | var promise = getpromise()
85 |
86 | worker.postMessage({
87 | 'seed': seed,
88 | 'action': 'ident',
89 | 'id': promise.id
90 | })
91 |
92 | return promise
93 | }
94 |
95 |
96 | crypt.decrypt = function (file, seed) {
97 | var promise = getpromise()
98 |
99 | var fr = new FileReader()
100 |
101 | fr.onload = function () {
102 | worker.postMessage({
103 | 'data': this.result,
104 | 'action': 'decrypt',
105 | 'seed': seed,
106 | 'id': promise.id
107 | })
108 | }
109 |
110 | fr.readAsArrayBuffer(file)
111 |
112 | return promise
113 | }
114 |
--------------------------------------------------------------------------------
/client/js/updown.js:
--------------------------------------------------------------------------------
1 | upload.modules.addmodule({
2 | name: 'updown',
3 | init: function () {
4 | // We do this to try to hide the fragment from the referral in IE
5 | this.requestframe = document.createElement('iframe')
6 | this.requestframe.src = 'about:blank'
7 | this.requestframe.style.visibility = 'hidden'
8 | document.body.appendChild(this.requestframe)
9 | },
10 | downloadfromident: function(seed, progress, done, ident) {
11 | var xhr = new this.requestframe.contentWindow.XMLHttpRequest()
12 | xhr.onload = this.downloaded.bind(this, seed, progress, done)
13 | xhr.open('GET', (upload.config.server ? upload.config.server : '') + 'i/' + ident.ident)
14 | xhr.responseType = 'blob'
15 | xhr.onerror = this.onerror.bind(this, progress)
16 | xhr.addEventListener('progress', progress, false)
17 | xhr.send()
18 | },
19 | onerror: function(progress) {
20 | progress('error')
21 | },
22 | downloaded: function (seed, progress, done, response) {
23 | if (response.target.status != 200) {
24 | this.onerror(progress)
25 | } else {
26 | this.cache(seed, response.target.response)
27 | progress('decrypting')
28 | crypt.decrypt(response.target.response, seed).done(done)
29 | }
30 | },
31 | encrypted: function(progress, done, data) {
32 | var formdata = new FormData()
33 | formdata.append('api_key', upload.config.api_key)
34 | formdata.append('ident', data.ident)
35 | formdata.append('file', data.encrypted)
36 | $.ajax({
37 | url: (upload.config.server ? upload.config.server : '') + 'up',
38 | data: formdata,
39 | cache: false,
40 | processData: false,
41 | contentType: false,
42 | dataType: 'json',
43 | xhr: function () {
44 | var xhr = new XMLHttpRequest()
45 | xhr.upload.addEventListener('progress', progress, false)
46 | return xhr
47 | },
48 | type: 'POST'
49 | }).done(done.bind(undefined, data))
50 | },
51 | cache: function(seed, data) {
52 | this.cached = data
53 | this.cached_seed = seed
54 | },
55 | cacheresult: function(data) {
56 | this.cache(data.seed, data.encrypted)
57 | },
58 | download: function (seed, progress, done) {
59 | if (this.cached_seed == seed) {
60 | progress('decrypting')
61 | crypt.decrypt(this.cached, seed).done(done).progress(progress)
62 | } else {
63 | crypt.ident(seed).done(this.downloadfromident.bind(this, seed, progress, done))
64 | }
65 | },
66 | upload: function (blob, progress, done) {
67 | crypt.encrypt(blob).done(this.encrypted.bind(this, progress, done)).done(this.cacheresult.bind(this)).progress(progress)
68 | }
69 | })
70 |
--------------------------------------------------------------------------------
/client/js/dragresize.js:
--------------------------------------------------------------------------------
1 | $(function () {
2 | window.dragresize = true
3 |
4 | var dragging
5 |
6 | var lastx
7 | var lasty
8 |
9 | var dragsizew
10 | var dragsizeh
11 |
12 | var minw
13 | var minh
14 |
15 | var maxw
16 | var maxh
17 |
18 | $(document).on('dblclick', '.dragresize', function (e) {
19 | var target = $(e.target)
20 | target.toggleClass('full')
21 | if (target.hasClass('full')) {
22 | target.addClass('dragged')
23 | target.width(e.target.naturalWidth)
24 | target.height(e.target.naturalHeight)
25 | } else {
26 | target.removeClass('dragged')
27 | target.width('auto')
28 | target.height('auto')
29 | }
30 | })
31 |
32 | var MIN_WIDTH_PX = 100
33 | var MAX_WIDTH_RATIO = 100
34 |
35 | $(document).on('mousedown', '.dragresize', function (e) {
36 | if (e.which && e.which != 1) {
37 | return
38 | }
39 | e.preventDefault();
40 | dragging = $(e.target)
41 | dragging.addClass('dragging')
42 | dragsizew = e.target.naturalWidth
43 | dragsizeh = e.target.naturalHeight
44 |
45 | if (dragsizew > dragsizeh) {
46 | minw = MIN_WIDTH_PX
47 | minh = MIN_WIDTH_PX * (dragsizeh / dragsizew)
48 | maxw = dragsizew * MAX_WIDTH_RATIO
49 | maxh = (dragsizew * MAX_WIDTH_RATIO) * (dragsizeh / dragsizew)
50 | } else {
51 | minh = MIN_WIDTH_PX
52 | minw = MIN_WIDTH_PX * (dragsizew / dragsizeh)
53 | maxh = dragsizeh * MAX_WIDTH_RATIO
54 | maxw = (dragsizeh * MAX_WIDTH_RATIO) * (dragsizew / dragsizeh)
55 | }
56 |
57 | lastx = e.clientX
58 | lasty = e.clientY
59 | })
60 |
61 | $(document).on('mousemove', function (e) {
62 | if (!dragging) {
63 | return
64 | }
65 |
66 | var px = e.clientX
67 | var py = e.clientY
68 |
69 | var newx = px - lastx
70 | var newy = py - lasty
71 |
72 | if (Math.abs(newx) < 1 && Math.abs(newy) < 1) {
73 | return;
74 | }
75 |
76 | e.preventDefault();
77 |
78 | var width = dragging.width()
79 | var height = dragging.height()
80 |
81 | dragging.addClass('dragged')
82 |
83 | if (Math.abs(newx) > Math.abs(newy)) {
84 | dragging.css({ 'width': Math.min(maxw, Math.max(width + (width * (.0025 * newx)), minw)) + 'px', 'height': 'auto' })
85 | } else {
86 | dragging.css({ 'height': Math.min(maxh, Math.max(height + (height * (.0025 * newy)), minh)) + 'px', 'width': 'auto' })
87 | }
88 |
89 |
90 | lastx = px
91 | lasty = py
92 |
93 | })
94 |
95 | $(document).on('mouseup', function (e) {
96 | if (!dragging) {
97 | return
98 | }
99 | dragging.removeClass('dragging')
100 | dragging = undefined
101 | });
102 | })
103 |
--------------------------------------------------------------------------------
/client/js/encryption.js:
--------------------------------------------------------------------------------
1 | importScripts('../deps/sjcl.min.js')
2 |
3 | function parametersfrombits(seed) {
4 | var out = sjcl.hash.sha512.hash(seed)
5 | return {
6 | 'seed': seed,
7 | 'key': sjcl.bitArray.bitSlice(out, 0, 256),
8 | 'iv': sjcl.bitArray.bitSlice(out, 256, 384),
9 | 'ident': sjcl.bitArray.bitSlice(out, 384, 512)
10 | }
11 | }
12 |
13 | function parameters(seed) {
14 | if (typeof seed == 'string') {
15 | seed = sjcl.codec.base64url.toBits(seed)
16 | } else {
17 | seed = sjcl.codec.bytes.toBits(seed)
18 | }
19 | return parametersfrombits(seed)
20 | }
21 |
22 | function encrypt(file, seed, id) {
23 | var params = parameters(seed)
24 | var uarr = new Uint8Array(file)
25 | var before = sjcl.codec.bytes.toBits(uarr)
26 | var prp = new sjcl.cipher.aes(params.key)
27 | var after = sjcl.mode.ccm.encrypt(prp, before, params.iv)
28 | var afterarray = new Uint8Array(sjcl.codec.bytes.fromBits(after))
29 | postMessage({
30 | 'id': id,
31 | 'seed': sjcl.codec.base64url.fromBits(params.seed),
32 | 'ident': sjcl.codec.base64url.fromBits(params.ident),
33 | 'encrypted': new Blob([afterarray], { type: 'application/octet-stream' })
34 | })
35 | }
36 |
37 | var fileheader = [
38 | 85, 80, 49, 0
39 | ]
40 |
41 | function decrypt(file, seed, id) {
42 | var params = parameters(seed)
43 | var uarr = new Uint8Array(file)
44 |
45 | // We support the servers jamming a header in to deter direct linking
46 | var hasheader = true
47 | for (var i = 0; i < fileheader.length; i++) {
48 | if (uarr[i] != fileheader[i]) {
49 | hasheader = false
50 | break
51 | }
52 | }
53 | if (hasheader) {
54 | uarr = uarr.subarray(fileheader.length)
55 | }
56 |
57 | var before = sjcl.codec.bytes.toBits(uarr);
58 | var prp = new sjcl.cipher.aes(params.key);
59 | var after = sjcl.mode.ccm.decrypt(prp, before, params.iv);
60 | var afterarray = new Uint8Array(sjcl.codec.bytes.fromBits(after));
61 |
62 | // Parse the header, which is a null-terminated UTF-16 string containing JSON
63 | var header = ''
64 | var headerview = new DataView(afterarray.buffer)
65 | var i = 0;
66 | for (; ; i++) {
67 | var num = headerview.getUint16(i * 2, false)
68 | if (num == 0) {
69 | break;
70 | }
71 | header += String.fromCharCode(num);
72 | }
73 | var header = JSON.parse(header)
74 |
75 | var data = new Blob([afterarray])
76 | postMessage({
77 | 'id': id,
78 | 'ident': sjcl.codec.base64url.fromBits(params.ident),
79 | 'header': header,
80 | 'decrypted': data.slice((i * 2) + 2, data.size, header.mime)
81 | })
82 | }
83 |
84 | function ident(seed, id) {
85 | var params = parameters(seed)
86 | postMessage({
87 | 'id': id,
88 | 'ident': sjcl.codec.base64url.fromBits(params.ident)
89 | })
90 | }
91 |
92 | function onprogress(id, progress) {
93 | postMessage({
94 | 'id': id,
95 | 'eventsource': 'encrypt',
96 | 'loaded': progress,
97 | 'total': 1,
98 | 'type': 'progress'
99 | })
100 | }
101 |
102 | onmessage = function (e) {
103 | var progress = onprogress.bind(undefined, e.data.id)
104 | sjcl.mode.ccm.listenProgress(progress)
105 | if (e.data.action == 'decrypt') {
106 | decrypt(e.data.data, e.data.seed, e.data.id)
107 | } else if (e.data.action == 'ident') {
108 | ident(e.data.seed, e.data.id)
109 | } else {
110 | sjcl.random.addEntropy(e.data.entropy, 2048, 'runtime')
111 | encrypt(e.data.data, e.data.seed, e.data.id)
112 | }
113 | sjcl.mode.ccm.unListenProgress(progress)
114 | }
115 |
--------------------------------------------------------------------------------
/client/js/main.js:
--------------------------------------------------------------------------------
1 | (function(window) {
2 | "use strict";
3 | window.upload = {}
4 | }(window));
5 |
6 |
7 | (function(upload) {
8 | upload.config = {}
9 |
10 | upload.load = {
11 | loaded: 0,
12 | doneloaded: function() {
13 | this.loaded -= 1
14 | if (this.loaded <= 0) {
15 | this.cb()
16 | }
17 | },
18 | load: function(filename, test, onload) {
19 | if (test && test()) {
20 | return false
21 | }
22 | var head = document.getElementsByTagName('head')[0]
23 | var script = document.createElement('script')
24 | script.src = './' + filename
25 | script.async = true
26 | script.onload = onload
27 | head.appendChild(script)
28 | return true
29 | },
30 | needsome: function() {
31 | this.loaded += 1
32 | return this
33 | },
34 | done: function(callback) {
35 | this.loaded -= 1
36 | this.cb = callback
37 | return this
38 | },
39 | then: function(callback) {
40 | this.deferred.then(callback)
41 | return this
42 | },
43 | need: function(filename, test) {
44 | this.loaded += 1
45 | if(!this.load(filename, test, this.doneloaded.bind(this))) {
46 | this.loaded -= 1
47 | }
48 | return this
49 | }
50 | }
51 |
52 | upload.modules = {
53 | modules: [],
54 | addmodule: function (module) {
55 | this.modules.unshift(module)
56 | upload[module.name] = module
57 | },
58 | initmodule: function (module) {
59 | module.init()
60 | },
61 | setdefault: function (module) {
62 | this.default = module
63 | },
64 | init: function () {
65 | this.modules.forEach(this.initmodule.bind(this))
66 | }
67 | }
68 |
69 | upload.modules.addmodule({
70 | name: 'footer',
71 | init: function() {
72 | $('#footer').html(upload.config.footer)
73 | }
74 | })
75 |
76 | upload.modules.addmodule({
77 | name: 'route',
78 | init: function () {
79 | window.addEventListener('hashchange', this.hashchange.bind(this))
80 | this.hashchange()
81 | },
82 | setroute: function (module, routeroot, route) {
83 | view = $('.modulecontent.modulearea')
84 | if (!this.currentmodule || this.currentmodule != module) {
85 | // TODO: better
86 | if (this.currentmodule) {
87 | this.currentmodule.unrender()
88 | }
89 | this.currentmodule = module
90 | view.id = 'module_' + module.name
91 | module.render(view)
92 | }
93 | module.initroute(route, routeroot)
94 | },
95 | tryroute: function (route) {
96 | var isroot = route.startsWith('/')
97 | var normalroute = isroot ? route.substring(1) : route
98 | var route = normalroute.substr(normalroute.indexOf('/') + 1)
99 | var routeroot = normalroute.substr(0, normalroute.indexOf('/'))
100 | var chosenmodule
101 | if (!normalroute) {
102 | chosenmodule = upload.modules.default
103 | } else {
104 | upload.modules.modules.every(function (module) {
105 | if (!module.route) {
106 | return true
107 | }
108 | if (module.route(routeroot, route)) {
109 | chosenmodule = module
110 | return false
111 | }
112 | return true
113 | })
114 | }
115 | if (!chosenmodule) {
116 | chosenmodule = upload.modules.default
117 | }
118 | setTimeout(this.setroute.bind(this, chosenmodule, routeroot, route), 0)
119 | },
120 | hashchange: function () {
121 | this.tryroute(window.location.hash.substring(1))
122 | }
123 | })
124 | }(window.upload));
125 |
126 |
127 | (function () {
128 | upload.load.needsome().need('config.js').need('js/shims.js').need('deps/zepto.min.js').done(function() {
129 | upload.load.needsome().need('js/home.js', function() {return upload.home}).done(function() {
130 | if (typeof upload.config != 'undefined') {
131 | upload.modules.init()
132 | } else {
133 | alert("Please configure with config.js (see config.js.example)")
134 | }
135 | })
136 | })
137 | }(upload))
138 |
--------------------------------------------------------------------------------
/server/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/hmac"
5 | "crypto/sha256"
6 | "encoding/hex"
7 | "encoding/json"
8 | "flag"
9 | "fmt"
10 | "io"
11 | "log"
12 | "net/http"
13 | "net/url"
14 | "os"
15 | "path"
16 | "path/filepath"
17 | "sync"
18 | )
19 |
20 | type Config struct {
21 | Listen string `json:"listen"`
22 | ApiKey string `json:"api_key"`
23 | DeleteKey string `json:"delete_key"`
24 | MaxFileSize int64 `json:"maximum_file_size"`
25 |
26 | Path struct {
27 | I string `json:"i"`
28 | Client string `json:"client"`
29 | } `json:"path"`
30 |
31 | Http struct {
32 | Enabled bool `json:"enabled"`
33 | Listen string `json:"listen"`
34 | } `json:"http"`
35 |
36 | Https struct {
37 | Enabled bool `json:"enabled"`
38 | Listen string `json:"listen"`
39 | Cert string `json:"cert"`
40 | Key string `json:"key"`
41 | } `json:"https"`
42 |
43 | CfCacheInvalidate struct {
44 | Enabled bool `json:"enabled"`
45 | Token string `json:"token"`
46 | Email string `json:"email"`
47 | Domain string `json:"domain"`
48 | Url string `json:"url"`
49 | } `json:"cloudflare-cache-invalidate"`
50 | }
51 |
52 | var config Config
53 |
54 | type ErrorMessage struct {
55 | Error string `json:"error"`
56 | Code int `json:"code"`
57 | }
58 |
59 | type SuccessMessage struct {
60 | Delkey string `json:"delkey"`
61 | }
62 |
63 | func readConfig(name string) Config {
64 | file, _ := os.Open(name)
65 | decoder := json.NewDecoder(file)
66 | config := Config{}
67 | err := decoder.Decode(&config)
68 | if err != nil {
69 | fmt.Println("Error reading config: ", err)
70 | }
71 | return config
72 | }
73 |
74 | func validateConfig(config Config) {
75 | if !config.Http.Enabled && !config.Https.Enabled {
76 | log.Fatal("At least one of http or https must be enabled!")
77 | }
78 | if len(config.ApiKey) == 0 {
79 | log.Fatal("A static key must be defined in the configuration!")
80 | }
81 | if len(config.DeleteKey) == 0 {
82 | log.Fatal("A static delete key must be defined in the configuration!")
83 | }
84 | if len(config.Path.I) == 0 {
85 | config.Path.I = "../i"
86 | }
87 | if len(config.Path.Client) == 0 {
88 | config.Path.Client = "../client"
89 | }
90 | }
91 |
92 | func makeDelkey(ident string) string {
93 | key := []byte(config.DeleteKey)
94 | h := hmac.New(sha256.New, key)
95 | h.Write([]byte(ident))
96 | return hex.EncodeToString(h.Sum(nil))
97 | }
98 |
99 | func index(w http.ResponseWriter, r *http.Request) {
100 | if r.URL.Path == "/" {
101 | http.ServeFile(w, r, filepath.Join(config.Path.Client, "index.html"))
102 | } else {
103 | http.ServeFile(w, r, filepath.Join(config.Path.Client, r.URL.Path[1:]))
104 | }
105 | }
106 |
107 | func upload(w http.ResponseWriter, r *http.Request) {
108 | if r.ContentLength > config.MaxFileSize {
109 | msg, _ := json.Marshal(&ErrorMessage{Error: "File size too large", Code: 1})
110 | w.Write(msg)
111 | return
112 | }
113 |
114 | r.ParseMultipartForm(50000000)
115 | file, _, err := r.FormFile("file")
116 |
117 | if err != nil {
118 | msg, _ := json.Marshal(&ErrorMessage{Error: err.Error(), Code: 5})
119 | w.Write(msg)
120 | return
121 | }
122 |
123 | defer file.Close()
124 |
125 | apikey := r.FormValue("api_key")
126 | if apikey != config.ApiKey {
127 | msg, _ := json.Marshal(&ErrorMessage{Error: "API key doesn't match", Code: 2})
128 | w.Write(msg)
129 | return
130 | }
131 |
132 | ident := r.FormValue("ident")
133 | if len(ident) != 22 {
134 | msg, _ := json.Marshal(&ErrorMessage{Error: "Ident filename length is incorrect", Code: 3})
135 | w.Write(msg)
136 | return
137 | }
138 |
139 | identPath := path.Join(config.Path.I, path.Base(ident))
140 | if _, err := os.Stat(identPath); err == nil {
141 | msg, _ := json.Marshal(&ErrorMessage{Error: "Ident is already taken.", Code: 4})
142 | w.Write(msg)
143 | return
144 | }
145 |
146 | out, err := os.Create(identPath)
147 | if err != nil {
148 | msg, _ := json.Marshal(&ErrorMessage{Error: err.Error(), Code: 6})
149 | w.Write(msg)
150 | return
151 | }
152 |
153 | defer out.Close()
154 |
155 | out.Write([]byte{'U', 'P', '1', 0})
156 | _, err = io.Copy(out, file)
157 | if err != nil {
158 | msg, _ := json.Marshal(&ErrorMessage{Error: err.Error(), Code: 7})
159 | w.Write(msg)
160 | return
161 | }
162 |
163 | delkey := makeDelkey(ident)
164 |
165 | result, err := json.Marshal(&SuccessMessage{Delkey: delkey})
166 | if err != nil {
167 | msg, _ := json.Marshal(&ErrorMessage{Error: err.Error(), Code: 8})
168 | w.Write(msg)
169 | }
170 | w.Write(result)
171 | }
172 |
173 | func delfile(w http.ResponseWriter, r *http.Request) {
174 | ident := r.FormValue("ident")
175 | delkey := r.FormValue("delkey")
176 |
177 | if len(ident) != 22 {
178 | msg, _ := json.Marshal(&ErrorMessage{Error: "Ident filename length is incorrect", Code: 3})
179 | w.Write(msg)
180 | return
181 | }
182 |
183 | identPath := path.Join(config.Path.I, ident)
184 | if _, err := os.Stat(identPath); os.IsNotExist(err) {
185 | msg, _ := json.Marshal(&ErrorMessage{Error: "Ident does not exist.", Code: 9})
186 | w.Write(msg)
187 | return
188 | }
189 |
190 | if delkey != makeDelkey(ident) {
191 | msg, _ := json.Marshal(&ErrorMessage{Error: "Incorrect delete key", Code: 10})
192 | w.Write(msg)
193 | return
194 | }
195 |
196 | if config.CfCacheInvalidate.Enabled {
197 | if config.Http.Enabled {
198 | cfInvalidate(ident, false)
199 | }
200 | if config.Https.Enabled {
201 | cfInvalidate(ident, true)
202 | }
203 | }
204 |
205 | os.Remove(identPath)
206 | http.Redirect(w, r, "/", 301)
207 | }
208 |
209 | func cfInvalidate(ident string, https bool) {
210 | var invUrl string
211 | if https {
212 | invUrl = "https://" + config.CfCacheInvalidate.Url
213 | } else {
214 | invUrl = "http://" + config.CfCacheInvalidate.Url
215 | }
216 | invUrl += "/i/" + ident
217 |
218 | if _, err := http.PostForm("https://www.cloudflare.com/api_json.html", url.Values{
219 | "a": {"zone_file_purge"},
220 | "tkn": {config.CfCacheInvalidate.Token},
221 | "email": {config.CfCacheInvalidate.Email},
222 | "z": {config.CfCacheInvalidate.Domain},
223 | "url": {invUrl},
224 | }); err != nil {
225 | log.Printf("Cache invalidate failed for '%s': '%s'", ident, err.Error())
226 | }
227 | }
228 |
229 | func main() {
230 | configName := flag.String("config", "server.conf", "Configuration file")
231 | flag.Parse()
232 |
233 | config = readConfig(*configName)
234 | validateConfig(config)
235 |
236 | http.Handle("/i/", http.StripPrefix("/i", http.FileServer(http.Dir(config.Path.I))))
237 | http.HandleFunc("/up", upload)
238 | http.HandleFunc("/del", delfile)
239 | http.HandleFunc("/", index)
240 |
241 | var wg sync.WaitGroup
242 | wg.Add(2)
243 |
244 | go func() {
245 | defer wg.Done()
246 | if config.Http.Enabled {
247 | log.Printf("Starting HTTP server on %s\n", config.Http.Listen)
248 | log.Println(http.ListenAndServe(config.Http.Listen, nil))
249 | }
250 | }()
251 |
252 | go func() {
253 | defer wg.Done()
254 | if config.Https.Enabled {
255 | log.Printf("Starting HTTPS server on %s\n", config.Https.Listen)
256 | log.Println(http.ListenAndServeTLS(config.Https.Listen, config.Https.Cert, config.Https.Key, nil))
257 | }
258 | }()
259 |
260 | wg.Wait()
261 | }
262 |
--------------------------------------------------------------------------------
/client/css/up1.css:
--------------------------------------------------------------------------------
1 | #btnarea {
2 | bottom: 5px;
3 | position: fixed;
4 | z-index: 1;
5 | width: 100%;
6 | }
7 |
8 | #btnarea .right {
9 | margin-right: 5px;
10 | }
11 |
12 | #create_filename {
13 | background-color: rgba(0,0,0,.5);
14 | border: 1px solid #FFF;
15 | border-bottom-right-radius: initial;
16 | box-sizing: border-box;
17 | color: #FFF;
18 | font-size: 16px;
19 | left: 10px;
20 | margin: 0;
21 | max-width: 100%;
22 | opacity: .75;
23 | overflow: hidden;
24 | padding: 5px;
25 | position: fixed;
26 | top: 10px;
27 | white-space: nowrap;
28 | width: 170px;
29 | z-index: 1;
30 | }
31 |
32 | #downloaded_filename {
33 | background-color: rgba(0,0,0,.5);
34 | border-radius: 5px;
35 | box-sizing: border-box;
36 | color: #FFF;
37 | display: block;
38 | height: 40px;
39 | line-height: 30px;
40 | margin: 0;
41 | opacity: .75;
42 | overflow: hidden;
43 | padding: 5px;
44 | text-align: middle;
45 | text-overflow: ellipsis;
46 | vertical-align: middle;
47 | white-space: nowrap;
48 | z-index: 1;
49 | }
50 |
51 | #finallink,#downloadprogress {
52 | color: #FFF;
53 | }
54 |
55 | #create_linenos {
56 | color: #7d7d7d;
57 | float: left;
58 | font-family: monospace;
59 | left: 5px;
60 | overflow: hidden;
61 | position: absolute;
62 | text-align: right;
63 | top: 6.5px;
64 | width: 30px;
65 | z-index: -1;
66 | }
67 |
68 | .line {
69 | word-wrap: normal;
70 | white-space: pre-wrap;
71 | min-height: 1em;
72 | }
73 |
74 | .line:after {
75 | content: "";
76 | }
77 |
78 | .line .linenum {
79 | -webkit-user-select: none; /* Chrome all / Safari all */
80 | -moz-user-select: none; /* Firefox all */
81 | -ms-user-select: none; /* IE 10+ */
82 | user-select: none; /* Likely future */
83 | color: #7d7d7d;
84 | font-family: monospace;
85 | padding: 0 15px 0 10px;
86 | text-align: center;
87 | position: absolute;
88 | left: -8px;
89 | text-align: right;
90 | width: 30px;
91 | }
92 |
93 | #module_download .preview {
94 | display: block;
95 | margin: 0 auto;
96 | max-height: 100%;
97 | max-width: 100%;
98 | }
99 |
100 | #module_download,#downloaddetails {
101 | height: 100%;
102 | position: absolute;
103 | top: 0;
104 | width: 100%;
105 | }
106 |
107 | #pastearea {
108 | cursor: pointer;
109 | }
110 |
111 | #pastearea.dragover {
112 | background-color: rgba(255,255,255,.2);
113 | }
114 |
115 | #pastearea:hover {
116 | -moz-transition: background-color 100ms ease-in;
117 | -ms-transition: background-color 100ms ease-in;
118 | -o-transition: background-color 100ms ease-in;
119 | -webkit-transition: background-color 100ms ease-in;
120 | background-color: #313538;
121 | transition: background-color 100ms ease-in;
122 | }
123 |
124 | #pastecatcher {
125 | height: 0;
126 | left: 0;
127 | opacity: 0;
128 | overflow: hidden;
129 | position: absolute;
130 | top: 0;
131 | width: 0;
132 | }
133 |
134 | #previewimg {
135 | max-height: 100%;
136 | max-width: 100%;
137 | }
138 |
139 | #previewimg img, .preview video, .preview audio {
140 | display: block;
141 | margin: 0 auto;
142 | }
143 |
144 | #previewimg img:not(.dragged), .preview audio, .preview video {
145 | max-height: 100vh;
146 | max-width: 100vw;
147 | }
148 |
149 | #progressamountbg {
150 | background-color: rgba(0,10,0,.5);
151 | height: 100%;
152 | left: 0;
153 | position: absolute;
154 | top: 0;
155 | width: 0;
156 | z-index: -1;
157 | }
158 |
159 | #uploadview .centerview {
160 | display: table;
161 | height: 100%;
162 | width: 100%;
163 | }
164 |
165 | .boxarea {
166 | -moz-transition: background-color 400ms ease-out;
167 | -ms-transition: background-color 400ms ease-out;
168 | -o-transition: background-color 400ms ease-out;
169 | -webkit-transition: background-color 400ms ease-out;
170 | transition: background-color 400ms ease-out;
171 | }
172 |
173 | .btn {
174 | -webkit-box-align: start;
175 | background-color: rgba(0,0,0,.5);
176 | border: 2px solid #FFF;
177 | box-sizing: border-box;
178 | color: #FFF!important;
179 | cursor: pointer;
180 | display: inline-block;
181 | font: inherit;
182 | font-size: 16px;
183 | height: 40px;
184 | line-height: 16px;
185 | margin: 0 0 0 5px;
186 | opacity: .75;
187 | padding: 10px;
188 | text-align: center;
189 | text-decoration: none;
190 | transition: all 400ms ease-out;
191 | vertical-align: middle;
192 | white-space: nowrap;
193 | }
194 |
195 | .btn:hover {
196 | background-color: #313538;
197 | opacity: 1;
198 | transition: all 100ms ease-in;
199 | }
200 |
201 | .contentarea {
202 | border: 2px solid #FFF;
203 | bottom: 0;
204 | color: #FFF;
205 | height: 200px;
206 | left: 0;
207 | margin: auto;
208 | position: absolute;
209 | right: 0;
210 | text-align: center;
211 | top: 0;
212 | vertical-align: middle;
213 | width: 200px;
214 | }
215 |
216 | .contentarea .boxarea {
217 | display: table-cell;
218 | text-align: center;
219 | vertical-align: middle;
220 | }
221 |
222 | .contentarea h1 {
223 | margin: 0;
224 | }
225 |
226 | .downloadexplain {
227 | color: #fff;
228 | font-size: 30px;
229 | }
230 |
231 | .dragresize.dragging {
232 | cursor: nwse-resize;
233 | }
234 |
235 | .footer {
236 | bottom: 10px;
237 | margin: auto;
238 | position: fixed;
239 | text-align: center;
240 | width: 100%;
241 | color: #FFF;
242 | }
243 |
244 | .footer a {
245 | color: #FFF;
246 | font-size: 14px;
247 | opacity: .5;
248 | text-decoration: none;
249 | }
250 |
251 | .hidden {
252 | display: none!important;
253 | }
254 |
255 | .loadingtext {
256 | color: #FFF;
257 | text-align: center;
258 | }
259 |
260 | .noscript {
261 | color: #FFF;
262 | margin: 0;
263 | text-align: center;
264 | vertical-align: middle;
265 | }
266 |
267 | .previewtext {
268 | height: calc(100% - 110px);
269 | overflow: auto;
270 | position: absolute;
271 | top: 55px;
272 | width: 100%;
273 | }
274 |
275 | .previewtext > textarea {
276 | background: transparent none repeat scroll 0 0;
277 | border: 0 none;
278 | box-sizing: border-box;
279 | color: #c5c8c6!important;
280 | font-family: monospace!important;
281 | font-size: 13px;
282 | height: calc(100% - 5px);
283 | margin: 0;
284 | outline: medium none;
285 | padding: 6.5px 0 0 50px;
286 | position: absolute;
287 | resize: none;
288 | width: 100%;
289 | }
290 |
291 | .previewtext code {
292 | background: none;
293 | margin: 0;
294 | overflow: visible;
295 | }
296 |
297 | .previewtext pre {
298 | margin: 0;
299 |
300 | padding-left: 50px;
301 | }
302 |
303 | .topbar {
304 | box-sizing: border-box;
305 | display: flex;
306 | height: 40px;
307 | padding: 5px;
308 | position: fixed;
309 | width: 100%;
310 | z-index: 1;
311 | }
312 |
313 | .viewcontent {
314 | height: 100%;
315 | width: 100%;
316 | }
317 |
318 | .viewswitcher {
319 | -webkit-flex-shrink: 0;
320 | display: flex;
321 | flex-shrink: 0;
322 | margin-left: auto;
323 | vertical-align: top;
324 | white-space: nowrap;
325 | }
326 |
327 | .centertext {
328 | text-align: center;
329 | }
330 |
331 | .centerable {
332 | -webkit-transform: translateY(-50%);
333 | margin: 0 auto;
334 | position: relative;
335 | top: 50%;
336 | transform: translateY(-50%);
337 | }
338 |
339 | body {
340 | background-color: #1d1f21;
341 | overflow: auto;
342 | }
343 |
344 | body,html {
345 | font-family: Sans-Serif;
346 | height: 100%;
347 | margin: 0;
348 | overflow: auto;
349 | padding: 0;
350 | }
351 |
352 | .right {
353 | float: right;
354 | }
355 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | var crypto = require('crypto');
2 | var fs = require('fs');
3 | var path = require('path');
4 |
5 | var Busboy = require('busboy');
6 | var express = require('express');
7 | var http = require('http');
8 | var https = require('https');
9 | var request = require('request');
10 | var tmp = require('tmp');
11 |
12 |
13 | // Different headers can be pushed depending on data format
14 | // to allow for changes with backwards compatibility
15 | var UP1_HEADERS = {
16 | v1: new Buffer.from("UP1\0", 'binary')
17 | }
18 |
19 | function handle_upload(req, res) {
20 | var config = req.app.locals.config
21 | var busboy = new Busboy({
22 | headers: req.headers,
23 | limits: {
24 | fileSize: config.maximum_file_size,
25 | files: 1,
26 | parts: 3
27 | }
28 | });
29 |
30 | var fields = {};
31 | var tmpfname = null;
32 |
33 | busboy.on('field', function(fieldname, value) {
34 | fields[fieldname] = value;
35 | });
36 |
37 | busboy.on('file', function(fieldname, file, filename) {
38 | try {
39 | var ftmp = tmp.fileSync({ postfix: '.tmp', dir: req.app.locals.config.path.i, keep: true });
40 | tmpfname = ftmp.name;
41 |
42 | var fstream = fs.createWriteStream('', {fd: ftmp.fd, defaultEncoding: 'binary'});
43 | fstream.write(UP1_HEADERS.v1, 'binary', () => file.pipe(fstream));
44 | } catch (err) {
45 | console.error("Error on file:", err);
46 | res.send("Internal Server Error");
47 | req.unpipe(busboy);
48 | res.close();
49 | }
50 | });
51 |
52 | busboy.on('finish', function() {
53 | try {
54 | if (!tmpfname) {
55 | res.send("Internal Server Error");
56 | } else if (fields.api_key !== config['api_key']) {
57 | res.send('{"error": "API key doesn\'t match", "code": 2}');
58 | } else if (!fields.ident) {
59 | res.send('{"error": "Ident not provided", "code": 11}');
60 | } else if (fields.ident.length !== 22) {
61 | res.send('{"error": "Ident length is incorrect", "code": 3}');
62 | } else if (ident_exists(fields.ident)) {
63 | res.send('{"error": "Ident is already taken.", "code": 4}');
64 | } else {
65 | var delhmac = crypto.createHmac('sha256', config.delete_key)
66 | .update(fields.ident)
67 | .digest('hex');
68 | fs.rename(tmpfname, ident_path(fields.ident), function() {
69 | res.json({delkey: delhmac});
70 | });
71 | }
72 | } catch (err) {
73 | console.error("Error on finish:", err);
74 | res.send("Internal Server Error");
75 | }
76 | });
77 |
78 | return req.pipe(busboy);
79 | };
80 |
81 |
82 | function handle_delete(req, res) {
83 | var config = req.app.locals.config
84 | if (!req.query.ident) {
85 | res.send('{"error": "Ident not provided", "code": 11}');
86 | return;
87 | }
88 | if (!req.query.delkey) {
89 | res.send('{"error": "Delete key not provided", "code": 12}');
90 | return;
91 | }
92 | var delhmac = crypto.createHmac('sha256', config.delete_key)
93 | .update(req.query.ident)
94 | .digest('hex');
95 | if (req.query.ident.length !== 22) {
96 | res.send('{"error": "Ident length is incorrect", "code": 3}');
97 | } else if (delhmac !== req.query.delkey) {
98 | res.send('{"error": "Incorrect delete key", "code": 10}');
99 | } else if (!ident_exists(req.query.ident)) {
100 | res.send('{"error": "Ident does not exist", "code": 9}');
101 | } else {
102 | fs.unlink(ident_path(req.query.ident), function() {
103 | cf_invalidate(req.query.ident, config);
104 | res.redirect('/');
105 | });
106 | }
107 | };
108 |
109 | function ident_path(ident) {
110 | return '../i/' + path.basename(ident);
111 | }
112 |
113 | function ident_exists(ident) {
114 | try {
115 | fs.lstatSync(ident_path(ident));
116 | return true;
117 | } catch (err) {
118 | return false;
119 | }
120 | }
121 |
122 | function cf_do_invalidate(ident, mode, cfconfig) {
123 | var inv_url = mode + '://' + cfconfig.url + '/i/' + ident;
124 |
125 | request.post({
126 | url: 'https://www.cloudflare.com/api_json.html',
127 | form: {
128 | a: 'zone_file_purge',
129 | tkn: cfconfig.token,
130 | email: cfconfig.email,
131 | z: cfconfig.domain,
132 | url: inv_url
133 | }
134 | }, function(err, response, body) {
135 | if (err) {
136 | console.error("Cache invalidate failed for", ident);
137 | console.error("Body:", body);
138 | return;
139 | }
140 | try {
141 | var result = JSON.parse(body)
142 | if (result.result === 'error') {
143 | console.error("Cache invalidate failed for", ident);
144 | console.error("Message:", msg);
145 | }
146 | } catch(err) {}
147 | });
148 | }
149 |
150 | function cf_invalidate(ident, config) {
151 | var cfconfig = config['cloudflare-cache-invalidate']
152 | if (!cfconfig.enabled) {
153 | return;
154 | }
155 | if (config.http.enabled)
156 | cf_do_invalidate(ident, 'http', cfconfig);
157 | if (config.https.enabled)
158 | cf_do_invalidate(ident, 'https', cfconfig);
159 | }
160 |
161 | function create_app(config) {
162 | var app = express();
163 | app.locals.config = config
164 | app.use('', express.static(config.path.client));
165 | app.use('/i', express.static(config.path.i));
166 | app.post('/up', handle_upload);
167 | app.get('/del', handle_delete);
168 | return app
169 | }
170 |
171 | /* Convert an IP:port string to a split IP and port */
172 | function get_addr_port(s) {
173 | var spl = s.split(":");
174 | if (spl.length === 1)
175 | return { host: spl[0], port: 80 };
176 | else if (spl[0] === '')
177 | return { port: parseInt(spl[1]) };
178 | else
179 | return { host: spl[0], port: parseInt(spl[1]) };
180 | }
181 |
182 | function serv(server, serverconfig, callback) {
183 | var ap = get_addr_port(serverconfig.listen);
184 | return server.listen(ap.port, ap.host, callback);
185 | }
186 |
187 | function init_defaults(config) {
188 | config.path = config.path ? config.path : {};
189 | config.path.i = config.path.i ? config.path.i : "../i";
190 | config.path.client = config.path.client ? config.path.client : "../client";
191 | }
192 |
193 | function init(config) {
194 | init_defaults(config)
195 |
196 | var app = create_app(config);
197 |
198 | if (config.http.enabled) {
199 | serv(http.createServer(app), config.http, function() {
200 | console.info('Started server at http://%s:%s', this.address().address, this.address().port);
201 | });
202 | }
203 |
204 | if (config.https.enabled) {
205 | var sec_creds = {
206 | key: fs.readFileSync(config.https.key),
207 | cert: fs.readFileSync(config.https.cert)
208 | };
209 | serv(https.createServer(sec_creds, app), config.https, function() {
210 | console.info('Started server at https://%s:%s', this.address().address, this.address().port);
211 | });
212 | }
213 | }
214 |
215 | function main(configpath) {
216 | init(JSON.parse(fs.readFileSync(configpath)));
217 | }
218 |
219 | main('./server.conf')
220 |
--------------------------------------------------------------------------------
/client/js/home.js:
--------------------------------------------------------------------------------
1 | upload.load.need('js/download.js', function() { return upload.download })
2 | upload.load.need('js/textpaste.js', function() { return upload.textpaste })
3 | upload.load.need('js/loadencryption.js', function() { return window.crypt })
4 | upload.load.need('js/updown.js', function() { return upload.updown })
5 |
6 | upload.modules.addmodule({
7 | name: 'home',
8 | // Dear santa, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings
9 | template: '\
10 |
\
15 |
\
16 |
\
17 |
\
18 |
Upload
\
19 | \
20 |
\
21 |
\
22 |
\
23 |
\
24 |
\
25 |
\
28 |
\
31 |
\
32 |
',
33 | init: function () {
34 | upload.modules.setdefault(this)
35 | $(document).on('change', '#filepicker', this.pickerchange.bind(this))
36 | $(document).on('click', '#pastearea', this.pickfile.bind(this))
37 | $(document).on('dragover', '#pastearea', this.dragover.bind(this))
38 | $(document).on('dragleave', '#pastearea', this.dragleave.bind(this))
39 | $(document).on('drop', '#pastearea', this.drop.bind(this))
40 | $(document).on('click', '#newpaste', this.newpaste.bind(this))
41 | $(document).on('click', this.triggerfocuspaste.bind(this))
42 | this.initpastecatcher()
43 | $(document).on('paste', this.pasted.bind(this))
44 | },
45 | dragleave: function (e) {
46 | e.preventDefault()
47 | e.stopPropagation()
48 | this._.pastearea.removeClass('dragover')
49 | },
50 | drop: function (e) {
51 | e.preventDefault()
52 | this._.pastearea.removeClass('dragover')
53 | if (e.dataTransfer.files.length > 0) {
54 | this.doupload(e.dataTransfer.files[0])
55 | }
56 | },
57 | dragover: function (e) {
58 | e.preventDefault()
59 | this._.pastearea.addClass('dragover')
60 | },
61 | pickfile: function(e) {
62 | this._.filepicker.click()
63 | },
64 | pickerchange: function(e) {
65 | if (e.target.files.length > 0) {
66 | this.doupload(e.target.files[0])
67 | $(e.target).parents('form')[0].reset()
68 | }
69 | },
70 | route: function (route, content) {
71 | if (content && content != 'noref') {
72 | return upload.download
73 | }
74 | return this
75 | },
76 | render: function (view) {
77 | view.html(this.template)
78 | this._ = {}
79 | this._.view = view
80 | this._.filepicker = view.find('#filepicker')
81 | this._.pastearea = view.find('#pastearea')
82 | this._.newpaste = view.find('#newpaste')
83 | this._.progress = {}
84 | this._.progress.main = view.find('#uploadprogress')
85 | this._.progress.type = view.find('#progresstype')
86 | this._.progress.amount = view.find('#progressamount')
87 | this._.progress.bg = view.find('#progressamountbg')
88 | $('#footer').show()
89 | },
90 | initroute: function () {
91 | this.focuspaste()
92 | },
93 | unrender: function() {
94 | delete this['_']
95 | },
96 | initpastecatcher: function () {
97 | this.pastecatcher = $('
').prop('id', 'pastecatcher')
98 | this.pastecatcher.prop('contenteditable', true)
99 | $('body').append(this.pastecatcher)
100 | },
101 | focuspaste: function () {
102 | setTimeout(function () {
103 | this.pastecatcher.focus()
104 | }, 100)
105 | },
106 | triggerfocuspaste: function(e) {
107 | if (e.which != 1) {
108 | return
109 | }
110 |
111 | if (e.target == document.body && this._ && !this._.pastearea.hasClass('hidden')) {
112 | e.preventDefault()
113 | this.focuspaste()
114 | }
115 | },
116 | progress: function(e) {
117 | if (e.eventsource != 'encrypt') {
118 | this._.progress.type.text('Uploading')
119 | } else {
120 | this._.progress.type.text('Encrypting')
121 | }
122 | var percent = (e.loaded / e.total) * 100
123 | this._.progress.bg.css('width', percent + '%')
124 | this._.progress.amount.text(Math.floor(percent) + '%')
125 | },
126 | doupload: function (blob) {
127 | this._.pastearea.addClass('hidden')
128 | this._.progress.main.removeClass('hidden')
129 | this._.progress.type.text('Encrypting')
130 | this._.progress.bg.css('width', 0)
131 | this._.newpaste.addClass('hidden')
132 | upload.updown.upload(blob, this.progress.bind(this), this.uploaded.bind(this))
133 | },
134 | closepaste: function() {
135 | this._.pastearea.removeClass('hidden')
136 | this._.view.find('#uploadview').show()
137 | this._.view.find('.viewswitcher').show()
138 | },
139 | dopasteupload: function (data) {
140 | this._.pastearea.addClass('hidden')
141 | this._.view.find('#uploadview').hide()
142 | this._.view.find('.viewswitcher').hide()
143 | upload.textpaste.render(this._.view, 'Pasted text.txt', data, 'text/plain', this.closepaste.bind(this))
144 | },
145 | uploaded: function (data, response) {
146 | upload.download.delkeys[data.ident] = response.delkey
147 |
148 | try {
149 | localStorage.setItem('delete-' + data.ident, response.delkey)
150 | } catch (e) {
151 | console.log(e)
152 | }
153 |
154 | if (window.location.hash == '#noref') {
155 | history.replaceState(undefined, undefined, '#' + data.seed)
156 | upload.route.setroute(upload.download, undefined, data.seed)
157 | } else {
158 | window.location = '#' + data.seed
159 | }
160 | },
161 | newpaste: function() {
162 | this.dopasteupload('')
163 | },
164 | pasted: function (e) {
165 | if (!this._ || this._.pastearea.hasClass('hidden')) {
166 | return
167 | }
168 |
169 | var items = e.clipboardData.items
170 |
171 | var text = e.clipboardData.getData('text/plain')
172 |
173 | if (text) {
174 | e.preventDefault()
175 | this.dopasteupload(text)
176 | } else if (typeof items == 'undefined') {
177 | self = this
178 | setTimeout(function () {
179 | if (self.pastecatcher.find('img').length) {
180 | var src = self.pastecatcher.find('img').prop('src')
181 | if (src.startsWith('data:')) {
182 | self.doupload(dataURItoBlob(src))
183 | } else {
184 | // TODO: Firefox
185 | }
186 | }
187 | }, 0)
188 | } else if (items.length >= 1) {
189 | e.preventDefault()
190 |
191 | for (var i = 0; i < items.length; i++) {
192 | var blob = items[i].getAsFile()
193 | if (blob) {
194 | this.doupload(blob)
195 | break
196 | }
197 | }
198 |
199 | }
200 | },
201 | })
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | Up1: A Client-side Encrypted Image Host
4 | ===
5 |
6 | Up1 is a simple host that client-side encrypts images, text, and other data, and stores them, with the server knowing nothing about the contents.
7 | It has the ability to view images, text with syntax highlighting, short videos, and arbitrary binaries as downloadables.
8 |
9 | Public Server
10 | ---
11 | There was a public, free to use server at https://up1.ca.
12 | This demo instance is no longer available or being maintained. However, there are several public hosts which use Up1. An online search should turn up some results.
13 |
14 | #### Client Utilities:
15 | * [upclient](https://github.com/Upload/upclient), a command-line tool for uploading to Up1 servers
16 | * ~~ShareX~~, unfortunately, the Up1 support in ShareX has been removed since shutting down the Up1 demo server.
17 |
18 | Quick start
19 | ---
20 | To install and run the server with default settings:
21 |
22 | apt install nodejs
23 | git clone https://github.com/Upload/Up1
24 | cd Up1
25 | cp server/server.conf.example server/server.conf
26 | cp client/config.js.example client/config.js
27 | cd server
28 | npm install
29 | node server.js
30 |
31 | Server configuration is done through the [`server.conf`](https://github.com/Upload/Up1/server.conf.example) file. For a quick start, simply move `server.conf.example` to `server.conf`.
32 |
33 | `listen` is an `address:port`-formatted string, where either one are optional. Some examples include `":9000"` to listen on any interface, port 9000; `"127.0.0.1"` to listen on localhost port 80; `"1.1.1.1:8080"` to listen on 1.1.1.1 port 8080; or even `""` to listen on any interface, port 80.
34 |
35 | `api_key` is a very basic security measure, requiring any client making an upload to know this key. This doesn't seem very useful and should be revamped; replace it with HTTP auth maybe?
36 |
37 | `delete_key` is a key used to secure the deletion keys. Set this to something that only the server knows.
38 |
39 | `maximum_file_size` is the largest file, in bytes, that's allowed to be uploaded to the server. The default here is a decimal 50MB.
40 |
41 | There are three additional sections in the configuration file: `http`, `https` and `cloudflare-cache-invalidate`. The first two are fairly self-explanitory (and at least one must be enabled).
42 |
43 | `cloudflare-cache-invalidate` is disabled by default and only useful if you choose to run the Up1 server behind Cloudflare. When this section is enabled, it ensures that when an upload is deleted, Cloudflare doesn't hold on to copies of the upload on its edge servers by sending an API call to invalidate it.
44 |
45 | For the web application configuration, a [`config.js.example`](https://github.com/Upload/Up1/config.js.example) file is provided. Make sure the `api_key` here matches the one in `server.conf`.
46 |
47 | External Tools
48 | ---
49 |
50 | Currently, there is a command-line program that works with Up1: ~~[ShareX](https://github.com/ShareX/ShareX) ([relevant code](https://github.com/ShareX/ShareX/pull/751))~~, and [upclient](https://github.com/Upload/upclient).
51 |
52 | Upclient is a CLI tool which can send files or data to Up1 servers either via unix pipe (`ps aux | up`), or via argument (`up image.png`), and returns a URL to the uploaded file on stdout. It runs on nodejs and uses SJCL for the crypto.
53 |
54 | How it works
55 | ---
56 |
57 | Before an image is uploaded, a "seed" is generated. This seed can be of any length (because really, the server will never be able to tell), but has a length of 25 characters by default. The seed is then run through SHA512, giving the AES key in bytes 0-256, the CCM IV in bytes 256-384, and the server's file identifier in bytes 384-512. Using this output, the image data is then encrypted using said AES key and IV using SJCL's AES-CCM methods, and sent to the server with an identifier. Within the encryption, there is also a prepended JSON object that contains metadata (currently just the filename and mime-type). The (decrypted) blob format starts with 2 bytes denoting the JSON character length, the JSON data itself, and then the file data at the end.
58 |
59 | Image deletion functionality is also available. When an image is uploaded, a delete token is returned. Sending this delete token back to the server will delete the image. On the server side, `HMAC-SHA256(static_delete_key, identifier)` is used, where the key is a secret on the server.
60 |
61 | Technologies
62 | ---
63 |
64 | The browser-side is written in plain Javascript using SJCL for the AES-CCM encryption, with entropy obtained using the WebCrypto APIs and encryption performed within a Web Worker. The video and audio players just use the HTML5 players hopefully built into the browser. The paste viewer uses highlight.js for the syntax highlighting and line numbers.
65 |
66 | Additionally, the repository copy of SJCL comes from the source at https://github.com/bitwiseshiftleft/sjcl, commit `fb1ba931a46d41a7c238717492b66201b2995840` (Version 1.0.3), built with the command line `./configure --without-all --with-aes --with-sha512 --with-codecBytes --with-random --with-codecBase64 --with-ccm`, and compressed using Closure Compiler. If all goes well, a self-built copy should match up byte-for-byte to the contents of `static/deps/sjcl.min.js`.
67 |
68 | The server-side is written in Node, although we also have a Go server which uses no dependencies outside of the standard library. The only cryptography it uses is for generating deletion keys, using HMAC and SHA256 in the built-in `crypto/hmac` and `crypto/sha256` packages, respectively.
69 |
70 | Caveats
71 | ---
72 |
73 | * **Encryption/Decryption are not streamed or chunked.** This means that (at the time) any download must fit fully in memory, or the browser may crash. This is not a problem with sub-10MB images, but may be a problem if you want to share a long gameplay video or recorded meeting minutes. We would love help and contributions, even if they break backwards compatibilty.
74 |
75 | * **CCM is kinda slow.** Compared to other authenticated encryption modes out there such as GCM or OCB, CCM is considered one of the slower modes (slightly slower than GCM, and almost twice as slow as OCB), isn't parallelizable and [didn't make the best design decisions](http://crypto.stackexchange.com/a/19446). The reason that we chose this algorithm, however, is twofold: primarily, this is the most-audited, oldest and most commonly used algorithm contained in SJCL; as this is used for viewing data, security there is important - and secondly, the other two mentioned algorithms in SJCL were actually *slower* than CCM. There are other crypto libraries which may be allegedly faster, such as [asmcrypto.js](https://github.com/vibornoff/asmcrypto.js/), but it seems new, we don't know anything about it and currently prefer SJCL for its familiarity. With an audit from a trusted party, we may take a second look at asmcrypto.js.
76 |
77 | * **By its very nature, this uses cryptography in Javascript.** There are reasons as to why it's bad to use cryptography in Javascript. We're working on browser extensions to mitigate some of these reasons (and non-Javascript clients are always welcome!), however by the very nature of how Up1 works, cryptography in the browser is required. In the event of a breach of trust on the server, the client code could still be modified to read your decryption keys.
78 |
79 | * **As a new project, this code hasn't been audited by a trusted party.** There have been (to date) very few eyes on the code that we're aware of, and even fewer trusted eyes on the code. While we've put as much effort as possible into offloading the hard crypto stuff to SJCL, we still might have made a mistake somewhere (reading over `static/js/encryption.js` and letting us know if you find issues would be very helpful to us!), and so for that reason, using this software is at your own risk.
80 |
81 | * **The server will, in most cases, receive referrer headers.** If a server decides to log requests, they will also be able to receive `Referer` headers. For private/protected websites and direct links sent via IM or email, this isn't a big deal. If the link is on a public website however, it means the server owner might be able to find the original image. We've added some mitigations for some scenarios, however unfortunately there's nothing that the software or server owner can do about this (apart from hosting behind a CDN and offloading the Referer header to the edge), however when posting a link you have a couple of options:
82 | 1. Put `rel="noreferrer"` into any `` links that are directed at the Up1 server.
83 | 2. If you don't have control over the link attributes, you can use a referrer breaker such as https://anon.click/ or https://href.li/, amongst many.
84 |
85 | Contributing
86 | ---
87 | Any contributions, whether to our existing code or as separate applications, are very welcome!
88 |
89 | We don't ask for any CLAs - you don't have to give up copyright on your code - however we prefer that you contribute under the MIT license, just for consistency.
90 |
91 | If you find serious security issues, please email us at `security@up1.ca`.
92 |
93 | Thank you for you contributions!
94 |
95 | License
96 | ---
97 |
98 | The Up1 server and browser code are both licensed under MIT.
99 |
100 | ShareX's base code is licensed under GPLv2, however the modifications (namely, the C# encryption code) is licensed under MIT.
101 |
102 | Upclient is licensed fully under MIT.
103 |
--------------------------------------------------------------------------------
/client/js/download.js:
--------------------------------------------------------------------------------
1 | upload.load.need('js/dragresize.js', function() { return window.dragresize })
2 |
3 | upload.modules.addmodule({
4 | name: 'download',
5 | delkeys: {},
6 | // Dear santa, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/template_strings
7 | template: '\
8 | \
9 |
\
16 |
\
17 |
\
24 |
\
25 | ',
26 | init: function () {
27 | $(document).on('click', '#editpaste', this.editpaste.bind(this))
28 | },
29 | route: function (route, content) {
30 | if (content != 'noref') {
31 | return this
32 | }
33 | },
34 | render: function (view) {
35 | view.html(this.template)
36 | this._ = {}
37 | this._.view = view
38 | this._.detailsarea = view.find('#downloaddetails')
39 | this._.filename = view.find('#downloaded_filename')
40 | this._.btns = view.find('#btnarea')
41 | this._.deletebtn = view.find('#deletebtn')
42 | this._.dlbtn = view.find('#dlbtn')
43 | this._.nextbtn = view.find('#nextbtn')
44 | this._.prevbtn = view.find('#prevbtn')
45 | this._.viewbtn = view.find('#inbrowserbtn')
46 | this._.viewswitcher = view.find('.viewswitcher')
47 | this._.newupload = view.find('#newupload')
48 | this._.editpaste = view.find('#editpaste')
49 | this._.dlarea = view.find('#dlarea')
50 | this._.title = $('title')
51 | $('#footer').hide()
52 | },
53 | initroute: function (content, contentroot) {
54 | contentroot = contentroot ? contentroot : content
55 | this._.nextbtn.hide()
56 | this._.prevbtn.hide()
57 | if (contentroot.indexOf('&') > -1) {
58 | var which = 0
59 | var values = contentroot.split('&')
60 | var howmany = values.length
61 | if (content != contentroot) {
62 | which = parseInt(content) - 1
63 | }
64 | content = values[which]
65 | this._.nextbtn.attr('href', '#' + contentroot + '/' + (which + 2))
66 | this._.prevbtn.attr('href', '#' + contentroot + '/' + (which))
67 | if (!(which >= howmany - 1)) {
68 | this._.nextbtn.show()
69 | }
70 | if (!(which <= 0)) {
71 | this._.prevbtn.show()
72 | }
73 | }
74 | console.log(contentroot)
75 | delete this._['text']
76 | this._.filename.hide()
77 | this._.title.text("Up1")
78 | this._.btns.hide()
79 | this._.editpaste.hide()
80 | this._.newupload.hide()
81 | this._.content = {}
82 | this._.content.main = this._.content.loading = $('