snowflake/proxy/proxypair.coffee
David Fifield aa668bdc92 Set binaryType="arraybuffer" for RTCDataChannel, just as with WebSocket.
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.
2018-12-19 21:30:39 -07:00

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