begin JS "snowflake" proxy

This commit is contained in:
Serene Han 2015-12-30 09:54:20 -08:00
parent e4d54b5ac6
commit a16a4b43a5
6 changed files with 332 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.swp
*.swo
*.swn
ignore/

View file

@ -1,2 +1,10 @@
# snowflake-pt
WebRTC Pluggable Transport
### Usage
`go build webrtc-client.go`
`tor -f torrc`
More documentation on the way.

BIN
proxy/koch.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

78
proxy/snowflake.html Normal file
View file

@ -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>

239
proxy/snowflake.js Normal file
View file

@ -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;

View file

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