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
This commit is contained in:
Yuri Iozzelli 2022-09-30 12:20:27 +02:00 committed by Carlo Piovesan
parent d6df3009d8
commit d0e3852b59
6 changed files with 93 additions and 266 deletions

View file

@ -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
login.html Normal file
View file

@ -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>

Binary file not shown.

View file

@ -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}`] || ""
},
}
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 setupIpStack = () => {
ipn.tun.onmessage = function(ev) {
console.log("received on tun:", ev.data)
IpStack.input(ev.data)
};
IpStack.output(function(p){
console.log("sending from tun:", p)
ipn.tun.postMessage(p);
});
IpStack.init();
};
setupIpStack();
let ipn = null;
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),
});
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) {
IpStack.input(ev.data)
};
IpStack.output(function(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),
});
};
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";

View file

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

View file

@ -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,
}
}