Skip to content

Commit

Permalink
feat: http fetch tunnel over webrtc datachannel
Browse files Browse the repository at this point in the history
  • Loading branch information
ivelin committed Jan 24, 2020
1 parent 9a1c24c commit be18440
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 50 deletions.
140 changes: 140 additions & 0 deletions src/remote/peer-fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Emulates HTML Fetch API over peer-to-peer
* DataConnection (WebRTC DataChannel)
*/
export class PeerFetch {
constructor (dataConnection) {
// the DataConnection that PeerFetch rides on
this._dataConnection = dataConnection
// Map of pending requests awaiting responses
this._requestMap = new Map()
// incrementing counter to the next available
// unused ticket number
// Each request is assigned a ticket
// which can be used to claim the response
this._nextAvailableTicket = 0
// points to the next ticket assigned to a pending request
// Requests are processed in FIFO order.
this._nextTicketInLine = 0
this._configureDataConnection()
}

/**
Return the next available ticket number
and simultaneously increment the ticket counter.
*/
_drawNewTicket () {
return this._nextAvailableTicket++
}

/**
Move on to next pending ticket
*/
_ticketProcessed (ticket) {
const errorMsg = 'response received out of order!'
const nextTicket = this._nextTicketInLine
console.assert(
ticket === nextTicket,
{ ticket, nextTicket, errorMsg }
)
// remove entry from pending request map
this._requestMap.delete(ticket)
this._nextTicketInLine++
}

_configureDataConnection () {
// Handle incoming data (messages only since this is the signal sender)
const peerFetch = this
this._dataConnection.on('data', function (data) {
console.debug('Remote Peer Data message received (type %s): %s',
typeof (data), data)
// we expect data to be a response to a previously sent request message
const response = data
const ticket = peerFetch._nextTicketInLine
console.debug(peerFetch, peerFetch._requestMap, ticket, response)
// update request map entry with this response
const pair = peerFetch._requestMap.get(ticket)
pair.response = response
})
}

/**
* REST API over HTTP GET
*/
async get ({ url = '/', params = {} }) {
console.debug('PeerFetch.get', url, params)
if (params.size > 0) {
var esc = encodeURIComponent
var query = Object.keys(params)
.map(k => esc(k) + '=' + esc(params[k]))
.join('&')
url += '?' + query
}
const request = {
url,
method: 'GET'
}
// get a ticket that matches the request
// and use it to claim the corresponding
// response when availably
const ticket = this._pushRequest(request)
const response = await this._receiveResponse(ticket)
return response
}

_pushRequest (request) {
const ticket = this._drawNewTicket()
console.debug(this._requestMap)
this._requestMap.set(ticket, { request })
if (this._requestMap.size === 1) {
// there are no other pending requests
// let's send this one on the wire
this._sendNextRequest(ticket)
}
return ticket
}

/**
Send next pending request to remote peer.
Requests are sent one at a time.
Only when a previous request response arrives
is the next request sent across the wire.
In the future we can look at handling multiple requests
and responses in parallel over the same data connection or
even a pool of connections.
*/
_sendNextRequest (ticket) {
const { request } = this._requestMap.get(ticket)
const jsonRequest = JSON.stringify(request)
const requestMap = this._requestMap
console.debug('Sending request to remote peer',
{ requestMap, ticket, request })
this._dataConnection.send(jsonRequest)
}

async _receiveResponse (ticket) {
const timeout = 30 * 1000 // 30 seconds
const timerStart = Date.now()
let timeElapsed = timerStart
let request, response
do {
({ request, response } = this._requestMap.get(ticket))
if (response) {
// if (typeof(response) === 'string') {
this._ticketProcessed(ticket)
console.debug('Received response', { ticket, request, response })
return response
}
timeElapsed = Date.now() - timeElapsed
await sleep(100)
} while (!response && timeElapsed < timeout)
if (!response) {
throw Error('PeerFetch Timeout while waiting for response.')
}
}
}

function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
4 changes: 0 additions & 4 deletions src/remote/pnp.js → src/remote/peer-room.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
/**
Manage plug and play connection to Ambianic Edge.
*/

/**
* Local room management for a peer
*/
Expand Down
1 change: 1 addition & 0 deletions src/store/mutation-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export const USER_MESSAGE = 'USER_MESSAGE'
export const NEW_PEER_ID = 'NEW_PEER_ID'
export const NEW_REMOTE_PEER_ID = 'NEW_REMOTE_PEER_ID'
export const REMOTE_PEER_ID_REMOVED = 'REMOTE_PEER_ID_REMOVED'
export const PEER_FETCH = 'PEER_FETCH'
88 changes: 45 additions & 43 deletions src/store/pnp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
USER_MESSAGE,
NEW_PEER_ID,
NEW_REMOTE_PEER_ID,
REMOTE_PEER_ID_REMOVED
REMOTE_PEER_ID_REMOVED,
PEER_FETCH
} from '../mutation-types.js'
import {
INITIALIZE_PNP,
Expand All @@ -27,7 +28,8 @@ import {
} from '../action-types.js'
import { ambianicConf } from '@/config'
import Peer from 'peerjs'
import { PeerRoom } from '@/remote/pnp'
import { PeerRoom } from '@/remote/peer-room'
import { PeerFetch } from '@/remote/peer-fetch'
const STORAGE_KEY = 'ambianic-pnp-settings'

/**
Expand Down Expand Up @@ -68,13 +70,18 @@ const state = {
/**
Status of current peer connection to remote pnp service
*/
pnpServiceConnectionStatus: PNP_SERVICE_DISCONNECTED
pnpServiceConnectionStatus: PNP_SERVICE_DISCONNECTED,
/**
PeerFetch instance
*/
peerFetch: PeerFetch
}

