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:
parent
d6df3009d8
commit
d0e3852b59
6 changed files with 93 additions and 266 deletions
25
index.html
25
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
login.html
Normal file
12
login.html
Normal 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.
|
@ -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";
|
||||
|
|
|
@ -70,13 +70,14 @@ export async function autoConf({loginUrlCb, stateUpdateCb, netmapUpdateCb}) {
|
|||
}
|
||||
};
|
||||
|
||||
up(settings);
|
||||
|
||||
return {
|
||||
bind,
|
||||
connect,
|
||||
listen,
|
||||
parseIP,
|
||||
up: async () => {
|
||||
await up(settings);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue