From be18440bd7030bd60c5e9546e21a397d5f4dc5df Mon Sep 17 00:00:00 2001 From: Ivelin Ivanov Date: Fri, 24 Jan 2020 16:02:10 -0600 Subject: [PATCH] feat: http fetch tunnel over webrtc datachannel --- src/remote/peer-fetch.js | 140 ++++++++++++++++++++++++++++ src/remote/{pnp.js => peer-room.js} | 4 - src/store/mutation-types.js | 1 + src/store/pnp/index.js | 88 ++++++++--------- src/views/EdgeConnect.vue | 13 ++- 5 files changed, 196 insertions(+), 50 deletions(-) create mode 100644 src/remote/peer-fetch.js rename src/remote/{pnp.js => peer-room.js} (97%) diff --git a/src/remote/peer-fetch.js b/src/remote/peer-fetch.js new file mode 100644 index 00000000..8245f08d --- /dev/null +++ b/src/remote/peer-fetch.js @@ -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)) +} diff --git a/src/remote/pnp.js b/src/remote/peer-room.js similarity index 97% rename from src/remote/pnp.js rename to src/remote/peer-room.js index dd5be9d4..baa2b120 100644 --- a/src/remote/pnp.js +++ b/src/remote/peer-room.js @@ -1,7 +1,3 @@ -/** -Manage plug and play connection to Ambianic Edge. -*/ - /** * Local room management for a peer */ diff --git a/src/store/mutation-types.js b/src/store/mutation-types.js index b89ac651..6285fc02 100644 --- a/src/store/mutation-types.js +++ b/src/store/mutation-types.js @@ -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' diff --git a/src/store/pnp/index.js b/src/store/pnp/index.js index c0b609de..ce349801 100644 --- a/src/store/pnp/index.js +++ b/src/store/pnp/index.js @@ -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, @@ -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' /** @@ -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 @@ -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 } } @@ -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('Peer: ' + 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) @@ -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) }, @@ -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) diff --git a/src/views/EdgeConnect.vue b/src/views/EdgeConnect.vue index ff267ed1..88823999 100644 --- a/src/views/EdgeConnect.vue +++ b/src/views/EdgeConnect.vue @@ -176,16 +176,23 @@ Remove device? - If you remove the connection to this Ambianic Edge device, +

+ 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. +

+

+ 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. +