diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..60ceebc45d065739ee9ba5f0bad447e7c215e5f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.swp +*.swo +*.swn +ignore/ diff --git a/README.md b/README.md index 609b3daffaea53a36db9e532e8f65da9171cff01..3ded99be05e6aecbb9c8f9ee62952485104bed46 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 0000000000000000000000000000000000000000..cf210efc941edc5675d91a7b80a53f56a60b851c 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 0000000000000000000000000000000000000000..cff00a9f9e68190d58aecd9684af6a8671368e26 --- /dev/null +++ b/proxy/snowflake.html @@ -0,0 +1,78 @@ +<!doctype html> +<html> +<head> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <script src="snowflake.js"></script> + <style> + * { + box-sizing: border-box; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + transition: all 0.3s; + } + body { + position: absolute; + width: 100%; height: 100%; top: 0; margin: 0 auto; + background-color: #424; + color: #000; + text-align: center; + font-size: 24px; + font-family: monospace; + background-image: url('koch.jpg'); + } + textarea { + background-color: rgba(0,0,0,0.8); + color: #fff; + resize: none; + } + .chatarea { + position: relative; border: none; + width: 50%; min-width: 40em; + padding: 0.5em; margin: auto; + } + .active { background-color: #252; } + #chatlog { + display: block; + width: 100%; + min-height: 40em; + margin-bottom: 1em; + padding: 8px; + } + .inputarea { + position: relative; + width: 100%; + height: 3em; + display: block; + } + #input { + display: inline-block; + position: absolute; left: 0; + width: 89%; height: 100%; + padding: 8px 30px; + font-size: 80%; + color: #fff; + background-color: rgba(0,0,0,0.9); + border: 1px solid #999; + } + #send { + display: inline-block; position: absolute; + right: 0; top: 0; height: 100%; width: 10%; + background-color: #202; color: #f8f; + font-variant: small-caps; font-size: 100%; + border: none; // box-shadow: 0 2px 5px #000; + } + #send:hover { background-color: #636; } + </style> +</head> +<body> + <div class="chatarea"> + <textarea id="chatlog" readonly> + </textarea> + <div class="inputarea"> + <input type="text" id="input"> + <input type="submit" id="send" value="send"> + </div> + </div> + +</body> +</html> diff --git a/proxy/snowflake.js b/proxy/snowflake.js new file mode 100644 index 0000000000000000000000000000000000000000..d6fc0256acd036087b2bd6d531261966efa011c4 --- /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 09df4fb3ed6ea2881c09ce7cb662db3417e6c422..b17ac7012318cea0bc66aec64c657088a4876862 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() {