mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-13 20:11:19 -04:00
begin JS "snowflake" proxy
This commit is contained in:
parent
e4d54b5ac6
commit
a16a4b43a5
6 changed files with 332 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.swn
|
||||||
|
ignore/
|
|
@ -1,2 +1,10 @@
|
||||||
# snowflake-pt
|
# 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
BIN
proxy/koch.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
78
proxy/snowflake.html
Normal file
78
proxy/snowflake.html
Normal 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
239
proxy/snowflake.js
Normal 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;
|
|
@ -35,6 +35,9 @@ func handler(conn *pt.SocksConn) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For now, the Go client is always the offerer.
|
||||||
|
// TODO: Copy paste signaling
|
||||||
|
|
||||||
pc.OnNegotiationNeeded = func() {
|
pc.OnNegotiationNeeded = func() {
|
||||||
// log.Println("OnNegotiationNeeded")
|
// log.Println("OnNegotiationNeeded")
|
||||||
go func() {
|
go func() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue