Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9f65330

Browse files
committedNov 27, 2019
Initial commit
0 parents  commit 9f65330

22 files changed

+6271
-0
lines changed
 

‎.gitignore

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
gdprshare
8+
9+
# Test binary, build with `go test -c`
10+
*.test
11+
12+
# Output of the go coverage tool, specifically when used with LiteIDE
13+
*.out
14+
15+
*.db
16+
17+
# npm
18+
node_modules/
19+
public/scripts/bundle.js
20+
npm-debug.log
21+
disc.html
22+
23+
# vim
24+
*.sw[a-z]
25+
26+
config_test.yml
27+
test/

‎Dockerfile

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
FROM alpine:edge
2+
3+
WORKDIR /opt/gdprshare/
4+
RUN apk add --update --no-cache --virtual .build-deps \
5+
# install build tools
6+
npm \
7+
go \
8+
git \
9+
# get source
10+
&& cd .. \
11+
&& git clone https://github.com/lixmal/gdprshare \
12+
&& cd gdprshare \
13+
# build go binary
14+
&& go build \
15+
# install js dependencies
16+
&& npm install \
17+
# build bundle.js
18+
&& npm run build \
19+
&& rm -rf node_modules \
20+
# remove build tools
21+
&& apk del --purge .build-deps \
22+
&& rm -rf src *.go go.* *.json \
23+
# create dir
24+
&& mkdir /conf \
25+
# move config to volume
26+
&& mv config.yml /conf/
27+
28+
EXPOSE 8080
29+
30+
VOLUME ["/conf"]
31+
32+
STOPSIGNAL SIGTERM
33+
34+
USER nobody:nogroup
35+
36+
ENV GIN_MODE release
37+
38+
CMD ["./gdprshare", "--config", "/conf/config.yml"]

‎LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 lixmal
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

‎README.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# gdprshare
2+
3+
## BUILDING
4+
go build
5+
npm install
6+
npm run build

‎config.yml

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# post size in MiB
2+
maxuploadsize: 25
3+
4+
idlength: 20
5+
6+
# directory to store uploaded files
7+
storepath: 'files'
8+
9+
# listen address/port
10+
listenaddr: ':8080'
11+
12+
tls:
13+
use: false
14+
key: '/etc/ssl/private/ssl-cert-snakeoil.key'
15+
cert: '/etc/ssl/certs/ssl-cert-snakeoil.pem'
16+
17+
database:
18+
# see https://godoc.org/github.com/jinzhu/gorm#Open
19+
driver: 'sqlite3'
20+
args: 'gdprshare.db'
21+
22+
mail:
23+
smtphost: 'localhost'
24+
smtpport: 25
25+
smtpuser: ''
26+
smtppass: ''
27+
from: 'root@localhost'
28+
subject: 'File was downloaded: %s'
29+
30+
# available variables:
31+
# .FileID
32+
# .Addr
33+
# .UserAgent
34+
# .SrcTLSVersion
35+
# .SrcTLSCipherSuite
36+
# .DstTLSVersion
37+
# .DstTLSCipherSuite
38+
body: 'File with id {{.FileID}} was downloaded.'
39+
40+
41+
# headers in case app is behind a reverse proxy
42+
header:
43+
tlsversion: 'X-TLS-Version'
44+
tlsciphersuite: 'X-TLS-CipherSuite'
45+
46+
# saves receiver IP addr and user agent in database
47+
saveclientinfo: false
48+
49+
# length of password pregenerated on the frontend
50+
passwordlength: 12
51+
52+
53+
# for config via env vars see https://github.com/jinzhu/configor#advanced-usage

‎files/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

‎go.mod

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module github.com/lixmal/gdprshare
2+
3+
go 1.13
4+
5+
require (
6+
github.com/gin-contrib/size v0.0.0-20190911145601-c5a150d91bbf
7+
github.com/gin-contrib/sse v0.1.0 // indirect
8+
github.com/gin-contrib/static v0.0.0-20190913125243-df30d4057ba1 // indirect
9+
github.com/gin-gonic/gin v1.4.0
10+
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
11+
github.com/go-playground/validator/v10 v10.0.1
12+
github.com/golang/protobuf v1.3.2 // indirect
13+
github.com/jinzhu/configor v1.1.1
14+
github.com/jinzhu/gorm v1.9.11
15+
github.com/json-iterator/go v1.1.8 // indirect
16+
github.com/mattn/go-isatty v0.0.10 // indirect
17+
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
18+
github.com/ugorji/go v1.1.7 // indirect
19+
golang.org/x/sys v0.0.0-20191115151921-52ab43148777 // indirect
20+
gopkg.in/yaml.v2 v2.2.5 // indirect
21+
)

‎go.sum

+218
Large diffs are not rendered by default.

‎main.go

+616
Large diffs are not rendered by default.

‎package-lock.json

+4,246
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "gdprshare",
3+
"version": "0.1.0",
4+
"description": "share files according to gdpr rules",
5+
"main": "src/scripts/app.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"dev": "node_modules/.bin/browserify -t browserify-css -t [ babelify --presets [ @babel/preset-env @babel/preset-react ] ] src/scripts/app.js -o public/scripts/bundle.js; sed -i 's/node_modules\\/bootstrap\\/dist\\///g' public/scripts/bundle.js",
9+
"build": "NODE_ENV=production node_modules/.bin/browserify -p [ minifyify --no-map ] -t browserify-css -t [ babelify --presets [ @babel/preset-env @babel/preset-react ] ] src/scripts/app.js -o public/scripts/bundle.js; sed -i 's/node_modules\\/bootstrap\\/dist\\///g' public/scripts/bundle.js"
10+
},
11+
"repository": {
12+
"type": "git",
13+
"url": "https://github.com/lixmal/gdprshare"
14+
},
15+
"keywords": [
16+
"gdpr",
17+
"fileshare"
18+
],
19+
"author": "Viktor Liu",
20+
"license": "MIT",
21+
"bugs": {
22+
"url": "https://github.com/lixmal/gdprshare/issues"
23+
},
24+
"devDependencies": {
25+
"@babel/core": "^7.7.2",
26+
"@babel/preset-env": "^7.7.1",
27+
"@babel/preset-react": "^7.7.0",
28+
"babelify": "^10.0.0",
29+
"browserify": "^16.5.0",
30+
"browserify-css": "^0.15.0",
31+
"minifyify": "^7.3.5"
32+
},
33+
"dependencies": {
34+
"@primer/octicons-react": "^9.3.0",
35+
"bootstrap": "^4.3.1",
36+
"bs-custom-file-input": "^1.3.2",
37+
"classnames": "^2.2.6",
38+
"clipboard-polyfill": "^2.8.6",
39+
"react": "^16.12.0",
40+
"react-dom": "^16.12.0",
41+
"react-router-dom": "^5.1.2"
42+
},
43+
"browserify-css": {
44+
"autoInject": true,
45+
"minify": true,
46+
"rootDir": "."
47+
}
48+
}

‎public/index.html

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
6+
<title>GDPR Share</title>
7+
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
8+
</head>
9+
<body ontouchstart>
10+
<noscript>
11+
<h4>Your browser doesn't support JavaScript or it is disabled. Please enable JavaScript.</h4>
12+
</noscript>
13+
<div id="app-content"></div>
14+
<script type="text/javascript" src="/assets/scripts/bundle.js"></script>
15+
</body>
16+
</html>

‎public/scripts/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*
2+
!.gitignore

‎src/scripts/Alert.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react'
2+
import Octicon, { Alert as AlertI } from '@primer/octicons-react'
3+
4+
export default class Alert extends React.Component {
5+
constructor() {
6+
super()
7+
}
8+
9+
render() {
10+
return this.props.error ? (
11+
<div className="alert alert-danger alert-dismissible col-sm-12 file-error text-center">
12+
<Octicon icon={AlertI} />
13+
<span className="sr-only">
14+
Error:
15+
</span>
16+
{this.props.error}
17+
</div>
18+
) : null
19+
}
20+
}

‎src/scripts/Download.js

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import React from 'react'
2+
import { Link } from 'react-router-dom'
3+
import Classnames from 'classnames'
4+
import Alert from './Alert'
5+
6+
export default class Download extends React.Component {
7+
constructor() {
8+
super()
9+
this.url = '/api/v1/download'
10+
11+
this.handleDownload = this.handleDownload.bind(this)
12+
this.downloadFile = this.downloadFile.bind(this)
13+
14+
this.state = {
15+
error: null,
16+
mask: false,
17+
direct: false,
18+
finished: false,
19+
}
20+
}
21+
22+
componentDidMount() {
23+
var url = new URL(window.location.href)
24+
var password = url.searchParams.get('p')
25+
26+
// don't render password field
27+
if (password) {
28+
this.setState({
29+
direct: true,
30+
})
31+
32+
this.handleDownload(null, password)
33+
}
34+
}
35+
36+
classes() {
37+
return Classnames({
38+
'app-outer': true,
39+
'loading-mask': this.state.mask,
40+
})
41+
}
42+
43+
downloadFile(data, filename) {
44+
var blob
45+
if (typeof File === 'function') {
46+
try {
47+
blob = new File([data], filename)
48+
} catch (e) { /* Edge */ }
49+
}
50+
if (typeof blob === 'undefined') {
51+
blob = new Blob([this.response], { type: type })
52+
}
53+
54+
if (typeof window.navigator.msSaveBlob !== 'undefined') {
55+
// IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
56+
window.navigator.msSaveBlob(blob, filename)
57+
} else {
58+
var URL = window.URL || window.webkitURL
59+
var downloadUrl = URL.createObjectURL(blob)
60+
61+
if (filename) {
62+
// use HTML5 a[download] attribute to specify filename
63+
var a = document.createElement('a')
64+
// safari doesn't support this yet
65+
if (typeof a.download === 'undefined') {
66+
window.location = downloadUrl
67+
} else {
68+
a.href = downloadUrl
69+
a.download = filename
70+
document.body.appendChild(a)
71+
a.click()
72+
}
73+
} else {
74+
window.location = downloadUrl
75+
}
76+
77+
setTimeout(function () { URL.revokeObjectURL(downloadUrl) }, 100)
78+
}
79+
this.setState({
80+
mask: false,
81+
// no second download allowed
82+
finished: true,
83+
})
84+
}
85+
86+
decrypt(cipherText, salt, password, callback) {
87+
window.crypto.subtle.importKey(
88+
'raw',
89+
new TextEncoder().encode(password),
90+
{ name: 'PBKDF2' },
91+
false,
92+
[ 'deriveBits', 'deriveKey' ]
93+
).then(function (keyMaterial) {
94+
gdprshare.deriveKey(keyMaterial, salt, function (key) {
95+
var iv = cipherText.slice(0,12)
96+
var cipherT = cipherText.slice(12)
97+
var gcmParams = {
98+
name: 'aes-gcm',
99+
iv: iv,
100+
}
101+
102+
window.crypto.subtle.decrypt(gcmParams, key, cipherT).then(callback, function(error) {
103+
console.log(error)
104+
var err = error.toString()
105+
if (err === 'OperationError')
106+
err = 'Decryption error. Wrong password?'
107+
this.setState({
108+
error: err,
109+
mask: false,
110+
})
111+
}.bind(this))
112+
}.bind(this))
113+
}.bind(this), gdprshare.rejecterr.bind(this))
114+
}
115+
116+
handleDownload(event, password) {
117+
if (event)
118+
event.preventDefault()
119+
120+
if (this.state.mask)
121+
return
122+
123+
if (!password)
124+
password = this.refs.password.value
125+
126+
this.setState({
127+
error: null,
128+
mask: true
129+
})
130+
131+
let fileId = window.location.pathname.split('/').pop()
132+
133+
window.fetch(this.url + '/' + fileId, {
134+
method: 'GET',
135+
}).then(function (response) {
136+
if (response.ok) {
137+
var buf = Buffer.from(response.headers.get('X-Filename'), 'base64')
138+
var salt = buf.slice(0,32)
139+
var filename = buf.slice(32)
140+
141+
// decryption of filename
142+
this.decrypt(filename, salt, password, function (clearText) {
143+
var filename = new TextDecoder().decode(clearText)
144+
145+
response.arrayBuffer().then(function (file) {
146+
this.decrypt(file, salt, password, function(clearText) {
147+
this.downloadFile(clearText, filename)
148+
}.bind(this), gdprshare.rejecterr.bind(this))
149+
}.bind(this), gdprshare.rejecterr.bind(this))
150+
}.bind(this), gdprshare.rejecterr.bind(this))
151+
}
152+
else {
153+
response.clone().json().then(function(data) {
154+
this.setState({
155+
error: data.message,
156+
})
157+
}.bind(this), gdprshare.fetcherr.bind(this, response))
158+
}
159+
160+
this.setState({
161+
mask: false
162+
})
163+
}.bind(this), gdprshare.rejecterr.bind(this))
164+
}
165+
166+
render() {
167+
var form = (
168+
<form className="app-inner" onSubmit={this.handleDownload}>
169+
<div className="form-group row">
170+
<label htmlFor="password" className="col-sm-3 col-form-label">Password</label>
171+
<div className="col-sm-9">
172+
<input className="form-control" id="password" type="password" ref="password" placeholder="Password" maxLength="255" autoFocus="autoFocus" required="required"/>
173+
</div>
174+
</div>
175+
<div className="text-center col-sm-12">
176+
<input type="submit" className="btn btn-primary" value="Download"/>
177+
</div>
178+
</form>
179+
)
180+
return (
181+
<div className="container-fluid col-sm-4">
182+
<div className={this.classes()}>
183+
<h4 className="text-center">GDPRShare Download</h4>
184+
{ this.state.direct || this.state.finished ? null : form }
185+
<br />
186+
<Alert error={this.state.error} />
187+
188+
<div className="text-center col-sm-12">
189+
<Link to="/">Upload new file</Link>
190+
</div>
191+
</div>
192+
</div>
193+
)
194+
}
195+
}

‎src/scripts/ErrPage.js

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from 'react'
2+
import Classnames from 'classnames'
3+
import Alert from './Alert'
4+
5+
export default class ErrPage extends React.Component {
6+
constructor() {
7+
super()
8+
this.url = '/stats'
9+
}
10+
11+
UNSAFE_componentWillMount() {
12+
window.fetch(this.url, {
13+
method: 'POST',
14+
body: {
15+
url: window.document.location.toString(),
16+
},
17+
})
18+
}
19+
20+
render() {
21+
return (
22+
<div className="container-fluid text-center col-sm-4">
23+
<h4>BROWSER ERROR</h4>
24+
<br />
25+
<Alert error="Your browser doesn't support required operations. Please try a different browser." />
26+
</div>
27+
)
28+
}
29+
}

