mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-13 11:11:30 -04:00
239 lines
5.3 KiB
JavaScript
239 lines
5.3 KiB
JavaScript
/*
|
|
JS WebRTC proxy
|
|
Copy-paste signaling.
|
|
*/
|
|
|
|
// DOM elements
|
|
var $chatlog, $input, $send, $name;
|
|
|
|
var config = {
|
|
iceServers: [
|
|
{ urls: ["stun:stun.l.google.com:19302"] }
|
|
]
|
|
}
|
|
|
|
// Chrome / Firefox compatibility
|
|
window.PeerConnection = window.RTCPeerConnection ||
|
|
window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
|
|
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate;
|
|
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription;
|
|
|
|
var pc; // PeerConnection
|
|
var answer;
|
|
// Janky state machine
|
|
var MODE = {
|
|
INIT: 0,
|
|
CONNECTING: 1,
|
|
CHAT: 2
|
|
}
|
|
var currentMode = MODE.INIT;
|
|
|
|
// Signalling channel - just tells user to copy paste to the peer.
|
|
var Signalling = {
|
|
send: function(msg) {
|
|
log("---- Please copy the below to peer ----\n");
|
|
log(JSON.stringify(msg));
|
|
log("\n");
|
|
},
|
|
receive: function(msg) {
|
|
var recv;
|
|
try {
|
|
recv = JSON.parse(msg);
|
|
} catch(e) {
|
|
log("Invalid JSON.");
|
|
return;
|
|
}
|
|
if (!pc) {
|
|
start(false);
|
|
}
|
|
var desc = recv['sdp']
|
|
var ice = recv['candidate']
|
|
if (!desc && ! ice) {
|
|
log("Invalid SDP.");
|
|
return false;
|
|
}
|
|
if (desc) { receiveDescription(recv); }
|
|
if (ice) { receiveICE(recv); }
|
|
}
|
|
}
|
|
|
|
function welcome() {
|
|
log("== snowflake JS proxy ==");
|
|
log("Input offer from the snowflake client:");
|
|
}
|
|
|
|
function start(initiator) {
|
|
username + ": " + msg;
|
|
log("Starting up RTCPeerConnection...");
|
|
pc = new PeerConnection(config, {
|
|
optional: [
|
|
{ DtlsSrtpKeyAgreement: true },
|
|
{ RtpDataChannels: false },
|
|
],
|
|
});
|
|
pc.onicecandidate = function(evt) {
|
|
var candidate = evt.candidate;
|
|
// Chrome sends a null candidate once the ICE gathering phase completes.
|
|
// In this case, it makes sense to send one copy-paste blob.
|
|
if (null == candidate) {
|
|
log("Finished gathering ICE candidates.");
|
|
Signalling.send(pc.localDescription);
|
|
return;
|
|
}
|
|
}
|
|
pc.onnegotiationneeded = function() {
|
|
sendOffer();
|
|
}
|
|
pc.ondatachannel = function(dc) {
|
|
console.log(dc);
|
|
channel = dc.channel;
|
|
log("Data Channel established... ");
|
|
prepareDataChannel(channel);
|
|
}
|
|
|
|
// Creating the first data channel triggers ICE negotiation.
|
|
if (initiator) {
|
|
channel = pc.createDataChannel("test");
|
|
prepareDataChannel(channel);
|
|
}
|
|
}
|
|
|
|
// Local input from keyboard into chat window.
|
|
function acceptInput(is) {
|
|
var msg = $input.value;
|
|
switch (currentMode) {
|
|
case MODE.INIT:
|
|
if (msg.startsWith("start")) {
|
|
start(true);
|
|
} else {
|
|
Signalling.receive(msg);
|
|
}
|
|
break;
|
|
case MODE.CONNECTING:
|
|
Signalling.receive(msg);
|
|
break;
|
|
case MODE.CHAT:
|
|
var data = msg;
|
|
log(data);
|
|
channel.send(data);
|
|
break;
|
|
default:
|
|
log("ERROR: " + msg);
|
|
}
|
|
$input.value = "";
|
|
$input.focus();
|
|
}
|
|
|
|
// Chrome uses callbacks while Firefox uses promises.
|
|
// Need to support both - same for createAnswer below.
|
|
function sendOffer() {
|
|
var next = function(sdp) {
|
|
log("webrtc: Created Offer");
|
|
offer = sdp;
|
|
pc.setLocalDescription(sdp);
|
|
}
|
|
var promise = pc.createOffer(next);
|
|
if (promise) {
|
|
promise.then(next);
|
|
}
|
|
}
|
|
|
|
function sendAnswer() {
|
|
var next = function (sdp) {
|
|
log("webrtc: Created Answer");
|
|
answer = sdp;
|
|
pc.setLocalDescription(sdp)
|
|
}
|
|
var promise = pc.createAnswer(next);
|
|
if (promise) {
|
|
promise.then(next);
|
|
}
|
|
}
|
|
|
|
function receiveDescription(desc) {
|
|
var sdp = new RTCSessionDescription(desc);
|
|
try {
|
|
err = pc.setRemoteDescription(sdp);
|
|
} catch (e) {
|
|
log("Invalid SDP message.");
|
|
return false;
|
|
}
|
|
log("SDP " + sdp.type + " successfully received.");
|
|
if ("offer" == sdp.type) {
|
|
sendAnswer();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function receiveICE(ice) {
|
|
var candidate = new RTCIceCandidate(ice);
|
|
try {
|
|
pc.addIceCandidate(candidate);
|
|
} catch (e) {
|
|
log("Could not add ICE candidate.");
|
|
return;
|
|
}
|
|
log("ICE candidate successfully received: " + ice.candidate);
|
|
}
|
|
|
|
function waitForSignals() {
|
|
currentMode = MODE.CONNECTING;
|
|
}
|
|
|
|
function prepareDataChannel(channel) {
|
|
channel.onopen = function() {
|
|
log("Data channel opened!");
|
|
startChat();
|
|
}
|
|
channel.onclose = function() {
|
|
log("Data channel closed.");
|
|
currentMode = MODE.INIT;
|
|
$chatlog.className = "";
|
|
log("------- chat disabled -------");
|
|
}
|
|
channel.onerror = function() {
|
|
log("Data channel error!!");
|
|
}
|
|
channel.onmessage = function(msg) {
|
|
var recv = msg.data;
|
|
console.log(msg);
|
|
// Go sends only raw bytes.
|
|
if ("[object ArrayBuffer]" == recv.toString()) {
|
|
var bytes = new Uint8Array(recv);
|
|
line = String.fromCharCode.apply(null, bytes);
|
|
} else {
|
|
line = recv;
|
|
}
|
|
line = line.trim();
|
|
log(line);
|
|
}
|
|
}
|
|
|
|
// Get DOM elements and setup interactions.
|
|
function init() {
|
|
console.log("loaded");
|
|
// Setup chatwindow.
|
|
$chatlog = document.getElementById('chatlog');
|
|
$chatlog.value = "";
|
|
|
|
$send = document.getElementById('send');
|
|
$send.onclick = acceptInput
|
|
|
|
$input = document.getElementById('input');
|
|
$input.focus();
|
|
$input.onkeydown = function(e) {
|
|
if (13 == e.keyCode) { // enter
|
|
$send.onclick();
|
|
}
|
|
}
|
|
welcome();
|
|
}
|
|
|
|
var log = function(msg) {
|
|
$chatlog.value += msg + "\n";
|
|
console.log(msg);
|
|
// Scroll to latest.
|
|
$chatlog.scrollTop = $chatlog.scrollHeight;
|
|
}
|
|
|
|
window.onload = init;
|