summaryrefslogtreecommitdiffhomepage
path: root/ssh/browser/src
diff options
context:
space:
mode:
authorMihai Parparita <mihai@tailscale.com>2022-06-08 22:55:55 -0700
committerMihai Parparita <mihai@tailscale.com>2022-06-08 22:55:55 -0700
commit2cbcdc4ba8bae7704b11089993e831161dc64ce2 (patch)
tree550f07d21fb9f0ffa20c114070121235cb116df0 /ssh/browser/src
parentdecc3ee30d78a68575a74ffced766c5bead8ea08 (diff)
downloadtailscale-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.js18
-rw-r--r--ssh/browser/src/fs.js101
-rw-r--r--ssh/browser/src/index.css13
-rw-r--r--ssh/browser/src/index.js28
-rw-r--r--ssh/browser/src/notifier.js13
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)
+ }
+}