‎src/scripts/Polyfills.js

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//import 'promiz'
2+
//import 'webcrypto-shim'
3+
4+
// IE11
5+
if (!window.crypto)
6+
window.crypto = window.msCrypto
7+
8+
// Edge
9+
if (typeof TextEncoder === "undefined") {
10+
window.TextEncoder = function TextEncoder() {}
11+
TextEncoder.prototype.encode = function encode(str) {
12+
'use strict'
13+
var Len = str.length, resPos = -1
14+
// The Uint8Array's length must be at least 3x the length of the string because an invalid UTF-16
15+
// takes up the equivelent space of 3 UTF-8 characters to encode it properly. However, Array's
16+
// have an auto expanding length and 1.5x should be just the right balance for most uses.
17+
var resArr = typeof Uint8Array === "undefined" ? new Array(Len * 1.5) : new Uint8Array(Len * 3)
18+
for (var point=0, nextcode=0, i = 0; i !== Len; ) {
19+
point = str.charCodeAt(i), i += 1
20+
if (point >= 0xD800 && point <= 0xDBFF) {
21+
if (i === Len) {
22+
resArr[resPos += 1] = 0xef/*0b11101111*/; resArr[resPos += 1] = 0xbf/*0b10111111*/
23+
resArr[resPos += 1] = 0xbd/*0b10111101*/; break
24+
}
25+
// https://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
26+
nextcode = str.charCodeAt(i)
27+
if (nextcode >= 0xDC00 && nextcode <= 0xDFFF) {
28+
point = (point - 0xD800) * 0x400 + nextcode - 0xDC00 + 0x10000
29+
i += 1
30+
if (point > 0xffff) {
31+
resArr[resPos += 1] = (0x1e/*0b11110*/<<3) | (point>>>18)
32+
resArr[resPos += 1] = (0x2/*0b10*/<<6) | ((point>>>12)&0x3f/*0b00111111*/)
33+
resArr[resPos += 1] = (0x2/*0b10*/<<6) | ((point>>>6)&0x3f/*0b00111111*/)
34+
resArr[resPos += 1] = (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/)
35+
continue
36+
}
37+
} else {
38+
resArr[resPos += 1] = 0xef/*0b11101111*/; resArr[resPos += 1] = 0xbf/*0b10111111*/
39+
resArr[resPos += 1] = 0xbd/*0b10111101*/; continue
40+
}
41+
}
42+
if (point <= 0x007f) {
43+
resArr[resPos += 1] = (0x0/*0b0*/<<7) | point
44+
} else if (point <= 0x07ff) {
45+
resArr[resPos += 1] = (0x6/*0b110*/<<5) | (point>>>6)
46+
resArr[resPos += 1] = (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/)
47+
} else {
48+
resArr[resPos += 1] = (0xe/*0b1110*/<<4) | (point>>>12)
49+
resArr[resPos += 1] = (0x2/*0b10*/<<6) | ((point>>>6)&0x3f/*0b00111111*/)
50+
resArr[resPos += 1] = (0x2/*0b10*/<<6) | (point&0x3f/*0b00111111*/)
51+
}
52+
}
53+
if (typeof Uint8Array !== "undefined") return resArr.subarray(0, resPos + 1)
54+
// else // IE 6-9
55+
resArr.length = resPos + 1; // trim off extra weight
56+
return resArr
57+
}
58+
TextEncoder.prototype.toString = function(){return "[object TextEncoder]"}
59+
try { // Object.defineProperty only works on DOM prototypes in IE8
60+
Object.defineProperty(TextEncoder.prototype,"encoding",{
61+
get:function(){if(TextEncoder.prototype.isPrototypeOf(this)) return"utf-8"
62+
else throw TypeError("Illegal invocation");}
63+
})
64+
} catch(e) { /*IE6-8 fallback*/ TextEncoder.prototype.encoding = "utf-8"; }
65+
if(typeof Symbol!=="undefined")TextEncoder.prototype[Symbol.toStringTag]="TextEncoder"
66+
}

‎src/scripts/Upload.js

