mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-13 11:11:30 -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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// For now, the Go client is always the offerer.
|
||||
// TODO: Copy paste signaling
|
||||
|
||||
pc.OnNegotiationNeeded = func() {
|
||||
// log.Println("OnNegotiationNeeded")
|
||||
go func() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue