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() {