+301
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
import React from 'react'
2+
import Classnames from 'classnames'
3+
import Octicon, { Key } from '@primer/octicons-react'
4+
import BsCustomFileInput from 'bs-custom-file-input'
5+
import Alert from './Alert'
6+
7+
export default class Upload extends React.Component {
8+
constructor() {
9+
super()
10+
this.url = '/api/v1/upload'
11+
12+
this.handleFile = this.handleFile.bind(this)
13+
this.handleUpload = this.handleUpload.bind(this)
14+
this.handleDrop = this.handleDrop.bind(this)
15+
this.handleDragOn = this.handleDragOn.bind(this)
16+
this.handleDragOff = this.handleDragOff.bind(this)
17+
this.uploadFile = this.uploadFile.bind(this)
18+
this.reGenPassword = this.reGenPassword.bind(this)
19+
20+
this.state = {
21+
error: null,
22+
mask: false,
23+
}
24+
}
25+
26+
UNSAFE_componentDidMount() {
27+
BsCustomFileInput.init()
28+
}
29+
30+
classes() {
31+
return Classnames({
32+
'app-outer': true,
33+
'loading-mask': this.state.mask,
34+
})
35+
}
36+
37+
dndClasses() {
38+
return Classnames({
39+
'col-sm-4': true,
40+
'container-fluid': true,
41+
'is-dragover': this.state.isDragOver,
42+
})
43+
}
44+
45+
genPassword(length) {
46+
function genString() {
47+
var array = new Uint16Array(length)
48+
window.crypto.getRandomValues(array)
49+
var array = Array.apply([], array)
50+
array = array.filter(function(x) {
51+
// -.0-9A-Za-z
52+
return x >= 45 && x <=46 || x >= 48 && x<=57 || x >= 65 && x<= 90 || x >= 97 && x <= 122
53+
})
54+
return String.fromCharCode.apply(String, array)
55+
}
56+
var randomString = genString()
57+
while (randomString.length < length) {
58+
randomString += genString()
59+
}
60+
return randomString
61+
}
62+
63+
reGenPassword(event) {
64+
var btn = event.currentTarget
65+
btn.blur()
66+
var val = btn.parentNode.nextSibling.value = this.genPassword(gdprshare.config.passwordLength)
67+
}
68+
69+
uploadFile(data, filename) {
70+
var formData = new FormData()
71+
var file = new File(
72+
[ data ],
73+
{
74+
type: 'application/octet-stream'
75+
},
76+
)
77+
78+
var email = this.refs.email.value
79+
formData.append('file', file, filename)
80+
formData.append('filename', filename)
81+
formData.append('count', this.refs.count.value)
82+
formData.append('expiry', this.refs.expiry.value)
83+
formData.append('email', email)
84+
85+
window.localStorage.setItem('email', email)
86+
87+
window.fetch(this.url, {
88+
method: 'POST',
89+
body: formData,
90+
}).then(function (response) {
91+
if (response.ok)
92+
this.props.history.push(
93+
'uploaded',
94+
{
95+
location: location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : '') + response.headers.get('Location'),
96+
// unencrypted filename
97+
filename: this.refs.file.files[0].name,
98+
password: this.refs.password.value,
99+
}
100+
)
101+
else {
102+
response.clone().json().then(function(data) {
103+
this.setState({
104+
error: data.message,
105+
mask: false,
106+
})
107+
}.bind(this), gdprshare.fetcherr.bind(this, response))
108+
}
109+
}.bind(this), gdprshare.rejecterr.bind(this))
110+
}
111+
112+
encrypt(clearText, salt, password, callback) {
113+
window.crypto.subtle.importKey(
114+
'raw',
115+
new TextEncoder().encode(password),
116+
{ name: 'PBKDF2' },
117+
false,
118+
[ 'deriveBits', 'deriveKey' ]
119+
).then(function (keyMaterial) {
120+
var iv = window.crypto.getRandomValues(new Uint8Array(12))
121+
122+
gdprshare.deriveKey(keyMaterial, salt, function (key) {
123+
var gcmParams = {
124+
name: 'aes-gcm',
125+
iv: iv,
126+
}
127+
window.crypto.subtle.encrypt(gcmParams, key, clearText).then(function(cipherText) {
128+
callback(Buffer.concat([iv, Buffer.from(cipherText)]))
129+
}.bind(this), gdprshare.rejecterr.bind(this))
130+
}.bind(this))
131+
}.bind(this), gdprshare.rejecterr.bind(this))
132+
}
133+
134+
handleDrop(event) {
135+
event.preventDefault()
136+
event.stopPropagation()
137+
138+
this.setState({
139+
isDragOver: false
140+
})
141+
142+
var files = event.dataTransfer.files
143+
if (!this.checkFileSize(files[0], event))
144+
return
145+
146+
this.refs.file.files = files
147+
//this.handleUpload(event)
148+
this.refs.submit.click()
149+
}
150+
151+
handleUpload(event) {
152+
event.preventDefault()
153+
if (this.state.mask)
154+
return
155+
156+
this.setState({
157+
error: null,
158+
mask: true,
159+
})
160+
161+
var password = this.refs.password.value
162+
var salt = window.crypto.getRandomValues(new Uint8Array(32))
163+
var file = this.refs.file.files[0]
164+
165+
// encryption of filename
166+
this.encrypt(new TextEncoder().encode(file.name), salt, password, function (cipherText) {
167+
var filename = Buffer.concat([salt, Buffer.from(cipherText)]).toString('base64')
168+
169+
var reader = new FileReader()
170+
reader.onload = function () {
171+
// encryption of file
172+
this.encrypt(reader.result, salt, password, function (clearText) {
173+
this.uploadFile(clearText, filename)
174+
}.bind(this))
175+
}.bind(this)
176+
reader.readAsArrayBuffer(file)
177+
}.bind(this))
178+
}
179+
180+
checkFileSize(file) {
181+
if (!file)
182+
return
183+
var allowedSize = gdprshare.config.maxFileSize
184+
185+
if (file.size > allowedSize * 1024 * 1024) {
186+
this.setState({
187+
error: 'File to big, maximum allowed size: ' + allowedSize + ' MiB.',
188+
})
189+
this.refs.file.value = null
190+
return false
191+
}
192+
return true
193+
}
194+
195+
handleFile(event) {
196+
var file = event.currentTarget.files[0]
197+
if (!this.checkFileSize(file, event))
198+
return
199+
}
200+
201+
handleDragOn(event) {
202+
event.preventDefault()
203+
event.stopPropagation()
204+
this.setState({
205+
isDragOver: true
206+
})
207+
}
208+
209+
handleDragOff(event) {
210+
event.preventDefault()
211+
event.stopPropagation()
212+
this.setState({
213+
isDragOver: false
214+
})
215+
}
216+
217+
render() {
218+
return (
219+
<div className={this.dndClasses()} onDrop={this.handleDrop} onDragEnter={this.handleDragOn} onDragOver={this.handleDragOn} onDragLeave={this.handleDragOff} onDragEnd={this.handleDragOff}>
220+
<div className={this.classes()}>
221+
<h4 className="text-center">GDPRShare Upload</h4>
222+
<form ref="form" className="app-inner" onSubmit={this.handleUpload}>
223+
<div className="form-group row">
224+
<label htmlFor="file" className="col-sm-3 col-form-label col-form-label-sm">
225+
File
226+
</label>
227+
<div className="col-sm-9">
228+
<div className="custom-file">
229+
<input className="custom-file-input form-control form-control-sm" id="file" type="file" ref="file" onChange={this.handleFile} required="required" autoFocus="autoFocus" />
230+
<label className="custom-file-label col-form-label col-form-label-sm" htmlFor="file">
231+
Select or drop file
232+
</label>
233+
</div>
234+
</div>
235+
</div>
236+
237+
<div>
238+
<div className="form-group row">
239+
<label htmlFor="email" className="col-sm-3 col-form-label col-form-label-sm">
240+
Notification
241+
</label>
242+
<div className="col-sm-9">
243+
<input className="form-control form-control-sm" id="email" type="email" ref="email" placeholder="Enter email (optional)" maxLength="255" aria-describedby="emailHelp"
244+
defaultValue={window.localStorage.getItem('email')}
245+
/>
246+
<small id="emailHelp" className="form-text text-muted">Email address to receive download notifications</small>
247+
</div>
248+
</div>
249+
250+
<div className="form-group row">
251+
<label htmlFor="count" className="col-sm-3 col-form-label col-form-label-sm">
252+
Count
253+
</label>
254+
<div className="col-sm-9">
255+
<input className="form-control form-control-sm" className="form-control form-control-sm" id="count" type="number" ref="count" min="1" max="15" defaultValue="1" required="required" aria-describedby="countHelp" />
256+
<small id="countHelp" className="form-text text-muted">Maximum number of downloads before file is deleted</small>
257+
</div>
258+
</div>
259+
260+
<div className="form-group row">
261+
<label htmlFor="expiry" className="col-sm-3 col-form-label col-form-label-sm">
262+
Expiry
263+
</label>
264+
<div className="col-sm-9">
265+
<input className="form-control form-control-sm" id="expiry" type="number" ref="expiry" min="1" max="14" defaultValue="7" required="required" aria-describedby="expiryHelp" />
266+
<small id="expiryHelp" className="form-text text-muted">Maximum days before file is deleted</small>
267+
</div>
268+
</div>
269+
</div>
270+
271+
<div className="form-group row">
272+
<label htmlFor="password" className="col-sm-3 col-form-label col-form-label-sm">
273+
Password
274+
</label>
275+
<div className="col-sm-9">
276+
<div className="input-group">
277+
<div className="input-group-prepend">
278+
<button onClick={this.reGenPassword} type="button" className="input-group-text">
279+
<Octicon icon={Key} />
280+
</button>
281+
</div>
282+
<input className="form-control form-control-sm" id="password" type="text" ref="password" placeholder="Password" maxLength="255"
283+
defaultValue={this.genPassword(gdprshare.config.passwordLength)} required="required"
284+
/>
285+
</div>
286+
</div>
287+
</div>
288+
<div className="text-center col-sm-12">
289+
<input type="submit" ref="submit" className="btn btn-primary" value="Upload"/>
290+
</div>
291+
292+
</form>
293+
294+
<br />
295+
296+
<Alert error={this.state.error} />
297+
</div>
298+
</div>
299+
)
300+
}
301+
}

