diff --git a/README.md b/README.md index 60205b0..9c21295 100644 --- a/README.md +++ b/README.md @@ -35,103 +35,17 @@ app.listen(process.env.PORT || 3000) ## RESTful API -For example clients, see the following: -+ [webrtc-native-peerconnection](https://github.com/svn2github/webrtc/tree/master/talk/examples/peerconnection/client) - -### GET /sign_in - -> Takes `peer_name` query parameter - -Indicates a peer is available to peer with. The response will contain the unique peer_id assigned to the caller in the `Pragma` header, and a `csv` formatted list of peers in the `body`. - -``` -GET http://localhost:3000/sign_in?peer_name=test HTTP/1.1 -Host: localhost:3000 - -=> +> The default API version is `1` and will be used if no version is specified. -HTTP/1.1 200 OK -Pragma: 1 -Content-Type: text/plain; charset=utf-8 -Content-Length: 8 +The RESTful API is verioned, with different versions supporting different capabilities. To select a version, use the `Accept` header with a value of `'application/vnd.webrtc-signal.[+]` where `` is the api version, and `[]` indicates an optional component, `` where type is a valid application mime type. For example, `text` or `json`. API version `1` has complete API compatibility with the WebRTC example server. API version `2` makes some logical improvements, as documented [here](https://github.com/bengreenier/webrtc-signal-http/issues/3). -test,1,1 -``` - -### GET /sign_out - -> Takes `peer_id` query parameter - -Indicates a peer is no longer available to peer with. It is expected this method be called when a peer is no longer intending to use signaling. The response will be empty. - -``` -GET http://localhost:3000/sign_out?peer_id=1 HTTP/1.1 -Host: localhost:3000 - -=> - -HTTP/1.1 200 OK -Content-Length: 0 -``` - -### POST /message - -> Takes `peer_id` (indicating the caller id) and `to` (indicating whom we're sending to) - -Provides a messaging mechanism for one peer to send data to another. There are no requirements around the type of data that can be sent. The message may be buffered until the receiving peer connects to the service. The response will be empty. - -``` -POST http://localhost:3000/message?peer_id=2&to=3 HTTP/1.1 -Host: localhost:3000 -Content-Type: text/plain -Content-Length: 12 - -test content - -=> - -HTTP/1.1 200 OK -Content-Length: 0 -``` +You can view the following OpenAPI specifications for each API version: -### GET /wait ++ v1 ([raw](https://github.com/bengreenier/webrtc-signal-http/blob/master/swagger-v1.yml) or [hosted](https://rebilly.github.io/ReDoc/?url=https://raw.githubusercontent.com/bengreenier/webrtc-signal-http/master/swagger-v1.yml)) ++ v2 ([raw](https://github.com/bengreenier/webrtc-signal-http/blob/master/swagger-v2.yml) or [hosted](https://rebilly.github.io/ReDoc/?url=https://raw.githubusercontent.com/bengreenier/webrtc-signal-http/master/swagger-v2.yml)) -> Takes `peer_id` query parameter - -Provides a mechanism for simulated server push, using vanilla http long polling. That is, the TCP socket behind this request will remain open to the server until there is content the server needs to send. In the event of a TCP timeout the client should reconnect. Messages that contain a `Pragma` value that matches the client `peer_id` are peer status updates and should be handled the same as the status update provided in the `GET /sign_in` response. `Content-Type` headers will not reflect the type of the original content. - -Peer status update: - -``` -GET http://localhost:3000/wait?peer_id=2 HTTP/1.1 -Host: localhost:3000 - -=> - -HTTP/1.1 200 OK -Pragma: 2 -Content-Type: text/html; charset=utf-8 -Content-Length: 18 - -test2,3,1 -test,2,0 -``` - -Peer message: - -``` -GET http://localhost:3000/wait?peer_id=2 HTTP/1.1 -Host: localhost:3000 - -=> - -HTTP/1.1 200 OK -Pragma: 3 -Content-Type: text/html; charset=utf-8 -Content-Length: 12 - -test content -``` +For example clients, see the following: ++ [webrtc-native-peerconnection](https://github.com/svn2github/webrtc/tree/master/talk/examples/peerconnection/client) (targets API v1) ## Extension API @@ -144,7 +58,19 @@ For example extensions, see the following: [Function] - takes a [SignalOpts](#signalopts) indicating if the bunyan logger should be enabled. __Returns__ an [express](https://expressjs.com) `router` object. -#### router.peerList +#### PeerList + +exposes the constructor for [PeerList](#peerlist) off the module root. + +#### version + +exposes the latest `Server` version, as will be set in the `Server` header. + +#### latestApiVersion + +exposes the latest api version that can be used via the `Accept` header. + +### router.peerList [Object] - can be used to retrieve a `PeerList` from the express `router`. __Returns__ a [PeerList](#peerlist) object. @@ -168,13 +94,17 @@ For example extensions, see the following: [Function] - takes nothing. Retrieves all the peer id's in the PeerList. __Returns__ an [Array] of id's (Numbers). +#### nextId + +[Function] - takes nothing. Returns the next valid peer id that can be handed out (but does not hand it out). __Returns__ a Number. + #### setPeerSocket [Function] - takes `id` (a Number), and `res` (a http.Response object). Updates a representation of the peer with a new response object for signaling. __Returns__ nothing. #### pushPeerData -[Function] - takes `srcId` (a Number), `destId` (a Number), `data` (an Object). Pushs arbitrary data onto a stack for a particular destination peer. __Returns__ nothing. +[Function] - takes `srcId` (a Number), `destId` (a Number), `data` (an Object), `dataMime` (a String). Pushs arbitrary data of a given MIME type onto a stack for a particular destination peer. __Returns__ nothing. #### popPeerData diff --git a/lib/index.js b/lib/index.js index 45e02bb..6f5a165 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,133 +1,208 @@ -const express = require('express') -const bodyParser = require('body-parser') -const expressBunyan = require('express-bunyan-logger') -const PeerList = require('./peer-list') - -module.exports = (opts) => { - const router = express.Router() - - if (opts.peerList && !(opts.peerList instanceof PeerList)) { - throw new Error('Invalid peerList') - } - - // store the peer list on the router - router.peerList = opts.peerList || new PeerList() - - // only use logging if configured to do so - if (opts.enableLogging) { - router.use(expressBunyan()) - } - - // abstracted peer message sender logic - // this will direct send if possible, otherwise - // it will buffer into the peerList - const sendPeerMessage = (srcId, destId, data) => { - // find the current peer - const peer = router.peerList.getPeer(destId) - - if (peer.status()) { - peer.res - .status(200) - .set('Pragma', srcId) - .send(data) - } - // otherwise we buffer - else { - router.peerList.pushPeerData(srcId, destId, data) - } - } - - router.get('/sign_in', (req, res) => { - if (!req.query.peer_name) { - return res.status(400).end() - } - - // add the peer - const peerId = router.peerList.addPeer(req.query.peer_name, res) - const peerListStr = router.peerList.format() - - // send back the list of peers - res.status(200) - .set('Pragma', peerId) - .set('Content-Type', 'text/plain') - .send(peerListStr) - - // send an updated peer list to all peers - router.peerList.getPeerIds().filter(id => id != peerId).forEach((id) => { - // updated peer lists must always appear to come from - // "ourselves", namely the srcId == destId - sendPeerMessage(id, id, peerListStr) - }) - }) - - router.post('/message', - bodyParser.text(), - bodyParser.urlencoded({ extended: false }), - (req, res) => { - - if (!req.query.peer_id || - !req.query.to) { - return res.status(400).end() - } - - // find the current peer - const peer = router.peerList.getPeer(req.query.to) - - if (!peer) { - return res.status(404).end() - } - - // send data to the peer - // (this will write to the `to` socket, or buffer if needed) - sendPeerMessage(req.query.peer_id, req.query.to, req.body) - - // whether we send directly or buffer we tell the sender everything is 'OK' - res.status(200).end() - } - ) - - router.get('/wait', (req, res) => { - if (!req.query.peer_id) { - return res.status(400).end() - } - - const pop = router.peerList.popPeerData(req.query.peer_id) - - // if we have data to send, just send it now - if (pop) { - return res.status(200) - .set('Pragma', pop.srcId) - .send(pop.data) - } - // otherwise, capture the socket so we can write to it later - else { - // set the socket for the given peer and let it hang - // this is the critical piece that let's us send data - // using 'push'-ish technology - router.peerList.setPeerSocket(req.query.peer_id, res) - } - }) - - router.get('/sign_out', (req, res) => { - if (!req.query.peer_id) { - return res.status(400).end() - } - - // remove the peer - router.peerList.removePeer(req.query.peer_id) - - // format the updated peerList - const peerListStr = router.peerList.format() - - // send an updated peer list to all peers - router.peerList.getPeerIds().forEach((id) => { - // updated peer lists must always appear to come from - // "ourselves", namely the srcId == destId - sendPeerMessage(id, id, peerListStr) - }) - - res.status(200).end() - }) - - return router -} \ No newline at end of file +const express = require('express') +const bodyParser = require('body-parser') +const expressBunyan = require('express-bunyan-logger') +const PeerList = require('./peer-list') +const pkgJson = require('../package.json') +const serverVersion = `WebRtcSignalHttp/${pkgJson.version}` +const latestApiVersion = 2 + +const defaultExport = (opts) => { + if (opts.peerList && !(opts.peerList instanceof PeerList)) { + throw new Error('Invalid peerList') + } + + const router = express.Router() + + // determine which API the request is targeting + router.use((req, res, next) => { + const acceptHeader = req.get('Accept') + const versionRegex = /application\/vnd\.webrtc-signal\.([0-9]+)/g + + // default version is 1 + req.apiVersion = 1 + + if (acceptHeader) { + const versionMatch = versionRegex.exec(acceptHeader) + const version = versionMatch[1] + + if (version) { + req.apiVersion = Number.parseInt(version) + } + } + + // unsupported api version, fail + if (req.apiVersion > latestApiVersion) { + return res.status(400).send({error: 'invalid api version'}) + } + + next() + }) + + // set all the service-constant headers that need to be present + router.use((req, res, next) => { + res.set('Connection', 'close') + res.set('Server', serverVersion) + res.set('Access-Control-Allow-Credentials', 'true') + res.set('Access-Control-Allow-Headers', 'Accept, Content-Type, Content-Length, Connection, Cache-Control') + res.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') + res.set('Access-Control-Allow-Origin', '*') + res.set('Access-Control-Expose-Headers', 'Accept, Content-Length') + res.set('Cache-Control', 'no-cache') + next() + }) + + // store the peer list on the router + router.peerList = opts.peerList || new PeerList() + + // only use logging if configured to do so + if (opts.enableLogging) { + router.use(expressBunyan()) + } + + // abstracted peer message sender logic + // this will direct send if possible, otherwise + // it will buffer into the peerList + const sendPeerMessage = (srcId, destId, data, dataMime) => { + // find the current peer + const peer = router.peerList.getPeer(destId) + + if (peer.status()) { + peer.res + .status(200) + .set('Pragma', srcId) + .set('Content-Type', dataMime) + .send(data) + } + // otherwise we buffer + else { + router.peerList.pushPeerData(srcId, destId, data, dataMime) + } + } + + router.get('/sign_in', (req, res) => { + let peerName + + if (req.apiVersion == 1) { + peerName = Object.keys(req.query).map(k => k + '=' + req.query[k])[0] || 'peer_' + router.peerList.nextId() + } else if (req.apiVersion == 2) { + if (!req.query.peer_name) { + return res.status(400).end() + } + peerName = req.query.peer_name + } + + // add the peer + const peerId = router.peerList.addPeer(peerName, res) + const peerListStr = router.peerList.format() + + // send back the list of peers + res.status(200) + .set('Pragma', peerId) + .set('Content-Type', 'text/plain') + .send(peerListStr) + + // send an updated peer list to all peers + router.peerList.getPeerIds().filter(id => id != peerId).forEach((id) => { + // updated peer lists must always appear to come from + // "ourselves", namely the srcId == destId + sendPeerMessage(id, id, peerListStr, 'text/plain') + }) + }) + + router.post('/message', + bodyParser.raw({ type: '*/*' }), + (req, res) => { + + if (!req.query.peer_id || + !req.query.to) { + if (req.apiVersion == 1) { + return res.status(500).end() + } else if (req.apiVersion == 2) { + return res.status(400).end() + } + } + + // find the current peer + const peer = router.peerList.getPeer(req.query.to) + + if (!peer) { + if (req.apiVersion == 1) { + return res.status(500).end() + } else if (req.apiVersion == 2) { + return res.status(404).end() + } + } + + // send data to the peer + // (this will write to the `to` socket, or buffer if needed) + sendPeerMessage(req.query.peer_id, req.query.to, req.body, req.get('Content-Type') || 'text/html') + + // whether we send directly or buffer we tell the sender everything is 'OK' + res.status(200).end() + } + ) + + router.get('/wait', (req, res) => { + if (!req.query.peer_id) { + if (req.apiVersion == 1) { + return res.status(500).end() + } else if (req.apiVersion == 2) { + return res.status(400).end() + } + } + + const pop = router.peerList.popPeerData(req.query.peer_id) + + // if we have data to send, just send it now + if (pop) { + return res.status(200) + .set('Pragma', pop.srcId) + .set('Content-Type', pop.dataMime) + .send(pop.data) + } + // otherwise, capture the socket so we can write to it later + else { + // set the socket for the given peer and let it hang + // this is the critical piece that let's us send data + // using 'push'-ish technology + router.peerList.setPeerSocket(req.query.peer_id, res) + } + }) + + router.get('/sign_out', (req, res) => { + if (!req.query.peer_id) { + if (req.apiVersion == 1) { + return res.status(500).end() + } else if (req.apiVersion == 2) { + return res.status(400).end() + } + } + + // remove the peer + router.peerList.removePeer(req.query.peer_id) + + // format the updated peerList + const peerListStr = router.peerList.format() + + // send an updated peer list to all peers + router.peerList.getPeerIds().forEach((id) => { + // updated peer lists must always appear to come from + // "ourselves", namely the srcId == destId + sendPeerMessage(id, id, peerListStr, 'text/plain') + }) + + res.status(200).end() + }) + + return router +} + +// expose a version +defaultExport.version = serverVersion +defaultExport.latestApiVersion = latestApiVersion + +// expose PeerList +defaultExport.PeerList = PeerList + +// export our behavior +module.exports = defaultExport \ No newline at end of file diff --git a/lib/peer-list.js b/lib/peer-list.js index 0fe7ddf..3e98b3f 100644 --- a/lib/peer-list.js +++ b/lib/peer-list.js @@ -31,17 +31,22 @@ module.exports = class PeerList { return Object.keys(this._peers) } + nextId() { + return this._nextPeerId + } + setPeerSocket(id, res) { if (this._peers[id]) { this._peers[id].res = res } } - pushPeerData(srcId, destId, data) { + pushPeerData(srcId, destId, data, dataMime) { if (this._peers[destId] && !this._peers[destId].status()) { this._peers[destId].buffer.push({ srcId: srcId, - data: data + data: data, + dataMime: dataMime }) } } diff --git a/package.json b/package.json index 9dd2643..bc8e4da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webrtc-signal-http", - "version": "1.1.1", + "version": "2.0.0-alpha.1", "description": "opinionated webrtc signal provider using http as a protocol", "main": "lib/index.js", "directories": { diff --git a/swagger-v1.yml b/swagger-v1.yml new file mode 100644 index 0000000..9c25d73 --- /dev/null +++ b/swagger-v1.yml @@ -0,0 +1,136 @@ +swagger: '2.0' +info: + description: "opinionated webrtc signal provider using `http` as a protocol + \n + ![logo + gif](https://github.com/bengreenier/webrtc-signal-http/raw/master/readme_example.gif) + \n\n + We needed a simple to use, easy to extend [WebRTC](https://webrtc.org/) + signaling server that communicated over regular old `HTTP/1.1` for + [3dtoolkit](https://github.com/catalystcode/3dtoolkit) - this is it. It's + designed to mirror [the WebRTC example + server](https://github.com/svn2github/webrtc/tree/master/talk/examples/peerconnection/server) + at an API level, while allowing developers to consume and extend the base + functionality. + \n\n + __This is the documentation for the v1 API__" + version: 2.0.0 + title: webrtc-signal-http (v1 API) + license: + name: MIT + url: 'https://opensource.org/licenses/MIT' +schemes: + - http +paths: + /sign_in: + get: + summary: Indicates a peer is available to peer with + description: >- + Indicates a peer is available to peer with. The response will contain + the unique peer_id assigned to the caller in the `Pragma` header, and a + `csv` formatted list of peers in the `body`. + operationId: addPeer + produces: + - text/plain + parameters: + - name: '' + in: query + description: >- + a friendly description of the client for user identification + purposes. if not provided, one will be generated + required: false + type: string + responses: + '200': + description: successful response + schema: + $ref: '#/definitions/PeerList' + '500': + description: error occured + /sign_out: + get: + summary: Indicates a peer is no longer available to peer with + description: >- + Indicates a peer is no longer available to peer with. It is expected + this method be called when a peer is no longer intending to use + signaling. The response will be empty. + operationId: removePeer + parameters: + - in: query + name: peer_id + description: the unique id of the client + required: true + type: string + responses: + '200': + description: successful response + schema: + $ref: '#/definitions/PeerList' + '500': + description: error occured + /wait: + get: + summary: >- + Provides a mechanism for simulated server push, using vanilla http long + polling + description: >- + Provides a mechanism for simulated server push, using vanilla http long + polling. That is, the TCP socket behind this request will remain open to + the server until there is content the server needs to send. In the event + of a TCP timeout the client should reconnect. Messages that contain a + `Pragma` value that matches the client `peer_id` are peer status updates + and should be handled the same as the status update provided in the `GET + /sign_in` response. `Content-Type` headers will not reflect the type of + the original content. + operationId: setPeerSocket + parameters: + - in: query + name: peer_id + description: the unique id of the client + required: true + type: string + responses: + '200': + description: successful response + schema: + $ref: '#/definitions/PeerList' + '500': + description: error occured + /message: + post: + summary: Provides a messaging mechanism for one peer to send data to another + description: >- + Provides a messaging mechanism for one peer to send data to another. + There are no requirements around the type of data that can be sent. The + message may be buffered until the receiving peer connects to the + service. The response will be empty. + operationId: sendMessage + parameters: + - in: query + name: peer_id + description: the unique id of the client + required: true + type: string + - in: query + name: to + description: the unique id of the peer we wish to send data to + required: true + type: string + responses: + '200': + description: successful response + schema: + $ref: '#/definitions/PeerList' + '500': + description: error occured +definitions: + PeerList: + type: string + description: >- + csv representation of a collection of peers. Each line indicates the peer + name, the peer id, and a 1 or a 0 representing the connection status of + the peer + example: 'testPeer,1,1\notherPeer,2,1\nthirdPeer,3,0' +externalDocs: + description: Find out more on Github + url: 'https://github.com/bengreenier/webrtc-signal-http' diff --git a/swagger.yml b/swagger-v2.yml similarity index 91% rename from swagger.yml rename to swagger-v2.yml index f220533..70fd865 100644 --- a/swagger.yml +++ b/swagger-v2.yml @@ -1,134 +1,138 @@ -swagger: '2.0' -info: - description: "opinionated webrtc signal provider using `http` as a protocol - \n - ![logo - gif](https://github.com/bengreenier/webrtc-signal-http/raw/master/readme_example.gif) - \n\n - We needed a simple to use, easy to extend [WebRTC](https://webrtc.org/) - signaling server that communicated over regular old `HTTP/1.1` for - [3dtoolkit](https://github.com/catalystcode/3dtoolkit) - this is it. It's - designed to mirror [the WebRTC example - server](https://github.com/svn2github/webrtc/tree/master/talk/examples/peerconnection/server) - at an API level, while allowing developers to consume and extend the base - functionality." - version: 1.0.0 - title: webrtc-signal-http - license: - name: MIT - url: 'https://opensource.org/licenses/MIT' -schemes: - - http -paths: - /sign_in: - get: - summary: Indicates a peer is available to peer with - description: >- - Indicates a peer is available to peer with. The response will contain - the unique peer_id assigned to the caller in the `Pragma` header, and a - `csv` formatted list of peers in the `body`. - operationId: addPeer - produces: - - text/plain - parameters: - - name: peer_name - in: query - description: >- - a friendly description of the client for user identification - purposes - required: true - type: string - responses: - '200': - description: successful response - schema: - $ref: '#/definitions/PeerList' - '400': - description: missing peer_name - /sign_out: - get: - summary: Indicates a peer is no longer available to peer with - description: >- - Indicates a peer is no longer available to peer with. It is expected - this method be called when a peer is no longer intending to use - signaling. The response will be empty. - operationId: removePeer - parameters: - - in: query - name: peer_id - description: the unique id of the client - required: true - type: string - responses: - '200': - description: successful response - schema: - $ref: '#/definitions/PeerList' - '400': - description: invalid peer_name - /wait: - get: - summary: >- - Provides a mechanism for simulated server push, using vanilla http long - polling - description: >- - Provides a mechanism for simulated server push, using vanilla http long - polling. That is, the TCP socket behind this request will remain open to - the server until there is content the server needs to send. In the event - of a TCP timeout the client should reconnect. Messages that contain a - `Pragma` value that matches the client `peer_id` are peer status updates - and should be handled the same as the status update provided in the `GET - /sign_in` response. `Content-Type` headers will not reflect the type of - the original content. - operationId: setPeerSocket - parameters: - - in: query - name: peer_id - description: the unique id of the client - required: true - type: string - responses: - '200': - description: successful response - schema: - $ref: '#/definitions/PeerList' - '400': - description: invalid peer_name - /message: - post: - summary: Provides a messaging mechanism for one peer to send data to another - description: >- - Provides a messaging mechanism for one peer to send data to another. - There are no requirements around the type of data that can be sent. The - message may be buffered until the receiving peer connects to the - service. The response will be empty. - operationId: sendMessage - parameters: - - in: query - name: peer_id - description: the unique id of the client - required: true - type: string - - in: query - name: to - description: the unique id of the peer we wish to send data to - required: true - type: string - responses: - '200': - description: successful response - schema: - $ref: '#/definitions/PeerList' - '400': - description: invalid peer_name -definitions: - PeerList: - type: string - description: >- - csv representation of a collection of peers. Each line indicates the peer - name, the peer id, and a 1 or a 0 representing the connection status of - the peer - example: 'testPeer,1,1\notherPeer,2,1\nthirdPeer,3,0' -externalDocs: - description: Find out more on Github - url: 'https://github.com/bengreenier/webrtc-signal-http' +swagger: '2.0' +info: + description: "opinionated webrtc signal provider using `http` as a protocol + \n + ![logo + gif](https://github.com/bengreenier/webrtc-signal-http/raw/master/readme_example.gif) + \n\n + We needed a simple to use, easy to extend [WebRTC](https://webrtc.org/) + signaling server that communicated over regular old `HTTP/1.1` for + [3dtoolkit](https://github.com/catalystcode/3dtoolkit) - this is it. It's + designed to mirror [the WebRTC example + server](https://github.com/svn2github/webrtc/tree/master/talk/examples/peerconnection/server) + at an API level, while allowing developers to consume and extend the base + functionality. + \n\n + __This is the documentation for the v2 API__" + version: 2.0.0 + title: webrtc-signal-http (v2 API) + license: + name: MIT + url: 'https://opensource.org/licenses/MIT' +schemes: + - http +paths: + /sign_in: + get: + summary: Indicates a peer is available to peer with + description: >- + Indicates a peer is available to peer with. The response will contain + the unique peer_id assigned to the caller in the `Pragma` header, and a + `csv` formatted list of peers in the `body`. + operationId: addPeer + produces: + - text/plain + parameters: + - name: peer_name + in: query + description: >- + a friendly description of the client for user identification + purposes + required: true + type: string + responses: + '200': + description: successful response + schema: + $ref: '#/definitions/PeerList' + '400': + description: missing peer_name + /sign_out: + get: + summary: Indicates a peer is no longer available to peer with + description: >- + Indicates a peer is no longer available to peer with. It is expected + this method be called when a peer is no longer intending to use + signaling. The response will be empty. + operationId: removePeer + parameters: + - in: query + name: peer_id + description: the unique id of the client + required: true + type: string + responses: + '200': + description: successful response + schema: + $ref: '#/definitions/PeerList' + '400': + description: invalid request + /wait: + get: + summary: >- + Provides a mechanism for simulated server push, using vanilla http long + polling + description: >- + Provides a mechanism for simulated server push, using vanilla http long + polling. That is, the TCP socket behind this request will remain open to + the server until there is content the server needs to send. In the event + of a TCP timeout the client should reconnect. Messages that contain a + `Pragma` value that matches the client `peer_id` are peer status updates + and should be handled the same as the status update provided in the `GET + /sign_in` response. `Content-Type` headers will not reflect the type of + the original content. + operationId: setPeerSocket + parameters: + - in: query + name: peer_id + description: the unique id of the client + required: true + type: string + responses: + '200': + description: successful response + schema: + $ref: '#/definitions/PeerList' + '400': + description: invalid request + /message: + post: + summary: Provides a messaging mechanism for one peer to send data to another + description: >- + Provides a messaging mechanism for one peer to send data to another. + There are no requirements around the type of data that can be sent. The + message may be buffered until the receiving peer connects to the + service. The response will be empty. + operationId: sendMessage + parameters: + - in: query + name: peer_id + description: the unique id of the client + required: true + type: string + - in: query + name: to + description: the unique id of the peer we wish to send data to + required: true + type: string + responses: + '200': + description: successful response + schema: + $ref: '#/definitions/PeerList' + '400': + description: invalid request + '404': + description: invalid peer (not found) +definitions: + PeerList: + type: string + description: >- + csv representation of a collection of peers. Each line indicates the peer + name, the peer id, and a 1 or a 0 representing the connection status of + the peer + example: 'testPeer,1,1\notherPeer,2,1\nthirdPeer,3,0' +externalDocs: + description: Find out more on Github + url: 'https://github.com/bengreenier/webrtc-signal-http' diff --git a/test/basic.js b/test/basic.js index da0b4bf..2797c9c 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,295 +1,348 @@ -const assert = require('assert') -const request = require('supertest') -const express = require('express') -const signalRouter = require('../lib') -const PeerList = require('../lib/peer-list') -const Peer = require('../lib/peer') - -const appCreator = (enableLogging) => { - const router = signalRouter({ - enableLogging: enableLogging - }) - const app = express() - - app.use(router) - - // for testing, we also further expose peerList - app.peerList = router.peerList - - return app -} - -describe('webrtc-signal-http', () => { - describe('creator', () => { - it('should validate peerList', () => { - assert.throws(() => { - signalRouter({ - peerList: {} - }) - }, /peerList/) - }) - }) - - describe('http', () => { - it('should support sign_in', (done) => { - const expectedPeerName = 'myName' - - request(appCreator(false)) - .get(`/sign_in?peer_name=${expectedPeerName}`) - .expect('Content-Type', /text\/plain/) - .expect(200, `${expectedPeerName},1,1`, done) - }) - - it('should support multiple sign_in', (done) => { - const expectedPeerName = 'myName' - const expectedPeerName2 = 'myOtherName' - - const test = request(appCreator(false)) - - test - .get(`/sign_in?peer_name=${expectedPeerName}`) - .expect('Content-Type', /text\/plain/) - .expect(200, `${expectedPeerName},1,1`) - .then(() => { - return test - .get(`/sign_in?peer_name=${expectedPeerName2}`) - .expect('Content-Type', /text\/plain/) - // the order here is significant, recent clients should be listed first - // expectedPeerName has a status 0, because supertest doesn't keep TCP open - .expect(200, `${expectedPeerName2},2,1\n${expectedPeerName},1,0`) - .then(() => { /* on success, empty the chainable promise result */ }) - }) - .then(done, done) - }) - - it('should support /message posting (buffered)', (done) => { - const app = appCreator(false) - - const senderPeerId = app.peerList.addPeer('sendPeer', {}) - const receiverPeerId = app.peerList.addPeer('receivePeer', {}) - - const test = request(app) - - test.post(`/message?peer_id=${senderPeerId}&to=${receiverPeerId}`) - .set('Content-Type', 'text/plain') - .send('testMessage') - .expect(200, '') - .then(() => { - return test.get(`/wait?peer_id=${receiverPeerId}`) - .expect('Pragma', `${senderPeerId}`) - .expect(200, 'testMessage') - .then(() => { /* on success, empty the chainable promise result */ }) - }).then(done, done) - }) - - it('should support /message posting (un-buffered)', (done) => { - const app = appCreator(false) - - // simulate adding two peers - const senderPeerId = app.peerList.addPeer('sendPeer', {}) - const receiverPeerId = app.peerList.addPeer('receivePeer', {}) - - const test = request(app) - - Promise.all([ - // start making the wait call - test.get(`/wait?peer_id=${receiverPeerId}`) - .expect('Pragma', `${senderPeerId}`) - .expect(200, 'testMessage') - .then(() => { /* on success, empty the chainable promise result */ }), - - // start waiting 500ms, then start making the message call - new Promise((resolve, reject) => { setTimeout(resolve, 500) }).then(() => { - return test.post(`/message?peer_id=${senderPeerId}&to=${receiverPeerId}`) - .set('Content-Type', 'text/plain') - .send('testMessage') - .expect(200) - .then(() => { /* on success, empty the chainable promise result */ }) - }) - ]).then(() => { /* on success, empty the chainable promise result */ }).then(done, done) - }) - - it('should support /sign_out', (done) => { - const app = appCreator(false) - - // simulate adding two peers - const firstPeerId = app.peerList.addPeer('firstPeer', {}) - const secondPeerId = app.peerList.addPeer('secondPeer', {}) - - const test = request(app) - - test - .get(`/sign_out?peer_id=${firstPeerId}`) - .expect(200) - .then(() => { - assert.deepEqual(app.peerList.getPeerIds(), [secondPeerId]) - }) - .then(done, done) - }) - - it('should support sign_in notifications', (done) => { - const app = appCreator(false) - - // simulate adding two peers - const firstPeerId = app.peerList.addPeer('firstPeer', {}) - - const test = request(app) - - Promise.all([ - // start making the wait call - test.get(`/wait?peer_id=${firstPeerId}`) - .expect('Pragma', `${firstPeerId}`) - .expect(200, 'secondPeer,2,1\nfirstPeer,1,1') - .then(() => { /* on success, empty the chainable promise result */ }), - - // start waiting 500ms, then start making the sign_in call - new Promise((resolve, reject) => { setTimeout(resolve, 500) }).then(() => { - return test.get(`/sign_in?peer_name=secondPeer`) - .expect(200) - .then(() => { /* on success, empty the chainable promise result */ }) - }) - ]).then(() => { /* on success, empty the chainable promise result */ }).then(done, done) - }) - - it('should support sign_out notifications', (done) => { - const app = appCreator(false) - - // simulate adding two peers - const firstPeerId = app.peerList.addPeer('firstPeer', {}) - const secondPeerId = app.peerList.addPeer('secondPeer', {}) - - const test = request(app) - - Promise.all([ - // start making the wait call - test.get(`/wait?peer_id=${firstPeerId}`) - .expect('Pragma', `${firstPeerId}`) - .expect(200, 'firstPeer,1,1') - .then(() => { /* on success, empty the chainable promise result */ }), - - // start waiting 500ms, then start making the sign_out call - new Promise((resolve, reject) => { setTimeout(resolve, 500) }).then(() => { - return test.get(`/sign_out?peer_id=${secondPeerId}`) - .expect(200) - .then(() => { /* on success, empty the chainable promise result */ }) - }) - ]).then(() => { /* on success, empty the chainable promise result */ }).then(done, done) - }) - }) - - describe('PeerList', () => { - it('should support adding peers', () => { - const instance = new PeerList() - - const id = instance.addPeer('test', {obj: true}) - const peer = instance.getPeer(id) - - const internalMap = instance._peers - - assert.equal(id, 1) - assert.equal(peer.name, 'test') - assert.equal(peer.id, id) - assert.equal(peer.status(), 0) - assert.equal(internalMap[id], peer) - assert.equal(Object.keys(internalMap), 1) - }) - - it('should support removing peers', () => { - const instance = new PeerList() - - const id = instance.addPeer('test', {obj: true}) - - instance.removePeer(id) - - const internalMap = instance._peers - - assert.equal(Object.keys(internalMap), 0) - }) - - it('should support socket replacement', () => { - const expectedSocket = {obj: true} - const expectedSocket2 = {obj: false} - const instance = new PeerList() - - const id = instance.addPeer('test', expectedSocket) - const peer = instance.getPeer(id) - - assert.equal(peer.res, expectedSocket) - - instance.setPeerSocket(id, expectedSocket2) - - assert.equal(peer.res, expectedSocket2) - }) - - it('should support push/pop peerData', () => { - const expectedData = {value: 1} - const expectedDataSrcId = 2 - const instance = new PeerList() - - const id = instance.addPeer('test', {}) - - assert.equal(instance.popPeerData(id), null) - - instance.pushPeerData(expectedDataSrcId, id, expectedData) - - assert.deepEqual(instance.popPeerData(id), {srcId: expectedDataSrcId, data: expectedData}) - assert.equal(instance.popPeerData(id), null) - }) - - it('should support formatting', () => { - const instance = new PeerList() - - instance.addPeer('test', {obj: true}) - - assert.equal(instance.format(), 'test,1,0') - - instance.addPeer('test2', {obj: true}) - - assert.equal(instance.format(), 'test2,2,0\ntest,1,0') - }) - }) - - describe('Peer', () => { - it('should have (mostly) immutable properties', () => { - const expectedName = "testName" - const expectedId = 1 - const instance = new Peer(expectedName, expectedId) - - assert.equal(instance.name, expectedName) - assert.equal(instance.id, expectedId) - - assert.throws(() => { - instance.name = "newName" - }) - assert.throws(() => { - instance.id = 50 - }) - assert.throws(() => { - instance.buffer = [] - }) - assert.doesNotThrow(() => { - instance.res = {} - }) - }) - - it('should have status logic', () => { - const instance = new Peer(null, null) - - assert.ok(instance.status() === false) - - instance.res = { - socket: null - } - - assert.ok(instance.status() === false) - - instance.res = { - socket: { - writable: true - } - } - - assert.ok(instance.status() === true) - }) - }) +const assert = require('assert') +const request = require('supertest') +const express = require('express') +const signalRouter = require('../lib') +const PeerList = require('../lib/peer-list') +const Peer = require('../lib/peer') + +const appCreator = (enableLogging) => { + const router = signalRouter({ + enableLogging: enableLogging + }) + const app = express() + + app.use(router) + + // for testing, we also further expose peerList + app.peerList = router.peerList + + return app +} + +describe('webrtc-signal-http', () => { + describe('creator', () => { + it('should validate peerList', () => { + assert.throws(() => { + signalRouter({ + peerList: {} + }) + }, /peerList/) + }) + + it('should expose PeerList', () => { + assert.equal(signalRouter.PeerList, PeerList) + }) + + it('should expose versions', () => { + assert.ok(signalRouter.version) + assert.ok(signalRouter.latestApiVersion) + }) + }) + + describe('http', () => { + it('should fail unsupported api versions', (done) => { + + request(appCreator(false)) + .get(`/sign_in`) + .set('Accept', `application/vnd.webrtc-signal.${signalRouter.latestApiVersion + 1}`) + .expect(400, {error: 'invalid api version'}, done) + }) + + it('should set specific headers', (done) => { + const expectedPeerName = 'myName' + + request(appCreator(false)) + .get(`/sign_in?peer_name=${expectedPeerName}`) + .expect('Connection', 'close') + .expect('Server', signalRouter.version) + .expect('Access-Control-Allow-Credentials', 'true') + .expect('Access-Control-Allow-Headers', 'Accept, Content-Type, Content-Length, Connection, Cache-Control') + .expect('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') + .expect('Access-Control-Allow-Origin', '*') + .expect('Access-Control-Expose-Headers', 'Accept, Content-Length') + .expect('Cache-Control', 'no-cache') + .expect(200, done) + }) + + it('should support /message posting (buffered)', (done) => { + const app = appCreator(false) + + const senderPeerId = app.peerList.addPeer('sendPeer', {}) + const receiverPeerId = app.peerList.addPeer('receivePeer', {}) + + const test = request(app) + + test.post(`/message?peer_id=${senderPeerId}&to=${receiverPeerId}`) + .set('Content-Type', 'text/plain') + .send('testMessage') + .expect(200, '') + .then(() => { + return test.get(`/wait?peer_id=${receiverPeerId}`) + .expect('Pragma', `${senderPeerId}`) + .expect(200, 'testMessage') + .then(() => { /* on success, empty the chainable promise result */ }) + }).then(done, done) + }) + + it('should support /sign_out', (done) => { + const app = appCreator(false) + + // simulate adding two peers + const firstPeerId = app.peerList.addPeer('firstPeer', {}) + const secondPeerId = app.peerList.addPeer('secondPeer', {}) + + const test = request(app) + + test + .get(`/sign_out?peer_id=${firstPeerId}`) + .expect(200) + .then(() => { + assert.deepEqual(app.peerList.getPeerIds(), [secondPeerId]) + }) + .then(done, done) + }) + + it('should support sign_out notifications', (done) => { + const app = appCreator(false) + + // simulate adding two peers + const firstPeerId = app.peerList.addPeer('firstPeer', {}) + const secondPeerId = app.peerList.addPeer('secondPeer', {}) + + const test = request(app) + + Promise.all([ + // start making the wait call + test.get(`/wait?peer_id=${firstPeerId}`) + .expect('Pragma', `${firstPeerId}`) + .expect(200, 'firstPeer,1,1') + .then(() => { /* on success, empty the chainable promise result */ }), + + // start waiting 500ms, then start making the sign_out call + new Promise((resolve, reject) => { setTimeout(resolve, 500) }).then(() => { + return test.get(`/sign_out?peer_id=${secondPeerId}`) + .expect(200) + .then(() => { /* on success, empty the chainable promise result */ }) + }) + ]).then(() => { /* on success, empty the chainable promise result */ }).then(done, done) + }) + + describe('v1', () => { + it('should support sign_in', (done) => { + + request(appCreator(false)) + .get(`/sign_in`) + .expect('Content-Type', /text\/plain/) + .expect('Pragma', '1') + .expect(200, `peer_1,1,1`, done) + }) + }) + + describe('v2', () => { + it('should support sign_in', (done) => { + const expectedPeerName = 'myName' + + request(appCreator(false)) + .get(`/sign_in?peer_name=${expectedPeerName}`) + .set('Accept', 'application/vnd.webrtc-signal.2') + .expect('Content-Type', /text\/plain/) + .expect('Pragma', '1') + .expect(200, `${expectedPeerName},1,1`, done) + }) + + it('should support multiple sign_in', (done) => { + const expectedPeerName = 'myName' + const expectedPeerName2 = 'myOtherName' + + const test = request(appCreator(false)) + + test + .get(`/sign_in?peer_name=${expectedPeerName}`) + .set('Accept', 'application/vnd.webrtc-signal.2') + .expect('Content-Type', /text\/plain/) + .expect(200, `${expectedPeerName},1,1`) + .then(() => { + return test + .get(`/sign_in?peer_name=${expectedPeerName2}`) + .set('Accept', 'application/vnd.webrtc-signal.2') + .expect('Content-Type', /text\/plain/) + // the order here is significant, recent clients should be listed first + // expectedPeerName has a status 0, because supertest doesn't keep TCP open + .expect(200, `${expectedPeerName2},2,1\n${expectedPeerName},1,0`) + .then(() => { /* on success, empty the chainable promise result */ }) + }) + .then(done, done) + }) + + it('should support /message posting (un-buffered)', (done) => { + const app = appCreator(false) + + // simulate adding two peers + const senderPeerId = app.peerList.addPeer('sendPeer', {}) + const receiverPeerId = app.peerList.addPeer('receivePeer', {}) + + const test = request(app) + + Promise.all([ + // start making the wait call + test.get(`/wait?peer_id=${receiverPeerId}`) + .expect('Pragma', `${senderPeerId}`) + .expect('Content-Type', 'application/vnd.unit.test+text; charset=utf-8') + .expect(200, 'testMessage') + .then(() => { /* on success, empty the chainable promise result */ }), + + // start waiting 500ms, then start making the message call + new Promise((resolve, reject) => { setTimeout(resolve, 500) }).then(() => { + return test.post(`/message?peer_id=${senderPeerId}&to=${receiverPeerId}`) + .set('Content-Type', 'application/vnd.unit.test+text; charset=utf-8') + .send('testMessage') + .expect(200) + .then(() => { /* on success, empty the chainable promise result */ }) + }) + ]).then(() => { /* on success, empty the chainable promise result */ }).then(done, done) + }) + + it('should support sign_in notifications', (done) => { + const app = appCreator(false) + + // simulate adding two peers + const firstPeerId = app.peerList.addPeer('firstPeer', {}) + + const test = request(app) + + Promise.all([ + // start making the wait call + test.get(`/wait?peer_id=${firstPeerId}`) + .expect('Pragma', `${firstPeerId}`) + .expect(200, 'secondPeer,2,1\nfirstPeer,1,1') + .then(() => { /* on success, empty the chainable promise result */ }), + + // start waiting 500ms, then start making the sign_in call + new Promise((resolve, reject) => { setTimeout(resolve, 500) }).then(() => { + return test.get(`/sign_in?peer_name=secondPeer`) + .set('Accept', 'application/vnd.webrtc-signal.2') + .expect(200) + .then(() => { /* on success, empty the chainable promise result */ }) + }) + ]).then(() => { /* on success, empty the chainable promise result */ }).then(done, done) + }) + }) + }) + + describe('PeerList', () => { + it('should support adding peers', () => { + const instance = new PeerList() + + const id = instance.addPeer('test', {obj: true}) + const peer = instance.getPeer(id) + + const internalMap = instance._peers + + assert.equal(id, 1) + assert.equal(peer.name, 'test') + assert.equal(peer.id, id) + assert.equal(peer.status(), 0) + assert.equal(internalMap[id], peer) + assert.equal(Object.keys(internalMap), 1) + }) + + it('should support removing peers', () => { + const instance = new PeerList() + + const id = instance.addPeer('test', {obj: true}) + + instance.removePeer(id) + + const internalMap = instance._peers + + assert.equal(Object.keys(internalMap), 0) + }) + + it('should support socket replacement', () => { + const expectedSocket = {obj: true} + const expectedSocket2 = {obj: false} + const instance = new PeerList() + + const id = instance.addPeer('test', expectedSocket) + const peer = instance.getPeer(id) + + assert.equal(peer.res, expectedSocket) + + instance.setPeerSocket(id, expectedSocket2) + + assert.equal(peer.res, expectedSocket2) + }) + + it('should support push/pop peerData', () => { + const expectedData = {value: 1} + const expectedDataSrcId = 2 + const expectedMime = 'mime' + const instance = new PeerList() + + const id = instance.addPeer('test', {}) + + assert.equal(instance.popPeerData(id), null) + + instance.pushPeerData(expectedDataSrcId, id, expectedData, expectedMime) + + assert.deepEqual(instance.popPeerData(id), {srcId: expectedDataSrcId, data: expectedData, dataMime: expectedMime}) + assert.equal(instance.popPeerData(id), null) + }) + + it('should support formatting', () => { + const instance = new PeerList() + + instance.addPeer('test', {obj: true}) + + assert.equal(instance.format(), 'test,1,0') + + instance.addPeer('test2', {obj: true}) + + assert.equal(instance.format(), 'test2,2,0\ntest,1,0') + }) + }) + + describe('Peer', () => { + it('should have (mostly) immutable properties', () => { + const expectedName = "testName" + const expectedId = 1 + const instance = new Peer(expectedName, expectedId) + + assert.equal(instance.name, expectedName) + assert.equal(instance.id, expectedId) + + assert.throws(() => { + instance.name = "newName" + }) + assert.throws(() => { + instance.id = 50 + }) + assert.throws(() => { + instance.buffer = [] + }) + assert.doesNotThrow(() => { + instance.res = {} + }) + }) + + it('should have status logic', () => { + const instance = new Peer(null, null) + + assert.ok(instance.status() === false) + + instance.res = { + socket: null + } + + assert.ok(instance.status() === false) + + instance.res = { + socket: { + writable: true + } + } + + assert.ok(instance.status() === true) + }) + }) }) \ No newline at end of file