1490 lines
40 KiB
JavaScript
1490 lines
40 KiB
JavaScript
/* global log, dbg, snowflake */
|
|
|
|
/**
|
|
Communication with the snowflake broker.
|
|
|
|
Browser snowflakes must register with the broker in order
|
|
to get assigned to clients.
|
|
*/
|
|
|
|
// Represents a broker running remotely.
|
|
class Broker {
|
|
|
|
/**
|
|
* When interacting with the Broker, snowflake must generate a unique session
|
|
* ID so the Broker can keep track of each proxy's signalling channels.
|
|
* On construction, this Broker object does not do anything until
|
|
* `getClientOffer` is called.
|
|
* @param {Config} config
|
|
*/
|
|
constructor(config) {
|
|
this.getClientOffer = this.getClientOffer.bind(this);
|
|
this._postRequest = this._postRequest.bind(this);
|
|
this.setNATType = this.setNATType.bind(this);
|
|
|
|
this.config = config;
|
|
this.url = config.brokerUrl;
|
|
this.natType = "unknown";
|
|
if (0 === this.url.indexOf('localhost', 0)) {
|
|
// Ensure url has the right protocol + trailing slash.
|
|
this.url = 'http://' + this.url;
|
|
}
|
|
if (0 !== this.url.indexOf('http', 0)) {
|
|
this.url = 'https://' + this.url;
|
|
}
|
|
if ('/' !== this.url.substr(-1)) {
|
|
this.url += '/';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Promises some client SDP Offer.
|
|
* Registers this Snowflake with the broker using an HTTP POST request, and
|
|
* waits for a response containing some client offer that the Broker chooses
|
|
* for this proxy..
|
|
* TODO: Actually support multiple clients.
|
|
*/
|
|
getClientOffer(id, numClientsConnected) {
|
|
return new Promise((fulfill, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.DONE !== xhr.readyState) {
|
|
return;
|
|
}
|
|
switch (xhr.status) {
|
|
case Broker.CODE.OK: {
|
|
const response = JSON.parse(xhr.responseText);
|
|
if (response.Status == Broker.STATUS.MATCH) {
|
|
return fulfill(response); // Should contain offer.
|
|
} else if (response.Status == Broker.STATUS.TIMEOUT) {
|
|
return reject(Broker.MESSAGE.TIMEOUT);
|
|
} else {
|
|
log('Broker ERROR: Unexpected ' + response.Status);
|
|
return reject(Broker.MESSAGE.UNEXPECTED);
|
|
}
|
|
}
|
|
default:
|
|
log('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText);
|
|
snowflake.ui.setStatus(' failure. Please refresh.');
|
|
return reject(Broker.MESSAGE.UNEXPECTED);
|
|
}
|
|
};
|
|
this._xhr = xhr; // Used by spec to fake async Broker interaction
|
|
const clients = Math.floor(numClientsConnected / 8) * 8;
|
|
const data = {
|
|
Version: "1.3",
|
|
Sid: id,
|
|
Type: this.config.proxyType,
|
|
NAT: this.natType,
|
|
Clients: clients,
|
|
AcceptedRelayPattern: this.config.allowedRelayPattern,
|
|
};
|
|
this._postRequest(xhr, 'proxy', JSON.stringify(data));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Assumes getClientOffer happened, and a WebRTC SDP answer has been generated.
|
|
* Sends it back to the broker, which passes it to back to the original client.
|
|
* @param {string} id
|
|
* @param {RTCSessionDescription} answer
|
|
*/
|
|
sendAnswer(id, answer) {
|
|
dbg(id + ' - Sending answer back to broker...\n');
|
|
dbg(answer.sdp);
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.DONE !== xhr.readyState) {
|
|
return;
|
|
}
|
|
switch (xhr.status) {
|
|
case Broker.CODE.OK:
|
|
dbg('Broker: Successfully replied with answer.');
|
|
dbg(xhr.responseText);
|
|
break;
|
|
default:
|
|
dbg('Broker ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText);
|
|
snowflake.ui.setStatus(' failure. Please refresh.');
|
|
break;
|
|
}
|
|
};
|
|
const data = {"Version": "1.0", "Sid": id, "Answer": JSON.stringify(answer)};
|
|
this._postRequest(xhr, 'answer', JSON.stringify(data));
|
|
}
|
|
|
|
setNATType(natType) {
|
|
this.natType = natType;
|
|
}
|
|
|
|
/**
|
|
* @param {XMLHttpRequest} xhr
|
|
* @param {string} urlSuffix for the broker is different depending on what action
|
|
* is desired.
|
|
* @param {string} payload
|
|
*/
|
|
_postRequest(xhr, urlSuffix, payload) {
|
|
try {
|
|
xhr.open('POST', this.url + urlSuffix);
|
|
} catch (err) {
|
|
/*
|
|
An exception happens here when, for example, NoScript allows the domain
|
|
on which the proxy badge runs, but not the domain to which it's trying
|
|
to make the HTTP xhr. The exception message is like "Component
|
|
returned failure code: 0x805e0006 [nsIXMLHttpRequest.open]" on Firefox.
|
|
*/
|
|
log('Broker: exception while connecting: ' + err.message);
|
|
return;
|
|
}
|
|
xhr.send(payload);
|
|
}
|
|
|
|
}
|
|
|
|
Broker.CODE = {
|
|
OK: 200,
|
|
BAD_REQUEST: 400,
|
|
INTERNAL_SERVER_ERROR: 500
|
|
};
|
|
|
|
Broker.STATUS = {
|
|
MATCH: "client match",
|
|
TIMEOUT: "no match"
|
|
};
|
|
|
|
Broker.MESSAGE = {
|
|
TIMEOUT: 'Timed out waiting for a client offer.',
|
|
UNEXPECTED: 'Unexpected status.'
|
|
};
|
|
|
|
|
|
class Config {
|
|
constructor(proxyType) {
|
|
this.proxyType = proxyType || '';
|
|
}
|
|
}
|
|
|
|
Config.prototype.brokerUrl = 'snowflake-broker.freehaven.net';
|
|
|
|
Config.prototype.relayAddr = {
|
|
host: 'snowflake.freehaven.net',
|
|
port: '443'
|
|
};
|
|
|
|
// Original non-wss relay:
|
|
// host: '192.81.135.242'
|
|
// port: 9902
|
|
Config.prototype.cookieName = "snowflake-allow";
|
|
|
|
// Bytes per second. Set to undefined to disable limit.
|
|
Config.prototype.rateLimitBytes = undefined;
|
|
|
|
Config.prototype.minRateLimit = 10 * 1024;
|
|
|
|
Config.prototype.rateLimitHistory = 5.0;
|
|
|
|
Config.prototype.defaultBrokerPollInterval = 60.0 * 1000; //1 poll every minutes
|
|
Config.prototype.slowestBrokerPollInterval = 6 * 60 * 60.0 * 1000; //1 poll every 6 hours
|
|
Config.prototype.pollAdjustment = 100.0 * 1000;
|
|
Config.prototype.fastBrokerPollInterval = 30 * 1000; //1 poll every 30 seconds
|
|
|
|
// Recheck our NAT type once every 2 days
|
|
Config.prototype.natCheckInterval = 2 * 24 * 60 * 60 * 1000;
|
|
|
|
// Timeout after sending answer before datachannel is opened
|
|
Config.prototype.datachannelTimeout = 20 * 1000;
|
|
|
|
// Timeout to close proxypair if no messages are sent
|
|
Config.prototype.messageTimeout = 30 * 1000;
|
|
|
|
Config.prototype.maxNumClients = 1;
|
|
|
|
Config.prototype.proxyType = "";
|
|
|
|
// TODO: Different ICE servers.
|
|
Config.prototype.pcConfig = {
|
|
iceServers: [
|
|
{
|
|
urls: ['stun:stun.l.google.com:19302']
|
|
}
|
|
]
|
|
};
|
|
|
|
Config.PROBEURL = "https://snowflake-broker.freehaven.net:8443/probe";
|
|
|
|
Config.prototype.allowedRelayPattern="snowflake.torproject.net";
|
|
/* global snowflake, log, dbg, Util, Parse, WS */
|
|
|
|
/**
|
|
Represents a single:
|
|
|
|
client <-- webrtc --> snowflake <-- websocket --> relay
|
|
|
|
Every ProxyPair has a Snowflake ID, which is necessary when responding to the
|
|
Broker with an WebRTC answer.
|
|
*/
|
|
|
|
class ProxyPair {
|
|
|
|
/**
|
|
* @param relayAddr the destination relay
|
|
* @param {*} rateLimit specifies a rate limit on traffic
|
|
* @param {Config} config
|
|
*/
|
|
constructor(relayAddr, rateLimit, config) {
|
|
this.prepareDataChannel = this.prepareDataChannel.bind(this);
|
|
this.connectRelay = this.connectRelay.bind(this);
|
|
this.onClientToRelayMessage = this.onClientToRelayMessage.bind(this);
|
|
this.onRelayToClientMessage = this.onRelayToClientMessage.bind(this);
|
|
this.onError = this.onError.bind(this);
|
|
this.flush = this.flush.bind(this);
|
|
|
|
/** @type {string | undefined} */
|
|
this.relayURL = undefined;
|
|
this.relayAddr = relayAddr;
|
|
this.rateLimit = rateLimit;
|
|
this.config = config;
|
|
this.pcConfig = config.pcConfig;
|
|
this.id = Util.genSnowflakeID();
|
|
this.c2rSchedule = [];
|
|
this.r2cSchedule = [];
|
|
this.counted = false;
|
|
}
|
|
|
|
/** Prepare a WebRTC PeerConnection and await for an SDP offer. */
|
|
begin() {
|
|
this.pc = new RTCPeerConnection(this.pcConfig);
|
|
this.pc.onicecandidate = (evt) => {
|
|
// Browser sends a null candidate once the ICE gathering completes.
|
|
if (null === evt.candidate && this.pc.connectionState !== 'closed') {
|
|
dbg('Finished gathering ICE candidates.');
|
|
snowflake.broker.sendAnswer(this.id, this.pc.localDescription);
|
|
}
|
|
};
|
|
// OnDataChannel triggered remotely from the client when connection succeeds.
|
|
this.pc.ondatachannel = (dc) => {
|
|
const channel = dc.channel;
|
|
dbg('Data Channel established...');
|
|
this.prepareDataChannel(channel);
|
|
this.client = channel;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {RTCSessionDescription} offer
|
|
* @returns {boolean} `true` on success, `false` on fail.
|
|
*/
|
|
receiveWebRTCOffer(offer) {
|
|
if ('offer' !== offer.type) {
|
|
log('Invalid SDP received -- was not an offer.');
|
|
return false;
|
|
}
|
|
try {
|
|
this.pc.setRemoteDescription(offer);
|
|
} catch (error) {
|
|
log('Invalid SDP message.');
|
|
return false;
|
|
}
|
|
dbg('SDP ' + offer.type + ' successfully received.');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Given a WebRTC DataChannel, prepare callbacks.
|
|
* @param {RTCDataChannel} channel
|
|
*/
|
|
prepareDataChannel(channel) {
|
|
channel.onopen = () => {
|
|
log('WebRTC DataChannel opened!');
|
|
snowflake.ui.increaseClients();
|
|
this.counted = true;
|
|
// This is the point when the WebRTC datachannel is done, so the next step
|
|
// is to establish websocket to the server.
|
|
this.connectRelay();
|
|
};
|
|
channel.onclose = () => {
|
|
log('WebRTC DataChannel closed.');
|
|
snowflake.ui.setStatus('disconnected by webrtc.');
|
|
if (this.counted) {
|
|
snowflake.ui.decreaseClients();
|
|
this.counted = false;
|
|
}
|
|
this.flush();
|
|
this.close();
|
|
};
|
|
channel.onerror = function () {
|
|
log('Data channel error!');
|
|
};
|
|
channel.binaryType = "arraybuffer";
|
|
channel.onmessage = this.onClientToRelayMessage;
|
|
}
|
|
|
|
/** Assumes WebRTC datachannel is connected. */
|
|
connectRelay() {
|
|
dbg('Connecting to relay...');
|
|
// Get a remote IP address from the PeerConnection, if possible. Add it to
|
|
// the WebSocket URL's query string if available.
|
|
// MDN marks remoteDescription as "experimental". However the other two
|
|
// options, currentRemoteDescription and pendingRemoteDescription, which
|
|
// are not marked experimental, were undefined when I tried them in Firefox
|
|
// 52.2.0.
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDescription
|
|
const desc = this.pc.remoteDescription;
|
|
const peer_ip = Parse.ipFromSDP(desc!= null ? desc.sdp : undefined);
|
|
const params = [];
|
|
if (peer_ip != null) {
|
|
params.push(["client_ip", peer_ip]);
|
|
}
|
|
const relay = this.relay =
|
|
(this.relayURL === undefined) ?
|
|
WS.makeWebsocket(this.relayAddr, params) :
|
|
WS.makeWebsocketFromURL(this.relayURL, params);
|
|
this.relay.label = 'websocket-relay';
|
|
this.relay.onopen = () => {
|
|
clearTimeout(this.connectToRelayTimeoutId);
|
|
log(relay.label + ' connected!');
|
|
snowflake.ui.setStatus('connected');
|
|
};
|
|
this.relay.onclose = () => {
|
|
log(relay.label + ' closed.');
|
|
snowflake.ui.setStatus('disconnected.');
|
|
if (this.counted) {
|
|
snowflake.ui.decreaseClients();
|
|
this.counted = false;
|
|
}
|
|
this.flush();
|
|
this.close();
|
|
};
|
|
this.relay.onerror = this.onError;
|
|
this.relay.onmessage = this.onRelayToClientMessage;
|
|
// TODO: Better websocket timeout handling.
|
|
this.connectToRelayTimeoutId = setTimeout((() => {
|
|
log(relay.label + ' timed out connecting.');
|
|
relay.onclose();
|
|
}), 5000);
|
|
}
|
|
|
|
/**
|
|
* WebRTC --> websocket
|
|
* @param {MessageEvent} msg
|
|
*/
|
|
onClientToRelayMessage(msg) {
|
|
clearTimeout(this.messageTimer);
|
|
dbg('WebRTC --> websocket data: ' + msg.data.byteLength + ' bytes');
|
|
this.c2rSchedule.push(msg.data);
|
|
|
|
// if we don't receive any keep-alive messages from the client, close the
|
|
// connection
|
|
this.messageTimer = setTimeout((() => {
|
|
console.log("Closing stale connection.");
|
|
this.flush();
|
|
this.close();
|
|
}), this.config.messageTimeout);
|
|
this.flush();
|
|
}
|
|
|
|
/**
|
|
* websocket --> WebRTC
|
|
* @param {MessageEvent} event
|
|
*/
|
|
onRelayToClientMessage(event) {
|
|
dbg('websocket --> WebRTC data: ' + event.data.byteLength + ' bytes');
|
|
this.r2cSchedule.push(event.data);
|
|
this.flush();
|
|
}
|
|
|
|
onError(event) {
|
|
const ws = event.target;
|
|
log(ws.label + ' error.');
|
|
this.close();
|
|
}
|
|
|
|
/** Close both WebRTC and websocket. */
|
|
close() {
|
|
clearTimeout(this.connectToRelayTimeoutId);
|
|
clearTimeout(this.messageTimer);
|
|
if (this.webrtcIsReady()) {
|
|
this.client.close();
|
|
}
|
|
if (this.peerConnOpen()) {
|
|
this.pc.close();
|
|
}
|
|
if (this.relayIsReady()) {
|
|
this.relay.close();
|
|
}
|
|
this.onCleanup();
|
|
}
|
|
|
|
/** Send as much data in both directions as the rate limit currently allows. */
|
|
flush() {
|
|
if (this.flush_timeout_id) {
|
|
clearTimeout(this.flush_timeout_id);
|
|
}
|
|
this.flush_timeout_id = null;
|
|
let busy = true;
|
|
const checkChunks = () => {
|
|
busy = false;
|
|
// WebRTC --> websocket
|
|
if (this.relayIsReady() && this.relay.bufferedAmount < this.MAX_BUFFER && this.c2rSchedule.length > 0) {
|
|
const chunk = this.c2rSchedule.shift();
|
|
this.rateLimit.update(chunk.byteLength);
|
|
this.relay.send(chunk);
|
|
busy = true;
|
|
}
|
|
// websocket --> WebRTC
|
|
if (this.webrtcIsReady() && this.client.bufferedAmount < this.MAX_BUFFER && this.r2cSchedule.length > 0) {
|
|
const chunk = this.r2cSchedule.shift();
|
|
this.rateLimit.update(chunk.byteLength);
|
|
this.client.send(chunk);
|
|
busy = true;
|
|
}
|
|
};
|
|
while (busy && !this.rateLimit.isLimited()) {
|
|
checkChunks();
|
|
}
|
|
if (this.r2cSchedule.length > 0 || this.c2rSchedule.length > 0 || (this.relayIsReady() && this.relay.bufferedAmount > 0) || (this.webrtcIsReady() && this.client.bufferedAmount > 0)) {
|
|
this.flush_timeout_id = setTimeout(this.flush, this.rateLimit.when() * 1000);
|
|
}
|
|
}
|
|
|
|
webrtcIsReady() {
|
|
return null !== this.client && 'open' === this.client.readyState;
|
|
}
|
|
|
|
relayIsReady() {
|
|
return (null !== this.relay) && (WebSocket.OPEN === this.relay.readyState);
|
|
}
|
|
|
|
/**
|
|
* @param {WebSocket} ws
|
|
*/
|
|
isClosed(ws) {
|
|
return undefined === ws || WebSocket.CLOSED === ws.readyState;
|
|
}
|
|
|
|
peerConnOpen() {
|
|
return (null !== this.pc) && ('closed' !== this.pc.connectionState);
|
|
}
|
|
|
|
/**
|
|
* @param {typeof this.relayURL} relayURL
|
|
*/
|
|
setRelayURL(relayURL) {
|
|
this.relayURL = relayURL;
|
|
}
|
|
|
|
}
|
|
|
|
ProxyPair.prototype.MAX_BUFFER = 10 * 1024 * 1024;
|
|
|
|
ProxyPair.prototype.pc = null;
|
|
ProxyPair.prototype.client = null; // WebRTC Data channel
|
|
ProxyPair.prototype.relay = null; // websocket
|
|
|
|
ProxyPair.prototype.connectToRelayTimeoutId = 0;
|
|
ProxyPair.prototype.messageTimer = 0;
|
|
ProxyPair.prototype.flush_timeout_id = null;
|
|
|
|
ProxyPair.prototype.onCleanup = null;
|
|
/* global log, dbg, DummyRateLimit, BucketRateLimit, ProxyPair */
|
|
|
|
/**
|
|
A JavaScript WebRTC snowflake proxy
|
|
|
|
Uses WebRTC from the client, and Websocket to the server.
|
|
|
|
Assume that the webrtc client plugin is always the offerer, in which case
|
|
this proxy must always act as the answerer.
|
|
|
|
TODO: More documentation
|
|
*/
|
|
|
|
class Snowflake {
|
|
|
|
/**
|
|
* Prepare the Snowflake with a Broker (to find clients) and optional UI.
|
|
* @param {Config} config
|
|
* @param {WebExtUI | BadgeUI | DebugUI} ui
|
|
* @param {Broker} broker
|
|
*/
|
|
constructor(config, ui, broker) {
|
|
this.receiveOffer = this.receiveOffer.bind(this);
|
|
|
|
this.config = config;
|
|
this.ui = ui;
|
|
this.broker = broker;
|
|
this.broker.setNATType(ui.natType);
|
|
this.proxyPairs = [];
|
|
this.natFailures = 0;
|
|
this.pollInterval = this.config.defaultBrokerPollInterval;
|
|
if (undefined === this.config.rateLimitBytes) {
|
|
this.rateLimit = new DummyRateLimit();
|
|
} else {
|
|
this.rateLimit = new BucketRateLimit(this.config.rateLimitBytes * this.config.rateLimitHistory, this.config.rateLimitHistory);
|
|
}
|
|
this.retries = 0;
|
|
}
|
|
|
|
/**
|
|
* Set the target relay address spec, which is expected to be websocket.
|
|
* TODO: Should potentially fetch the target from broker later, or modify
|
|
* entirely for the Tor-independent version.
|
|
* @param {{ host: string; port: string; }} relayAddr
|
|
*/
|
|
setRelayAddr(relayAddr) {
|
|
this.relayAddr = relayAddr;
|
|
log('Using ' + relayAddr.host + ':' + relayAddr.port + ' as Relay.');
|
|
}
|
|
|
|
/**
|
|
* Initialize WebRTC PeerConnection, which requires beginning the signalling
|
|
* process. `pollBroker` automatically arranges signalling.
|
|
*/
|
|
beginWebRTC() {
|
|
this.pollBroker();
|
|
this.pollTimeoutId = setTimeout((() => {
|
|
this.beginWebRTC();
|
|
}), this.pollInterval);
|
|
}
|
|
|
|
/**
|
|
* Regularly poll Broker for clients to serve until this snowflake is
|
|
* serving at capacity, at which point stop polling.
|
|
*/
|
|
pollBroker() {
|
|
// Poll broker for clients.
|
|
if (this.proxyPairs.length >= this.config.maxNumClients) {
|
|
log('At client capacity.');
|
|
return;
|
|
}
|
|
const pair = this.makeProxyPair();
|
|
log('Polling broker..');
|
|
// Do nothing until a new proxyPair is available.
|
|
let msg = 'Polling for client ... ';
|
|
if (this.retries > 0) {
|
|
msg += '[retries: ' + this.retries + ']';
|
|
}
|
|
this.ui.setStatus(msg);
|
|
//update NAT type
|
|
console.log("NAT type: " + this.ui.natType);
|
|
this.broker.setNATType(this.ui.natType);
|
|
const recv = this.broker.getClientOffer(pair.id, this.proxyPairs.length);
|
|
recv.then((resp) => {
|
|
const clientNAT = resp.NAT;
|
|
if (!this.receiveOffer(pair, resp.Offer, resp.RelayURL)) {
|
|
pair.close();
|
|
return;
|
|
}
|
|
//set a timeout for channel creation
|
|
setTimeout((() => {
|
|
if (!pair.webrtcIsReady()) {
|
|
log('proxypair datachannel timed out waiting for open');
|
|
pair.close();
|
|
// increase poll interval
|
|
this.pollInterval =
|
|
Math.min(this.pollInterval + this.config.pollAdjustment,
|
|
this.config.slowestBrokerPollInterval);
|
|
if (clientNAT == "restricted") {
|
|
this.natFailures++;
|
|
}
|
|
// if we fail to connect to a restricted client 3 times in
|
|
// a row, assume we have a restricted NAT
|
|
if (this.natFailures >= 3) {
|
|
this.ui.natType = "restricted";
|
|
console.log("Learned NAT type: restricted");
|
|
this.natFailures = 0;
|
|
this.config.maxNumClients = 1;
|
|
}
|
|
this.broker.setNATType(this.ui.natType);
|
|
} else {
|
|
// decrease poll interval
|
|
this.pollInterval =
|
|
Math.max(this.pollInterval - this.config.pollAdjustment,
|
|
this.config.defaultBrokerPollInterval);
|
|
this.natFailures = 0;
|
|
if (this.ui.natType == "unrestricted") {
|
|
this.pollInterval = this.config.fastBrokerPollInterval;
|
|
this.config.maxNumClients = 2;
|
|
}
|
|
}
|
|
}), this.config.datachannelTimeout);
|
|
}, function () {
|
|
//on error, close proxy pair
|
|
pair.close();
|
|
});
|
|
this.retries++;
|
|
}
|
|
|
|
/**
|
|
* Receive an SDP offer from some client assigned by the Broker
|
|
* @param {ProxyPair} pair an available ProxyPair.
|
|
* @param {string} desc
|
|
* @param {string | undefined} relayURL
|
|
* @returns {boolean} `true` on success, `false` on fail.
|
|
*/
|
|
receiveOffer(pair, desc, relayURL) {
|
|
try {
|
|
if (relayURL !== undefined) {
|
|
const relayURLParsed = new URL(relayURL);
|
|
const hostname = relayURLParsed.hostname;
|
|
const protocol = relayURLParsed.protocol;
|
|
if (protocol !== "wss:") {
|
|
log('incorrect relay url protocol');
|
|
return false;
|
|
}
|
|
if (!this.checkRelayPattern(this.config.allowedRelayPattern, hostname)) {
|
|
log('relay url hostname does not match allowed pattern');
|
|
return false;
|
|
}
|
|
pair.setRelayURL(relayURL);
|
|
}
|
|
/** @type {RTCSessionDescriptionInit} */
|
|
const offer = JSON.parse(desc);
|
|
dbg('Received:\n\n' + offer.sdp + '\n');
|
|
const sdp = new RTCSessionDescription(offer);
|
|
if (pair.receiveWebRTCOffer(sdp)) {
|
|
this.sendAnswer(pair);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
log('ERROR: Unable to receive Offer: ' + e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {ProxyPair} pair
|
|
*/
|
|
sendAnswer(pair) {
|
|
/** @param {RTCLocalSessionDescriptionInit} sdp */
|
|
const next = function (sdp) {
|
|
dbg('webrtc: Answer ready.');
|
|
pair.pc.setLocalDescription(sdp).catch(fail);
|
|
};
|
|
const fail = function () {
|
|
pair.close();
|
|
dbg('webrtc: Failed to create or set Answer');
|
|
};
|
|
pair.pc.createAnswer().then(next).catch(fail);
|
|
}
|
|
|
|
/**
|
|
* @returns {ProxyPair}
|
|
*/
|
|
makeProxyPair() {
|
|
const pair = new ProxyPair(this.relayAddr, this.rateLimit, this.config);
|
|
this.proxyPairs.push(pair);
|
|
|
|
log('Snowflake IDs: ' + (this.proxyPairs.map(p => p.id)).join(' | '));
|
|
|
|
pair.onCleanup = () => {
|
|
// Delete from the list of proxy pairs.
|
|
const ind = this.proxyPairs.indexOf(pair);
|
|
if (ind > -1) {
|
|
this.proxyPairs.splice(ind, 1);
|
|
}
|
|
};
|
|
pair.begin();
|
|
return pair;
|
|
}
|
|
|
|
/** Stop all proxypairs. */
|
|
disable() {
|
|
log('Disabling Snowflake.');
|
|
clearTimeout(this.pollTimeoutId);
|
|
while (this.proxyPairs.length > 0) {
|
|
this.proxyPairs.pop().close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* checkRelayPattern match str against patten
|
|
* @param {string} pattern
|
|
* @param {string} str typically a domain name to be checked
|
|
* @return {boolean}
|
|
*/
|
|
checkRelayPattern(pattern, str) {
|
|
if (typeof pattern !== "string") {
|
|
throw 'invalid checkRelayPattern input: pattern';
|
|
}
|
|
if (typeof str !== "string") {
|
|
throw 'invalid checkRelayPattern input: str';
|
|
}
|
|
|
|
let exactMatch = false;
|
|
if (pattern.charAt(0) === "^") {
|
|
exactMatch = true;
|
|
pattern = pattern.substring(1);
|
|
}
|
|
|
|
if (exactMatch) {
|
|
return pattern.localeCompare(str) === 0;
|
|
}
|
|
return str.endsWith(pattern);
|
|
}
|
|
|
|
}
|
|
|
|
Snowflake.prototype.relayAddr = null;
|
|
Snowflake.prototype.rateLimit = null;
|
|
|
|
Snowflake.MESSAGE = {
|
|
CONFIRMATION: 'You\'re currently serving a Tor user via Snowflake.'
|
|
};
|
|
/**
|
|
All of Snowflake's DOM manipulation and inputs.
|
|
*/
|
|
|
|
class UI {
|
|
|
|
constructor() {
|
|
this.initStats();
|
|
}
|
|
|
|
initStats() {
|
|
this.stats = [0];
|
|
setInterval((() => {
|
|
this.stats.unshift(0);
|
|
this.stats.splice(24);
|
|
this.postActive();
|
|
}), 60 * 60 * 1000);
|
|
}
|
|
|
|
setStatus() {}
|
|
|
|
get active() {
|
|
return this.clients > 0;
|
|
}
|
|
|
|
postActive() {}
|
|
|
|
increaseClients() {
|
|
this.clients += 1;
|
|
this.stats[0] += 1;
|
|
this.postActive();
|
|
return this.clients;
|
|
}
|
|
|
|
decreaseClients() {
|
|
this.clients -= 1;
|
|
if(this.clients < 0) {
|
|
this.clients = 0;
|
|
}
|
|
this.postActive();
|
|
return this.clients;
|
|
}
|
|
|
|
log() {}
|
|
|
|
}
|
|
|
|
UI.prototype.clients = 0;
|
|
UI.prototype.stats = null;
|
|
/* exported Util, Params, DummyRateLimit */
|
|
/* global Config */
|
|
|
|
/**
|
|
A JavaScript WebRTC snowflake proxy
|
|
|
|
Contains helpers for parsing query strings and other utilities.
|
|
*/
|
|
|
|
class Util {
|
|
|
|
static genSnowflakeID() {
|
|
return Math.random().toString(36).substring(2);
|
|
}
|
|
|
|
static hasWebRTC() {
|
|
return typeof RTCPeerConnection === 'function';
|
|
}
|
|
|
|
static hasCookies() {
|
|
return navigator.cookieEnabled;
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<"restricted" | "unrestricted">}
|
|
* resolves to "restricted" if we
|
|
* fail to make a test connection to a known restricted
|
|
* NAT, "unrestricted" if the test connection succeeds, and
|
|
* "unknown" if we fail to reach the probe test server
|
|
* @param {number} timeout
|
|
*/
|
|
static checkNATType(timeout) {
|
|
let pc = new RTCPeerConnection({iceServers: [
|
|
{urls: 'stun:stun1.l.google.com:19302'}
|
|
]});
|
|
let channel = pc.createDataChannel("NAT test");
|
|
return (new Promise((fulfill, reject) => {
|
|
let open = false;
|
|
channel.onopen = function() {
|
|
open = true;
|
|
fulfill("unrestricted");
|
|
};
|
|
pc.onicecandidate = (evt) => {
|
|
if (evt.candidate == null) {
|
|
//ice gathering is finished
|
|
Util.sendOffer(pc.localDescription)
|
|
.then((answer) => {
|
|
setTimeout(() => {
|
|
if(!open) {
|
|
fulfill("restricted");
|
|
}
|
|
}, timeout);
|
|
pc.setRemoteDescription(JSON.parse(answer));
|
|
}).catch((e) => {
|
|
console.log(e);
|
|
reject("Error receiving probetest answer");
|
|
});
|
|
}
|
|
};
|
|
pc.createOffer()
|
|
.then((offer) => pc.setLocalDescription(offer))
|
|
.catch((e) => {
|
|
console.log(e);
|
|
reject("Error creating offer for probetest");
|
|
});
|
|
}).finally(() => {
|
|
channel.close();
|
|
pc.close();
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Assumes getClientOffer happened, and a WebRTC SDP answer has been generated.
|
|
* Sends it back to the broker, which passes it back to the original client.
|
|
* @param {RTCSessionDescription} offer
|
|
*/
|
|
static sendOffer(offer) {
|
|
return new Promise((fulfill, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.timeout = 30 * 1000;
|
|
xhr.onreadystatechange = function() {
|
|
if (xhr.DONE !== xhr.readyState) {
|
|
return;
|
|
}
|
|
switch (xhr.status) {
|
|
case 200: {
|
|
const response = JSON.parse(xhr.responseText);
|
|
return fulfill(response.Answer); // Should contain offer.
|
|
}
|
|
default:
|
|
console.log('Probe ERROR: Unexpected ' + xhr.status + ' - ' + xhr.statusText);
|
|
return reject('Failed to get answer from probe service');
|
|
}
|
|
};
|
|
const data = {"Status": "client match", "Offer": JSON.stringify(offer)};
|
|
try {
|
|
xhr.open('POST', Config.PROBEURL);
|
|
} catch (error) {
|
|
console.log('Signaling Server: exception while connecting: ' + error.message);
|
|
return reject('unable to connect to signaling server');
|
|
}
|
|
xhr.send(JSON.stringify(data));
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
class Parse {
|
|
|
|
/**
|
|
* @param {typeof document.cookie} cookies
|
|
* Parse a cookie data string (usually document.cookie). The return type is an
|
|
* object mapping cookies names to values. Returns null on error.
|
|
* http://www.w3.org/TR/DOM-Level-2-HTML/html.html#ID-8747038
|
|
*/
|
|
static cookie(cookies) {
|
|
const result = {};
|
|
const strings = cookies ? cookies.split(';') : [];
|
|
for (let i = 0, len = strings.length; i < len; i++) {
|
|
const string = strings[i];
|
|
const j = string.indexOf('=');
|
|
if (-1 === j) {
|
|
return null;
|
|
}
|
|
const name = decodeURIComponent(string.substr(0, j).trim());
|
|
const value = decodeURIComponent(string.substr(j + 1).trim());
|
|
if (!(name in result)) {
|
|
result[name] = value;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @param {string} spec
|
|
* Parse an address in the form 'host:port'. Returns an Object with keys 'host'
|
|
* (String) and 'port' (int). Returns null on error.
|
|
*/
|
|
static address(spec) {
|
|
let m = null;
|
|
if (!m) {
|
|
// IPv6 syntax.
|
|
m = spec.match(/^\[([\0-9a-fA-F:.]+)\]:([0-9]+)$/);
|
|
}
|
|
if (!m) {
|
|
// IPv4 syntax.
|
|
m = spec.match(/^([0-9.]+):([0-9]+)$/);
|
|
}
|
|
if (!m) {
|
|
// TODO: Domain match
|
|
return null;
|
|
}
|
|
const host = m[1];
|
|
const port = parseInt(m[2], 10);
|
|
if (isNaN(port) || port < 0 || port > 65535) {
|
|
return null;
|
|
}
|
|
return {
|
|
host: host,
|
|
port: port
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parse a count of bytes. A suffix of 'k', 'm', or 'g' (or uppercase)
|
|
* does what you would think. Returns null on error.
|
|
*/
|
|
static byteCount(spec) {
|
|
let matches = spec.match(/^(\d+(?:\.\d*)?)(\w*)$/);
|
|
if (matches === null) {
|
|
return null;
|
|
}
|
|
let count = Number(matches[1]);
|
|
if (isNaN(count)) {
|
|
return null;
|
|
}
|
|
const UNITS = new Map([
|
|
['', 1],
|
|
['k', 1024],
|
|
['m', 1024*1024],
|
|
['g', 1024*1024*1024],
|
|
]);
|
|
let unit = matches[2].toLowerCase();
|
|
if (!UNITS.has(unit)) {
|
|
return null;
|
|
}
|
|
let multiplier = UNITS.get(unit);
|
|
return count * multiplier;
|
|
}
|
|
|
|
/**
|
|
* Parse a remote connection-address out of the "c=" Connection Data field
|
|
* or the "a=" attribute fields of the session description.
|
|
* Return undefined if none is found.
|
|
* https://tools.ietf.org/html/rfc4566#section-5.7
|
|
* https://tools.ietf.org/html/rfc5245#section-15
|
|
*/
|
|
static ipFromSDP(sdp) {
|
|
console.log(sdp);
|
|
const ref = [
|
|
/^a=candidate:[a-zA-Z0-9+/]+ \d+ udp \d+ ([\d.]+) /mg,
|
|
/^a=candidate:[a-zA-Z0-9+/]+ \d+ udp \d+ ([0-9A-Fa-f:.]+) /mg,
|
|
/^c=IN IP4 ([\d.]+)(?:(?:\/\d+)?\/\d+)?(:? |$)/mg,
|
|
/^c=IN IP6 ([0-9A-Fa-f:.]+)(?:\/\d+)?(:? |$)/mg
|
|
];
|
|
for (let i = 0, len = ref.length; i < len; i++) {
|
|
const pattern = ref[i];
|
|
let m = pattern.exec(sdp);
|
|
while (m != null) {
|
|
if(Parse.isRemoteIP(m[1])) return m[1];
|
|
m = pattern.exec(sdp);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse the mapped port out of an ice candidate returned from the
|
|
* onicecandidate callback
|
|
*/
|
|
static portFromCandidate(c) {
|
|
const pattern = /(?:[\d.]+|[0-9A-Fa-f:.]+) (\d+) typ srflx/m;
|
|
const m = pattern.exec(c);
|
|
if (m != null) {
|
|
return m[1];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Determine whether an IP address is a local, unspecified, or loopback address */
|
|
static isRemoteIP(ip) {
|
|
if (ip.includes(":")) {
|
|
const ip6 = ip.split(':');
|
|
// Loopback address
|
|
const loopback = /^(?:0*:)*?:?0*1$/m;
|
|
// Unspecified address
|
|
const unspecified = /^(?:0*:)*?:?0*$/m;
|
|
// Local IPv6 addresses are defined in https://tools.ietf.org/html/rfc4193
|
|
return !((loopback.exec(ip) != null) || (unspecified.exec(ip) != null) ||
|
|
(parseInt(ip6[0],16)&0xfe00) == 0xfc00);
|
|
}
|
|
|
|
// Local IPv4 addresses are defined in https://tools.ietf.org/html/rfc1918
|
|
const ip4 = ip.split('.');
|
|
return !(ip4[0] == 10 || ip4[0] == 127 || ip == "0.0.0.0" ||
|
|
(ip4[0] == 172 && (ip4[1]&0xf0) == 16) ||
|
|
(ip4[0] == 192 && ip4[1] == 168) ||
|
|
// Carrier-Grade NAT as per https://tools.ietf.org/htm/rfc6598
|
|
(ip4[0] == 100 && (ip4[1]&0xc0) == 64) ||
|
|
// Dynamic Configuration as per https://tools.ietf.org/htm/rfc3927
|
|
(ip4[0] == 169 && ip4[1] == 254));
|
|
}
|
|
|
|
}
|
|
|
|
|
|
class Params {
|
|
|
|
static getBool(query, param, defaultValue) {
|
|
if (!query.has(param)) {
|
|
return defaultValue;
|
|
}
|
|
const val = query.get(param);
|
|
if ('true' === val || '1' === val || '' === val) {
|
|
return true;
|
|
}
|
|
if ('false' === val || '0' === val) {
|
|
return false;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get an object value and parse it as a byte count. Example byte counts are
|
|
* '100' and '1.3m'. Returns |defaultValue| if param is not a key. Return null
|
|
* on a parsing error.
|
|
*/
|
|
static getByteCount(query, param, defaultValue) {
|
|
if (!query.has(param)) {
|
|
return defaultValue;
|
|
}
|
|
return Parse.byteCount(query.get(param));
|
|
}
|
|
|
|
}
|
|
|
|
|
|
class BucketRateLimit {
|
|
|
|
constructor(capacity, time) {
|
|
this.capacity = capacity;
|
|
this.time = time;
|
|
}
|
|
|
|
age() {
|
|
const now = new Date();
|
|
const delta = (now - this.lastUpdate) / 1000.0;
|
|
this.lastUpdate = now;
|
|
this.amount -= delta * this.capacity / this.time;
|
|
if (this.amount < 0.0) {
|
|
this.amount = 0.0;
|
|
}
|
|
}
|
|
|
|
update(n) {
|
|
this.age();
|
|
this.amount += n;
|
|
return this.amount <= this.capacity;
|
|
}
|
|
|
|
/** How many seconds in the future will the limit expire? */
|
|
when() {
|
|
this.age();
|
|
return (this.amount - this.capacity) / (this.capacity / this.time);
|
|
}
|
|
|
|
isLimited() {
|
|
this.age();
|
|
return this.amount > this.capacity;
|
|
}
|
|
|
|
}
|
|
|
|
BucketRateLimit.prototype.amount = 0.0;
|
|
|
|
BucketRateLimit.prototype.lastUpdate = new Date();
|
|
|
|
|
|
/** A rate limiter that never limits. */
|
|
class DummyRateLimit {
|
|
|
|
constructor(capacity, time) {
|
|
this.capacity = capacity;
|
|
this.time = time;
|
|
}
|
|
|
|
update() {
|
|
return true;
|
|
}
|
|
|
|
when() {
|
|
return 0.0;
|
|
}
|
|
|
|
isLimited() {
|
|
return false;
|
|
}
|
|
|
|
}
|
|
/*
|
|
Only websocket-specific stuff.
|
|
*/
|
|
|
|
class WS {
|
|
|
|
/**
|
|
* Build an escaped URL string from unescaped components. Only scheme and host
|
|
* are required. See RFC 3986, section 3.
|
|
*/
|
|
static buildUrl(scheme, host, port, path, params) {
|
|
const parts = [];
|
|
parts.push(encodeURIComponent(scheme));
|
|
parts.push('://');
|
|
// If it contains a colon but no square brackets, treat it as IPv6.
|
|
if (host.match(/:/) && !host.match(/[[\]]/)) {
|
|
parts.push('[');
|
|
parts.push(host);
|
|
parts.push(']');
|
|
} else {
|
|
parts.push(encodeURIComponent(host));
|
|
}
|
|
if (undefined !== port && this.DEFAULT_PORTS[scheme] !== port) {
|
|
parts.push(':');
|
|
parts.push(encodeURIComponent(port.toString()));
|
|
}
|
|
if (undefined !== path && '' !== path) {
|
|
if (!path.match(/^\//)) {
|
|
path = '/' + path;
|
|
}
|
|
path = path.replace(/[^/]+/, function (m) {
|
|
return encodeURIComponent(m);
|
|
});
|
|
parts.push(path);
|
|
}
|
|
if (undefined !== params) {
|
|
parts.push('?');
|
|
parts.push(new URLSearchParams(params).toString());
|
|
}
|
|
return parts.join('');
|
|
}
|
|
|
|
static makeWebsocket(addr, params) {
|
|
const wsProtocol = this.WSS_ENABLED ? 'wss' : 'ws';
|
|
const url = this.buildUrl(wsProtocol, addr.host, addr.port, '/', params);
|
|
const ws = new WebSocket(url);
|
|
/*
|
|
'User agents can use this as a hint for how to handle incoming binary data:
|
|
if the attribute is set to 'blob', it is safe to spool it to disk, and if it
|
|
is set to 'arraybuffer', it is likely more efficient to keep the data in
|
|
memory.'
|
|
*/
|
|
ws.binaryType = 'arraybuffer';
|
|
return ws;
|
|
}
|
|
|
|
/**
|
|
* Creates a websocket connection from a URL and params to override
|
|
* @param {URL|string} url
|
|
* @param {URLSearchParams|string[][]} params
|
|
* @return {WebSocket}
|
|
*/
|
|
static makeWebsocketFromURL(url, params) {
|
|
let parsedURL = new URL(url);
|
|
let urlpa = new URLSearchParams(params);
|
|
urlpa.forEach(function (value, key) {
|
|
parsedURL.searchParams.set(key, value);
|
|
});
|
|
|
|
let ws = new WebSocket(url);
|
|
/*
|
|
'User agents can use this as a hint for how to handle incoming binary data:
|
|
if the attribute is set to 'blob', it is safe to spool it to disk, and if it
|
|
is set to 'arraybuffer', it is likely more efficient to keep the data in
|
|
memory.'
|
|
*/
|
|
ws.binaryType = 'arraybuffer';
|
|
return ws;
|
|
}
|
|
|
|
static probeWebsocket(addr) {
|
|
return /** @type {Promise<void>} */(new Promise((resolve, reject) => {
|
|
const ws = WS.makeWebsocket(addr);
|
|
ws.onopen = () => {
|
|
resolve();
|
|
ws.close();
|
|
};
|
|
ws.onerror = () => {
|
|
reject();
|
|
ws.close();
|
|
};
|
|
}));
|
|
}
|
|
|
|
}
|
|
|
|
WS.WSS_ENABLED = true;
|
|
|
|
WS.DEFAULT_PORTS = {
|
|
http: 80,
|
|
https: 443
|
|
};
|
|
/* global module, require */
|
|
|
|
/*
|
|
WebRTC shims for multiple browsers.
|
|
*/
|
|
|
|
if (typeof module !== "undefined" && module !== null ? module.exports : undefined) {
|
|
window = {};
|
|
document = {
|
|
getElementById: function() {
|
|
return null;
|
|
}
|
|
};
|
|
chrome = {};
|
|
location = { search: '' };
|
|
({ URLSearchParams } = require('url'));
|
|
if ((typeof TESTING === "undefined" || TESTING === null) || !TESTING) {
|
|
webrtc = require('wrtc');
|
|
RTCPeerConnection = webrtc.RTCPeerConnection;
|
|
RTCSessionDescription = webrtc.RTCSessionDescription;
|
|
WebSocket = require('ws');
|
|
({ XMLHttpRequest } = require('xmlhttprequest'));
|
|
}
|
|
}
|
|
/* global Util, chrome, Config, UI, Broker, Snowflake, WS */
|
|
/* eslint no-unused-vars: 0 */
|
|
|
|
/*
|
|
UI
|
|
*/
|
|
|
|
class WebExtUI extends UI {
|
|
|
|
constructor() {
|
|
super();
|
|
this.onConnect = this.onConnect.bind(this);
|
|
this.onMessage = this.onMessage.bind(this);
|
|
this.onDisconnect = this.onDisconnect.bind(this);
|
|
chrome.runtime.onConnect.addListener(this.onConnect);
|
|
}
|
|
|
|
checkNAT() {
|
|
Util.checkNATType(config.datachannelTimeout).then((type) => {
|
|
console.log("Setting NAT type: " + type);
|
|
this.natType = type;
|
|
}).catch((e) => {
|
|
console.log(e);
|
|
});
|
|
}
|
|
|
|
initNATType() {
|
|
this.natType = "unknown";
|
|
this.checkNAT();
|
|
setInterval(() => {this.checkNAT();}, config.natCheckInterval);
|
|
}
|
|
|
|
tryProbe() {
|
|
WS.probeWebsocket(config.relayAddr)
|
|
.then(
|
|
() => {
|
|
this.missingFeature = false;
|
|
this.setEnabled(true);
|
|
},
|
|
() => {
|
|
log('Could not connect to bridge.');
|
|
this.missingFeature = 'popupBridgeUnreachable';
|
|
this.setEnabled(false);
|
|
}
|
|
);
|
|
}
|
|
|
|
initToggle() {
|
|
// First, check if we have our status stored
|
|
(new Promise((resolve) => {
|
|
chrome.storage.local.get(["snowflake-enabled"], resolve);
|
|
}))
|
|
.then((result) => {
|
|
let enabled = this.enabled;
|
|
if (result['snowflake-enabled'] !== undefined) {
|
|
enabled = result['snowflake-enabled'];
|
|
} else {
|
|
log("Toggle state not yet saved");
|
|
}
|
|
// If it isn't enabled, stop
|
|
if (!enabled) {
|
|
this.setEnabled(enabled);
|
|
return;
|
|
}
|
|
// Otherwise, do feature checks
|
|
if (!Util.hasWebRTC()) {
|
|
this.missingFeature = 'popupWebRTCOff';
|
|
this.setEnabled(false);
|
|
return;
|
|
}
|
|
this.tryProbe();
|
|
});
|
|
}
|
|
|
|
postActive() {
|
|
this.setIcon();
|
|
if (!this.port) { return; }
|
|
this.port.postMessage({
|
|
clients: this.clients,
|
|
total: this.stats.reduce((t, c) => t + c, 0),
|
|
enabled: this.enabled,
|
|
missingFeature: this.missingFeature,
|
|
});
|
|
}
|
|
|
|
onConnect(port) {
|
|
this.port = port;
|
|
port.onDisconnect.addListener(this.onDisconnect);
|
|
port.onMessage.addListener(this.onMessage);
|
|
this.postActive();
|
|
}
|
|
|
|
onMessage(m) {
|
|
if (m.retry) {
|
|
// FIXME: Can set a retrying state here
|
|
this.tryProbe();
|
|
return;
|
|
}
|
|
(new Promise((resolve) => {
|
|
chrome.storage.local.set({ "snowflake-enabled": m.enabled }, resolve);
|
|
}))
|
|
.then(() => {
|
|
log("Stored toggle state");
|
|
this.initToggle();
|
|
});
|
|
}
|
|
|
|
onDisconnect() {
|
|
this.port = null;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} enabled
|
|
*/
|
|
setEnabled(enabled) {
|
|
this.enabled = enabled;
|
|
this.postActive();
|
|
update();
|
|
}
|
|
|
|
setIcon() {
|
|
let path = null;
|
|
if (!this.enabled) {
|
|
path = {
|
|
48: "assets/toolbar-off-48.png",
|
|
96: "assets/toolbar-off-96.png"
|
|
};
|
|
} else if (this.active) {
|
|
path = {
|
|
48: "assets/toolbar-running-48.png",
|
|
96: "assets/toolbar-running-96.png"
|
|
};
|
|
} else {
|
|
path = {
|
|
48: "assets/toolbar-on-48.png",
|
|
96: "assets/toolbar-on-96.png"
|
|
};
|
|
}
|
|
chrome.browserAction.setIcon({
|
|
path: path,
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
WebExtUI.prototype.port = null;
|
|
|
|
WebExtUI.prototype.enabled = true;
|
|
|
|
/*
|
|
Entry point.
|
|
*/
|
|
|
|
/** @typedef {WebExtUI} UIOfThisContext */
|
|
var
|
|
/** @type {boolean} */
|
|
debug,
|
|
/** @type {Snowflake | null} */
|
|
snowflake,
|
|
/** @type {Config | null} */
|
|
config,
|
|
/** @type {Broker | null} */
|
|
broker,
|
|
/** @type {UIOfThisContext | null} */
|
|
ui,
|
|
/** @type {(msg: unknown) => void} */
|
|
log,
|
|
/** @type {(msg: unknown) => void} */
|
|
dbg,
|
|
/** @type {() => void} */
|
|
init,
|
|
/** @type {() => void} */
|
|
update,
|
|
/** @type {boolean} */
|
|
silenceNotifications;
|
|
|
|
(function () {
|
|
|
|
silenceNotifications = false;
|
|
debug = false;
|
|
snowflake = null;
|
|
config = null;
|
|
broker = null;
|
|
ui = null;
|
|
|
|
// Log to both console and UI if applicable.
|
|
// Requires that the snowflake and UI objects are hooked up in order to
|
|
// log to console.
|
|
log = function(msg) {
|
|
console.log('Snowflake: ' + msg);
|
|
if (snowflake != null) {
|
|
snowflake.ui.log(msg);
|
|
}
|
|
};
|
|
|
|
dbg = function(msg) {
|
|
if (debug) {
|
|
log(msg);
|
|
}
|
|
};
|
|
|
|
init = function() {
|
|
config = new Config("webext");
|
|
ui = new WebExtUI();
|
|
broker = new Broker(config);
|
|
snowflake = new Snowflake(config, ui, broker);
|
|
log('== snowflake proxy ==');
|
|
ui.initToggle();
|
|
ui.initNATType();
|
|
};
|
|
|
|
update = function() {
|
|
if (!ui.enabled) {
|
|
// Do not activate the proxy if any number of conditions are true.
|
|
snowflake.disable();
|
|
log('Currently not active.');
|
|
return;
|
|
}
|
|
// Otherwise, begin setting up WebRTC and acting as a proxy.
|
|
dbg('Contacting Broker at ' + broker.url);
|
|
log('Starting snowflake');
|
|
snowflake.setRelayAddr(config.relayAddr);
|
|
snowflake.beginWebRTC();
|
|
};
|
|
|
|
window.onunload = function() {
|
|
if (snowflake !== null) { snowflake.disable(); }
|
|
return null;
|
|
};
|
|
|
|
window.onload = init;
|
|
|
|
}());
|