‎src/scripts/Uploaded.js

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React from 'react'
2+
import { Link } from 'react-router-dom'
3+
import * as Clipboard from "clipboard-polyfill/dist/clipboard-polyfill.promise"
4+
import Octicon, { Clippy } from '@primer/octicons-react'
5+
6+
export default class Uploaded extends React.Component {
7+
constructor() {
8+
super()
9+
10+
this.copyHandler = this.copyHandler.bind(this)
11+
}
12+
13+
showTooltip(btn, message) {
14+
// TODO
15+
}
16+
17+
copyHandler(event) {
18+
var btn = event.currentTarget
19+
btn.blur()
20+
var input = btn.parentNode.nextSibling
21+
Clipboard.writeText(input.value).then(
22+
function () {
23+
this.showTooltip(btn, 'Copied')
24+
}.bind(this),
25+
function () {
26+
this.showTooltip(btn, 'Failed to copy')
27+
}.bind(this),
28+
)
29+
}
30+
31+
render() {
32+
if (!this.props.history.location.state)
33+
return null
34+
35+
return (
36+
<div className="container-fluid col-sm-4">
37+
<div className="app-outer">
38+
<h4 className="text-center">File was uploaded</h4>
39+
<form className="app-inner">
40+
<div className="form-group row">
41+
<label htmlFor="filename" className="col-sm-3 col-form-label col-form-label-sm">
42+
Filename
43+
</label>
44+
<div className="col-sm-9">
45+
<input className="form-control form-control-sm" id="filename" type="text" ref="filename" readOnly="readOnly" defaultValue={this.props.history.location.state.filename} />
46+
</div>
47+
</div>
48+
49+
<div className="form-group row">
50+
<label htmlFor="password" className="col-sm-3 col-form-label col-form-label-sm">
51+
Password
52+
</label>
53+
<div className="col-sm-9">
54+
<div className="input-group">
55+
<div className="input-group-prepend">
56+
<button id="pw-copy" onClick={this.copyHandler} type="button" className="input-group-text">
57+
<Octicon icon={Clippy} />
58+
</button>
59+
</div>
60+
<input className="form-control form-control-sm" id="password" type="text" ref="password" readOnly="readOnly" defaultValue={this.props.history.location.state.password} />
61+
</div>
62+
</div>
63+
</div>
64+
65+
<br/>
66+
67+
<div className="form-group row">
68+
<label htmlFor="link" className="col-sm-3 col-form-label col-form-label-sm">
69+
Link
70+
</label>
71+
<div className="col-sm-9">
72+
<div className="input-group">
73+
<div className="input-group-prepend">
74+
<button id="link-copy" onClick={this.copyHandler} type="button" className="input-group-text">
75+
<Octicon icon={Clippy} />
76+
</button>
77+
</div>
78+
<input className="form-control form-control-sm" id="link" type="text" ref="link" placeholder="Link" readOnly="readOnly" aria-describedby="linkHelp"
79+
value={this.props.history.location.state.location}
80+
/>
81+
</div>
82+
<small id="linkHelp" className="form-text text-muted">Send link and password via different channels (e.g. one via email, one via chat or phone call)</small>
83+
</div>
84+
</div>
85+
86+
87+
<div className="form-group row">
88+
<label htmlFor="linkpw" className="col-sm-3 col-form-label col-form-label-sm">
89+
Link and Password
90+
</label>
91+
<div className="col-sm-9">
92+
<div className="input-group">
93+
<div className="input-group-prepend">
94+
<button id="linkpw-copy" onClick={this.copyHandler} type="button" className="input-group-text">
95+
<Octicon icon={Clippy} />
96+
</button>
97+
</div>
98+
<input className="form-control form-control-sm" id="linkpassword" type="text" ref="linkpassword" placeholder="Link and passwod" readOnly="readOnly" aria-describedby="linkpasswordHelp"
99+
value={ this.props.history.location.state.location + '?p=' + this.props.history.location.state.password }
100+
/>
101+
</div>
102+
<small id="linkpasswordHelp" className="form-text text-muted">Send link and password at once (less secure)</small>
103+
</div>
104+
</div>
105+
</form>
106+
107+
<br />
108+
109+
<div className="text-center col-sm-12">
110+
<Link to="/">Upload another file</Link>
111+
</div>
112+
</div>
113+
</div>
114+
)
115+
}
116+
}

