blob: d8eb744baf66c444517ec35b425571396851307a [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* globals buildcfg */
"use strict";
// connection state machine
var state_ = "idle";
// for attempting reconnections after resume
var retry_timer_ = undefined;
// state of our nacl process, null if stopped
var nacl_ = null;
// does this app own the system VPN from shill's perspective?
var vpnOwned_ = false;
// user's VPN configuration (URL, username, etc.)
var config_;
function sha256(der) {
return crypto.subtle.digest("SHA-256", der).then(function(digest) {
var a = new Uint8Array(digest);
var ret = "";
for (var i = 0; i < a.length; i++) {
if (a[i] < 0x10)
ret += "0";
ret += a[i].toString(16);
}
return ret;
});
}
function startNative() {
if (nacl_ !== null)
return;
var element = document.createElement('embed');
if (buildcfg.portable) {
element.setAttribute("type", "application/x-pnacl");
element.setAttribute("src", "portable.nmf");
} else {
element.setAttribute("type", "application/x-nacl");
element.setAttribute("src", "native.nmf");
}
element.setAttribute("width", 0);
element.setAttribute("height", 0);
element.addEventListener('load', function() {
console.debug("<-nacl loaded");
});
element.addEventListener('crash', function() {
console.debug("<-nacl crashed");
setConnectionState("failure");
stopNative();
});
element.addEventListener('message', handleMessage);
document.body.appendChild(element);
nacl_ = {"element": element};
}
function stopNative() {
if (nacl_ !== null) {
document.body.removeChild(nacl_.element);
console.log("terminating NaCl module");
}
nacl_ = null;
}
function postMessage(m) {
if (nacl_ === null) {
console.log("tried to post '" + m.cmd + "' while nacl is down");
return;
}
nacl_.element.postMessage(m);
}
function setConnectionState(newState) {
// notifyConnectionStateChanged throws an exception if our extension ID
// isn't permitted to manipulate the VPN interface
if (vpnOwned_ !== true)
return;
chrome.vpnProvider.notifyConnectionStateChanged(newState, function() {
console.debug("->notifyConnectionStateChanged: " + newState);
});
}
function handleConnectDisconnect(d) {
if (d.data === "connected") {
advanceStateMachine("nacl_connected");
} else if (d.data === "disconnected") {
advanceStateMachine("nacl_disconnected");
} else if (d.data === "reconnected") {
sendIpParameters();
} else {
console.error("unknown state: " + d.data);
}
}
function sendIpParameters() {
if (!nacl_.ipParams)
return;
var log_cb = function() { console.debug("->setParameters"); };
try {
chrome.vpnProvider.setParameters(nacl_.ipParams, log_cb);
} catch (e) {
// backward compatibility for <= Chrome M50
console.debug("Enabling M50 compatibility");
delete nacl_.ipParams.reconnect;
chrome.vpnProvider.setParameters(nacl_.ipParams, log_cb);
}
}
function handleIpParameters(d) {
nacl_.ipParams = {
"address": d.ipv4_addr + "/32",
"mtu": d.mtu.toString(),
"exclusionList": [d.gateway_ips + "/32"],
"inclusionList": ["0.0.0.0/0"],
"dnsServers": d.dns,
"reconnect": "true"
};
sendIpParameters();
}
function handleCertValidation(d) {
chrome.platformKeys.verifyTLSServerCertificate({
"serverCertificateChain": d.cert_chain,
"hostname": d.hostname
}, function(result) {
var res = "reject";
if (result.trusted) {
res = "accept";
console.debug("platformKeys: cert passed");
} else {
console.error("cert failed:");
console.error(result.debug_errors);
}
postMessage({"cmd": "peer_cert", "result": res});
});
}
function handleAuthForm(d) {
var opts = {};
for (var i = 0; i < d.opts.length; i++) {
var opt = d.opts[i];
if (opt.name === "username") {
if (!nacl_.usernameSent) {
opts.username = config_.username;
nacl_.usernameSent = true;
}
}
if (opt.name === "password") {
if (!nacl_.passwordSent) {
opts.password = config_.password;
nacl_.passwordSent = true;
}
}
}
if (Object.keys(opts).length > 0) {
postMessage({"cmd": "auth_form", "opts": opts, "result": "submit"});
} else {
console.error("don't know how to handle this auth form:");
console.error(d);
postMessage({"cmd": "auth_form", "result": "cancel"});
}
}
function handleCrypto(d) {
chrome.platformKeys.selectClientCertificates({
interactive: false,
request: {
certificateTypes: [ "rsaSign" ],
certificateAuthorities: []
}
}, function(certlist) {
if (certlist.length === 0) {
postMessage({"cmd": d.cmd, "success": false});
return;
}
for (var i = 0; i < certlist.length; i++) {
testOneCert(d, certlist[i].certificate);
}
});
}
function testOneCert(d, cert) {
sha256(cert).then(function(digest) {
if (config_.clientCert === digest) {
console.debug("testOneCert: found " + digest);
if (d.cmd === "crypto-getcert") {
postMessage({"cmd": d.cmd, "data": cert});
return;
} else {
handlePrivkeySign(d, cert);
}
}
});
}
function handlePrivkeySign(d, cert) {
var keyParams = {
hash: {
name: "none"
},
name: "RSASSA-PKCS1-v1_5"
};
chrome.platformKeys.getKeyPair(cert, keyParams,
function(pubKey, privKey) {
if (privKey === null) {
console.error("no private key for " + d.hash);
postMessage({"cmd": d.cmd, "success": false});
return;
}
if (d.cmd === "crypto-getprivkey") {
postMessage({
"cmd": d.cmd,
"success": true,
"privkey_type": privKey.algorithm.name
});
return;
}
// else: d.cmd === "crypto-sign"
var sc = chrome.platformKeys.subtleCrypto();
var future = sc.sign(privKey.algorithm, privKey, new Uint8Array(d.data));
future.then(function(result) {
postMessage({"cmd": d.cmd, "data": result});
}, function() {
console.error("crypto-sign failed");
postMessage({"cmd": d.cmd, /* "data" is null */});
});
});
}
function handleMessage(m) {
var d = m.data;
if (d instanceof ArrayBuffer) {
if (vpnOwned_ === true)
chrome.vpnProvider.sendPacket(d);
} else if (d instanceof Object) {
var cmd = d.cmd;
if (cmd === "status") {
handleConnectDisconnect(d);
} else if (cmd === "ip") {
handleIpParameters(d);
} else if (cmd === "peer_cert") {
handleCertValidation(d);
} else if (cmd === "auth_form") {
handleAuthForm(d);
} else if (cmd.startsWith("crypto-")) {
handleCrypto(d);
} else if (cmd === "abort") {
advanceStateMachine("nacl_disconnected");
} else {
console.debug(d);
}
}
}
function getConnection(name, f) {
chrome.storage.local.get("connection", function(val) {
f(val.connection);
});
}
function lookupConfig(id, callback) {
getConnection(id, function(conn) {
if (conn === undefined) {
setConnectionState("failure");
console.error("can't find VPN " + id);
openSettings();
} else {
config_ = conn;
callback();
}
});
}
function doConnect() {
startNative();
var msg = {
"cmd": "connect",
"url": config_.url
};
if (config_.clientCert !== undefined) {
msg.client_cert = "app:" + config_.clientCert;
}
postMessage(msg);
}
function clearRetryTimer() {
if (retry_timer_)
clearTimeout(retry_timer_);
retry_timer_ = undefined;
}
function badTransition(message) {
console.error("bad state transition: state=" + state_,
"platformMessage=" + message);
setConnectionState("failure");
}
function advanceStateMachine(message) {
console.debug("advanceStateMachine: message=" + message,
"oldState=" + state_);
if (state_ === "idle") {
if (message === "connected") {
vpnOwned_ = true;
doConnect();
state_ = "active";
} else if (message === "disconnected") {
// ignore
} else {
badTransition(message);
}
return;
}
// The system has forcibly disconnected us
if (message === "disconnected") {
// FIXME: do a clean nacl shutdown
clearRetryTimer();
vpnOwned_ = false;
stopNative();
state_ = "idle";
return;
}
if (state_ === "active") {
if (message === "linkChanged") {
postMessage({"cmd": "reconnect"});
} else if (message === "linkDown") {
postMessage({"cmd": "pause"});
state_ = "paused";
} else if (message === "suspend") {
stopNative();
state_ = "suspended";
} else if (message === "nacl_connected") {
setConnectionState("connected");
} else if (message === "nacl_disconnected") {
setConnectionState("failure");
vpnOwned_ = false;
stopNative();
state_ = "idle";
} else {
badTransition(message);
}
} else if (state_ === "suspended") {
if (message === "resume") {
state_ = "reconnecting";
doConnect();
} else if (message === "linkDown") {
state_ = "waiting";
} else if (message === "linkChanged") {
// no change - keep waiting for resume
} else {
badTransition(message);
}
} else if (state_ === "reconnecting") {
if (message === "nacl_connected") {
state_ = "active";
} else if (message === "linkDown") {
clearRetryTimer();
stopNative();
state_ = "waiting";
} else if (message === "suspend") {
clearRetryTimer();
stopNative();
state_ = "suspended";
} else if (message === "linkChanged") {
clearRetryTimer();
stopNative();
retry_timer_ = setTimeout(function() {
advanceStateMachine("retry");
}, 500);
} else if (message === "nacl_disconnected") {
clearRetryTimer();
stopNative();
retry_timer_ = setTimeout(function() {
advanceStateMachine("retry");
}, 2000);
} else if (message === "retry") {
doConnect();
} else {
badTransition(message);
}
} else if (state_ === "waiting") {
if (message === "linkUp") {
state_ = "reconnecting";
doConnect();
} else if (message === "resume") {
// no change - link is still down
} else if (message === "linkDown") {
// HACK: work around duplicate messages
} else {
badTransition(message);
}
} else if (state_ === "paused") {
if (message === "linkUp") {
postMessage({"cmd": "resume"});
state_ = "active";
} else if (message === "suspend") {
state_ = "waiting";
stopNative();
} else {
badTransition(message);
}
}
}
chrome.vpnProvider.onPlatformMessage.addListener(function(id, message, error) {
console.debug("<-onPlatformMessage", "id=" + id, "message=" + message, "error=" + error);
if (message === "connected") {
lookupConfig(id, function() { advanceStateMachine("connected"); });
} else {
advanceStateMachine(message);
}
});
function openSettings() {
chrome.app.window.create('settings.html', {
'outerBounds': {
'width': 400,
'height': 500
}
}, function(createdWindow) {
createdWindow.contentWindow.reloadConfig = reloadConfig;
createdWindow.contentWindow.sha256 = sha256;
});
}
chrome.vpnProvider.onUIEvent.addListener(openSettings);
chrome.vpnProvider.onPacketReceived.addListener(function(data) {
postMessage(data);
});
function reloadConfig() {
getConnection("Default", function(conn) {
if (conn !== undefined) {
chrome.vpnProvider.createConfig("Default", function() {
if (chrome.runtime.lastError) {
// ignore - this just means we're re-registering the same
// VPN name
}
});
}
});
}
reloadConfig();
chrome.app.runtime.onLaunched.addListener(openSettings);
chrome.runtime.onInstalled.addListener(function() {
// force the pexe to be compiled now, so the user doesn't have to wait
// a long time for his first connection to start
startNative();
reloadConfig();
});