mirror of
https://gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/snowflake.git
synced 2025-10-14 14:11:23 -04:00
The binaryType can be "arraybuffer" or "blob", and "blob" is the default. The code is only aware of "arraybuffer": I discovered a problem while running snowflake.html in debug mode; this code fails: if DEBUG # Go sends only raw bytes... if '[object ArrayBuffer]' == recv.toString() bytes = new Uint8Array recv line = String.fromCharCode.apply(null, bytes) line = line.trim() log 'WebRTC --> websocket data: ' + line with the error: TypeError: line.trim is not a function[Learn More] snowflake.js:497:16 because recv is of type Blob, not ArrayBuffer. Despite the unexpected type, the code seemed to work as expected when not in debug mode. Though the two types provide different interfaces, they are both valid to pass on to WebSocket.send. The only other thing we did with it was try to read the .length member for rate-limiting purposes: @rateLimit.update chunk.length but .length is incorrect for either type: Blob uses .size and ArrayBuffer uses .byteLength. It worked anyway, because DummyRateLimit.update doesn't actually look at its argument. We were already setting binaryType="arraybuffer" for WebSocket connections.
201 lines
6.3 KiB
CoffeeScript
201 lines
6.3 KiB
CoffeeScript
###
|
|
Represents a single:
|
|
|
|
client <-- webrtc --> snowflake <-- websocket --> relay
|
|
|
|
Every ProxyPair has a Snowflake ID, which is necessary when responding to the
|
|
Broker with an WebRTC answer.
|
|
###
|
|
|
|
class ProxyPair
|
|
|
|
MAX_BUFFER: 10 * 1024 * 1024
|
|
pc: null
|
|
c2rSchedule: []
|
|
r2cSchedule: []
|
|
client: null # WebRTC Data channel
|
|
relay: null # websocket
|
|
timer: 0
|
|
running: true
|
|
active: false # Whether serving a client.
|
|
flush_timeout_id: null
|
|
onCleanup: null
|
|
id: null
|
|
|
|
###
|
|
Constructs a ProxyPair where:
|
|
- @relayAddr is the destination relay
|
|
- @rateLimit specifies a rate limit on traffic
|
|
###
|
|
constructor: (@relayAddr, @rateLimit) ->
|
|
@active = false
|
|
@id = genSnowflakeID()
|
|
|
|
# Prepare a WebRTC PeerConnection and await for an SDP offer.
|
|
begin: ->
|
|
@pc = new PeerConnection config, {
|
|
optional: [
|
|
{ DtlsSrtpKeyAgreement: true }
|
|
{ RtpDataChannels: false }
|
|
] }
|
|
@pc.onicecandidate = (evt) =>
|
|
# Browser sends a null candidate once the ICE gathering completes.
|
|
# In this case, it makes sense to send one copy-paste blob.
|
|
if null == evt.candidate
|
|
# TODO: Use a promise.all to tell Snowflake about all offers at once,
|
|
# once multiple proxypairs are supported.
|
|
dbg 'Finished gathering ICE candidates.'
|
|
if COPY_PASTE_ENABLED
|
|
Signalling.send @pc.localDescription
|
|
else
|
|
snowflake.broker.sendAnswer @id, @pc.localDescription
|
|
# OnDataChannel triggered remotely from the client when connection succeeds.
|
|
@pc.ondatachannel = (dc) =>
|
|
channel = dc.channel
|
|
dbg 'Data Channel established...'
|
|
@prepareDataChannel channel
|
|
@client = channel
|
|
|
|
receiveWebRTCOffer: (offer) ->
|
|
if 'offer' != offer.type
|
|
log 'Invalid SDP received -- was not an offer.'
|
|
return false
|
|
try
|
|
err = @pc.setRemoteDescription offer
|
|
catch e
|
|
log 'Invalid SDP message.'
|
|
return false
|
|
dbg 'SDP ' + offer.type + ' successfully received.'
|
|
@active = true
|
|
true
|
|
|
|
# Given a WebRTC DataChannel, prepare callbacks.
|
|
prepareDataChannel: (channel) =>
|
|
channel.onopen = =>
|
|
log 'WebRTC DataChannel opened!'
|
|
snowflake.state = MODE.WEBRTC_READY
|
|
snowflake.ui?.setActive true
|
|
# This is the point when the WebRTC datachannel is done, so the next step
|
|
# is to establish websocket to the server.
|
|
@connectRelay()
|
|
channel.onclose = =>
|
|
log 'WebRTC DataChannel closed.'
|
|
snowflake.ui?.setStatus 'disconnected by webrtc.'
|
|
snowflake.ui?.setActive false
|
|
snowflake.state = MODE.INIT
|
|
@flush()
|
|
@close()
|
|
# TODO: Change this for multiplexing.
|
|
snowflake.reset()
|
|
channel.onerror = -> log 'Data channel error!'
|
|
channel.binaryType = "arraybuffer"
|
|
channel.onmessage = @onClientToRelayMessage
|
|
|
|
# Assumes WebRTC datachannel is connected.
|
|
connectRelay: =>
|
|
dbg 'Connecting to relay...'
|
|
|
|
# Get a remote IP address from the PeerConnection, if possible. Add it to
|
|
# the WebSocket URL's query string if available.
|
|
# MDN marks remoteDescription as "experimental". However the other two
|
|
# options, currentRemoteDescription and pendingRemoteDescription, which
|
|
# are not marked experimental, were undefined when I tried them in Firefox
|
|
# 52.2.0.
|
|
# https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/remoteDescription
|
|
peer_ip = Parse.ipFromSDP(@pc.remoteDescription?.sdp)
|
|
params = []
|
|
if peer_ip?
|
|
params.push(["client_ip", peer_ip])
|
|
|
|
@relay = makeWebsocket @relayAddr, params
|
|
@relay.label = 'websocket-relay'
|
|
@relay.onopen = =>
|
|
if @timer
|
|
clearTimeout @timer
|
|
@timer = 0
|
|
log @relay.label + ' connected!'
|
|
snowflake.ui?.setStatus 'connected'
|
|
@relay.onclose = =>
|
|
log @relay.label + ' closed.'
|
|
snowflake.ui?.setStatus 'disconnected.'
|
|
snowflake.ui?.setActive false
|
|
snowflake.state = MODE.INIT
|
|
@flush()
|
|
@close()
|
|
@relay.onerror = @onError
|
|
@relay.onmessage = @onRelayToClientMessage
|
|
# TODO: Better websocket timeout handling.
|
|
@timer = setTimeout((=>
|
|
return if 0 == @timer
|
|
log @relay.label + ' timed out connecting.'
|
|
@relay.onclose()), 5000)
|
|
|
|
# WebRTC --> websocket
|
|
onClientToRelayMessage: (msg) =>
|
|
line = recv = msg.data
|
|
if DEBUG
|
|
# Go sends only raw bytes...
|
|
if '[object ArrayBuffer]' == recv.toString()
|
|
bytes = new Uint8Array recv
|
|
line = String.fromCharCode.apply(null, bytes)
|
|
line = line.trim()
|
|
log 'WebRTC --> websocket data: ' + line
|
|
@c2rSchedule.push recv
|
|
@flush()
|
|
|
|
# websocket --> WebRTC
|
|
onRelayToClientMessage: (event) =>
|
|
@r2cSchedule.push event.data
|
|
# log 'websocket-->WebRTC data: ' + event.data
|
|
@flush()
|
|
|
|
onError: (event) =>
|
|
ws = event.target
|
|
log ws.label + ' error.'
|
|
@close()
|
|
|
|
# Close both WebRTC and websocket.
|
|
close: ->
|
|
if @timer
|
|
clearTimeout @timer
|
|
@timer = 0
|
|
@running = false
|
|
@client.close() if @webrtcIsReady()
|
|
@relay.close() if @relayIsReady()
|
|
relay = null
|
|
|
|
# Send as much data in both directions as the rate limit currently allows.
|
|
flush: =>
|
|
clearTimeout @flush_timeout_id if @flush_timeout_id
|
|
@flush_timeout_id = null
|
|
busy = true
|
|
checkChunks = =>
|
|
busy = false
|
|
# WebRTC --> websocket
|
|
if @relayIsReady() &&
|
|
@relay.bufferedAmount < @MAX_BUFFER &&
|
|
@c2rSchedule.length > 0
|
|
chunk = @c2rSchedule.shift()
|
|
@rateLimit.update chunk.length
|
|
@relay.send chunk
|
|
busy = true
|
|
# websocket --> WebRTC
|
|
if @webrtcIsReady() &&
|
|
@client.bufferedAmount < @MAX_BUFFER &&
|
|
@r2cSchedule.length > 0
|
|
chunk = @r2cSchedule.shift()
|
|
@rateLimit.update chunk.length
|
|
@client.send chunk
|
|
busy = true
|
|
|
|
checkChunks() while busy && !@rateLimit.isLimited()
|
|
|
|
if @r2cSchedule.length > 0 || @c2rSchedule.length > 0 ||
|
|
(@relayIsReady() && @relay.bufferedAmount > 0) ||
|
|
(@webrtcIsReady() && @client.bufferedAmount > 0)
|
|
@flush_timeout_id = setTimeout @flush, @rateLimit.when() * 1000
|
|
|
|
webrtcIsReady: -> null != @client && 'open' == @client.readyState
|
|
relayIsReady: -> (null != @relay) && (WebSocket.OPEN == @relay.readyState)
|
|
isClosed: (ws) -> undefined == ws || WebSocket.CLOSED == ws.readyState
|
|
|