瀏覽代碼

lazy load go wasm module only when logging in

To avoid waiting too much for the go module download and initialization,
and being considered an unwanted popup

Issue: the login window is blocked as a popup
Yuri Iozzelli 2 年之前
父節點
當前提交
d0e3852b59
共有 6 個文件被更改,包括 90 次插入263 次删除
  1. 22 3
      index.html
  2. 12 0
      login.html
  3. 二進制
      tun/tailscale.wasm
  4. 53 48
      tun/tailscale_tun.js
  5. 3 2
      tun/tailscale_tun_auto.js
  6. 0 210
      tun/tailscale_tun_ui.js

+ 22 - 3
index.html

@@ -43,9 +43,17 @@
         import { State } from "/tun/tailscale_tun.js";
         import { autoConf } from "/tun/tailscale_tun_auto.js";
 
+        let resolveLogin = null;
+        let loginPromise = new Promise((f,r) => {
+            resolveLogin = f;
+        });
         const loginUrlCb = (url) => {
             const a = document.getElementById("loginLink");
             a.href = url;
+            a.target = "_blank";
+            const status = document.getElementById("networkStatus");
+            status.innerHTML = "Tailscale Login";
+            resolveLogin(url);
         };
         const stateUpdateCb = (state) => {
             switch(state)
@@ -79,7 +87,7 @@
             const status = document.getElementById("networkStatus");
             status.innerHTML = "Ip: "+ip;
         };
