├── .github ├── stale.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .npmrc ├── .prettierrc.yaml ├── LICENSE ├── README.md ├── appveyor.yml ├── compose.js ├── index.js ├── package.json ├── plugins ├── net.js ├── noauth.js ├── onion.js ├── shs.js ├── unix-socket.js └── ws.js └── test ├── async-server-close.js ├── multi.js └── plugs.js /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | 12 | jobs: 13 | test: 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, windows-latest, macos-latest] 19 | node-version: [12.x, 14.x, 16.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - name: npm test 29 | run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: false 2 | singleQuote: true 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 'Dominic Tarr' 2 | 3 | The MIT License (MIT) 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and 7 | associated documentation files (the "Software"), to 8 | deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, 10 | merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom 12 | the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 22 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # multiserver 2 | 3 | A single interface that can work with multiple protocols, 4 | and multiple transforms of those protocols (eg, security layer) 5 | 6 | ## motivation 7 | 8 | Developing a p2p system is hard. Especially hard is upgrading protocol layers. 9 | The contemporary approach is to [update code via a backdoor](https://whispersystems.org/blog/the-ecosystem-is-moving/), 10 | but as easily as security can be added, [it can be taken away](https://nakamotoinstitute.org/trusted-third-parties/). 11 | 12 | Before you can have a protocol, you need a connection between peers. 13 | That connection is over some form of network transport, 14 | probably encrypted with some encryption scheme, possibly 15 | compression or other layers too. 16 | 17 | Usually, two peers connect over a standard networking transport 18 | (probably tcp) then they have a negotiation to decide 19 | what the next layer (of encryption, for example) should be. 20 | This allows protocol implementators to roll out improved 21 | versions of the encryption protocol. However, it does 22 | not allow them to upgrade the negotiation protocol! 23 | If a negotiation protocol has a vulnerability it's much 24 | harder to fix, and since the negotiation needs to be unencrypted, 25 | it tends to reveal a lot about program the server is running. 26 | [in my opinion, it's time to try a different way.](https://github.com/ipfs/go-ipfs/pull/34) 27 | 28 | Some HTTP APIs provide upgradability in a better, simpler way by 29 | putting a version number within the url. A new version of 30 | the API can then be used without touching the old one at all. 31 | 32 | multiserver adapts this approach to lower level protocols. 33 | Instead of negotiating which protocol to use, run multiple 34 | protocols side by side, and consider the protocol part of the address. 35 | 36 | Most network systems have some sort of address look up, 37 | there is peer identifier (such it's domain) and then 38 | a system that is queried to map that domain to the lower level 39 | network address (such as it's ip address, retrieved via a DNS (Domain Name System) request) 40 | To connect to a website secured with https, first 41 | you look up the domain via DNS, then connect to the server. 42 | Then start a tls connection to that server, in which 43 | a cyphersuite is negotiated, and a certificate is provided 44 | by the server. (this certifies that the server really 45 | owns that domain) 46 | 47 | If it was using multiserver, DNS would respond with a list of cyphersuites, 48 | (encoded as multiserver addresses) and then you'd connect directly to a server and start using the protocol, without negotiation. 49 | p2p systems like scuttlebutt also usually have a lookup, 50 | but usually mapping from a public key to an ip address. 51 | Since a look up is needed anyway, it's a good place 52 | to provide information about the protocol that server speaks! 53 | 54 | This enables you to do two things, upgrade and bridging. 55 | 56 | ### upgrade 57 | 58 | If a peer wants to upgrade from *weak* protocol 59 | to a *strong* one, they simply start serving *strong* via another port, 60 | and advertise that in the lookup system. 61 | Now peers that have support for *strong* can connect via that protocol. 62 | 63 | Once most peers have upgraded to strong, support for *weak* can be discontinued. 64 | 65 | This is just how some services (eg, github) have an API version 66 | in their URL scheme. It is now easy to use two different 67 | versions in parallel. later, they can close down the old API. 68 | 69 | ``` js 70 | var MultiServer = require('multiserver') 71 | var chloride = require('chloride') 72 | var keys = chloride.crypto_sign_keypair() 73 | var appKey = "dTuPysQsRoyWzmsK6iegSV4U3Qu912vPpkOyx6bPuEk=" 74 | 75 | function accept_all (id, cb) { 76 | cb(null, true) 77 | } 78 | var ms = MultiServer([ 79 | [ //net + secret-handshake 80 | require('multiserver/plugins/net')({port: 3333}), 81 | require('multiserver/plugins/shs')({ 82 | keys: keys, 83 | appKey: appKey, //application key 84 | auth: accept_all 85 | }), 86 | ], 87 | [ //net + secret-handshake2 88 | //(not implemented yet, but incompatible with shs) 89 | require('multiserver/plugins/net')({port: 4444}), 90 | //this protocol doesn't exist yet, but it could. 91 | require('secret-handshake2')({ 92 | keys: keys, 93 | appKey: appKey, //application key 94 | auth: accept_all 95 | }), 96 | ] 97 | ]) 98 | 99 | console.log(ms.stringify()) 100 | 101 | //=> net::3333~shs:;net::4444~shs2: 102 | 103 | //run two servers on two ports. 104 | //newer peers can connect directly to 4444 and use shs2. 105 | //this means the protocol can be _completely_ upgraded. 106 | ms.server(function (stream) { 107 | console.log('connection from', stream.address) 108 | }) 109 | 110 | //connect to legacy protocol 111 | ms.client('net::3333~shs:', function (err, stream) { 112 | //... 113 | }) 114 | 115 | //connect to modern protocol 116 | ms.client('net::4444~shs2:', function (err, stream) { 117 | //... 118 | }) 119 | 120 | ``` 121 | 122 | ### bridging 123 | 124 | By exposing multiple network transports as part of 125 | the same address, you can allow connections from 126 | peers that wouldn't have been able to connect otherwise. 127 | 128 | Regular servers can do TCP. Desktop clients can speak TCP, 129 | but can't create TCP servers that other desktop computers can connect to reliably. 130 | Browsers can use WebSockets and WebRTC. 131 | WebRTC gives you p2p, but needs an introducer. 132 | Another option is [utp](https://github.com/mafintosh/utp-native) 133 | - probably the most convenient, because it doesn't need an introducer 134 | on _every connection_ (but it does require some bootstrapping), 135 | but that doesn't work in the browser either. 136 | 137 | ``` js 138 | var MultiServer = require('multiserver') 139 | 140 | var ms = MultiServer([ 141 | require('multiserver/plugins/net')({port: 1234}), 142 | require('multiserver/plugins/ws')({port: 2345}) 143 | ]) 144 | 145 | //start a server (for both protocols!) 146 | //returns function to close the server. 147 | var close = ms.server(function (stream) { 148 | //handle incoming connection 149 | }) 150 | 151 | //connect to a protocol. uses whichever 152 | //handler understands the address (in this case, websockets) 153 | var abort = ms.client('ws://localhost:1234', function (err, stream) { 154 | //... 155 | }) 156 | 157 | //at any time abort() can be called to cancel the connection attempt. 158 | //if it's called after the connection is established, it will 159 | //abort the stream. 160 | ``` 161 | 162 | ## address format 163 | 164 | Addresses describe everything needed to connect to a peer. 165 | each address is divided into protocol sections separated by `~`. 166 | Each protocol section is divided itself by `:`. A protocol section 167 | starts with a name for that protocol, and then whatever arguments 168 | that protocol needs. The syntax of the address format is defined by [multiserver-address](https://github.com/ssbc/multiserver-address) 169 | 170 | For example, the address for my ssb pubserver is: 171 | ``` 172 | net:wx.larpa.net:8008~shs:DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ= 173 | ``` 174 | That says use the `net` protocol (TCP) to connect to the domain `wx.larpa.net` 175 | on port `8008`, and then encrypt the session using `shs` ([secret-handshake](https://github.com/auditdrivencrypto/secret-handshake)) 176 | to the public key `DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ=`. 177 | 178 | Usually, the first section is a network protocol, and the rest are transforms, 179 | such as encryption or compression. 180 | 181 | Multiserver makes it easy to use multiple protocols at once. For example, 182 | my pub server _also_ supports `shs` over websockets. 183 | 184 | So, this is another way to connect: 185 | 186 | ``` 187 | wss://wx.larpa.net~shs:DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ= 188 | ``` 189 | 190 | if your server supports multiple protocols, you can concatenate addresses with `;` 191 | and multiserver will connect to the first address it understands. 192 | 193 | ``` 194 | net:wx.larpa.net:8008~shs:DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ=;wss://wx.larpa.net~shs:DTNmX+4SjsgZ7xyDh5xxmNtFqa6pWi5Qtw7cE8aR9TQ= 195 | ``` 196 | This means use net, or wss. In some contexts, you might have a peer that understands 197 | websockets but not net (for example a browser), as long as a server speaks at least 198 | one protocol that a peer can understand, then they can communicate. 199 | 200 | ## scopes 201 | 202 | address also have a scope. This relates to where they 203 | can be connected to. Default supported scopes are: 204 | 205 | * device - can connect only if on the same device 206 | * local - can connect from same wifi (local network) 207 | * public - can connect from public global internet. 208 | 209 | some transport plugins work only on particular scopes. 210 | 211 | when `stringify(scope)` is called, it will return 212 | just the accessible addresses in that scope. 213 | 214 | ## plugins 215 | 216 | A multiserver instance is set up by composing a selection 217 | of plugins that construct the networking transports, 218 | and transforms that instance supports. 219 | 220 | There are two types of plugins, transports and transforms. 221 | 222 | ### `net({port,host,scope})` 223 | 224 | TCP is a `net:{host}:{port}` port is not optional. 225 | 226 | ``` js 227 | var Net = require('multiserver/plugins/net')` 228 | Net({port: 8889, host: 'mydomain.com'}).stringify() => 'net:mydomain.com:8889' 229 | Net({port: 8889, host: 'fe80::1065:74a4:4016:6266:4849'}).stringify() => 'net:fe80::1065:74a4:4016:6266:4849:8889' 230 | Net({port: 8889, host: 'fe80::1065:74a4:4016:6266:4849', scope: 'device'}).stringify() => 'net:fe80::1065:74a4:4016:6266:4849:8889' 231 | ``` 232 | 233 | ### `WebSockets({host,port,scope,handler?,key?,cert?})` 234 | 235 | create a websocket server. Since websockets are 236 | just a special mode of http, this also creates a http 237 | server. If `opts.handler` is provided, requests 238 | to the http server can be handled, this is optional. 239 | 240 | WebSockets `ws://{host}:{port}?` port defaults to 80 if not provided. 241 | 242 | WebSockets over https is `wss://{host}:{port}?` where port is 243 | 443 if not provided. 244 | 245 | If `opts.key` and `opts.cert` are provided as paths, a https server 246 | will be spawned. 247 | 248 | ``` js 249 | var WebSockets = require('multiserver/plugins/ws`) 250 | 251 | var ws = WebSockets({ 252 | port: 1234, 253 | host: 'mydomain.com', 254 | handler: function (req, res) { 255 | res.end('

hello

') 256 | }, 257 | scope:... 258 | }) 259 | 260 | ws.stringify() => 'ws://mydomain.com:1234' 261 | ``` 262 | 263 | ### `Onion()` 264 | 265 | Connect over tor using local proxy to dæmon (9050) or tor browser (9150). 266 | Both will be tried to find a suitable tor instance. 267 | The tor ports are unconfigurable. The standard 268 | tor ports are always used. 269 | 270 | This plugin does not support creating a server. 271 | You should use tor's configuration files to send incoming connections to a `net` 272 | instance as a hidden service. 273 | 274 | An accepted onion address looks like: `onion:{host}:{port}` 275 | port is not optional. This plugin does not return 276 | an address, so you must construct this address manually. 277 | 278 | ``` js 279 | var Onion = require('multiserver/plugins/onion`) 280 | 281 | 282 | var onion = WebSockets({ 283 | //no config is needed except scope, but you 284 | //surely will use this with "public" which is the default 285 | //scope:'public' 286 | }) 287 | 288 | ws.stringify() => null 289 | ``` 290 | 291 | 292 | ### `Bluetooth({bluetoothManager})` 293 | 294 | The [multiserver-bluetooth](https://github.com/Happy0/multiserver-bluetooth) module implements a multiserver protocol for to communicate over Bluetooth Serial port. 295 | 296 | ### `reactnative = require('multiserver-rn-channel')` 297 | 298 | The [multiserver-rn-channel](http://npm.im/multiserver-rn-channel) module implementes 299 | a multiserver protocol for use inbetween the reactnative nodejs process and browser process. 300 | 301 | ### `SHS({keys,timeout?,appKey,auth})` 302 | 303 | Secret-handshake is `shs:{public_key}:{seed}?`. `seed` is used to create 304 | a one-time shared private key, that may enable a special access. 305 | For example, you'll see that ssb invite codes have shs with two sections 306 | following. Normally, only a single argument (the remote public key) is necessary. 307 | 308 | ``` js 309 | var SHS = require('multiserver/plugins/shs') 310 | 311 | var shs = SHS({ 312 | keys: keys, 313 | timeout: //set handshake timeout, if unset falls through to secret-handshake default 314 | appKey: //sets an appkey 315 | auth: function (id, cb) { 316 | if(isNotAuthorized(id)) 317 | cb(new Error()) 318 | else 319 | cb(null, authenticationDetails) 320 | } 321 | }) 322 | shs.stringify() => 'shs:{keys.publicKey.toString('base64')} 323 | ``` 324 | 325 | note, if the `auth` function calls back a truthy value, 326 | it is considered authenticated. The value called back 327 | may be an object that represents details of the authentication. 328 | when a successful connection goes through `shs` plugin, 329 | the stream will have an `auth` property, which is the value called back from `auth`, 330 | and a `remote` property (the id of remote key). 331 | 332 | ### `Noauth({keys})` 333 | 334 | This authenticates any connection without any encryption. 335 | This should only be used on `device` scoped connections, 336 | such as if net is bound strictly to localhost, 337 | or a unix-socket. Do not use with ws or net bound to public addresses. 338 | 339 | ``` js 340 | var Noauth = require('multiserver/plugins/noauth') 341 | 342 | var noauth = Noauth({ 343 | keys: keys 344 | }) 345 | shs.stringify() => 'shs:{keys.publicKey.toString('base64')} 346 | 347 | ``` 348 | 349 | streams passing through this will look like an authenticated shs connection. 350 | 351 | ### `Unix = require('multiserver/plugins/unix-socket')` 352 | 353 | network transport is unix socket. to connect to this 354 | you must have access to the same file system as the server. 355 | 356 | ``` js 357 | var Unix = require('multiserver/plugins/unix-socket') 358 | 359 | var unix = Unix({ 360 | path: where_to_put_socket, 361 | scope: ... //defaults to device 362 | }) 363 | 364 | unix.stringify() => "unix:{where_to_put_socket}" 365 | ``` 366 | 367 | 368 | ### createMultiServer([[transport,transforms...],...]) 369 | 370 | A server that runs multiple protocols on different ports can simply join them 371 | with `;` and clients should connect to their preferred protocol. 372 | clients may try multiple protocols on the same server before giving up, 373 | but generally it's unlikely that protocols should not fail independently 374 | (unless there is a bug in one protocol). 375 | 376 | an example of a valid multiprotocol: 377 | `net:{host}:{port}~shs:{key};ws:{host}:{port}~shs:{key}` 378 | 379 | ``` js 380 | var MultiServer = require('multiserver') 381 | 382 | var ms = MultiServer([ 383 | [net, shs], 384 | [ws, shs], 385 | [unix, noauth] 386 | ]) 387 | 388 | ms.stringify('public') => "net:mydomain.com:8889~shs:;ws://mydomain.com:1234~shs:" 389 | ms.stringify('device') => "unix:{where_to_put_socket}" 390 | 391 | ms.server(function (stream) { 392 | //now that all the plugins are combined, 393 | //ready to use as an actual server. 394 | }) 395 | ``` 396 | 397 | ## interfaces 398 | 399 | To construct a useful multiserver instance, 400 | one or more transport is each connected with zero 401 | or more transforms. The combine function is 402 | the default export from the `multiserver` module. 403 | 404 | ``` js 405 | var MultiServer = require('multiserver') 406 | 407 | var ms = MultiServer([ 408 | [transport1, transform1], 409 | [transport2, transform2, transform3], 410 | ]) 411 | 412 | var close = ms.server(function (stream) { 413 | //called when a stream connects 414 | }, onError, onListening) 415 | ``` 416 | 417 | ``` 418 | createMultiServer([[Transform, Transports*,...]], *]) => MultiServer 419 | ``` 420 | 421 | a MultiServer has the same interface as a Transport, 422 | but using a combined multiserver instance as a transport 423 | is **not** supported. 424 | 425 | ## createTransport(Options) => Transport 426 | 427 | The transport exposes a name and the ability to 428 | create and connect to servers running that transport. 429 | 430 | ``` js 431 | Transport => { 432 | // that describes the sub protocols 433 | name, 434 | // connect to server with address addr. 435 | client (addr, cb), 436 | // start the server 437 | server (onConnect, onError, onListening), 438 | // return string describing how to connect to the server, aka, "the address" 439 | // the address applies to a `scope`. 440 | stringify(scope), 441 | // parse the addr, 442 | // normally this would probably return the 443 | // Options used to create the transport. 444 | parse(string) => Options 445 | } 446 | ``` 447 | 448 | ## createTransform(options) => Transform 449 | 450 | ``` js 451 | Transform => { 452 | name: string, 453 | create(Options) => (stream, cb(null, transformed_stream)), 454 | parse (str) => Options, 455 | stringify() => string, 456 | } 457 | ``` 458 | 459 | note the create method on a Transform takes Options, 460 | and returns a function that takes a stream and a callback, 461 | and then calls back the transformed stream. 462 | In all cases the stream is a [duplex stream](https://github.com/pull-stream/pull-stream) 463 | 464 | ## License 465 | 466 | MIT 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # https://www.appveyor.com/docs/lang/nodejs-iojs/ 2 | 3 | cache: 4 | - node_modules 5 | 6 | init: 7 | - git config --global core.autocrlf input 8 | 9 | environment: 10 | matrix: 11 | - nodejs_version: "8" 12 | - nodejs_version: "10" 13 | 14 | install: 15 | - ps: Install-Product node $env:nodejs_version 16 | - npm install 17 | 18 | test_script: 19 | - node --version 20 | - npm --version 21 | - node test/async-server-close.js 22 | - node test/multi.js 23 | - node test/plugs.js 24 | 25 | build: off 26 | -------------------------------------------------------------------------------- /compose.js: -------------------------------------------------------------------------------- 1 | const SEPARATOR = '~' 2 | const ESCAPE = '!' 3 | const SE = require('separator-escape')(SEPARATOR, ESCAPE) 4 | 5 | function head(x) { 6 | return Array.isArray(x) ? x[0] : x 7 | } 8 | 9 | function tail(x) { 10 | return Array.isArray(x) ? x.slice(1) : [] 11 | } 12 | 13 | function compose(stream, transforms, cb) { 14 | if (!stream) throw new Error('multiserver.compose: *must* pass stream') 15 | ;(function next(err, stream, i, addr) { 16 | if (err) { 17 | err.address = addr + '~' + err.address 18 | return cb(err) 19 | } else if (i >= transforms.length) { 20 | stream.address = addr 21 | return cb(null, stream) 22 | } else 23 | transforms[i](stream, (err, _stream) => { 24 | if (!err && !stream) throw new Error('expected error or stream') 25 | if (_stream) _stream.meta = _stream.meta || stream.meta 26 | next(err, _stream, i + 1, err ? addr : addr + '~' + _stream.address) 27 | }) 28 | })(null, stream, 0, stream.address) 29 | } 30 | 31 | function asyncify(f) { 32 | return function fnAsAsync(cb) { 33 | if (f.length) return f(cb) 34 | if (cb) { 35 | let result 36 | try { 37 | result = f() 38 | } catch (err) { 39 | return cb(err) 40 | } 41 | return cb(null, result) 42 | } 43 | return f() 44 | } 45 | } 46 | 47 | function identity(x) { 48 | return x 49 | } 50 | 51 | module.exports = function Compose(ary, wrap) { 52 | if (!wrap) wrap = identity 53 | const proto = head(ary) 54 | const trans = tail(ary) 55 | 56 | function parse(str) { 57 | const parts = SE.parse(str) 58 | const out = [] 59 | for (let i = 0; i < parts.length; i++) { 60 | const v = ary[i].parse(parts[i]) 61 | if (!v) return null 62 | out[i] = v 63 | } 64 | return out 65 | } 66 | 67 | function parseMaybe(str) { 68 | return typeof str === 'string' ? parse(str) : str 69 | } 70 | 71 | return { 72 | name: ary.map((e) => e.name).join(SEPARATOR), 73 | 74 | scope: proto.scope, 75 | 76 | client(_opts, cb) { 77 | const opts = parseMaybe(_opts) 78 | if (!opts) return cb(new Error('could not parse address:' + _opts)) 79 | return proto.client(head(opts), (err, stream) => { 80 | if (err) return cb(err) 81 | compose( 82 | wrap(stream), 83 | trans.map((tr, i) => tr.create(opts[i + 1])), 84 | cb 85 | ) 86 | }) 87 | }, 88 | 89 | // There should be a callback , called with null when the server started to 90 | // listen. (net.server.listen is async for example) 91 | server(onConnection, onError, onStart) { 92 | onError = 93 | onError || 94 | function onServerError(err) { 95 | console.error('server error, from', err.address) 96 | console.error(err) 97 | } 98 | return asyncify( 99 | proto.server(function onComposedConnection(stream) { 100 | compose( 101 | wrap(stream), 102 | trans.map((tr) => tr.create()), 103 | (err, stream) => { 104 | if (err) onError(err) 105 | else onConnection(stream) 106 | } 107 | ) 108 | }, onStart) 109 | ) 110 | }, 111 | 112 | parse: parse, 113 | 114 | stringify(scope) { 115 | const addresses = [] 116 | const fullAddress = proto.stringify(scope) 117 | if (!fullAddress) return 118 | else { 119 | const splittedAddresses = fullAddress.split(';') 120 | if (splittedAddresses.length > 1) { 121 | // More than one hostname needs to be updated 122 | addresses.push(...splittedAddresses) 123 | } else { 124 | addresses.push(fullAddress) 125 | } 126 | } 127 | return addresses 128 | .map((addr) => { 129 | const singleAddr = [addr].concat(trans.map((t) => t.stringify(scope))) 130 | return SE.stringify(singleAddr) 131 | }) 132 | .join(';') 133 | }, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const multicb = require('multicb') 2 | const compose = require('./compose') 3 | const isArray = Array.isArray 4 | 5 | function split(str) { 6 | return isArray(str) ? str : str.split(';') 7 | } 8 | 9 | module.exports = function Multiserver(plugs, wrap) { 10 | plugs = plugs.map((e) => (isArray(e) ? compose(e, wrap) : e)) 11 | 12 | const _self = { 13 | name: plugs.map((e) => e.name).join(';'), 14 | 15 | client(addr, cb) { 16 | let plug 17 | const _addr = split(addr).find((addr) => { 18 | // connect with the first plug that understands this string. 19 | plug = plugs.find((plug) => (plug.parse(addr) ? plug : null)) 20 | if (plug) return addr 21 | }) 22 | if (plug) plug.client(_addr, cb) 23 | else 24 | cb( 25 | new Error( 26 | 'could not connect to:' + addr + ', only know:' + _self.name 27 | ) 28 | ) 29 | }, 30 | 31 | server(onConnect, onError, startedCb) { 32 | //start all servers 33 | 34 | if (!startedCb) { 35 | // If a callback is not registered to be called back when the servers are 36 | // fully started, our default behaviour is just to print any errors starting 37 | // the servers to the log 38 | startedCb = (err, result) => { 39 | if (err) { 40 | console.error('Error starting multiserver server: ' + err) 41 | } 42 | } 43 | } 44 | 45 | const started = multicb() 46 | 47 | const closes = plugs 48 | .map((plug) => plug.server(onConnect, onError, started())) 49 | .filter(Boolean) 50 | 51 | started(startedCb) 52 | 53 | return function closeMultiserverServer(cb) { 54 | let done 55 | if (cb) done = multicb() 56 | for (const close of closes) { 57 | if (done && close.length) close(done()) 58 | else close() 59 | } 60 | if (done) done(cb) 61 | } 62 | }, 63 | 64 | stringify(scope) { 65 | if (!scope) scope = 'device' 66 | return plugs 67 | .filter((plug) => { 68 | const _scope = plug.scope() 69 | return Array.isArray(_scope) 70 | ? ~_scope.indexOf(scope) 71 | : _scope === scope 72 | }) 73 | .map((plug) => plug.stringify(scope)) 74 | .filter(Boolean) 75 | .join(';') 76 | }, 77 | 78 | // parse doesn't really make sense here... 79 | // like, what if you only have a partial match? 80 | // maybe just parse the ones you understand? 81 | parse(str) { 82 | return str.split(';').map((e, i) => plugs[i].parse(e)) 83 | }, 84 | } 85 | return _self 86 | } 87 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiserver", 3 | "description": "write a server which works over many protocols at once, or connect to the same", 4 | "version": "3.8.2", 5 | "homepage": "https://github.com/ssb-js/multiserver", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/ssb-js/multiserver.git" 9 | }, 10 | "files": [ 11 | "*.js", 12 | "plugins/*.js" 13 | ], 14 | "dependencies": { 15 | "debug": "^4.1.1", 16 | "multicb": "^1.2.2", 17 | "multiserver-scopes": "^2.0.0", 18 | "pull-stream": "^3.6.1", 19 | "pull-websocket": "^3.4.0", 20 | "secret-handshake": "^1.1.16", 21 | "separator-escape": "0.0.1", 22 | "socks": "^2.2.3", 23 | "stream-to-pull-stream": "^1.7.2" 24 | }, 25 | "devDependencies": { 26 | "chloride": "^2.2.8", 27 | "prettier": "^2.5.1", 28 | "pull-pushable": "^2.2.0", 29 | "tap-spec": "^5.0.0", 30 | "tape": "^5.0.1" 31 | }, 32 | "scripts": { 33 | "test": "tape test/*.js | tap-spec", 34 | "format-code": "prettier --write \"*.js\" \"(test|plugins)/*.js\"" 35 | }, 36 | "engines": { 37 | "node": ">=12" 38 | }, 39 | "browser": { 40 | "ws": false, 41 | "pull-ws/server": false, 42 | "socks": false 43 | }, 44 | "author": "'Dominic Tarr' (dominictarr.com)", 45 | "license": "MIT" 46 | } 47 | -------------------------------------------------------------------------------- /plugins/net.js: -------------------------------------------------------------------------------- 1 | const toPull = require('stream-to-pull-stream') 2 | const scopes = require('multiserver-scopes') 3 | const debug = require('debug')('multiserver:net') 4 | let net 5 | try { 6 | net = require('net') 7 | } catch (_) { 8 | // This only throws in browsers because they don't have access to the Node net 9 | // library, which is safe to ignore because they shouldn't be running any 10 | // methods that require the net library. Maybe we should be setting a flag 11 | // somewhere rather than checking whether `net == null`? 12 | } 13 | 14 | function toAddress(host, port) { 15 | return ['net', host, port].join(':') 16 | } 17 | 18 | function toDuplex(str) { 19 | const stream = toPull.duplex(str) 20 | stream.address = toAddress(str.remoteAddress, str.remotePort) 21 | return stream 22 | } 23 | 24 | // Choose a dynamic port between 49152 and 65535 25 | // https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Dynamic,_private_or_ephemeral_ports 26 | function getRandomPort() { 27 | return Math.floor(49152 + (65535 - 49152 + 1) * Math.random()) 28 | } 29 | 30 | module.exports = function Net({ 31 | scope = 'device', 32 | host, 33 | port, 34 | external, 35 | allowHalfOpen, 36 | pauseOnConnect, 37 | }) { 38 | // Arguments are `scope` and `external` plus selected options for 39 | // `net.createServer()` and `server.listen()`. 40 | host = host || (typeof scope === 'string' && scopes.host(scope)) 41 | port = port || getRandomPort() 42 | 43 | function isAllowedScope(s) { 44 | return s === scope || (Array.isArray(scope) && scope.includes(s)) 45 | } 46 | 47 | return { 48 | name: 'net', 49 | scope: () => scope, 50 | server(onConnection, startedCB) { 51 | debug('Listening on %s:%d', host, port) 52 | let tempStartedCB = startedCB 53 | 54 | // TODO: We convert `allowHalfOpen` to boolean for legacy reasons, this 55 | // might not be getting used anywhere but I'm too scared to change it. 56 | // This should probably be removed when we do a major version bump. 57 | const serverOpts = { 58 | allowHalfOpen: Boolean(allowHalfOpen), 59 | pauseOnConnect, 60 | } 61 | 62 | const server = net.createServer( 63 | serverOpts, 64 | function connectionListener(stream) { 65 | onConnection(toDuplex(stream)) 66 | } 67 | ) 68 | 69 | server.addListener('error', function onError(err) { 70 | if (tempStartedCB) { 71 | tempStartedCB(err) 72 | tempStartedCB = null 73 | } else { 74 | console.error(err) 75 | } 76 | }) 77 | 78 | server.listen(port, host, function onListening() { 79 | if (tempStartedCB) { 80 | tempStartedCB() 81 | tempStartedCB = null 82 | } 83 | }) 84 | 85 | return function closeNetServer(cb) { 86 | debug('Closing server on %s:%d', host, port) 87 | server.close(function onNetServerClosing(err) { 88 | if (err) console.error(err) 89 | else debug('No longer listening on %s:%d', host, port) 90 | if (cb) cb(err) 91 | }) 92 | } 93 | }, 94 | 95 | client(opts, cb) { 96 | let started = false 97 | const stream = net 98 | .connect(opts) 99 | .on('connect', function onConnect() { 100 | if (started) return 101 | started = true 102 | cb(null, toDuplex(stream)) 103 | }) 104 | .on('error', function onError(err) { 105 | if (started) return 106 | started = true 107 | cb(err) 108 | }) 109 | 110 | return function closeNetClient() { 111 | started = true 112 | stream.destroy() 113 | cb(new Error('multiserver.net: aborted')) 114 | } 115 | }, 116 | 117 | // MUST be net:: 118 | parse(s) { 119 | if (net == null) return null 120 | const ary = s.split(':') 121 | if (ary.length < 3) return null 122 | if ('net' !== ary.shift()) return null 123 | const port = Number(ary.pop()) 124 | if (isNaN(port)) return null 125 | return { 126 | name: 'net', 127 | host: ary.join(':') || 'localhost', 128 | port: port, 129 | } 130 | }, 131 | 132 | stringify(targetScope = 'device') { 133 | if (isAllowedScope(targetScope) === false) { 134 | return null 135 | } 136 | 137 | // We want to avoid using `host` if the target scope is public and some 138 | // external host (like example.com) is defined. 139 | const externalHost = targetScope === 'public' && external 140 | let resultHost = externalHost || host || scopes.host(targetScope) 141 | 142 | if (resultHost == null) { 143 | // The device has no network interface for a given `targetScope`. 144 | return null 145 | } 146 | 147 | // convert to an array for easier formatting 148 | if (typeof resultHost === 'string') { 149 | resultHost = [resultHost] 150 | } 151 | 152 | return resultHost 153 | .map((h) => { 154 | // Remove IPv6 scopeid suffix, if any, e.g. `%wlan0` 155 | return toAddress(h.replace(/(\%\w+)$/, ''), port) 156 | }) 157 | .join(';') 158 | }, 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /plugins/noauth.js: -------------------------------------------------------------------------------- 1 | module.exports = function Noauth(opts) { 2 | return { 3 | name: 'noauth', 4 | create(_opts) { 5 | return function noauthTransform(stream, cb) { 6 | cb(null, { 7 | remote: opts.keys.publicKey, 8 | auth: { allow: null, deny: null }, 9 | source: stream.source, 10 | sink: stream.sink, 11 | address: 'noauth:' + opts.keys.publicKey.toString('base64'), 12 | }) 13 | } 14 | }, 15 | parse(str) { 16 | return {} 17 | }, 18 | stringify() { 19 | return 'noauth' 20 | }, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /plugins/onion.js: -------------------------------------------------------------------------------- 1 | const socks = require('socks').SocksClient 2 | const toPull = require('stream-to-pull-stream') 3 | const debug = require('debug')('multiserver:onion') 4 | 5 | module.exports = function Onion(opts) { 6 | if (!socks) { 7 | // We are in browser 8 | debug('onion dialing through socks proxy not supported in browser setting') 9 | return { 10 | name: 'onion', 11 | scope() { 12 | return 'public' 13 | }, 14 | parse(s) { 15 | return null 16 | }, 17 | } 18 | } 19 | 20 | opts = opts || {} 21 | const daemonProxyOpts = { 22 | ipaddress: '127.0.0.1', 23 | port: 9050, 24 | type: 5, 25 | } 26 | const browserProxyOpts = { 27 | ipaddress: '127.0.0.1', 28 | port: 9150, 29 | type: 5, 30 | } 31 | 32 | return { 33 | name: 'onion', 34 | scope: () => opts.scope || 'public', 35 | server(onConnection, cb) { 36 | cb(new Error('Use net plugin for onion server instead')) 37 | }, 38 | client(opts, cb) { 39 | let _socket, destroy 40 | 41 | function tryConnect(connectOpts, onFail) { 42 | socks.createConnection(connectOpts, function onConnected(err, result) { 43 | if (err) return onFail(err) 44 | 45 | const socket = result.socket 46 | 47 | if (destroy) return socket.destroy() 48 | _socket = socket 49 | 50 | const duplexStream = toPull.duplex(socket) 51 | duplexStream.address = 52 | 'onion:' + 53 | connectOpts.destination.host + 54 | ':' + 55 | connectOpts.destination.port 56 | 57 | cb(null, duplexStream) 58 | 59 | // Remember to resume the socket stream. 60 | socket.resume() 61 | }) 62 | } 63 | 64 | function connectOpts(proxyOpts) { 65 | return { 66 | proxy: proxyOpts, 67 | command: 'connect', 68 | destination: { 69 | host: opts.host, 70 | port: opts.port, 71 | }, 72 | } 73 | } 74 | 75 | tryConnect(connectOpts(daemonProxyOpts), (err) => { 76 | tryConnect(connectOpts(browserProxyOpts), (err) => { 77 | cb(err) 78 | }) 79 | }) 80 | 81 | return function closeOnionClient() { 82 | if (_socket) _socket.destroy() 83 | else destroy = true 84 | } 85 | }, 86 | 87 | // MUST be onion:: 88 | parse(s) { 89 | const ary = s.split(':') 90 | if (ary.length < 3) return null 91 | if ('onion' !== ary.shift()) return null 92 | const port = +ary.pop() 93 | if (isNaN(port)) return null 94 | return { 95 | name: 'onion', 96 | host: ary.join(':') || 'localhost', 97 | port: port, 98 | } 99 | }, 100 | stringify(scope) { 101 | return null 102 | }, 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /plugins/shs.js: -------------------------------------------------------------------------------- 1 | const SecretHandshake = require('secret-handshake') 2 | const pull = require('pull-stream') 3 | 4 | module.exports = function Shs(opts) { 5 | const keys = SecretHandshake.toKeys(opts.keys || opts.seed) 6 | const appKey = 7 | typeof opts.appKey === 'string' 8 | ? Buffer.from(opts.appKey, 'base64') 9 | : opts.appKey 10 | 11 | const server = SecretHandshake.createServer( 12 | keys, 13 | opts.auth || opts.authenticate, 14 | appKey, 15 | opts.timeout 16 | ) 17 | const client = SecretHandshake.createClient(keys, appKey, opts.timeout) 18 | 19 | return { 20 | name: 'shs', 21 | create(_opts) { 22 | return function shsTransform(stream, cb) { 23 | function _cb(err, stream) { 24 | if (err) { 25 | // shs is designed so that we do not _know_ who is connecting if it 26 | // fails, so we probably can't add the connecting address. (unless 27 | // it was client unauthorized) 28 | err.address = 'shs:' 29 | return cb(err) 30 | } 31 | stream.address = 'shs:' + stream.remote.toString('base64') 32 | cb(null, stream) 33 | } 34 | pull( 35 | stream.source, 36 | _opts && _opts.key ? client(_opts.key, _opts.seed, _cb) : server(_cb), 37 | stream.sink 38 | ) 39 | } 40 | }, 41 | 42 | parse(str) { 43 | const ary = str.split(':') 44 | if (ary[0] !== 'shs') return null 45 | let seed = undefined 46 | // Seed of private key to connect with, optional. 47 | if (ary.length > 2) { 48 | seed = Buffer.from(ary[2], 'base64') 49 | if (seed.length !== 32) return null 50 | } 51 | const key = Buffer.from(ary[1], 'base64') 52 | if (key.length !== 32) return null 53 | return { key: key, seed: seed } 54 | }, 55 | 56 | stringify() { 57 | if (!keys) return 58 | return 'shs:' + keys.publicKey.toString('base64') 59 | }, 60 | publicKey: keys && keys.publicKey, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /plugins/unix-socket.js: -------------------------------------------------------------------------------- 1 | const toDuplex = require('stream-to-pull-stream').duplex 2 | const net = require('net') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const debug = require('debug')('multiserver:unix') 6 | const os = require('os') 7 | 8 | // hax on double transform 9 | let started = false 10 | 11 | module.exports = function Unix(opts) { 12 | if (process.platform === 'win32') { 13 | opts.path = 14 | opts.path || path.join('\\\\?\\pipe', process.cwd(), 'multiserver') 15 | } else { 16 | opts.path = 17 | opts.path || fs.mkdtempSync(path.join(os.tmpdir(), 'multiserver-')) 18 | } 19 | 20 | const socket = path.join(opts.path, 'socket') 21 | const addr = 'unix:' + socket 22 | let scope = opts.scope || 'device' 23 | opts = opts || {} 24 | 25 | return { 26 | name: 'unix', 27 | scope: () => scope, 28 | server(onConnection, cb) { 29 | if (started) return 30 | 31 | if (scope !== 'device') { 32 | debug('Insecure scope for unix socket! Reverting to device scope') 33 | scope = 'device' 34 | } 35 | 36 | debug('listening on socket %s', addr) 37 | 38 | const server = net 39 | .createServer(opts, function connectionListener(stream) { 40 | stream = toDuplex(stream) 41 | stream.address = addr 42 | onConnection(stream) 43 | }) 44 | .listen(socket, cb) 45 | 46 | server.on('error', function onError(err) { 47 | if (err.code === 'EADDRINUSE') { 48 | const clientSocket = new net.Socket() 49 | clientSocket.on('error', function onClientSocketError(e) { 50 | if (e.code === 'ECONNREFUSED') { 51 | fs.unlinkSync(socket) 52 | server.listen(socket) 53 | } 54 | }) 55 | 56 | clientSocket.connect( 57 | { path: socket }, 58 | function socketConnectionListener() { 59 | debug('someone else is listening on socket!') 60 | } 61 | ) 62 | } 63 | }) 64 | 65 | if (process.platform !== 'win32') { 66 | // mode is set to allow read and write 67 | const mode = fs.constants.S_IRUSR + fs.constants.S_IWUSR 68 | fs.chmodSync(socket, mode) 69 | } 70 | 71 | started = true 72 | 73 | return function closeUnixSocketServer() { 74 | server.close() 75 | } 76 | }, 77 | 78 | client(opts, cb) { 79 | debug('unix socket client') 80 | let started = false 81 | const stream = net 82 | .connect(opts.path) 83 | .on('connect', function onConnect() { 84 | if (started) return 85 | started = true 86 | var _stream = toDuplex(stream) 87 | _stream.address = addr 88 | cb(null, _stream) 89 | }) 90 | .on('error', function onError(err) { 91 | debug('err? %o', err) 92 | if (started) return 93 | started = true 94 | cb(err) 95 | }) 96 | 97 | return function closeUnixSocketClient() { 98 | started = true 99 | stream.destroy() 100 | cb(new Error('multiserver.unix: aborted')) 101 | } 102 | }, 103 | 104 | // MUST be unix:socket_path 105 | parse(s) { 106 | const ary = s.split(':') 107 | 108 | // Immediately return if there's no path. 109 | if (ary.length < 2) return null 110 | 111 | // Immediately return if the first item isn't 'unix'. 112 | if ('unix' !== ary.shift()) return null 113 | 114 | return { 115 | name: '', 116 | path: ary.join(':'), 117 | } 118 | }, 119 | 120 | stringify(_scope) { 121 | if (scope !== _scope) return null 122 | return ['unix', socket].join(':') 123 | }, 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /plugins/ws.js: -------------------------------------------------------------------------------- 1 | const pullWS = require('pull-websocket') 2 | const URL = require('url') 3 | const pull = require('pull-stream/pull') 4 | const Map = require('pull-stream/throughs/map') 5 | const scopes = require('multiserver-scopes') 6 | const http = require('http') 7 | const https = require('https') 8 | const fs = require('fs') 9 | const debug = require('debug')('multiserver:ws') 10 | 11 | function safeOrigin(origin, address, port) { 12 | // If the connection is not localhost, we shouldn't trust the origin header. 13 | // So, use address instead of origin if origin not set, then it's definitely 14 | // not a browser. 15 | if (!(address === '::1' || address === '127.0.0.1') || origin == undefined) 16 | return 'ws:' + address + (port ? ':' + port : '') 17 | 18 | // Note: origin "null" (as string) can happen a bunch of ways: 19 | // * it can be a html opened as a file 20 | // * or certain types of CORS 21 | // * https://www.w3.org/TR/cors/#resource-sharing-check-0 22 | // * and webworkers if loaded from data-url? 23 | if (origin === 'null') return 'ws:null' 24 | 25 | // A connection from the browser on localhost, we choose to trust this came 26 | // from a browser. 27 | return origin.replace(/^http/, 'ws') 28 | } 29 | 30 | // Choose a dynamic port between 49152 and 65535 31 | // https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Dynamic,_private_or_ephemeral_ports 32 | function getRandomPort() { 33 | return Math.floor(49152 + (65535 - 49152 + 1) * Math.random()) 34 | } 35 | 36 | module.exports = function WS(opts = {}) { 37 | // This takes options for `WebSocket.Server()`: 38 | // https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback 39 | 40 | opts.binaryType = opts.binaryType || 'arraybuffer' 41 | const scope = opts.scope || 'device' 42 | 43 | function isAllowedScope(s) { 44 | return s === scope || (Array.isArray(scope) && ~scope.indexOf(s)) 45 | } 46 | 47 | const secure = 48 | (opts.server && !!opts.server.key) || (!!opts.key && !!opts.cert) 49 | return { 50 | name: 'ws', 51 | scope: () => scope, 52 | server(onConnect, startedCb) { 53 | if (pullWS.createServer == null) return null 54 | 55 | // Maybe weird: this sets a random port each time that `server()` is run 56 | // whereas the net plugin sets the port when the outer function is run. 57 | // 58 | // This server has a random port generated at runtime rather than when 59 | // the interface is instantiated. Is that the way it should work? 60 | opts.port = opts.port || getRandomPort() 61 | 62 | if (typeof opts.key === 'string') opts.key = fs.readFileSync(opts.key) 63 | if (typeof opts.cert === 'string') opts.cert = fs.readFileSync(opts.cert) 64 | 65 | const server = 66 | opts.server || 67 | (opts.key && opts.cert 68 | ? https.createServer({ key: opts.key, cert: opts.cert }, opts.handler) 69 | : http.createServer(opts.handler)) 70 | 71 | const serverOpts = Object.assign({}, opts, { server: server }) 72 | const wsServer = pullWS.createServer( 73 | serverOpts, 74 | function connectionListener(stream) { 75 | stream.address = safeOrigin( 76 | stream.headers.origin, 77 | stream.remoteAddress, 78 | stream.remotePort 79 | ) 80 | onConnect(stream) 81 | } 82 | ) 83 | 84 | if (!opts.server) { 85 | debug('Listening on %s:%d', opts.host, opts.port) 86 | server.listen(opts.port, opts.host, function onListening() { 87 | startedCb && startedCb(null, true) 88 | }) 89 | } else startedCb && startedCb(null, true) 90 | 91 | return function closeWsServer(cb) { 92 | debug('Closing server on %s:%d', opts.host, opts.port) 93 | wsServer.close((err) => { 94 | debug('after WS close', err) 95 | if (err) console.error(err) 96 | else debug('No longer listening on %s:%d', opts.host, opts.port) 97 | if (cb) cb(err) 98 | }) 99 | } 100 | }, 101 | 102 | client(addr, cb) { 103 | if (!addr.host) { 104 | addr.hostname = addr.hostname || opts.host || 'localhost' 105 | addr.slashes = true 106 | addr = URL.format(addr) 107 | } 108 | if (typeof addr !== 'string') addr = URL.format(addr) 109 | 110 | const stream = pullWS.connect(addr, { 111 | binaryType: opts.binaryType, 112 | onConnect: function connectionListener(err) { 113 | // Ensure stream is a stream of node buffers 114 | stream.source = pull(stream.source, Map(Buffer.from.bind(Buffer))) 115 | cb(err, stream) 116 | }, 117 | }) 118 | stream.address = addr 119 | 120 | return function closeWsClient() { 121 | stream.close() 122 | } 123 | }, 124 | 125 | stringify(targetScope = 'device') { 126 | if (pullWS.createServer == null) { 127 | return null 128 | } 129 | if (isAllowedScope(targetScope) === false) { 130 | return null 131 | } 132 | 133 | const port = opts.server ? opts.server.address().port : opts.port 134 | const externalHost = targetScope === 'public' && opts.external 135 | let resultHost = externalHost || opts.host || scopes.host(targetScope) 136 | 137 | if (resultHost == null) { 138 | // The device has no network interface for a given `targetScope`. 139 | return null 140 | } 141 | 142 | if (typeof resultHost === 'string') { 143 | resultHost = [resultHost] 144 | } 145 | 146 | return resultHost 147 | .map((h) => 148 | URL.format({ 149 | protocol: secure ? 'wss' : 'ws', 150 | slashes: true, 151 | hostname: h, 152 | port: (secure ? port === 443 : port === 80) ? undefined : port, 153 | }) 154 | ) 155 | .join(';') 156 | }, 157 | 158 | parse(str) { 159 | const addr = URL.parse(str) 160 | if (!/^wss?\:$/.test(addr.protocol)) return null 161 | return addr 162 | }, 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /test/async-server-close.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const Ms = require('../') 3 | 4 | function sync_server(t) { 5 | return { 6 | server: function () { 7 | return function close() { 8 | t.pass('sync close') 9 | } 10 | }, 11 | } 12 | } 13 | 14 | function async_server(t) { 15 | return { 16 | server: function () { 17 | return function close(cb) { 18 | setTimeout(function () { 19 | t.comment('async close') 20 | t.async_calls = (t.async_calls || 0) + 1 21 | cb(null) 22 | }, 100) 23 | } 24 | }, 25 | } 26 | } 27 | 28 | test('all calls are sync', function (t) { 29 | const ms = Ms([sync_server(t), sync_server(t)]) 30 | const close = ms.server() 31 | t.plan(2) 32 | close() 33 | }) 34 | 35 | test('all calls are async', function (t) { 36 | const ms = Ms([async_server(t), async_server(t)]) 37 | const close = ms.server() 38 | close(function (err) { 39 | t.error(err) 40 | t.equal(t.async_calls, 2, 'Should have waited for both servers') 41 | t.end() 42 | }) 43 | }) 44 | 45 | /* 46 | // this stops other tests for some reason? 47 | test.only('async caller, sync callee', function(t) { 48 | const ms = Ms([ 49 | sync_server(t), 50 | sync_server(t) 51 | ]) 52 | const close = ms.server() 53 | close(function(err) { 54 | t.error(err) 55 | t.end() 56 | }) 57 | }) 58 | */ 59 | 60 | test('all calls are async', function (t) { 61 | const ms = Ms([async_server(t), async_server(t)]) 62 | const close = ms.server() 63 | close(function (err) { 64 | t.error(err) 65 | t.equal(t.async_calls, 2, 'Should have waited for both servers') 66 | t.end() 67 | }) 68 | }) 69 | 70 | test('async caller, mixed callees', function (t) { 71 | const ms = Ms([sync_server(t), async_server(t)]) 72 | const close = ms.server() 73 | t.plan(3) 74 | close(function (err) { 75 | t.error(err) 76 | t.equal(t.async_calls, 1, 'Should have waited for async servers') 77 | t.end() 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /test/multi.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape') 2 | const pull = require('pull-stream') 3 | 4 | const Compose = require('../compose') 5 | const Net = require('../plugins/net') 6 | const Ws = require('../plugins/ws') 7 | const Shs = require('../plugins/shs') 8 | const MultiServer = require('../') 9 | 10 | const cl = require('chloride') 11 | const seed = cl.crypto_hash_sha256(Buffer.from('TESTSEED')) 12 | const keys = cl.crypto_sign_seed_keypair(seed) 13 | const appKey = cl.crypto_hash_sha256(Buffer.from('TEST')) 14 | 15 | // this gets overwritten in the last test. 16 | let check = function (id, cb) { 17 | cb(null, true) 18 | } 19 | 20 | const net = Net({ port: 4848, scope: 'device' }) 21 | const ws = Ws({ port: 4849, scope: 'device' }) 22 | //console.log('appKey', appKey) 23 | const shs = Shs({ 24 | keys: keys, 25 | appKey: appKey, 26 | auth: function (id, cb) { 27 | check(id, cb) 28 | }, 29 | }) 30 | 31 | const combined = Compose([net, shs]) 32 | const combined_ws = Compose([ws, shs]) 33 | 34 | const multi = MultiServer([combined, combined_ws]) 35 | 36 | const multi_ws = MultiServer([combined_ws]) 37 | const multi_net = MultiServer([combined]) 38 | 39 | let client_addr 40 | 41 | let close 42 | 43 | //listen, with new async interface 44 | tape('listen', function (t) { 45 | close = multi.server( 46 | function (stream) { 47 | console.log('onConnect', stream.address) 48 | client_addr = stream.address 49 | pull(stream, stream) 50 | }, 51 | null, 52 | t.end 53 | ) 54 | }) 55 | 56 | const server_addr = 'fake:peer.ignore~nul:what;' + multi.stringify('device') 57 | //"fake" in a unkown protocol, just to make sure it gets skipped. 58 | 59 | tape('connect to either server (net)', function (t) { 60 | t.ok(multi.stringify('device')) 61 | multi.client(server_addr, function (err, stream) { 62 | if (err) throw err 63 | //console.log(stream) 64 | t.ok(/^net/.test(client_addr), 'client connected via net') 65 | t.ok(/^net/.test(stream.address), 'client connected via net') 66 | pull( 67 | pull.values([Buffer.from('Hello')]), 68 | stream, 69 | pull.collect(function (err, ary) { 70 | const data = Buffer.concat(ary).toString('utf8') 71 | console.log('OUTPUT', data) 72 | t.end() 73 | }) 74 | ) 75 | }) 76 | }) 77 | 78 | tape('connect to either server (ws)', function (t) { 79 | multi_ws.client(server_addr, function (err, stream) { 80 | if (err) throw err 81 | t.ok(/^ws/.test(client_addr), 'client connected via ws') 82 | t.ok(/^ws/.test(stream.address), 'client connected via ws') 83 | pull( 84 | pull.values([Buffer.from('Hello')]), 85 | stream, 86 | pull.collect(function (err, ary) { 87 | const data = Buffer.concat(ary).toString('utf8') 88 | console.log('OUTPUT', data) 89 | t.end() 90 | }) 91 | ) 92 | }) 93 | }) 94 | 95 | tape('close', function (t) { 96 | close() 97 | t.end() 98 | }) 99 | -------------------------------------------------------------------------------- /test/plugs.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape') 2 | const pull = require('pull-stream') 3 | const Pushable = require('pull-pushable') 4 | const fs = require('fs') 5 | 6 | const Compose = require('../compose') 7 | const Net = require('../plugins/net') 8 | const Unix = require('../plugins/unix-socket') 9 | const Ws = require('../plugins/ws') 10 | const Shs = require('../plugins/shs') 11 | const Onion = require('../plugins/onion') 12 | const MultiServer = require('../') 13 | 14 | const cl = require('chloride') 15 | const seed = cl.crypto_hash_sha256(Buffer.from('TESTSEED')) 16 | const keys = cl.crypto_sign_seed_keypair(seed) 17 | const appKey = cl.crypto_hash_sha256(Buffer.from('TEST')) 18 | 19 | //this gets overwritten in the last test. 20 | let check = function (id, cb) { 21 | cb(null, true) 22 | } 23 | 24 | const net = Net({ port: 4848 }) 25 | const ws = Ws({ port: 4849 }) 26 | const shs = Shs({ 27 | keys: keys, 28 | appKey: appKey, 29 | auth: function (id, cb) { 30 | check(id, cb) 31 | }, 32 | }) 33 | 34 | const combined = Compose([net, shs]) 35 | const combined_ws = Compose([ws, shs]) 36 | 37 | // travis currently does not support ipv6, becaue GCE does not. 38 | const has_ipv6 = process.env.TRAVIS === undefined 39 | 40 | tape('parse, stringify', function (t) { 41 | t.equal(net.stringify('device'), 'net:localhost:4848') 42 | t.equal(ws.stringify('device'), 'ws://localhost:4849') 43 | t.equal(shs.stringify(), 'shs:' + keys.publicKey.toString('base64')) 44 | t.equal( 45 | combined.stringify('device'), 46 | net.stringify('device') + '~' + shs.stringify('device') 47 | ) 48 | t.equal( 49 | combined_ws.stringify('device'), 50 | ws.stringify() + '~' + shs.stringify() 51 | ) 52 | console.log(Compose([net, shs]).stringify('device')) 53 | t.equal( 54 | MultiServer([combined, combined_ws]).stringify(), 55 | 56 | [combined.stringify('device'), combined_ws.stringify('device')].join(';') 57 | ) 58 | 59 | t.end() 60 | }) 61 | 62 | function echo(stream) { 63 | pull( 64 | stream, 65 | pull.map(function (data) { 66 | return Buffer.from(data.toString().toUpperCase()) 67 | }), 68 | stream 69 | ) 70 | } 71 | 72 | tape('combined', function (t) { 73 | const close = combined.server(echo, null, () => { 74 | combined.client(combined.stringify('device'), function (err, stream) { 75 | if (err) throw err 76 | pull( 77 | pull.values([Buffer.from('hello world')]), 78 | stream, 79 | pull.collect(function (err, ary) { 80 | if (err) throw err 81 | t.equal(Buffer.concat(ary).toString(), 'HELLO WORLD') 82 | close(t.end) 83 | }) 84 | ) 85 | }) 86 | }) 87 | }) 88 | 89 | if (has_ipv6) 90 | tape('combined, ipv6', function (t) { 91 | const combined = Compose([ 92 | Net({ 93 | port: 4848, 94 | host: '::', 95 | }), 96 | shs, 97 | ]) 98 | const close = combined.server(echo, null, () => { 99 | const addr = combined.stringify('device') 100 | console.log('addr', addr) 101 | 102 | combined.client(addr, function (err, stream) { 103 | if (err) throw err 104 | t.ok(stream.address, 'has an address') 105 | pull( 106 | pull.values([Buffer.from('hello world')]), 107 | stream, 108 | pull.collect(function (err, ary) { 109 | if (err) throw err 110 | t.equal(Buffer.concat(ary).toString(), 'HELLO WORLD') 111 | close(t.end) 112 | }) 113 | ) 114 | }) 115 | }) 116 | }) 117 | 118 | if (has_ipv6) 119 | tape('stringify() does not show scopeid from ipv6', function (t) { 120 | const combined = Compose([ 121 | Net({ 122 | scope: 'private', 123 | port: 4849, 124 | host: 'fe80::1065:74a4:4016:6266%wlan0', 125 | }), 126 | shs, 127 | ]) 128 | const addr = combined.stringify('private') 129 | t.equal( 130 | addr, 131 | 'net:fe80::1065:74a4:4016:6266:4849~shs:' + 132 | keys.publicKey.toString('base64') 133 | ) 134 | t.end() 135 | }) 136 | 137 | tape('net: do not listen on all addresses', function (t) { 138 | const combined = Compose([ 139 | Net({ 140 | scope: 'device', 141 | port: 4850, 142 | host: 'localhost', 143 | // external: scopes.host('private') // unroutable IP, but not localhost (e.g. 192.168 ...) 144 | }), 145 | shs, 146 | ]) 147 | const close = combined.server(echo, null, () => { 148 | //fake 149 | const fake_combined = Compose([ 150 | Net({ 151 | scope: 'local', 152 | port: 4851, 153 | //host: 'localhost', 154 | // external: scopes.host('local') // unroutable IP, but not localhost (e.g. 192.168 ...) 155 | }), 156 | shs, 157 | ]) 158 | 159 | const addr = fake_combined.stringify('local') // returns external 160 | console.log('addr local scope', addr) 161 | combined.client(addr, function (err, stream) { 162 | t.ok(err, 'should only listen on localhost') 163 | close(t.end) 164 | }) 165 | }) 166 | }) 167 | 168 | tape('net: do not crash if listen() fails', function (t) { 169 | const combined = Compose([ 170 | Net({ 171 | scope: 'private', 172 | port: 4852, 173 | host: '$not-a-valid-ip-addr$', 174 | }), 175 | shs, 176 | ]) 177 | const close = combined.server(echo, null, function (err) { 178 | t.ok(err, 'should propagate listen error up') 179 | t.match(err.code, /^(ENOTFOUND|EAI_AGAIN)$/, 'the error is expected') 180 | close(() => t.end()) 181 | }) 182 | }) 183 | 184 | tape('net: stringify support external being a string', function (t) { 185 | const combined = Compose([ 186 | Net({ 187 | scope: 'public', 188 | port: 4853, 189 | host: 'localhost', 190 | external: 'scuttlebutt.nz', 191 | }), 192 | shs, 193 | ]) 194 | const addr = combined.stringify('public') 195 | t.equals( 196 | addr, 197 | 'net:scuttlebutt.nz:4853~shs:' + keys.publicKey.toString('base64') 198 | ) 199 | t.end() 200 | }) 201 | 202 | tape('combined, unix', function (t) { 203 | const combined = Compose([ 204 | Unix({ 205 | server: true, 206 | }), 207 | shs, 208 | ]) 209 | const close = combined.server(echo, null, () => { 210 | const addr = combined.stringify('device') 211 | console.log('unix addr', addr) 212 | 213 | combined.client(addr, function (err, stream) { 214 | if (err) throw err 215 | t.ok(stream.address, 'has an address') 216 | pull( 217 | pull.values([Buffer.from('hello world')]), 218 | stream, 219 | pull.collect(function (err, ary) { 220 | if (err) throw err 221 | t.equal(Buffer.concat(ary).toString(), 'HELLO WORLD') 222 | close(t.end) 223 | }) 224 | ) 225 | }) 226 | }) 227 | }) 228 | 229 | tape('ws with combined', function (t) { 230 | const close = combined_ws.server( 231 | function (stream) { 232 | console.log('combined_ws address', stream.address) 233 | t.ok(stream.address, 'has an address') 234 | echo(stream) 235 | }, 236 | null, 237 | function () { 238 | combined_ws.client(combined_ws.stringify(), function (err, stream) { 239 | if (err) throw err 240 | t.ok(stream.address, 'has an address') 241 | console.log('combined_ws address', stream.address) 242 | const pushable = Pushable() 243 | pushable.push(Buffer.from('hello world')) 244 | pull( 245 | pushable, 246 | stream, 247 | pull.through(function () { 248 | pushable.end() 249 | }), 250 | pull.collect(function (err, ary) { 251 | t.equal(Buffer.concat(ary).toString(), 'HELLO WORLD') 252 | close(t.end) 253 | }) 254 | ) 255 | }) 256 | } 257 | ) 258 | }) 259 | 260 | tape('error if try to connect on wrong protocol', function (t) { 261 | t.equal(combined_ws.parse(combined.stringify()), null) 262 | 263 | combined_ws.client(combined.stringify(), function (err, stream) { 264 | t.ok(err) 265 | t.end() 266 | }) 267 | }) 268 | 269 | tape('shs with seed', function (t) { 270 | const close = combined.server(echo, null, () => { 271 | const seed = cl.crypto_hash_sha256(Buffer.from('TEST SEED')) 272 | const bob = cl.crypto_sign_seed_keypair(seed) 273 | 274 | let checked 275 | check = function (id, cb) { 276 | checked = id 277 | if (id.toString('base64') === bob.publicKey.toString('base64')) 278 | cb(null, true) 279 | else cb(null, false) 280 | } 281 | 282 | const addr_with_seed = combined.stringify() + ':' + seed.toString('base64') 283 | 284 | combined.client(addr_with_seed, function (err, stream) { 285 | t.notOk(err) 286 | t.deepEqual(checked, bob.publicKey) 287 | stream.source(true, function () {}) 288 | close(t.end) 289 | }) 290 | }) 291 | }) 292 | 293 | tape('ws default port', function (t) { 294 | const ws = Ws({ 295 | external: 'domain.de', 296 | scope: 'public', 297 | server: { 298 | key: null, 299 | address: function () { 300 | return { port: 80 } 301 | }, 302 | }, 303 | }) 304 | t.equal(ws.stringify('public'), 'ws://domain.de') 305 | t.equal(ws.stringify('local'), null) 306 | t.equal(ws.stringify('device'), null) 307 | t.end() 308 | }) 309 | 310 | tape('wss default port', function (t) { 311 | const ws = Ws({ 312 | external: 'domain.de', 313 | scope: 'public', 314 | server: { 315 | key: true, 316 | address: function () { 317 | return { port: 443 } 318 | }, 319 | }, 320 | }) 321 | t.equal(ws.stringify('public'), 'wss://domain.de') 322 | t.equal(ws.stringify('local'), null) 323 | t.equal(ws.stringify('device'), null) 324 | t.end() 325 | }) 326 | 327 | tape('wss with key and cert', function (t) { 328 | const ws = Ws({ 329 | external: 'domain.de', 330 | scope: 'public', 331 | key: 'path', 332 | cert: 'path', 333 | }) 334 | t.equal(ws.stringify('public'), 'wss://domain.de') 335 | t.equal(ws.stringify('local'), null) 336 | t.equal(ws.stringify('device'), null) 337 | t.end() 338 | }) 339 | 340 | const onion = Onion({ scope: 'public' }) 341 | 342 | tape('onion plug', function (t) { 343 | // onion has no server 344 | t.equal(onion.stringify('public'), null) 345 | t.equal(onion.stringify('device'), null) 346 | t.equal(onion.stringify('local'), null) 347 | 348 | t.deepEqual(onion.parse('onion:3234j5sv346bpih2.onion:2349'), { 349 | name: 'onion', 350 | host: '3234j5sv346bpih2.onion', 351 | port: 2349, 352 | }) 353 | 354 | const oshs = Compose([onion, shs]) 355 | 356 | //should not return an address 357 | t.notOk(oshs.stringify()) 358 | t.end() 359 | }) 360 | 361 | function testServerId(combined, name, port) { 362 | tape('id of stream from server', function (t) { 363 | check = function (id, cb) { 364 | cb(null, true) 365 | } 366 | const close = combined.server( 367 | function (stream) { 368 | console.log('raw address on server:', stream.address) 369 | const addr = combined.parse(stream.address) 370 | t.ok(addr) 371 | console.log('address as seen on server', addr) 372 | t.equal((addr[0].name || addr[0].protocol).replace(':', ''), name) 373 | t.deepEqual(addr[1], combined.parse(combined.stringify())[1]) 374 | 375 | pull(stream.source, stream.sink) //echo 376 | }, 377 | function (err) { 378 | if (err) throw err 379 | }, 380 | function () { 381 | combined.client(combined.stringify(), function (err, stream) { 382 | if (err) throw err 383 | const addr = combined.parse(stream.address) 384 | t.equal((addr[0].name || addr[0].protocol).replace(':', ''), name) 385 | if (addr[0].protocol === 'ws:') t.equal(+addr[0].port, 4849) 386 | else t.equal(+addr[0].port, 4848) 387 | t.deepEqual(addr[1], combined.parse(combined.stringify())[1]) 388 | stream.source(true, function () { 389 | close(t.end) 390 | }) 391 | }) 392 | } 393 | ) 394 | }) 395 | } 396 | 397 | testServerId(combined, 'net') 398 | testServerId(combined_ws, 'ws') 399 | 400 | function testAbort(name, combined) { 401 | tape(name + ', aborted', function (t) { 402 | const close = combined.server(function onConnection() { 403 | throw new Error('should never happen') 404 | }) 405 | 406 | const abort = combined.client(combined.stringify(), function (err, stream) { 407 | t.ok(err) 408 | console.error('CLIENT ABORTED', err) 409 | // NOTE: without the timeout, we try to close the server 410 | // before it actually started listening, which fails and then 411 | // the server keeps runnung, causing the next test to fail with EADDRINUSE 412 | // 413 | // This is messy, combined.server should be a proper async call 414 | setTimeout(function () { 415 | console.log('Calling close') 416 | close(t.end) 417 | }, 500) 418 | }) 419 | 420 | abort() 421 | }) 422 | } 423 | 424 | testAbort('combined', combined) 425 | testAbort('combined.ws', combined_ws) 426 | 427 | function testErrorAddress(combined, type) { 428 | tape('error should have client address on it:' + type, function (t) { 429 | check = function (id, cb) { 430 | throw new Error('should never happen') 431 | } 432 | const close = combined.server( 433 | function (stream) { 434 | throw new Error('should never happen') 435 | }, 436 | function (err) { 437 | const addr = err.address 438 | t.ok(err.address.indexOf(type) == 0) //net or ws 439 | t.ok(/\~shs\:/.test(err.address)) 440 | //the shs address won't actually parse, because it doesn't have the key in it 441 | //because the key is not known in a wrong number. 442 | }, 443 | function () { 444 | //very unlikely this is the address, which will give a wrong number at the server. 445 | const addr = combined.stringify().replace(/shs:......../, 'shs:XXXXXXXX') 446 | combined.client(addr, function (err, stream) { 447 | //client should see client auth rejected 448 | t.ok(err) 449 | close(() => { 450 | if (type === 'ws') 451 | // we need to wait for the kill 452 | setTimeout(t.end, 1100) 453 | else t.end() 454 | }) 455 | }) 456 | } 457 | ) 458 | }) 459 | } 460 | 461 | testErrorAddress(combined, 'net') 462 | testErrorAddress(combined_ws, 'ws') 463 | 464 | tape('multiple public different hosts', function (t) { 465 | const net1 = Net({ host: '127.0.0.1', port: 4854, scope: 'public' }) 466 | const net2 = Net({ host: '::1', port: 4855, scope: 'public' }) 467 | 468 | const combined1 = Compose([net1, shs]) 469 | const combined2 = Compose([net2, shs]) 470 | 471 | t.equal( 472 | MultiServer([combined1, combined2]).stringify('public'), 473 | [combined1.stringify('public'), combined2.stringify('public')].join(';') 474 | ) 475 | 476 | t.end() 477 | }) 478 | 479 | tape('multiple scopes different hosts', function (t) { 480 | const net1 = Net({ 481 | host: '127.0.0.1', 482 | port: 4856, 483 | scope: ['local', 'device', 'public'], 484 | }) 485 | const net2 = Net({ 486 | host: '::1', 487 | port: 4857, 488 | scope: ['local', 'device', 'public'], 489 | }) 490 | 491 | const combined1 = Compose([net1, shs]) 492 | const combined2 = Compose([net2, shs]) 493 | 494 | t.equal( 495 | MultiServer([combined1, combined2]).stringify('public'), 496 | [combined1.stringify('public'), combined2.stringify('public')].join(';') 497 | ) 498 | 499 | t.end() 500 | }) 501 | 502 | tape('net: external is a string', function (t) { 503 | const net = Net({ 504 | external: 'domain.de', 505 | scope: 'public', 506 | port: '9966', 507 | server: { 508 | key: null, 509 | address: function () { 510 | return { port: 9966 } 511 | }, 512 | }, 513 | }) 514 | t.equal(net.stringify('public'), 'net:domain.de:9966') 515 | t.equal(net.stringify('local'), null) 516 | t.equal(net.stringify('device'), null) 517 | t.end() 518 | }) 519 | 520 | tape('net: external is an array', function (t) { 521 | const net = Net({ 522 | external: ['domain.de', 'funtime.net'], 523 | scope: 'public', 524 | port: '9966', 525 | server: { 526 | key: null, 527 | address: function () { 528 | return { port: 9966 } 529 | }, 530 | }, 531 | }) 532 | t.equal(net.stringify('public'), 'net:domain.de:9966;net:funtime.net:9966') 533 | t.equal(net.stringify('local'), null) 534 | t.equal(net.stringify('device'), null) 535 | t.end() 536 | }) 537 | 538 | tape( 539 | 'net: external is an array w/ a single entry & shs transform', 540 | function (t) { 541 | const net = Net({ 542 | external: ['domain.de'], 543 | scope: 'public', 544 | port: '9966', 545 | server: { 546 | key: null, 547 | address: function () { 548 | return { port: 9966 } 549 | }, 550 | }, 551 | }) 552 | const combined = Compose([net, shs]) 553 | t.equal( 554 | combined.stringify('public'), 555 | 'net:domain.de:9966~shs:+y42DK+BGzqvU00EWMKiyj4fITskSm+Drxq1Dt2s3Yw=' 556 | ) 557 | t.end() 558 | } 559 | ) 560 | 561 | tape( 562 | 'net: external is an array w/ multiple entries & shs transform', 563 | function (t) { 564 | const net = Net({ 565 | external: ['domain.de', 'funtime.net'], 566 | scope: 'public', 567 | port: '9966', 568 | server: { 569 | key: null, 570 | address: function () { 571 | return { port: 9966 } 572 | }, 573 | }, 574 | }) 575 | const combined = Compose([net, shs]) 576 | t.equal( 577 | combined.stringify('public'), 578 | 'net:domain.de:9966~shs:+y42DK+BGzqvU00EWMKiyj4fITskSm+Drxq1Dt2s3Yw=;net:funtime.net:9966~shs:+y42DK+BGzqvU00EWMKiyj4fITskSm+Drxq1Dt2s3Yw=' 579 | ) 580 | t.end() 581 | } 582 | ) 583 | 584 | tape('ws: external is a string', function (t) { 585 | const ws = Ws({ 586 | external: 'domain.de', 587 | scope: 'public', 588 | port: '9966', 589 | server: { 590 | key: null, 591 | address: function () { 592 | return { port: 9966 } 593 | }, 594 | }, 595 | }) 596 | t.equal(ws.stringify('public'), 'ws://domain.de:9966') 597 | t.equal(ws.stringify('local'), null) 598 | t.equal(ws.stringify('device'), null) 599 | t.end() 600 | }) 601 | 602 | tape('ws: external is an array', function (t) { 603 | const ws = Ws({ 604 | external: ['domain.de', 'funtime.net'], 605 | scope: 'public', 606 | port: '9966', 607 | server: { 608 | key: null, 609 | address: function () { 610 | return { port: 9966 } 611 | }, 612 | }, 613 | }) 614 | t.equal(ws.stringify('public'), 'ws://domain.de:9966;ws://funtime.net:9966') 615 | t.equal(ws.stringify('local'), null) 616 | t.equal(ws.stringify('device'), null) 617 | t.end() 618 | }) 619 | 620 | tape( 621 | 'ws: external is an array w/ a single entry & shs transform', 622 | function (t) { 623 | const ws = Ws({ 624 | external: ['domain.de'], 625 | scope: 'public', 626 | port: '9966', 627 | server: { 628 | key: null, 629 | address: function () { 630 | return { port: 9966 } 631 | }, 632 | }, 633 | }) 634 | const combined = Compose([ws, shs]) 635 | t.equal( 636 | combined.stringify('public'), 637 | 'ws://domain.de:9966~shs:+y42DK+BGzqvU00EWMKiyj4fITskSm+Drxq1Dt2s3Yw=' 638 | ) 639 | t.end() 640 | } 641 | ) 642 | 643 | tape( 644 | 'ws: external is an array w/ multiple entries & shs transform', 645 | function (t) { 646 | const ws = Ws({ 647 | external: ['domain.de', 'funtime.net'], 648 | scope: 'public', 649 | port: '9966', 650 | server: { 651 | key: null, 652 | address: function () { 653 | return { port: 9966 } 654 | }, 655 | }, 656 | }) 657 | const combined = Compose([ws, shs]) 658 | t.equal( 659 | combined.stringify('public'), 660 | 'ws://domain.de:9966~shs:+y42DK+BGzqvU00EWMKiyj4fITskSm+Drxq1Dt2s3Yw=;ws://funtime.net:9966~shs:+y42DK+BGzqvU00EWMKiyj4fITskSm+Drxq1Dt2s3Yw=' 661 | ) 662 | t.end() 663 | } 664 | ) 665 | --------------------------------------------------------------------------------