From bfaf95f9efbf09c8e34e07499da39274202bc2ef Mon Sep 17 00:00:00 2001 From: Marc Ole Bulling Date: Thu, 10 Mar 2022 14:31:36 +0100 Subject: [PATCH] Host serviceworker on same domain if using https #49 --- cmd/gokapi/Main.go | 4 +- internal/configuration/Configuration.go | 8 + internal/webserver/Webserver.go | 2 + .../web/static/serviceworker/index.html | 168 ++++++++++++++++++ .../webserver/web/static/serviceworker/sw.js | 128 +++++++++++++ .../web/templates/html_download.tmpl | 5 + .../web/templates/string_constants.tmpl | 2 +- 7 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 internal/webserver/web/static/serviceworker/index.html create mode 100644 internal/webserver/web/static/serviceworker/sw.js diff --git a/cmd/gokapi/Main.go b/cmd/gokapi/Main.go index 72de0d0..172a05e 100644 --- a/cmd/gokapi/Main.go +++ b/cmd/gokapi/Main.go @@ -29,9 +29,9 @@ import ( // Version is the current version in readable form. // The go generate call below needs to be modified as well -const Version = "1.5.0" +const Version = "1.5.1" -//go:generate sh "../../build/setVersionTemplate.sh" "1.5.0" +//go:generate sh "../../build/setVersionTemplate.sh" "1.5.1" //go:generate sh -c "cp \"$(go env GOROOT)/misc/wasm/wasm_exec.js\" ../../internal/webserver/web/static/js/ && echo Copied wasm_exec.js" //go:generate sh -c "GOOS=js GOARCH=wasm go build -o ../../internal/webserver/web/main.wasm github.com/forceu/gokapi/cmd/wasmdownloader && echo Compiled WASM module" diff --git a/internal/configuration/Configuration.go b/internal/configuration/Configuration.go index bd4ee3a..57e00ed 100644 --- a/internal/configuration/Configuration.go +++ b/internal/configuration/Configuration.go @@ -20,6 +20,7 @@ import ( "github.com/forceu/gokapi/internal/models" "io" "os" + "strings" ) // Min length of admin password in characters @@ -31,6 +32,8 @@ var Environment environment.Environment // ServerSettings is an object containing the server configuration var serverSettings models.Configuration +var usesHttps bool + // Exists returns true if configuration files are present func Exists() bool { configPath, _, _, _ := environment.GetConfigPaths() @@ -58,6 +61,11 @@ func Load() { } helper.CreateDir(serverSettings.DataDir) log.Init(Environment.DataDir) + usesHttps = strings.HasPrefix(strings.ToLower(serverSettings.ServerUrl), "https://") +} + +func UsesHttps() bool { + return usesHttps } // Get returns a pointer to the server configuration diff --git a/internal/webserver/Webserver.go b/internal/webserver/Webserver.go index 9805082..77bdad8 100644 --- a/internal/webserver/Webserver.go +++ b/internal/webserver/Webserver.go @@ -284,6 +284,7 @@ func showDownload(w http.ResponseWriter, r *http.Request) { Size: file.Size, Id: file.Id, IsFailedLogin: false, + UsesHttps: configuration.UsesHttps(), } if storage.RequiresClientDecryption(file) { @@ -369,6 +370,7 @@ type DownloadView struct { IsFailedLogin bool IsAdminView bool ClientSideDecryption bool + UsesHttps bool Cipher string } diff --git a/internal/webserver/web/static/serviceworker/index.html b/internal/webserver/web/static/serviceworker/index.html new file mode 100644 index 0000000..6910545 --- /dev/null +++ b/internal/webserver/web/static/serviceworker/index.html @@ -0,0 +1,168 @@ + + + +Downloading, please wait... + + diff --git a/internal/webserver/web/static/serviceworker/sw.js b/internal/webserver/web/static/serviceworker/sw.js new file mode 100644 index 0000000..68ea791 --- /dev/null +++ b/internal/webserver/web/static/serviceworker/sw.js @@ -0,0 +1,128 @@ +/* global self ReadableStream Response */ + +self.addEventListener('install', () => { + self.skipWaiting() +}) + +self.addEventListener('activate', event => { + event.waitUntil(self.clients.claim()) +}) + +const map = new Map() + +// This should be called once per download +// Each event has a dataChannel that the data will be piped through +self.onmessage = event => { + // We send a heartbeat every x secound to keep the + // service worker alive if a transferable stream is not sent + if (event.data === 'ping') { + return + } + + const data = event.data + const downloadUrl = data.url || self.registration.scope + Math.random() + '/' + (typeof data === 'string' ? data : data.filename) + const port = event.ports[0] + const metadata = new Array(3) // [stream, data, port] + + metadata[1] = data + metadata[2] = port + + // Note to self: + // old streamsaver v1.2.0 might still use `readableStream`... + // but v2.0.0 will always transfer the stream throught MessageChannel #94 + if (event.data.readableStream) { + metadata[0] = event.data.readableStream + } else if (event.data.transferringReadable) { + port.onmessage = evt => { + port.onmessage = null + metadata[0] = evt.data.readableStream + } + } else { + metadata[0] = createStream(port) + } + + map.set(downloadUrl, metadata) + port.postMessage({ download: downloadUrl }) +} + +function createStream (port) { + // ReadableStream is only supported by chrome 52 + return new ReadableStream({ + start (controller) { + // When we receive data on the messageChannel, we write + port.onmessage = ({ data }) => { + if (data === 'end') { + return controller.close() + } + + if (data === 'abort') { + controller.error('Aborted the download') + return + } + + controller.enqueue(data) + } + }, + cancel () { + console.log('user aborted') + } + }) +} + +self.onfetch = event => { + const url = event.request.url + + // this only works for Firefox + if (url.endsWith('/ping')) { + return event.respondWith(new Response('pong')) + } + + const hijacke = map.get(url) + + if (!hijacke) return null + + const [ stream, data, port ] = hijacke + + map.delete(url) + + // Not comfortable letting any user control all headers + // so we only copy over the length & disposition + const responseHeaders = new Headers({ + 'Content-Type': 'application/octet-stream; charset=utf-8', + + // To be on the safe side, The link can be opened in a iframe. + // but octet-stream should stop it. + 'Content-Security-Policy': "default-src 'none'", + 'X-Content-Security-Policy': "default-src 'none'", + 'X-WebKit-CSP': "default-src 'none'", + 'X-XSS-Protection': '1; mode=block' + }) + + let headers = new Headers(data.headers || {}) + + if (headers.has('Content-Length')) { + responseHeaders.set('Content-Length', headers.get('Content-Length')) + } + + if (headers.has('Content-Disposition')) { + responseHeaders.set('Content-Disposition', headers.get('Content-Disposition')) + } + + // data, data.filename and size should not be used anymore + if (data.size) { + console.warn('Depricated') + responseHeaders.set('Content-Length', data.size) + } + + let fileName = typeof data === 'string' ? data : data.filename + if (fileName) { + console.warn('Depricated') + // Make filename RFC5987 compatible + fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A') + responseHeaders.set('Content-Disposition', "attachment; filename*=UTF-8''" + fileName) + } + + event.respondWith(new Response(stream, { headers: responseHeaders })) + + port.postMessage({ debug: 'Download started' }) +} diff --git a/internal/webserver/web/templates/html_download.tmpl b/internal/webserver/web/templates/html_download.tmpl index 42e2ea9..d0a09d4 100644 --- a/internal/webserver/web/templates/html_download.tmpl +++ b/internal/webserver/web/templates/html_download.tmpl @@ -69,7 +69,12 @@ const response = await GokapiDecrypt({{ .Cipher }}, "./downloadFile?id={{ .Id }}"); const readableStream = response.body; const reader = response.body.getReader(); +{{ if .UsesHttps }} + streamSaver.mitm = './serviceworker/index.html'; +{{ else }} + console.log("Gokapi is not being accessed through https, therefore an external serviceworker will be used"); streamSaver.mitm = 'https://bulling-it.de/gokapi/serviceworker.html'; +{{ end }} const fileStream = streamSaver.createWriteStream({{.Name }}); window.writer = fileStream.getWriter(); diff --git a/internal/webserver/web/templates/string_constants.tmpl b/internal/webserver/web/templates/string_constants.tmpl index 07142bf..17b6d6f 100644 --- a/internal/webserver/web/templates/string_constants.tmpl +++ b/internal/webserver/web/templates/string_constants.tmpl @@ -1,2 +1,2 @@ {{define "app_name"}}Gokapi{{end}} -{{define "version"}}1.5.0{{end}} +{{define "version"}}1.5.1{{end}}