diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60ceebc --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.swp +*.swo +*.swn +ignore/ diff --git a/README.md b/README.md index 609b3da..3ded99b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # snowflake-pt +WebRTC Pluggable Transport + +### Usage + +`go build webrtc-client.go` +`tor -f torrc` + +More documentation on the way. diff --git a/proxy/koch.jpg b/proxy/koch.jpg new file mode 100644 index 0000000..cf210ef Binary files /dev/null and b/proxy/koch.jpg differ diff --git a/proxy/snowflake.html b/proxy/snowflake.html new file mode 100644 index 0000000..cff00a9 --- /dev/null +++ b/proxy/snowflake.html @@ -0,0 +1,78 @@ + + + + + + + + +
+ +
+ + +
+
+ + + diff --git a/proxy/snowflake.js b/proxy/snowflake.js new file mode 100644 index 0000000..d6fc025 --- /dev/null +++ b/proxy/snowflake.js @@ -0,0 +1,239 @@ +/* +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; diff --git a/webrtc-client.go b/webrtc-client.go index 09df4fb..b17ac70 100644 --- a/webrtc-client.go +++ b/webrtc-client.go @@ -35,6 +35,9 @@ func handler(conn *pt.SocksConn) error { return err } + // For now, the Go client is always the offerer. + // TODO: Copy paste signaling + pc.OnNegotiationNeeded = func() { // log.Println("OnNegotiationNeeded") go func() {