-        const { listen, connect, bind, parseIP } = await autoConf({
+        const { listen, connect, bind, up } = await autoConf({
             loginUrlCb,
             stateUpdateCb,
             netmapUpdateCb,
@@ -87,7 +95,18 @@
         window.networkInterface.bind = bind;
         window.networkInterface.connect = connect;
         window.networkInterface.listen = listen;
-        window.parseIP = parseIP;
+        window.startTailscaleAndGetLogin = async () => {
+            const a = document.getElementById("loginLink");
+            a.onclick = null;
+            const status = document.getElementById("networkStatus");
+            status.innerHTML = "Downloading network code...";
+            const w = window.open("login.html", "_blank");
+            await up();
+            w.document.body.innerHTML = "Starting login...";
+            status.innerHTML = "Starting login...";
+            const url = await loginPromise;
+            w.location.href = url;
+        };
     </script>
     <script src="./xterm/xterm.js"></script>
     <script src="./xterm/xterm-addon-fit.js"></script>
@@ -126,7 +145,7 @@
             </a>
           </li>
           <li style=" margin-right: 50px; height: 100%; display: flex; align-items: center;">
-            <a id="loginLink" href="#" target="_blank">
+            <a id="loginLink" href="#" onclick="startTailscaleAndGetLogin()">
               <div id="networkStatus" style="color: white; font-family: montserrat; font-weight: 700; font-size: large;">Tailscale Login</div>
             </a>
           </li>

+ 12 - 0
login.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Tailscale login</title>
+</head>
+<body>
+    Loading network code...
+</body>
+</html>

二進制
tun/tailscale.wasm


+ 53 - 48
tun/tailscale_tun.js

@@ -14,11 +14,7 @@ export const State = {
 
 export async function init() {
 	const {IpStack} = await ipStackAwait();
-
-	const wasmUrl = new URL("tailscale.wasm", import.meta.url);
-	const go = new window.Go();
-	let {instance} = await WebAssembly.instantiateStreaming(fetch(wasmUrl),go.importObject);
-	go.run(instance);
+	IpStack.init();
 
 	const listeners = {
 		onstateupdate: () => {},
@@ -26,61 +22,70 @@ export async function init() {
 		onloginurl: () => {},
 	}
 
-	const sessionStateStorage = {
-		setState(id, value) {
-		window.sessionStorage[`ipn-state-${id}`] = value
-		},
-		getState(id) {
-		return window.sessionStorage[`ipn-state-${id}`] || ""
-		},
-	}
+	let ipn = null;
+	let localIp = null;
+	let dnsIp = null;
 
-	const ipn = newIPN({
-		// Persist IPN state in sessionStorage in development, so that we don't need
-		// to re-authorize every time we reload the page.
-		stateStorage: sessionStateStorage,
-	});
+	const lazyRunIpn = async () => {
+		const wasmUrl = new URL("tailscale.wasm", import.meta.url);
+		const go = new window.Go();
+		let {instance} = await WebAssembly.instantiateStreaming(fetch(wasmUrl),go.importObject);
+		go.run(instance);
+
+		const sessionStateStorage = {
+			setState(id, value) {
+			window.sessionStorage[`ipn-state-${id}`] = value
+			},
+			getState(id) {
+			return window.sessionStorage[`ipn-state-${id}`] || ""
+			},
+		}
+		ipn = newIPN({
+			// Persist IPN state in sessionStorage in development, so that we don't need
+			// to re-authorize every time we reload the page.
+			stateStorage: sessionStateStorage,
+		});
 
-	const setupIpStack = () => {
-		ipn.tun.onmessage = function(ev) {
-		console.log("received on tun:", ev.data)
-		IpStack.input(ev.data)
+		const setupIpStack = () => {
+			ipn.tun.onmessage = function(ev) {
+				IpStack.input(ev.data)
+			};
+			IpStack.output(function(p){
+				ipn.tun.postMessage(p);
+			});
 		};
-		IpStack.output(function(p){
-		console.log("sending from tun:", p)
-		ipn.tun.postMessage(p);
+		setupIpStack();
+
+		ipn.run({
+			notifyState: (s) => listeners.onstateupdate(s),
+			notifyNetMap: (s) => {
+				const netMap = JSON.parse(s);
+				listeners.onnetmap(netMap);
+				const newLocalIp = netMap.self.addresses[0];
+				if (localIp != newLocalIp)
+				{
+					localIp = newLocalIp;
+					IpStack.up({localIp, ipMap: {
+						["127.0.0.53"]: dnsIp,
+						[dnsIp]: "127.0.0.53",
+					}});
+				}
+			},
+			notifyBrowseToURL: (l) => listeners.onloginurl(l),
 		});
-		IpStack.init();
-	};
-	setupIpStack();
 
-	let localIp = null;
-	let dnsIp = null;
+	};
 
-	ipn.run({
-		notifyState: (s) => listeners.onstateupdate(s),
-		notifyNetMap: (s) => {
-			const netMap = JSON.parse(s);
-			listeners.onnetmap(netMap);
-			const newLocalIp = netMap.self.addresses[0];
-			if (localIp != newLocalIp)
-			{
-				localIp = newLocalIp;
-				IpStack.up({localIp, ipMap: {
-					["127.0.0.53"]: dnsIp,
-					[dnsIp]: "127.0.0.53",
-				}});
-			}
-		},
-		notifyBrowseToURL: (l) => listeners.onloginurl(l),
-	});
 
 	return {
 		connect: IpStack.connect,
 		listen: IpStack.listen,
 		bind: IpStack.bind,
 		parseIP: IpStack.parseIP,
-		up: (conf) => {
+		up: async (conf) => {
+			if (ipn == null) {
+				await lazyRunIpn();
+			}
 			ipn.up(conf);
 			localIp = null;
 			dnsIp = conf.dnsIp || "127.0.0.53";

+ 3 - 2
tun/tailscale_tun_auto.js

@@ -70,13 +70,14 @@ export async function autoConf({loginUrlCb, stateUpdateCb, netmapUpdateCb}) {
 		}
 	};
 
-	up(settings);
-
 	return {
 		bind,
 		connect,
 		listen,
 		parseIP,
+		up: async () => {
+			await up(settings);
+		},
 	}
 }
 

+ 0 - 210
tun/tailscale_tun_ui.js

@@ -1,210 +0,0 @@
-const State = {
-	NoState: 0,
-	InUseOtherUser: 1,
-	NeedsLogin: 2,
-	NeedsMachineAuth: 3,
-	Stopped: 4,
-	Starting: 5,
-	Running: 6,
-};
-export const createUi = (parent, {upCb,downCb,loginCb,logoutCb}) => {
-	const html = `
-<div id="networkModalOverlay" style="width:100%;height:100vh;position:absolute;display:none ;align-items:center;justify-content:center;background:rgba(0,0,0,0.7);color:black;z-index:100">
-	<div id="networkModal" style="max-width:650px;width:100%;background:white;height:400px;display:flex;flex-direction:row;padding:10px;justify-content:space-around">
-
-        <div class="networkModalLeft">
-			<h2>Network Configuration</h2>
-			<form id="networkModalForm">
-				<label for="controlUrl">Control URL: </label>
-				<input type="text" id="controlUrl" name="controlUrl"><br><br>
-				<label for="exitNode">Exit Node: </label>
-				<input type="text" id="exitNode" name="exitNode"><br><br>
-				<label for="dns">DNS Server: </label>
-				<input type="text" id="dns" name="dns"><br><br>
-				<button type="submit">Save</button>
-			</form>
-			<h2>Network Status</h2>
-			<div id="networkModalState">Disconnected</div>
-			<div id="networkModalAction"></div>
-		</div>
-
-		<div class="networkModalRight">
-			<h2>Peers</h2>
-			<div id="networkModalPeers"></div>
-		</div>
-	</div>
-</div>
-`;
-	const templ = document.createElement("template");
-	templ.innerHTML = html;
-	parent.prepend(templ.content);
-
-	const overlay = parent.querySelector("#networkModalOverlay");
-	const form = parent.querySelector("#networkModalForm");
-	const stateDiv = parent.querySelector("#networkModalState");
-	const actionDiv = parent.querySelector("#networkModalAction");
-	const peersDiv = parent.querySelector("#networkModalPeers");
-
-	const getSettings = () => {
-		const str = window.localStorage["networkSettings"] || "{}";
-		const v = JSON.parse(str);
-		return v;
-	};
-	const setSetting = (settings) => {
-		for (const k of Object.keys(settings))
-		{
-			if (settings[k] === "")
-				settings[k] = undefined;
-		}
-		window.localStorage["networkSettings"] = JSON.stringify(settings);
-	}
-	const populate = () => {
-		const settings = getSettings();
-		form.querySelector("#controlUrl").value = settings.controlUrl || "";
-		form.querySelector("#exitNode").value = settings.exitNodeIp || "";
-		form.querySelector("#dns").value = settings.dnsIp || "";
-	};
-	populate();
-
-	const showModal = () => {
-		overlay.style.display = "flex";
-	};
-	const hideModal = () => {
-		overlay.style.display = "none";
-	};
-	overlay.onclick = (e) => {
-		if (e.target === e.currentTarget)
-			hideModal();
-	};
-
-	form.onsubmit = (e) => {
-		e.preventDefault();
-		const settings = {
-			controlUrl: form.elements["controlUrl"].value,
-			exitNodeIp: form.elements["exitNode"].value,
-			dnsIp: form.elements["dns"].value,
-		};
-		setSetting(settings);
-	};
-
-	const updateState = (state) => {
-		switch(state)
-		{
-			case State.NeedsLogin:
-			{
-				loginCb();
-				break;
-			}
-			case State.Running:
-			{
-				const settings = getSettings();
-				settings.wantsRunning = true;
-				setSetting(settings);
-
-				stateDiv.innerHTML = "Running";
-				const action = document.createElement("button");
-				action.textContent = "Stop";
-				action.onclick = () => {
-					downCb();
-					action.disabled = true;
-				}
-				actionDiv.innerHTML = "";
-				actionDiv.appendChild(action);
-				break;
-			}
-			case State.Starting:
-			{
-				stateDiv.innerHTML = "Starting";
-				actionDiv.innerHTML = "";
-				break;
-			}
-			case State.Stopped:
-			{
-				const settings = getSettings();
-				settings.wantsRunning = false;
-				setSetting(settings);
-
-				stateDiv.innerHTML = "Stopped";
-				const actionLogout = document.createElement("button");
-				const actionStart = document.createElement("button");
-				actionStart.textContent = "Start";
-				actionStart.onclick = () => {
-					const settings = getSettings();
-					upCb(settings);
-					actionStart.disabled = true;
-					actionLogout.disabled = true;
-				}
-				actionLogout.textContent = "Logout";
-				actionLogout.onclick = () => {
-					logoutCb();
-					actionStart.disabled = true;
-					actionLogout.disabled = true;
-				}
-				actionDiv.innerHTML = "";
-				actionDiv.appendChild(actionStart);
-				actionDiv.appendChild(actionLogout);
-				break;
-			}
-			case State.NoState:
-			{
-				stateDiv.innerHTML = "Not Started";
-				const action = document.createElement("button");
-				action.textContent = "Start";
-				action.onclick = () => {
-					const settings = getSettings();
-					upCb(settings);
-					action.disabled = true;
-				}
-				actionDiv.innerHTML = "";
-				actionDiv.appendChild(action);
-				setTimeout(()=> {
-					const settings = getSettings();
-					if (settings.wantsRunning)
-					{
-						upCb(settings);
-						action.disabled = true;
-						return;
-					}
-				},0);
-				break;
-			}
-			default:
-			{
-				console.log(state);
-				stateDiv.innerHTML = "Loading";
-				actionDiv.innerHTML = "";
-				break;
-			}
-		}
-	};
-
-	const setLoginUrl = (login) => {
-		console.log("login url:",login);
-		stateDiv.innerHTML = "Need Login";
-		const action = document.createElement("button");
-		action.textContent = "Login";
-		action.onclick = () => {
-			window.open(login, "_blank");
-		}
-		actionDiv.innerHTML = "";
-		actionDiv.appendChild(action);
-	};
-
-	const updatePeers = (map) => {
-		const myIP = map.self.addresses[0];
-		let peers = `self -> ${myIP}<br/>`;
-		for (let p of map.peers) {
-			peers = `${peers}${p.name.split(".")[0]} -> ${p.addresses[0]}<br/>`;
-		}
-		peersDiv.innerHTML = peers;
-	};
-
-	return {
-		showModal,
-		updateState,
-		updatePeers,
-		setLoginUrl,
-		getSettings,
-	}
-}
-