diff options
| author | Mihai Parparita <mihai@tailscale.com> | 2022-06-08 22:55:55 -0700 |
|---|---|---|
| committer | Mihai Parparita <mihai@tailscale.com> | 2022-06-08 22:55:55 -0700 |
| commit | 2cbcdc4ba8bae7704b11089993e831161dc64ce2 (patch) | |
| tree | 550f07d21fb9f0ffa20c114070121235cb116df0 /ssh/browser/src | |
| parent | decc3ee30d78a68575a74ffced766c5bead8ea08 (diff) | |
| download | tailscale-mihaip/wasm-taildrop.tar.xz tailscale-mihaip/wasm-taildrop.zip | |
wasm: implement Taildrop receivingmihaip/wasm-taildrop
We need to make sure that there's a filesystem (implemented by BrowserFS
for now) and then things mostly work. File contents are sent to the JS
side as base64 encoded data, which may not work for large files.
Signed-off-by: Mihai Parparita <mihai@tailscale.com>
Diffstat (limited to 'ssh/browser/src')
| -rw-r--r-- | ssh/browser/src/files.js | 18 | ||||
| -rw-r--r-- | ssh/browser/src/fs.js | 101 | ||||
| -rw-r--r-- | ssh/browser/src/index.css | 13 | ||||
| -rw-r--r-- | ssh/browser/src/index.js | 28 | ||||
| -rw-r--r-- | ssh/browser/src/notifier.js | 13 |
5 files changed, 166 insertions, 7 deletions
diff --git a/ssh/browser/src/files.js b/ssh/browser/src/files.js new file mode 100644 index 000000000..444be7d92 --- /dev/null +++ b/ssh/browser/src/files.js @@ -0,0 +1,18 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +export function handleFile(file) { + const fileNode = document.createElement("div") + fileNode.addEventListener("click", () => fileNode.remove(), { once: true }) + fileNode.className = "file" + fileNode.appendChild(document.createTextNode("Received file: ")) + + const linkNode = document.createElement("a") + linkNode.href = `data:;base64,${file.data}` + linkNode.download = file.name + linkNode.textContent = file.name + fileNode.appendChild(linkNode) + + document.getElementById("files").appendChild(fileNode) +} diff --git a/ssh/browser/src/fs.js b/ssh/browser/src/fs.js new file mode 100644 index 000000000..25be8669e --- /dev/null +++ b/ssh/browser/src/fs.js @@ -0,0 +1,101 @@ +// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +import * as BrowserFS from "browserfs" + +export function injectFS() { + return new Promise((resolve, reject) => { + BrowserFS.configure({ fs: "InMemory" }, () => { + const goFs = globalThis.fs + const browserFs = BrowserFS.BFSRequire("fs") + const { Buffer } = BrowserFS.BFSRequire("buffer") + globalThis.fs = { + constants: { + O_WRONLY: 1, + O_RDWR: 2, + O_CREAT: 64, + O_TRUNC: 512, + O_APPEND: 1024, + O_EXCL: 128, + }, + ...browserFs, + open(path, flags, mode, callback) { + if (typeof flags === "number") { + flags &= 0x1fff + if (flags in FLAGS_TO_PERMISSION_STRING_MAP) { + flags = FLAGS_TO_PERMISSION_STRING_MAP[flags] + } else { + console.warn( + `Unknown flags ${flags}, will not map to permission string` + ) + } + } + return browserFs.open(path, flags, mode, callback) + }, + writeSync(fd, buf) { + if (fd <= 2) { + return goFs.writeSync(fd, buf) + } + return browserFs.writeSync(fb, buf) + }, + write(fd, buf, offset, length, position, callback) { + if (fd <= 2) { + return goFs.write(fd, buf, offset, length, position, callback) + } + return browserFs.write( + fd, + Buffer.from(buf), + offset, + length, + position, + callback + ) + }, + close(fd, callback) { + return browserFs.close(fd, (err) => { + callback(err === undefined ? null : err) + }) + }, + fstat(fd, callback) { + return browserFs.fstat(fd, (err, retStat) => { + delete retStat["fileData"] + retStat.atimeMs = retStat.atime.getTime() + retStat.mtimeMs = retStat.mtime.getTime() + retStat.ctimeMs = retStat.ctime.getTime() + retStat.birthtimeMs = retStat.birthtime.getTime() + return callback(err, retStat) + }) + }, + } + + resolve() + }) + }) +} + +const FLAGS_TO_PERMISSION_STRING_MAP = { + 0 /*O_RDONLY*/: "r", + 1 /*O_WRONLY*/: "r+", + 2 /*O_RDWR*/: "r+", + 64 /*O_CREAT*/: "r", + 65 /*O_WRONLY|O_CREAT*/: "r+", + 66 /*O_RDWR|O_CREAT*/: "r+", + 129 /*O_WRONLY|O_EXCL*/: "rx+", + 193 /*O_WRONLY|O_CREAT|O_EXCL*/: "rx+", + 514 /*O_RDWR|O_TRUNC*/: "w+", + 577 /*O_WRONLY|O_CREAT|O_TRUNC*/: "w", + 578 /*O_CREAT|O_RDWR|O_TRUNC*/: "w+", + 705 /*O_WRONLY|O_CREAT|O_EXCL|O_TRUNC*/: "wx", + 706 /*O_RDWR|O_CREAT|O_EXCL|O_TRUNC*/: "wx+", + 1024 /*O_APPEND*/: "a", + 1025 /*O_WRONLY|O_APPEND*/: "a", + 1026 /*O_RDWR|O_APPEND*/: "a+", + 1089 /*O_WRONLY|O_CREAT|O_APPEND*/: "a", + 1090 /*O_RDWR|O_CREAT|O_APPEND*/: "a+", + 1153 /*O_WRONLY|O_EXCL|O_APPEND*/: "ax", + 1154 /*O_RDWR|O_EXCL|O_APPEND*/: "ax+", + 1217 /*O_WRONLY|O_CREAT|O_EXCL|O_APPEND*/: "ax", + 1218 /*O_RDWR|O_CREAT|O_EXCL|O_APPEND*/: "ax+", + 4096 /*O_RDONLY|O_DSYNC*/: "rs", + 4098 /*O_RDWR|O_DSYNC*/: "rs+", +} diff --git a/ssh/browser/src/index.css b/ssh/browser/src/index.css index 83cd9c6fe..ef0878780 100644 --- a/ssh/browser/src/index.css +++ b/ssh/browser/src/index.css @@ -89,3 +89,16 @@ button { min-height: 20px; background-color: #ffffff20; } + +.file { + margin: 12px; + padding: 8px; + border-radius: 8px; + box-shadow: 1px 1px 3px rgb(0 0 0 / 50%); +} + +#files { + position: absolute; + bottom: 12px; + right: 12px; +} diff --git a/ssh/browser/src/index.js b/ssh/browser/src/index.js index f5095f873..72003963b 100644 --- a/ssh/browser/src/index.js +++ b/ssh/browser/src/index.js @@ -4,14 +4,25 @@ import "./wasm_exec" import wasmUrl from "./main.wasm" -import { notifyState, notifyNetMap, notifyBrowseToURL } from "./notifier" +import { + notifyState, + notifyNetMap, + notifyBrowseToURL, + notifyIncomingFiles, +} from "./notifier" import { sessionStateStorage } from "./js-state-store" +import { injectFS } from "./fs" -const go = new window.Go() -WebAssembly.instantiateStreaming( - fetch(`./dist/${wasmUrl}`), - go.importObject -).then((result) => { +async function main() { + // Inject in-memory filesystem (otherwise wasm_exec.js will use a stub that + // always returns errors). + await injectFS() + + const go = new globalThis.Go() + const result = await WebAssembly.instantiateStreaming( + fetch(`./dist/${wasmUrl}`), + go.importObject + ) go.run(result.instance) const ipn = newIPN({ // Persist IPN state in sessionStorage in development, so that we don't need @@ -22,5 +33,8 @@ WebAssembly.instantiateStreaming( notifyState: notifyState.bind(null, ipn), notifyNetMap: notifyNetMap.bind(null, ipn), notifyBrowseToURL: notifyBrowseToURL.bind(null, ipn), + notifyIncomingFiles: notifyIncomingFiles.bind(null, ipn), }) -}) +} + +main() diff --git a/ssh/browser/src/notifier.js b/ssh/browser/src/notifier.js index 71317f01e..bef79a69f 100644 --- a/ssh/browser/src/notifier.js +++ b/ssh/browser/src/notifier.js @@ -9,6 +9,7 @@ import { hideLogoutButton, } from "./login" import { showSSHPeers, hideSSHPeers } from "./ssh" +import { handleFile } from "./files" /** * @fileoverview Notification callback functions (bridged from ipn.Notify) @@ -73,3 +74,15 @@ export function notifyNetMap(ipn, netMapStr) { export function notifyBrowseToURL(ipn, url) { showLoginURL(url) } + +export function notifyIncomingFiles(ipn, filesStr) { + const files = JSON.parse(filesStr) + + if (DEBUG) { + console.log("Files: " + JSON.stringify(files, null, 2)) + } + + for (const file of files) { + handleFile(file) + } +} |