const mutations = {
[PEER_DISCONNECTED] (state) {
state.peerConnection = null
state.peerConnectionStatus = PEER_DISCONNECTED
state.peerFetch = null
},
[PEER_DISCOVERED] (state) {
state.peerConnectionStatus = PEER_DISCOVERED
Expand Down Expand Up @@ -117,6 +124,10 @@ const mutations = {
console.log('Removing remote Peer Id from local storage')
state.remotePeerId = null
window.localStorage.removeItem(`${STORAGE_KEY}.remotePeerId`)
},
[PEER_FETCH] (state, peerFetch) {
console.debug('Setting PeerFetch instance.')
state.peerFetch = peerFetch
}
}

Expand Down Expand Up @@ -211,33 +222,11 @@ function setPnPServiceConnectionHandlers (
function setPeerConnectionHandlers ({ state, commit, dispatch }, peerConnection) {
// setup connection progress callbacks
peerConnection.on('open', function () {
const peerFetch = new PeerFetch(peerConnection)
console.debug('Peer DataConnection is now open. Creating PeerFetch wrapper.')
commit(PEER_FETCH, peerFetch)
dispatch(PEER_AUTHENTICATE, peerConnection)
})
// Handle incoming data (messages only since this is the signal sender)
peerConnection.on('data', function (data) {
// addMessage('<span class=\'peerMsg\'>Peer:</span> ' + data)
console.debug('Remote Peer Data message received (type %s): %s',
typeof (data), data)
// if data is authentication challenge response, verify it
// for now we asume authentication passed
let authMessage
if (typeof (data) === 'string') {
try {
authMessage = JSON.parse(data)
} catch (e) {
console.error('Error while JSON parsing data message', data)
}
}
if (authMessage) {
const authPassed = authMessage.name === 'Ambianic-Edge'
if (authPassed) {
console.debug('Remote peer authenticated as:', authMessage.name)
commit(PEER_CONNECTED, peerConnection)
} else {
commit(USER_MESSAGE, 'Remote peer authentication failed.')
}
}
})

peerConnection.on('close', function () {
commit(PEER_DISCONNECTED)
Expand Down Expand Up @@ -353,7 +342,7 @@ const actions = {
console.log('Connecting to remote peer', remotePeerId)
commit(PEER_CONNECTING)
const peerConnection = peer.connect(remotePeerId, {
reliable: true, serialization: 'raw', somethingCrazy: 1234
reliable: true, serialization: 'raw'
})
setPeerConnectionHandlers({ state, commit, dispatch }, peerConnection)
},
Expand All @@ -365,23 +354,36 @@ const actions = {
commit(PEER_AUTHENTICATING)
commit(USER_MESSAGE, `Authenticating remote peer: ${peerConnection.peer}`)
console.log('Authenticating remote Peer ID: ', peerConnection.peer)
if (state.remotePeerId !== peerConnection.peer) {
// remote Peer ID is new
commit(NEW_REMOTE_PEER_ID, peerConnection.peer)
}
// Check URL params for commands that should be sent immediately
// var command = getUrlParam('command')
// if (command)
console.log('Preparing to send message via peer DataConnection')
const msg = JSON.stringify({
type: 'http-request',
method: 'GET',
path: '/fingerprint',
const msg = {
url: '/fingerprint',
params: {
user: 'ambianic-ui'
}
})
peerConnection.send(msg)
}
const response = await state.peerFetch.get(msg)
console.log('peerFetch.get returned response', { response })
// if data is authentication challenge response, verify it
// for now we asume authentication passed
let authMessage
if (typeof (response) === 'string') {
try {
authMessage = JSON.parse(response)
} catch (e) {
console.error('Error while JSON parsing response', response)
}
}
if (authMessage) {
const authPassed = authMessage.name === 'Ambianic-Edge'
if (authPassed) {
console.debug('Remote peer authenticated as:', authMessage.name)
commit(PEER_CONNECTED, peerConnection)
// remote Peer ID authenticated,
// lets store it for future (re)connections
commit(NEW_REMOTE_PEER_ID, peerConnection.peer)
} else {
commit(USER_MESSAGE, 'Remote peer authentication failed.')
}
}
console.log('Peer DataConnection sending message', msg)
console.log('DataChannel transport capabilities',
peerConnection.dataChannel)
Expand Down
13 changes: 10 additions & 3 deletions src/views/EdgeConnect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,23 @@
</v-col>
<v-dialog
v-model="removeEdgeDialog"
max-width="500"
>
<v-card>
<v-card-title class="headline">Remove device?</v-card-title>

<v-card-text>
If you remove the connection to this Ambianic Edge device,
<p>
Are you switching to a new Ambianic Edge device?
Removing a device association is usually done when switching to
a new edge device with a different Peer ID.
</p>
<p>
If you remove the connection to the current Ambianic Edge device,
you will not be able to connect to it remotely.
In order to reconnect to the same device, you will have to
go near it and connect on the same WiFi/LAN network
so it can be re-discovered.
join the same local network.
</p>
</v-card-text>

<v-card-actions>
Expand Down

0 comments on commit be18440

Please sign in to comment.