‎src/scripts/app.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import Css from '../style/app.css'
2+
import React from 'react'
3+
import ReactDOM from 'react-dom'
4+
5+
import { Switch, Router, Route } from 'react-router'
6+
import { createBrowserHistory } from 'history'
7+
import Classnames from 'classnames'
8+
9+
import ErrPage from './ErrPage'
10+
import Upload from './Upload'
11+
import Uploaded from './Uploaded'
12+
import Download from './Download'
13+
14+
import './Polyfills'
15+
16+
const history = createBrowserHistory()
17+
18+
// global namespace
19+
window.gdprshare = {}
20+
21+
// TODO: get config from server
22+
gdprshare.config = {
23+
maxFileSize: 25,
24+
passwordLength: 12,
25+
}
26+
27+
gdprshare.rejecterr = function (error) {
28+
console.log(error)
29+
this.setState({
30+
error: error.toString(),
31+
mask: false,
32+
})
33+
}
34+
35+
gdprshare.fetcherr = function (response, error) {
36+
console.log(error)
37+
response.text().then(function(data) {
38+
this.setState({
39+
error: data,
40+
mask: false,
41+
})
42+
}.bind(this), gdprshare.rejecterr.bind(this))
43+
}
44+
45+
var rootEl = document.getElementById('app-content')
46+
47+
// IE
48+
if (!window.crypto || !window.TextEncoder || !window.Promise || !window.File) {
49+
ReactDOM.render(
50+
<ErrPage />,
51+
rootEl
52+
)
53+
throw 'browser does not support required functions'
54+
}
55+
56+
gdprshare.deriveKey = function (keyMaterial, salt, callback) {
57+
window.crypto.subtle.deriveKey(
58+
{
59+
'name': 'PBKDF2',
60+
salt: salt,
61+
'iterations': 100000,
62+
'hash': 'SHA-256'
63+
},
64+
keyMaterial,
65+
{ 'name': 'AES-GCM', 'length': 256 },
66+
true,
67+
[ 'encrypt', 'decrypt' ]
68+
).then(callback, gdprshare.rejecterr.bind(this))
69+
}
70+
71+
72+
73+
ReactDOM.render(
74+
<Router history={history}>
75+
<Switch>
76+
<Route path="/" exact component={Upload} />
77+
<Route path="/uploaded" component={Uploaded} />
78+
<Route path="/d/:fileId" component={Download} />
79+
</Switch>
80+
</Router>,
81+
rootEl
82+
)

‎src/style/app.css

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
.is-dragover {
2+
background-color: gray;
3+
}
4+
5+
.app-outer {
6+
margin-top: 5px !important;
7+
padding: 10px;
8+
width: auto;
9+
height: auto;
10+
min-height: 200px;
11+
min-width: 320px;
12+
background-color: #f3f3f3;
13+
margin:0 auto;
14+
border-radius: 9px;
15+
border: 1px solid ##c5c5c5;
16+
}
17+
18+
.app-inner {
19+
margin:0 auto;
20+
max-width: 380px;
21+
width: auto;
22+
}
23+
24+
.app-outer>h4 {
25+
font-size: 20px;
26+
font-weight: 600;
27+
}
28+
29+
.input-group-text:active:focus {
30+
outline: none;
31+
}
32+
33+
.file-error {
34+
margin-top: 15px;
35+
animation: blinker 1s linear;
36+
margin-bottom: 0px;
37+
padding-bottom: 0px;
38+
}
39+
40+
.file-error>svg {
41+
margin-right: 5px;
42+
}
43+
44+
@keyframes blinker {
45+
50% { opacity: 0.0; }
46+
}
47+
48+
.panel-body {
49+
background-color: #EEEEEE;
50+
}
51+
52+
.loading-mask {
53+
position: relative;
54+
}
55+
56+
.loading-mask::before {
57+
content: '';
58+
position: absolute;
59+
top: 0;
60+
right: 0;
61+
bottom: 0;
62+
left: 0;
63+
background-color: rgba(0, 0, 0, 0.25);
64+
z-index: 100;
65+
border-radius: 9px;
66+
}
67+
@keyframes spin {
68+
from {
69+
transform: rotate(0deg);
70+
}
71+
to {
72+
transform: rotate(359deg);
73+
}
74+
}
75+
76+
.loading-mask::after {
77+
content: '';
78+
z-index: 100;
79+
position: absolute;
80+
border-width: 3px;
81+
border-style: solid;
82+
border-color: transparent rgb(255, 255, 255) rgb(255, 255, 255);
83+
border-radius: 50%;
84+
width: 24px;
85+
height: 24px;
86+
top: calc(50% - 12px);
87+
left: calc(50% - 12px);
88+
animation: 2s linear 0s normal none infinite running spin;
89+
filter: drop-shadow(0 0 2 rgba(0, 0, 0, 0.33));
90+
}
91+
92+
@import url('node_modules/bootstrap/dist/css/bootstrap.min.css');

‎v8_to_v10.go

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package main
2+
3+
import (
4+
"reflect"
5+
"sync"
6+
7+
"github.com/gin-gonic/gin/binding"
8+
"github.com/go-playground/validator/v10"
9+
)
10+
11+
type defaultValidator struct {
12+
once sync.Once
13+
validate *validator.Validate
14+
}
15+
16+
var _ binding.StructValidator = &defaultValidator{}
17+
18+
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
19+
20+
if kindOfData(obj) == reflect.Struct {
21+
22+
v.lazyinit()
23+
24+
if err := v.validate.Struct(obj); err != nil {
25+
return error(err)
26+
}
27+
}
28+
29+
return nil
30+
}
31+
32+
func (v *defaultValidator) Engine() interface{} {
33+
v.lazyinit()
34+
return v.validate
35+
}
36+
37+
func (v *defaultValidator) lazyinit() {
38+
v.once.Do(func() {
39+
v.validate = validator.New()
40+
v.validate.SetTagName("binding")
41+
42+
// add any custom validations etc. here
43+
})
44+
}
45+
46+
func kindOfData(data interface{}) reflect.Kind {
47+
48+
value := reflect.ValueOf(data)
49+
valueType := value.Kind()
50+
51+
if valueType == reflect.Ptr {
52+
valueType = value.Elem().Kind()
53+
}
54+
return valueType
55+
}
56+

0 commit comments

Comments
 (0)
Please sign in to comment.