├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── IMPLEMENTATION_NOTES.md ├── LICENSE ├── README.md ├── package.json ├── src ├── circuit.js ├── circuit │ ├── dialer.js │ ├── hop.js │ ├── stop.js │ ├── stream-handler.js │ └── utils.js ├── index.js ├── listener.js ├── multicodec.js └── protocol │ └── index.js └── test ├── dialer.spec.js ├── fixtures └── nodes.js ├── helpers ├── test-node.js └── utils.js ├── hop.spec.js ├── listener.spec.js ├── proto.spec.js └── stop.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | package-lock.json 3 | yarn.lock 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | node_modules 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Vim editor swap files 40 | *.swp 41 | 42 | dist 43 | 44 | .history 45 | .vscode 46 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | node_modules 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: npm 3 | stages: 4 | - check 5 | - test 6 | - cov 7 | 8 | node_js: 9 | - '10' 10 | 11 | os: 12 | - linux 13 | - osx 14 | - windows 15 | 16 | script: npx nyc -s npm run test:node -- --bail 17 | after_success: npx nyc report --reporter=text-lcov > coverage.lcov && npx codecov 18 | 19 | jobs: 20 | include: 21 | - stage: check 22 | script: 23 | - npx aegir commitlint --travis 24 | - npx aegir dep-check 25 | - npm run lint 26 | 27 | - stage: test 28 | name: chrome 29 | addons: 30 | chrome: stable 31 | script: npx aegir test -t browser 32 | 33 | - stage: test 34 | name: firefox 35 | addons: 36 | firefox: latest 37 | script: npx aegir test -t browser -- --browsers FirefoxHeadless 38 | 39 | notifications: 40 | email: false 41 | 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## [0.3.7](https://github.com/libp2p/js-libp2p-circuit/compare/v0.3.6...v0.3.7) (2019-05-29) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * shim setImmediate in browser ([#51](https://github.com/libp2p/js-libp2p-circuit/issues/51)) ([9ea72f3](https://github.com/libp2p/js-libp2p-circuit/commit/9ea72f3)) 8 | 9 | 10 | 11 | 12 | ## [0.3.6](https://github.com/libp2p/js-libp2p-circuit/compare/v0.3.5...v0.3.6) (2019-03-12) 13 | 14 | 15 | 16 | 17 | ## [0.3.5](https://github.com/libp2p/js-libp2p-circuit/compare/v0.3.4...v0.3.5) (2019-02-15) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * src/dst addrs validation ([68cccbe](https://github.com/libp2p/js-libp2p-circuit/commit/68cccbe)) 23 | 24 | 25 | 26 | 27 | ## [0.3.4](https://github.com/libp2p/js-libp2p-circuit/compare/v0.3.3...v0.3.4) (2019-01-10) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * reduce bundle size ([#40](https://github.com/libp2p/js-libp2p-circuit/issues/40)) ([2c8ef1d](https://github.com/libp2p/js-libp2p-circuit/commit/2c8ef1d)) 33 | 34 | 35 | 36 | 37 | ## [0.3.3](https://github.com/libp2p/js-libp2p-circuit/compare/v0.3.2...v0.3.3) (2019-01-04) 38 | 39 | 40 | 41 | 42 | ## [0.3.2](https://github.com/libp2p/js-libp2p-circuit/compare/v0.3.1...v0.3.2) (2019-01-02) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * connection establishment event handling ([#41](https://github.com/libp2p/js-libp2p-circuit/issues/41)) ([c27c344](https://github.com/libp2p/js-libp2p-circuit/commit/c27c344)) 48 | 49 | 50 | 51 | 52 | ## [0.3.1](https://github.com/libp2p/js-libp2p-circuit/compare/v0.3.0...v0.3.1) (2018-11-15) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * catch bad peerid and update deps ([#39](https://github.com/libp2p/js-libp2p-circuit/issues/39)) ([f17539a](https://github.com/libp2p/js-libp2p-circuit/commit/f17539a)) 58 | 59 | 60 | 61 | 62 | # [0.3.0](https://github.com/libp2p/js-libp2p-circuit/compare/v0.2.1...v0.3.0) (2018-10-01) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * listener should emit connection ([#30](https://github.com/libp2p/js-libp2p-circuit/issues/30)) ([1ac430d](https://github.com/libp2p/js-libp2p-circuit/commit/1ac430d)) 68 | 69 | 70 | 71 | 72 | ## [0.2.1](https://github.com/libp2p/js-libp2p-circuit/compare/v0.2.0...v0.2.1) (2018-08-13) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * close streams ([546af97](https://github.com/libp2p/js-libp2p-circuit/commit/546af97)) 78 | * options ([22e8518](https://github.com/libp2p/js-libp2p-circuit/commit/22e8518)) 79 | 80 | 81 | 82 | 83 | # [0.2.0](https://github.com/libp2p/js-libp2p-circuit/compare/v0.1.5...v0.2.0) (2018-04-05) 84 | 85 | 86 | 87 | 88 | ## [0.1.5](https://github.com/libp2p/js-libp2p-circuit/compare/v0.1.4...v0.1.5) (2018-03-14) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * creating a proper peerId instead of using a simple buffer or string ([d061cda](https://github.com/libp2p/js-libp2p-circuit/commit/d061cda)) 94 | 95 | 96 | 97 | 98 | ## [0.1.4](https://github.com/libp2p/js-libp2p-circuit/compare/v0.1.3...v0.1.4) (2017-11-03) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * switch protobufs to protons ([#19](https://github.com/libp2p/js-libp2p-circuit/issues/19)) ([56b37da](https://github.com/libp2p/js-libp2p-circuit/commit/56b37da)) 104 | 105 | 106 | 107 | 108 | ## [0.1.3](https://github.com/libp2p/js-libp2p-circuit/compare/v0.1.0...v0.1.3) (2017-10-26) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * listenner callback expects a connection from transport ([#17](https://github.com/libp2p/js-libp2p-circuit/issues/17)) ([13179cb](https://github.com/libp2p/js-libp2p-circuit/commit/13179cb)) 114 | 115 | 116 | 117 | 118 | # 0.1.0 (2017-10-21) 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /IMPLEMENTATION_NOTES.md: -------------------------------------------------------------------------------- 1 | EDIT: This document is outdated and here only for historical purposes 2 | 3 | NOTE: This document is structured in an `if-then/else[if]-then` manner, each line is a precondition for following lines with a higher number of indentation 4 | 5 | Example: 6 | 7 | - if there are apples 8 | - eat them 9 | - if not, check for pears 10 | - then eat them 11 | - if not, check for cherries 12 | - then eat them 13 | 14 | Or, 15 | 16 | - if there are apples 17 | - eat them 18 | - if not 19 | - check for pears 20 | - then eat them 21 | - if not 22 | - check for cherries 23 | - then eat them 24 | 25 | In order to minimize nesting, the first example is preferred 26 | 27 | # Relay flow 28 | 29 | ## Relay transport (dialer/listener) 30 | 31 | - ### Dial over a relay 32 | - See if there is a relay that's already connected to the destination peer, if not 33 | - Ask all the peer's known relays to dial the destination peer until an active relay (one that can dial on behalf of other peers), or a relay that may have recently acquired a connection to the destination peer is successful. 34 | - If successful 35 | - Write the `/ipfs/relay/circuit/1.0.0` header to the relay, followed by the destination address 36 | - e.g. `/ipfs/relay/circuit/1.0.0\n/p2p-circuit/ipfs/QmDest`. 37 | - If no relays could connect, fail the same way a regular transport would 38 | - Once the connection has been established, the swarm should treat it as a regular connection, 39 | - i.e. muxing, encrypt, etc should all be performed on the relayed connection 40 | 41 | - ### Listen for relayed connections 42 | - Peer mounts the `/ipfs/relay/circuit/1.0.0` proto and listens for relayed connections 43 | - A connection arrives 44 | - read the address of the source peer from the incoming connection stream 45 | - if valid, create a PeerInfo object for that peer and add the incoming address to its multiaddresses list 46 | - pass the connection to `protocolMuxer(swarm.protocols, conn)` to have it go through the regular muxing/encryption flow 47 | 48 | - ### Relay discovery and static relay addresses in swarm config 49 | 50 | - #### Relay address in swarm config 51 | - A peer has relay addresses in its swarm config section 52 | - On node startup, connect to the relays in swarm config 53 | - if successful add address to swarms PeerInfo's multiaddresses 54 | - `identify` should take care of announcing that the peer is reachable over the listed relays 55 | 56 | - #### Passive relay discovery 57 | - A peer that can dial over `/ipfs/relay/circuit/1.0.0` listens for the `peer-mux-established` swarm event, every time a new muxed connection arrives, it checks if the incoming peer is a relay. (How would this work? Some way of discovering if its a relay is required.) 58 | - *Useful in cases when the peer/node doesn't know of any relays on startup and also, to learn of as many additional relays in the network as possible* 59 | - *Useful during startup, when connecting to bootstrap nodes. It allows us to implicitly learn if its a relay without having to explicitly add `/p2p-circuit` addresses to the bootstrap list* 60 | - *Also useful if the relay communicates its capabilities upon connecting to it, as to avoid additional unnecessary requests/queries. I.e. if it supports weather its able to forward connections and weather it supports the `ls` or other commands.* 61 | - *Should it be possible to disable passive relay discovery?* 62 | - This could be useful when the peer wants to be reachable **only** over the listed relays 63 | - If the incoming peer is a relay, send an `ls` and record its peers 64 | 65 | ## Relay Nodes 66 | 67 | - ### Passive relay node 68 | - *A passive relay does not explicitly dial into any requested peer, only those that it's swarm already has connections to.* 69 | - When the relay gets a request, read the the destination peer's multiaddr from the connection stream and if its a valid address and peer id 70 | - check its swarm's peerbook(?) see if its a known peer, if it is 71 | - use the swarms existing connection and 72 | - send the multistream header and the source peer address to the dest peer 73 | - e.g. `/ipfs/relay/circuit/1.0.0\n/p2p-circuit/ipfs/QmSource` 74 | - circuit the source and dest connections 75 | - if couldn't dial, or the connection/stream to the dest peer closed prematurelly 76 | - close the src stream 77 | 78 | 79 | - ### Active relay node 80 | - *An active relay node can dial other peers even if its swarm doesnt know about those peers* 81 | - When the relay gets a request, read the the destination peer's multiaddr from the connection stream and if its a valid address and peer id 82 | - use the swarm to dial to the dest node 83 | - send the multistream header and the source peer address to the dest peer 84 | - e.g. `/ipfs/relay/circuit/1.0.0\n/p2p-circuit/ipfs/QmSource` 85 | - circuit the source and dest connections 86 | - if couldn't dial, or the connection/stream to the dest peer closed prematurely 87 | - close the src stream 88 | 89 | - ### `ls` command 90 | - *A relay node can allow the peers known to it's swarm to be listed* 91 | - *this should be possible to enable/disable from the config* 92 | - when a relay gets the `ls` request 93 | - if enabled, get its swarm's peerbook's known peers and return their ids and multiaddrs 94 | - e.g `[{id: /ipfs/QmPeerId, addrs: ['ma1', 'ma2', 'ma3']}, ...]` 95 | - if disabled, respond with `na` 96 | 97 | 98 | ## Relay Implementation notes 99 | 100 | - ### Relay transport 101 | - Currently I've implemented the dialer and listener parts of the relay as a transport, meaning that it *tries* to implement the `interface-transport` interface as closely as possible. This seems to work pretty well and it's makes the dialer/listener parts really easy to plug in into the swarm. I think this is the cleanest solution. 102 | 103 | - ### `circuit-relay` 104 | - This is implemented as a separate piece (not a transport), and it can be enabled/disabled with a config. The transport listener however, will do the initial parsing of the incoming header and figure out weather it's a connection that's needs to be handled by the circuit-relay, or its a connection that is being relayed from a circuit-relay. 105 | 106 | ## Relay swarm integration 107 | 108 | - The relay transport is mounted explicitly by calling the `swarm.connection.relay(config.relay)` from libp2p 109 | - Swarm will register the dialer and listener using the swarm `transport.add` and `transport.listen` methods 110 | 111 | - ### Listener 112 | - the listener registers itself as a multistream handler on the `/ipfs/relay/circuit/1.0.0` proto 113 | - if `circuit-relay` is enabled, the listener will delegate connections to it if appropriate 114 | - when the listener receives a connection, it will read the multiaddr and determine if its a connection that needs to be relayed, or its a connection that is being relayed 115 | 116 | - ### Dialer 117 | - When the swarm attempts to dial to a peer, it will filter the protocols that the peer can be reached on 118 | - *The relay will be used in two cases* 119 | - If the peer has an explicit relay address that it can be reached on 120 | - no other transport is available 121 | - The relay will attempt to dial the peer over that relay 122 | - If no explicit relay address is provided 123 | - no other transport is available 124 | - A generic circuit address will be added to the peers multiaddr list 125 | - i.e. `/p2p-circuit/ipfs/QmDest` 126 | - If another transport is available, then use that instead of the relay 127 | 128 | 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Protocol Labs Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⛔️ DEPRECATED: libp2p-circuit is now included in [js-libp2p](https://github.com/libp2p/js-libp2p) 2 | ====== 3 | 4 | # js-libp2p-circuit 5 | 6 | 7 | [![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) 8 | [![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) 9 | [![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) 10 | [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) 11 | [![](https://img.shields.io/travis/libp2p/js-libp2p-circuit.svg?style=flat-square)](https://travis-ci.com/libp2p/js-libp2p-circuit) 12 | [![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-circuit.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-circuit) 13 | [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-circuit.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-circuit) 14 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) 15 | 16 | ![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png) 17 | ![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png) 18 | 19 | > Node.js implementation of the Circuit module that libp2p uses, which implements the [interface-connection](https://github.com/libp2p/interface-connection) interface for dial/listen. 20 | 21 | `libp2p-circuit` implements the circuit-relay mechanism that allows nodes that don't speak the same protocol to communicate using a third _relay_ node. 22 | 23 | **Note:** This module uses [pull-streams](https://pull-stream.github.io) for all stream based interfaces. 24 | 25 | ### Why? 26 | 27 | `circuit-relaying` uses additional nodes in order to transfer traffic between two otherwise unreachable nodes. This allows nodes that don't speak the same protocols or are running in limited environments, e.g. browsers and IoT devices, to communicate, which would otherwise be impossible given the fact that for example browsers don't have any socket support and as such cannot be directly dialed. 28 | 29 | The use of circuit-relaying is not limited to routing traffic between browser nodes, other uses include: 30 | - routing traffic between private nets and circumventing NAT layers 31 | - route mangling for better privacy (matreshka/shallot dialing). 32 | 33 | It's also possible to use it for clients that implement exotic transports such as devices that only have bluetooth radios to be reachable over bluetooth enabled relays and become full p2p nodes. 34 | 35 | ### libp2p-circuit and IPFS 36 | 37 | Prior to `libp2p-circuit` there was a rift in the IPFS network, were IPFS nodes could only access content from nodes that speak the same protocol, for example TCP only nodes could only dial to other TCP only nodes, same for any other protocol combination. In practice, this limitation was most visible in JS-IPFS browser nodes, since they can only dial out but not be dialed in over WebRTC or WebSockets, hence any content that the browser node held was not reachable by the rest of the network even through it was announced on the DHT. Non browser IPFS nodes would usually speak more than one protocol such as TCP, WebSockets and/or WebRTC, this made the problem less severe outside of the browser. `libp2p-circuit` solves this problem completely, as long as there are `relay nodes` capable of routing traffic between those nodes their content should be available to the rest of the IPFS network. 38 | 39 | ## Lead Maintainer 40 | 41 | [Jacob Heun](https://github.com/jacobheun) 42 | 43 | ## Table of Contents 44 | 45 | - [Install](#install) 46 | - [npm](#npm) 47 | - [Usage](#usage) 48 | - [Example](#example) 49 | - [This module uses `pull-streams`](#this-module-uses-pull-streams) 50 | - [Converting `pull-streams` to Node.js Streams](#converting-pull-streams-to-nodejs-streams) 51 | - [API](#api) 52 | - [Contribute](#contribute) 53 | - [License](#license) 54 | 55 | ## Install 56 | 57 | ### npm 58 | 59 | ```sh 60 | > npm i libp2p-circuit 61 | ``` 62 | 63 | ## Usage 64 | 65 | ### Example 66 | 67 | #### Create dialer/listener 68 | 69 | ```js 70 | const Circuit = require('libp2p-circuit') 71 | const multiaddr = require('multiaddr') 72 | const pull = require('pull-stream') 73 | 74 | const mh1 = multiaddr('/p2p-circuit/ipfs/QmHash') // dial /ipfs/QmHash over any circuit 75 | 76 | const circuit = new Circuit(swarmInstance, options) // pass swarm instance and options 77 | 78 | const listener = circuit.createListener(mh1, (connection) => { 79 | console.log('new connection opened') 80 | pull( 81 | pull.values(['hello']), 82 | socket 83 | ) 84 | }) 85 | 86 | listener.listen(() => { 87 | console.log('listening') 88 | 89 | pull( 90 | circuit.dial(mh1), 91 | pull.log, 92 | pull.onEnd(() => { 93 | circuit.close() 94 | }) 95 | ) 96 | }) 97 | ``` 98 | 99 | Outputs: 100 | 101 | ```sh 102 | listening 103 | new connection opened 104 | hello 105 | ``` 106 | 107 | #### Create `relay` 108 | 109 | ```js 110 | const Relay = require('libp2p-circuit').Relay 111 | 112 | const relay = new Relay(options) 113 | 114 | relay.mount(swarmInstance) // start relaying traffic 115 | ``` 116 | 117 | ### This module uses `pull-streams` 118 | 119 | We expose a streaming interface based on `pull-streams`, rather then on the Node.js core streams implementation (aka Node.js streams). `pull-streams` offers us a better mechanism for error handling and flow control guarantees. If you would like to know more about why we did this, see the discussion at this [issue](https://github.com/ipfs/js-ipfs/issues/362). 120 | 121 | You can learn more about pull-streams at: 122 | 123 | - [The history of Node.js streams, nodebp April 2014](https://www.youtube.com/watch?v=g5ewQEuXjsQ) 124 | - [The history of streams, 2016](http://dominictarr.com/post/145135293917/history-of-streams) 125 | - [pull-streams, the simple streaming primitive](http://dominictarr.com/post/149248845122/pull-streams-pull-streams-are-a-very-simple) 126 | - [pull-streams documentation](https://pull-stream.github.io/) 127 | 128 | #### Converting `pull-streams` to Node.js Streams 129 | 130 | If you are a Node.js streams user, you can convert a pull-stream to a Node.js stream using the module [`pull-stream-to-stream`](https://github.com/dominictarr/pull-stream-to-stream), giving you an instance of a Node.js stream that is linked to the pull-stream. For example: 131 | 132 | ```js 133 | const pullToStream = require('pull-stream-to-stream') 134 | 135 | const nodeStreamInstance = pullToStream(pullStreamInstance) 136 | // nodeStreamInstance is an instance of a Node.js Stream 137 | ``` 138 | 139 | To learn more about this utility, visit https://pull-stream.github.io/#pull-stream-to-stream. 140 | 141 | ## API 142 | 143 | [![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/interface-transport) 144 | 145 | `libp2p-circuit` accepts Circuit addresses for both IPFS and non IPFS encapsulated addresses, i.e: 146 | 147 | `/p2p-circuit/ip4/127.0.0.1/tcp/4001/ipfs/QmHash` 148 | 149 | Both for dialing and listening. 150 | 151 | ### Implementation rational 152 | 153 | This module is not a transport, however it implements `interface-transport` interface in order to allow circuit to be plugged with `libp2p-swarm`. The rational behind it is that, `libp2p-circuit` has a dial and listen flow, which fits nicely with other transports, moreover, it requires the _raw_ connection to be encrypted and muxed just as a regular transport's connection does. All in all, `interface-transport` ended up being the correct level of abstraction for circuit, as well as allowed us to reuse existing integration points in `libp2p-swarm` and `libp2p` without adding any ad-hoc logic. All parts of `interface-transport` are used, including `.getAddr` which returns a list of `/p2p-circuit` addresses that circuit is currently listening. 154 | 155 | ``` 156 | libp2p libp2p-circuit (transport) 157 | +-------------------------------------------------+ +--------------------------+ 158 | | +---------------------------------+ | | | 159 | | | | | | +------------------+ | 160 | | | | | circuit-relay listens for the HOP | | | | 161 | | | libp2p-swarm <------------------------------------------------| circuit-relay | | 162 | | | | | message to handle incomming relay | | | | 163 | | | | | requests from other nodes | +------------------+ | 164 | | +---------------------------------+ | | | 165 | | ^ ^ ^ ^ ^ ^ | | +------------------+ | 166 | | | | | | | | | | | +-------------+ | | 167 | | | | | | | | | dialer uses libp2p-swarm to dial | | | | | | 168 | | | | | +----------------------------------------------------------------------> dialer | | | 169 | | | | transports | | to a circuit-relay node using the | | | | | | 170 | | | | | | | | HOP message | | +-------------+ | | 171 | | | | | | | | | | | | 172 | | v v | v v | | | | | 173 | |+------------------|----------------------------+| | | +-------------+ | | 174 | || | | | | || | | | | | | 175 | ||libp2p-tcp |libp2p-ws | .... |libp2p-circuit || listener handles STOP messages from| | | listener | | | 176 | || | +--------------------------------------------------------------------------> | | | 177 | || | | |plugs in just || circuit-relay nodes | | +-------------+ | | 178 | || | | |as any other || | | | | 179 | || | | |transport || | +------------------+ | 180 | |+-----------------------------------------------+| | | 181 | +-------------------------------------------------+ +--------------------------+ 182 | ``` 183 | 184 | ## Contribute 185 | 186 | Contributions are welcome! The libp2p implementation in JavaScript is a work in progress. As such, there's a few things you can do right now to help out: 187 | 188 | - [Check out the existing issues](//github.com/libp2p/js-libp2p-circuit/issues). 189 | - **Perform code reviews**. 190 | - **Add tests**. There can never be enough tests. 191 | 192 | Please be aware that all interactions related to libp2p are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). 193 | 194 | Small note: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 195 | 196 | ## License 197 | 198 | [MIT](LICENSE) © 2017 Protocol Labs 199 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "libp2p-circuit", 3 | "version": "0.3.7", 4 | "description": "JavaScript implementation of circuit/switch relaying", 5 | "leadMaintainer": "Jacob Heun ", 6 | "main": "src/index.js", 7 | "files": [ 8 | "src", 9 | "dist" 10 | ], 11 | "scripts": { 12 | "lint": "aegir lint", 13 | "build": "aegir build", 14 | "test": "aegir test --target node --target browser --no-parallel", 15 | "test:node": "aegir test --target node", 16 | "test:browser": "aegir test --target browser", 17 | "release": "aegir release --target node --target browser", 18 | "release-minor": "aegir release --type minor --target node --target browser", 19 | "release-major": "aegir release --type major --target node --target browser", 20 | "coverage": "aegir coverage", 21 | "coverage-publish": "aegir coverage --provider coveralls" 22 | }, 23 | "pre-push": [ 24 | "lint" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/libp2p/js-libp2p-circuit.git" 29 | }, 30 | "keywords": [ 31 | "IPFS" 32 | ], 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/libp2p/js-libp2p-circuit/issues" 36 | }, 37 | "homepage": "https://github.com/libp2p/js-libp2p-circuit#readme", 38 | "contributors": [ 39 | "David Dias ", 40 | "Dmitriy Ryajov ", 41 | "Friedel Ziegelmayer ", 42 | "Hugo Dias ", 43 | "Jacob Heun ", 44 | "Jacob Heun ", 45 | "Maciej Krüger ", 46 | "Oli Evans ", 47 | "Pedro Teixeira ", 48 | "Vasco Santos ", 49 | "Victor Bjelkholm ", 50 | "Yusef Napora ", 51 | "dirkmc " 52 | ], 53 | "devDependencies": { 54 | "aegir": "^18.2.2", 55 | "chai": "^4.2.0", 56 | "dirty-chai": "^2.0.1", 57 | "libp2p": "~0.25.0", 58 | "libp2p-secio": "~0.11.1", 59 | "pull-protocol-buffers": "~0.1.2", 60 | "sinon": "^7.3.1" 61 | }, 62 | "dependencies": { 63 | "async": "^2.6.2", 64 | "debug": "^4.1.1", 65 | "interface-connection": "~0.3.3", 66 | "mafmt": "^6.0.7", 67 | "multiaddr": "^6.0.6", 68 | "once": "^1.4.0", 69 | "peer-id": "~0.12.2", 70 | "peer-info": "~0.15.1", 71 | "protons": "^1.0.1", 72 | "pull-handshake": "^1.1.4", 73 | "pull-length-prefixed": "^1.3.2", 74 | "pull-pair": "^1.1.0", 75 | "pull-stream": "^3.6.9" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/circuit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mafmt = require('mafmt') 4 | const multiaddr = require('multiaddr') 5 | 6 | const CircuitDialer = require('./circuit/dialer') 7 | const utilsFactory = require('./circuit/utils') 8 | 9 | const debug = require('debug') 10 | const log = debug('libp2p:circuit:transportdialer') 11 | log.err = debug('libp2p:circuit:error:transportdialer') 12 | 13 | const createListener = require('./listener') 14 | 15 | class Circuit { 16 | static get tag () { 17 | return 'Circuit' 18 | } 19 | 20 | /** 21 | * Creates an instance of Dialer. 22 | * 23 | * @param {Swarm} swarm - the swarm 24 | * @param {any} options - config options 25 | * 26 | * @memberOf Dialer 27 | */ 28 | constructor (swarm, options) { 29 | this.options = options || {} 30 | 31 | this.swarm = swarm 32 | this.dialer = null 33 | this.utils = utilsFactory(swarm) 34 | this.peerInfo = this.swarm._peerInfo 35 | this.relays = this.filter(this.peerInfo.multiaddrs.toArray()) 36 | 37 | // if no explicit relays, add a default relay addr 38 | if (this.relays.length === 0) { 39 | this.peerInfo 40 | .multiaddrs 41 | .add(`/p2p-circuit/ipfs/${this.peerInfo.id.toB58String()}`) 42 | } 43 | 44 | this.dialer = new CircuitDialer(swarm, options) 45 | 46 | this.swarm.on('peer-mux-established', (peerInfo) => { 47 | this.dialer.canHop(peerInfo) 48 | }) 49 | this.swarm.on('peer-mux-closed', (peerInfo) => { 50 | this.dialer.relayPeers.delete(peerInfo.id.toB58String()) 51 | }) 52 | } 53 | 54 | /** 55 | * Dial the relays in the Addresses.Swarm config 56 | * 57 | * @param {Array} relays 58 | * @return {void} 59 | */ 60 | _dialSwarmRelays () { 61 | // if we have relay addresses in swarm config, then dial those relays 62 | this.relays.forEach((relay) => { 63 | let relaySegments = relay 64 | .toString() 65 | .split('/p2p-circuit') 66 | .filter(segment => segment.length) 67 | 68 | relaySegments.forEach((relaySegment) => { 69 | const ma = this.utils.peerInfoFromMa(multiaddr(relaySegment)) 70 | this.dialer._dialRelay(ma) 71 | }) 72 | }) 73 | } 74 | 75 | /** 76 | * Dial a peer over a relay 77 | * 78 | * @param {multiaddr} ma - the multiaddr of the peer to dial 79 | * @param {Object} options - dial options 80 | * @param {Function} cb - a callback called once dialed 81 | * @returns {Connection} - the connection 82 | * 83 | * @memberOf Dialer 84 | */ 85 | dial (ma, options, cb) { 86 | return this.dialer.dial(ma, options, cb) 87 | } 88 | 89 | /** 90 | * Create a listener 91 | * 92 | * @param {any} options 93 | * @param {Function} handler 94 | * @return {listener} 95 | */ 96 | createListener (options, handler) { 97 | if (typeof options === 'function') { 98 | handler = options 99 | options = this.options || {} 100 | } 101 | 102 | const listener = createListener(this.swarm, options, handler) 103 | listener.on('listen', this._dialSwarmRelays.bind(this)) 104 | return listener 105 | } 106 | 107 | /** 108 | * Filter check for all multiaddresses 109 | * that this transport can dial on 110 | * 111 | * @param {any} multiaddrs 112 | * @returns {Array} 113 | * 114 | * @memberOf Dialer 115 | */ 116 | filter (multiaddrs) { 117 | if (!Array.isArray(multiaddrs)) { 118 | multiaddrs = [multiaddrs] 119 | } 120 | return multiaddrs.filter((ma) => { 121 | return mafmt.Circuit.matches(ma) 122 | }) 123 | } 124 | } 125 | 126 | module.exports = Circuit 127 | -------------------------------------------------------------------------------- /src/circuit/dialer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const once = require('once') 4 | const PeerId = require('peer-id') 5 | const waterfall = require('async/waterfall') 6 | const setImmediate = require('async/setImmediate') 7 | const multiaddr = require('multiaddr') 8 | 9 | const Connection = require('interface-connection').Connection 10 | 11 | const utilsFactory = require('./utils') 12 | const StreamHandler = require('./stream-handler') 13 | 14 | const debug = require('debug') 15 | const log = debug('libp2p:circuit:dialer') 16 | log.err = debug('libp2p:circuit:error:dialer') 17 | 18 | const multicodec = require('../multicodec') 19 | const proto = require('../protocol') 20 | 21 | class Dialer { 22 | /** 23 | * Creates an instance of Dialer. 24 | * @param {Swarm} swarm - the swarm 25 | * @param {any} options - config options 26 | * 27 | * @memberOf Dialer 28 | */ 29 | constructor (swarm, options) { 30 | this.swarm = swarm 31 | this.relayPeers = new Map() 32 | this.relayConns = new Map() 33 | this.options = options 34 | this.utils = utilsFactory(swarm) 35 | } 36 | 37 | /** 38 | * Helper that returns a relay connection 39 | * 40 | * @param {*} relay 41 | * @param {*} callback 42 | * @returns {Function} - callback 43 | */ 44 | _dialRelayHelper (relay, callback) { 45 | if (this.relayConns.has(relay.id.toB58String())) { 46 | return callback(null, this.relayConns.get(relay.id.toB58String())) 47 | } 48 | 49 | return this._dialRelay(relay, callback) 50 | } 51 | 52 | /** 53 | * Dial a peer over a relay 54 | * 55 | * @param {multiaddr} ma - the multiaddr of the peer to dial 56 | * @param {Function} cb - a callback called once dialed 57 | * @returns {Connection} - the connection 58 | * 59 | */ 60 | dial (ma, cb) { 61 | cb = cb || (() => { }) 62 | const strMa = ma.toString() 63 | if (!strMa.includes('/p2p-circuit')) { 64 | log.err('invalid circuit address') 65 | return cb(new Error('invalid circuit address')) 66 | } 67 | 68 | const addr = strMa.split('p2p-circuit') // extract relay address if any 69 | const relay = addr[0] === '/' ? null : multiaddr(addr[0]) 70 | const peer = multiaddr(addr[1] || addr[0]) 71 | 72 | const dstConn = new Connection() 73 | setImmediate( 74 | this._dialPeer.bind(this), 75 | peer, 76 | relay, 77 | (err, conn) => { 78 | if (err) { 79 | log.err(err) 80 | return cb(err) 81 | } 82 | 83 | dstConn.setInnerConn(conn) 84 | cb(null, dstConn) 85 | }) 86 | 87 | return dstConn 88 | } 89 | 90 | /** 91 | * Does the peer support the HOP protocol 92 | * 93 | * @param {PeerInfo} peer 94 | * @param {Function} callback 95 | * @returns {void} 96 | */ 97 | canHop (peer, callback) { 98 | callback = once(callback || (() => { })) 99 | 100 | this._dialRelayHelper(peer, (err, conn) => { 101 | if (err) { 102 | return callback(err) 103 | } 104 | 105 | const sh = new StreamHandler(conn) 106 | waterfall([ 107 | (cb) => sh.write(proto.CircuitRelay.encode({ 108 | type: proto.CircuitRelay.Type.CAN_HOP 109 | }), cb), 110 | (cb) => sh.read(cb) 111 | ], (err, msg) => { 112 | if (err) { 113 | return callback(err) 114 | } 115 | const response = proto.CircuitRelay.decode(msg) 116 | 117 | if (response.code !== proto.CircuitRelay.Status.SUCCESS) { 118 | const err = new Error(`HOP not supported, skipping - ${this.utils.getB58String(peer)}`) 119 | log(err) 120 | return callback(err) 121 | } 122 | 123 | log('HOP supported adding as relay - %s', this.utils.getB58String(peer)) 124 | this.relayPeers.set(this.utils.getB58String(peer), peer) 125 | sh.close() 126 | callback() 127 | }) 128 | }) 129 | } 130 | 131 | /** 132 | * Dial the destination peer over a relay 133 | * 134 | * @param {multiaddr} dstMa 135 | * @param {Connection|PeerInfo} relay 136 | * @param {Function} cb 137 | * @return {Function|void} 138 | * @private 139 | */ 140 | _dialPeer (dstMa, relay, cb) { 141 | if (typeof relay === 'function') { 142 | cb = relay 143 | relay = null 144 | } 145 | 146 | if (!cb) { 147 | cb = () => {} 148 | } 149 | 150 | dstMa = multiaddr(dstMa) 151 | // if no relay provided, dial on all available relays until one succeeds 152 | if (!relay) { 153 | const relays = Array.from(this.relayPeers.values()) 154 | let next = (nextRelay) => { 155 | if (!nextRelay) { 156 | let err = `no relay peers were found or all relays failed to dial` 157 | log.err(err) 158 | return cb(err) 159 | } 160 | 161 | return this._negotiateRelay( 162 | nextRelay, 163 | dstMa, 164 | (err, conn) => { 165 | if (err) { 166 | log.err(err) 167 | return next(relays.shift()) 168 | } 169 | cb(null, conn) 170 | }) 171 | } 172 | next(relays.shift()) 173 | } else { 174 | return this._negotiateRelay( 175 | relay, 176 | dstMa, 177 | (err, conn) => { 178 | if (err) { 179 | log.err('An error has occurred negotiating the relay connection', err) 180 | return cb(err) 181 | } 182 | 183 | return cb(null, conn) 184 | }) 185 | } 186 | } 187 | 188 | /** 189 | * Negotiate the relay connection 190 | * 191 | * @param {Multiaddr|PeerInfo|Connection} relay - the Connection or PeerInfo of the relay 192 | * @param {multiaddr} dstMa - the multiaddr of the peer to relay the connection for 193 | * @param {Function} callback - a callback which gets the negotiated relay connection 194 | * @returns {void} 195 | * @private 196 | * 197 | * @memberOf Dialer 198 | */ 199 | _negotiateRelay (relay, dstMa, callback) { 200 | dstMa = multiaddr(dstMa) 201 | relay = this.utils.peerInfoFromMa(relay) 202 | const srcMas = this.swarm._peerInfo.multiaddrs.toArray() 203 | this._dialRelayHelper(relay, (err, conn) => { 204 | if (err) { 205 | log.err(err) 206 | return callback(err) 207 | } 208 | let sh = new StreamHandler(conn) 209 | waterfall([ 210 | (cb) => { 211 | log('negotiating relay for peer %s', dstMa.getPeerId()) 212 | let dstPeerId 213 | try { 214 | dstPeerId = PeerId.createFromB58String(dstMa.getPeerId()).id 215 | } catch (err) { 216 | return cb(err) 217 | } 218 | sh.write( 219 | proto.CircuitRelay.encode({ 220 | type: proto.CircuitRelay.Type.HOP, 221 | srcPeer: { 222 | id: this.swarm._peerInfo.id.id, 223 | addrs: srcMas.map((addr) => addr.buffer) 224 | }, 225 | dstPeer: { 226 | id: dstPeerId, 227 | addrs: [dstMa.buffer] 228 | } 229 | }), cb) 230 | }, 231 | (cb) => sh.read(cb) 232 | ], (err, msg) => { 233 | if (err) { 234 | return callback(err) 235 | } 236 | const message = proto.CircuitRelay.decode(msg) 237 | if (message.type !== proto.CircuitRelay.Type.STATUS) { 238 | return callback(new Error(`Got invalid message type - ` + 239 | `expected ${proto.CircuitRelay.Type.STATUS} got ${message.type}`)) 240 | } 241 | 242 | if (message.code !== proto.CircuitRelay.Status.SUCCESS) { 243 | return callback(new Error(`Got ${message.code} error code trying to dial over relay`)) 244 | } 245 | 246 | callback(null, new Connection(sh.rest())) 247 | }) 248 | }) 249 | } 250 | 251 | /** 252 | * Dial a relay peer by its PeerInfo 253 | * 254 | * @param {PeerInfo} peer - the PeerInfo of the relay peer 255 | * @param {Function} cb - a callback with the connection to the relay peer 256 | * @returns {void} 257 | * @private 258 | */ 259 | _dialRelay (peer, cb) { 260 | cb = once(cb || (() => { })) 261 | 262 | this.swarm.dial( 263 | peer, 264 | multicodec.relay, 265 | once((err, conn) => { 266 | if (err) { 267 | log.err(err) 268 | return cb(err) 269 | } 270 | cb(null, conn) 271 | })) 272 | } 273 | } 274 | 275 | module.exports = Dialer 276 | -------------------------------------------------------------------------------- /src/circuit/hop.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const pull = require('pull-stream/pull') 4 | const debug = require('debug') 5 | const PeerInfo = require('peer-info') 6 | const PeerId = require('peer-id') 7 | const EE = require('events').EventEmitter 8 | const once = require('once') 9 | const utilsFactory = require('./utils') 10 | const StreamHandler = require('./stream-handler') 11 | const proto = require('../protocol').CircuitRelay 12 | const multiaddr = require('multiaddr') 13 | const series = require('async/series') 14 | const waterfall = require('async/waterfall') 15 | const setImmediate = require('async/setImmediate') 16 | 17 | const multicodec = require('./../multicodec') 18 | 19 | const log = debug('libp2p:circuit:relay') 20 | log.err = debug('libp2p:circuit:error:relay') 21 | 22 | class Hop extends EE { 23 | /** 24 | * Construct a Circuit object 25 | * 26 | * This class will handle incoming circuit connections and 27 | * either start a relay or hand the relayed connection to 28 | * the swarm 29 | * 30 | * @param {Swarm} swarm 31 | * @param {Object} options 32 | */ 33 | constructor (swarm, options) { 34 | super() 35 | this.swarm = swarm 36 | this.peerInfo = this.swarm._peerInfo 37 | this.utils = utilsFactory(swarm) 38 | this.config = options || { active: false, enabled: false } 39 | this.active = this.config.active 40 | } 41 | 42 | /** 43 | * Handle the relay message 44 | * 45 | * @param {CircuitRelay} message 46 | * @param {StreamHandler} sh 47 | * @returns {*} 48 | */ 49 | handle (message, sh) { 50 | if (!this.config.enabled) { 51 | this.utils.writeResponse( 52 | sh, 53 | proto.Status.HOP_CANT_SPEAK_RELAY) 54 | return sh.close() 55 | } 56 | 57 | // check if message is `CAN_HOP` 58 | if (message.type === proto.Type.CAN_HOP) { 59 | this.utils.writeResponse( 60 | sh, 61 | proto.Status.SUCCESS) 62 | return sh.close() 63 | } 64 | 65 | // This is a relay request - validate and create a circuit 66 | let srcPeerId = null 67 | let dstPeerId = null 68 | try { 69 | srcPeerId = PeerId.createFromBytes(message.srcPeer.id).toB58String() 70 | dstPeerId = PeerId.createFromBytes(message.dstPeer.id).toB58String() 71 | } catch (err) { 72 | log.err(err) 73 | 74 | if (!srcPeerId) { 75 | this.utils.writeResponse( 76 | sh, 77 | proto.Status.HOP_SRC_MULTIADDR_INVALID) 78 | return sh.close() 79 | } 80 | 81 | if (!dstPeerId) { 82 | this.utils.writeResponse( 83 | sh, 84 | proto.Status.HOP_DST_MULTIADDR_INVALID) 85 | return sh.close() 86 | } 87 | } 88 | 89 | if (srcPeerId === dstPeerId) { 90 | this.utils.writeResponse( 91 | sh, 92 | proto.Status.HOP_CANT_RELAY_TO_SELF) 93 | return sh.close() 94 | } 95 | 96 | if (!message.dstPeer.addrs.length) { 97 | // TODO: use encapsulate here 98 | const addr = multiaddr(`/p2p-circuit/ipfs/${dstPeerId}`).buffer 99 | message.dstPeer.addrs.push(addr) 100 | } 101 | 102 | log('trying to establish a circuit: %s <-> %s', srcPeerId, dstPeerId) 103 | const noPeer = () => { 104 | // log.err(err) 105 | this.utils.writeResponse( 106 | sh, 107 | proto.Status.HOP_NO_CONN_TO_DST) 108 | return sh.close() 109 | } 110 | 111 | const isConnected = (cb) => { 112 | let dstPeer 113 | try { 114 | dstPeer = this.swarm._peerBook.get(dstPeerId) 115 | if (!dstPeer.isConnected() && !this.active) { 116 | const err = new Error(`No Connection to peer ${dstPeerId}`) 117 | noPeer(err) 118 | return cb(err) 119 | } 120 | } catch (err) { 121 | if (!this.active) { 122 | noPeer(err) 123 | return cb(err) 124 | } 125 | } 126 | cb() 127 | } 128 | 129 | series([ 130 | (cb) => this.utils.validateAddrs(message, sh, proto.Type.HOP, cb), 131 | (cb) => isConnected(cb), 132 | (cb) => this._circuit(sh, message, cb) 133 | ], (err) => { 134 | if (err) { 135 | log.err(err) 136 | sh.close() 137 | return setImmediate(() => this.emit('circuit:error', err)) 138 | } 139 | setImmediate(() => this.emit('circuit:success')) 140 | }) 141 | } 142 | 143 | /** 144 | * Connect to STOP 145 | * 146 | * @param {PeerInfo} peer 147 | * @param {StreamHandler} srcSh 148 | * @param {function} callback 149 | * @returns {void} 150 | */ 151 | _connectToStop (peer, srcSh, callback) { 152 | this._dialPeer(peer, (err, dstConn) => { 153 | if (err) { 154 | this.utils.writeResponse( 155 | srcSh, 156 | proto.Status.HOP_CANT_DIAL_DST) 157 | log.err(err) 158 | return callback(err) 159 | } 160 | 161 | return this.utils.writeResponse( 162 | srcSh, 163 | proto.Status.SUCCESS, 164 | (err) => { 165 | if (err) { 166 | log.err(err) 167 | return callback(err) 168 | } 169 | return callback(null, dstConn) 170 | }) 171 | }) 172 | } 173 | 174 | /** 175 | * Negotiate STOP 176 | * 177 | * @param {StreamHandler} dstSh 178 | * @param {StreamHandler} srcSh 179 | * @param {CircuitRelay} message 180 | * @param {function} callback 181 | * @returns {void} 182 | */ 183 | _negotiateStop (dstSh, srcSh, message, callback) { 184 | const stopMsg = Object.assign({}, message, { 185 | type: proto.Type.STOP // change the message type 186 | }) 187 | dstSh.write(proto.encode(stopMsg), 188 | (err) => { 189 | if (err) { 190 | this.utils.writeResponse( 191 | srcSh, 192 | proto.Status.HOP_CANT_OPEN_DST_STREAM) 193 | log.err(err) 194 | return callback(err) 195 | } 196 | 197 | // read response from STOP 198 | dstSh.read((err, msg) => { 199 | if (err) { 200 | log.err(err) 201 | return callback(err) 202 | } 203 | 204 | const message = proto.decode(msg) 205 | if (message.code !== proto.Status.SUCCESS) { 206 | return callback(new Error(`Unable to create circuit!`)) 207 | } 208 | 209 | return callback(null, msg) 210 | }) 211 | }) 212 | } 213 | 214 | /** 215 | * Attempt to make a circuit from A <-> R <-> B where R is this relay 216 | * 217 | * @param {StreamHandler} srcSh - the source stream handler 218 | * @param {CircuitRelay} message - the message with the src and dst entries 219 | * @param {Function} callback - callback to signal success or failure 220 | * @returns {void} 221 | * @private 222 | */ 223 | _circuit (srcSh, message, callback) { 224 | let dstSh = null 225 | waterfall([ 226 | (cb) => this._connectToStop(message.dstPeer, srcSh, cb), 227 | (_dstConn, cb) => { 228 | dstSh = new StreamHandler(_dstConn) 229 | this._negotiateStop(dstSh, srcSh, message, cb) 230 | } 231 | ], (err) => { 232 | if (err) { 233 | // close/end the source stream if there was an error 234 | if (srcSh) { 235 | srcSh.close() 236 | } 237 | 238 | if (dstSh) { 239 | dstSh.close() 240 | } 241 | return callback(err) 242 | } 243 | 244 | const src = srcSh.rest() 245 | const dst = dstSh.rest() 246 | 247 | const srcIdStr = PeerId.createFromBytes(message.srcPeer.id).toB58String() 248 | const dstIdStr = PeerId.createFromBytes(message.dstPeer.id).toB58String() 249 | 250 | // circuit the src and dst streams 251 | pull( 252 | src, 253 | dst, 254 | src 255 | ) 256 | log('circuit %s <-> %s established', srcIdStr, dstIdStr) 257 | callback() 258 | }) 259 | } 260 | 261 | /** 262 | * Dial the dest peer and create a circuit 263 | * 264 | * @param {Multiaddr} dstPeer 265 | * @param {Function} callback 266 | * @returns {void} 267 | * @private 268 | */ 269 | _dialPeer (dstPeer, callback) { 270 | const peerInfo = new PeerInfo(PeerId.createFromBytes(dstPeer.id)) 271 | dstPeer.addrs.forEach((a) => peerInfo.multiaddrs.add(a)) 272 | this.swarm.dial(peerInfo, multicodec.relay, once((err, conn) => { 273 | if (err) { 274 | log.err(err) 275 | return callback(err) 276 | } 277 | 278 | callback(null, conn) 279 | })) 280 | } 281 | } 282 | 283 | module.exports = Hop 284 | -------------------------------------------------------------------------------- /src/circuit/stop.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const setImmediate = require('async/setImmediate') 4 | 5 | const EE = require('events').EventEmitter 6 | const Connection = require('interface-connection').Connection 7 | const utilsFactory = require('./utils') 8 | const PeerInfo = require('peer-info') 9 | const proto = require('../protocol').CircuitRelay 10 | const series = require('async/series') 11 | 12 | const debug = require('debug') 13 | 14 | const log = debug('libp2p:circuit:stop') 15 | log.err = debug('libp2p:circuit:error:stop') 16 | 17 | class Stop extends EE { 18 | constructor (swarm) { 19 | super() 20 | this.swarm = swarm 21 | this.utils = utilsFactory(swarm) 22 | } 23 | 24 | /** 25 | * Handle the incoming STOP message 26 | * 27 | * @param {{}} msg - the parsed protobuf message 28 | * @param {StreamHandler} sh - the stream handler wrapped connection 29 | * @param {Function} callback - callback 30 | * @returns {undefined} 31 | */ 32 | handle (msg, sh, callback) { 33 | callback = callback || (() => {}) 34 | 35 | series([ 36 | (cb) => this.utils.validateAddrs(msg, sh, proto.Type.STOP, cb), 37 | (cb) => this.utils.writeResponse(sh, proto.Status.Success, cb) 38 | ], (err) => { 39 | if (err) { 40 | // we don't return the error here, 41 | // since multistream select don't expect one 42 | callback() 43 | return log(err) 44 | } 45 | 46 | const peerInfo = new PeerInfo(this.utils.peerIdFromId(msg.srcPeer.id)) 47 | msg.srcPeer.addrs.forEach((addr) => peerInfo.multiaddrs.add(addr)) 48 | const newConn = new Connection(sh.rest()) 49 | newConn.setPeerInfo(peerInfo) 50 | setImmediate(() => this.emit('connection', newConn)) 51 | callback(newConn) 52 | }) 53 | } 54 | } 55 | 56 | module.exports = Stop 57 | -------------------------------------------------------------------------------- /src/circuit/stream-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const values = require('pull-stream/sources/values') 4 | const collect = require('pull-stream/sinks/collect') 5 | const empty = require('pull-stream/sources/empty') 6 | const pull = require('pull-stream/pull') 7 | const lp = require('pull-length-prefixed') 8 | const handshake = require('pull-handshake') 9 | 10 | const debug = require('debug') 11 | const log = debug('libp2p:circuit:stream-handler') 12 | log.err = debug('libp2p:circuit:error:stream-handler') 13 | 14 | class StreamHandler { 15 | /** 16 | * Create a stream handler for connection 17 | * 18 | * @param {Connection} conn - connection to read/write 19 | * @param {Function|undefined} cb - handshake callback called on error 20 | * @param {Number} timeout - handshake timeout 21 | * @param {Number} maxLength - max bytes length of message 22 | */ 23 | constructor (conn, cb, timeout, maxLength) { 24 | this.conn = conn 25 | this.stream = null 26 | this.shake = null 27 | this.timeout = cb || 1000 * 60 28 | this.maxLength = maxLength || 4096 29 | 30 | if (typeof cb === 'function') { 31 | this.timeout = timeout || 1000 * 60 32 | } 33 | 34 | this.stream = handshake({ timeout: this.timeout }, cb) 35 | this.shake = this.stream.handshake 36 | 37 | pull(this.stream, conn, this.stream) 38 | } 39 | 40 | isValid () { 41 | return this.conn && this.shake && this.stream 42 | } 43 | 44 | /** 45 | * Read and decode message 46 | * 47 | * @param {Function} cb 48 | * @returns {void|Function} 49 | */ 50 | read (cb) { 51 | if (!this.isValid()) { 52 | return cb(new Error(`handler is not in a valid state`)) 53 | } 54 | 55 | lp.decodeFromReader( 56 | this.shake, 57 | { maxLength: this.maxLength }, 58 | (err, msg) => { 59 | if (err) { 60 | log.err(err) 61 | // this.shake.abort(err) 62 | return cb(err) 63 | } 64 | 65 | return cb(null, msg) 66 | }) 67 | } 68 | 69 | /** 70 | * Encode and write array of buffers 71 | * 72 | * @param {Buffer[]} msg 73 | * @param {Function} [cb] 74 | * @returns {Function} 75 | */ 76 | write (msg, cb) { 77 | cb = cb || (() => {}) 78 | 79 | if (!this.isValid()) { 80 | return cb(new Error(`handler is not in a valid state`)) 81 | } 82 | 83 | pull( 84 | values([msg]), 85 | lp.encode(), 86 | collect((err, encoded) => { 87 | if (err) { 88 | log.err(err) 89 | this.shake.abort(err) 90 | return cb(err) 91 | } 92 | 93 | encoded.forEach((e) => this.shake.write(e)) 94 | cb() 95 | }) 96 | ) 97 | } 98 | 99 | /** 100 | * Get the raw Connection 101 | * 102 | * @returns {null|Connection|*} 103 | */ 104 | getRawConn () { 105 | return this.conn 106 | } 107 | 108 | /** 109 | * Return the handshake rest stream and invalidate handler 110 | * 111 | * @return {*|{source, sink}} 112 | */ 113 | rest () { 114 | const rest = this.shake.rest() 115 | 116 | this.conn = null 117 | this.stream = null 118 | this.shake = null 119 | return rest 120 | } 121 | 122 | /** 123 | * Close the stream 124 | * 125 | * @returns {undefined} 126 | */ 127 | close () { 128 | if (!this.isValid()) { 129 | return 130 | } 131 | 132 | // close stream 133 | pull( 134 | empty(), 135 | this.rest() 136 | ) 137 | } 138 | } 139 | 140 | module.exports = StreamHandler 141 | -------------------------------------------------------------------------------- /src/circuit/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const multiaddr = require('multiaddr') 4 | const PeerInfo = require('peer-info') 5 | const PeerId = require('peer-id') 6 | const proto = require('../protocol') 7 | 8 | module.exports = function (swarm) { 9 | /** 10 | * Get b58 string from multiaddr or peerinfo 11 | * 12 | * @param {Multiaddr|PeerInfo} peer 13 | * @return {*} 14 | */ 15 | function getB58String (peer) { 16 | let b58Id = null 17 | if (multiaddr.isMultiaddr(peer)) { 18 | const relayMa = multiaddr(peer) 19 | b58Id = relayMa.getPeerId() 20 | } else if (PeerInfo.isPeerInfo(peer)) { 21 | b58Id = peer.id.toB58String() 22 | } 23 | 24 | return b58Id 25 | } 26 | 27 | /** 28 | * Helper to make a peer info from a multiaddrs 29 | * 30 | * @param {Multiaddr|PeerInfo|PeerId} ma 31 | * @param {Swarm} swarm 32 | * @return {PeerInfo} 33 | * @private 34 | */ 35 | // TODO: this is ripped off of libp2p, should probably be a generally available util function 36 | function peerInfoFromMa (peer) { 37 | let p 38 | // PeerInfo 39 | if (PeerInfo.isPeerInfo(peer)) { 40 | p = peer 41 | // Multiaddr instance (not string) 42 | } else if (multiaddr.isMultiaddr(peer)) { 43 | const peerIdB58Str = peer.getPeerId() 44 | try { 45 | p = swarm._peerBook.get(peerIdB58Str) 46 | } catch (err) { 47 | p = new PeerInfo(PeerId.createFromB58String(peerIdB58Str)) 48 | } 49 | p.multiaddrs.add(peer) 50 | // PeerId 51 | } else if (PeerId.isPeerId(peer)) { 52 | const peerIdB58Str = peer.toB58String() 53 | p = swarm._peerBook.has(peerIdB58Str) ? swarm._peerBook.get(peerIdB58Str) : peer 54 | } 55 | 56 | return p 57 | } 58 | 59 | /** 60 | * Checks if peer has an existing connection 61 | * 62 | * @param {String} peerId 63 | * @param {Swarm} swarm 64 | * @return {Boolean} 65 | */ 66 | function isPeerConnected (peerId) { 67 | return swarm.muxedConns[peerId] || swarm.conns[peerId] 68 | } 69 | 70 | /** 71 | * Write a response 72 | * 73 | * @param {StreamHandler} streamHandler 74 | * @param {CircuitRelay.Status} status 75 | * @param {Function} cb 76 | * @returns {*} 77 | */ 78 | function writeResponse (streamHandler, status, cb) { 79 | cb = cb || (() => {}) 80 | streamHandler.write(proto.CircuitRelay.encode({ 81 | type: proto.CircuitRelay.Type.STATUS, 82 | code: status 83 | })) 84 | return cb() 85 | } 86 | 87 | /** 88 | * Validate incomming HOP/STOP message 89 | * 90 | * @param {CircuitRelay} msg 91 | * @param {StreamHandler} streamHandler 92 | * @param {CircuitRelay.Type} type 93 | * @returns {*} 94 | * @param {Function} cb 95 | */ 96 | function validateAddrs (msg, streamHandler, type, cb) { 97 | try { 98 | msg.dstPeer.addrs.forEach((addr) => { 99 | return multiaddr(addr) 100 | }) 101 | } catch (err) { 102 | writeResponse(streamHandler, type === proto.CircuitRelay.Type.HOP 103 | ? proto.CircuitRelay.Status.HOP_DST_MULTIADDR_INVALID 104 | : proto.CircuitRelay.Status.STOP_DST_MULTIADDR_INVALID) 105 | return cb(err) 106 | } 107 | 108 | try { 109 | msg.srcPeer.addrs.forEach((addr) => { 110 | return multiaddr(addr) 111 | }) 112 | } catch (err) { 113 | writeResponse(streamHandler, type === proto.CircuitRelay.Type.HOP 114 | ? proto.CircuitRelay.Status.HOP_SRC_MULTIADDR_INVALID 115 | : proto.CircuitRelay.Status.STOP_SRC_MULTIADDR_INVALID) 116 | return cb(err) 117 | } 118 | 119 | return cb(null) 120 | } 121 | 122 | function peerIdFromId (id) { 123 | if (typeof id === 'string') { 124 | return PeerId.createFromB58String(id) 125 | } 126 | 127 | return PeerId.createFromBytes(id) 128 | } 129 | 130 | return { 131 | getB58String, 132 | peerInfoFromMa, 133 | isPeerConnected, 134 | validateAddrs, 135 | writeResponse, 136 | peerIdFromId 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('./circuit') 4 | -------------------------------------------------------------------------------- /src/listener.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const setImmediate = require('async/setImmediate') 4 | 5 | const multicodec = require('./multicodec') 6 | const EE = require('events').EventEmitter 7 | const multiaddr = require('multiaddr') 8 | const mafmt = require('mafmt') 9 | const Stop = require('./circuit/stop') 10 | const Hop = require('./circuit/hop') 11 | const proto = require('./protocol') 12 | const utilsFactory = require('./circuit/utils') 13 | 14 | const StreamHandler = require('./circuit/stream-handler') 15 | 16 | const debug = require('debug') 17 | 18 | const log = debug('libp2p:circuit:listener') 19 | log.err = debug('libp2p:circuit:error:listener') 20 | 21 | module.exports = (swarm, options, connHandler) => { 22 | const listener = new EE() 23 | const utils = utilsFactory(swarm) 24 | 25 | listener.stopHandler = new Stop(swarm) 26 | listener.stopHandler.on('connection', (conn) => listener.emit('connection', conn)) 27 | listener.hopHandler = new Hop(swarm, options.hop) 28 | 29 | /** 30 | * Add swarm handler and listen for incoming connections 31 | * 32 | * @param {Multiaddr} ma 33 | * @param {Function} callback 34 | * @return {void} 35 | */ 36 | listener.listen = (ma, callback) => { 37 | callback = callback || (() => {}) 38 | 39 | swarm.handle(multicodec.relay, (_, conn) => { 40 | const sh = new StreamHandler(conn) 41 | 42 | sh.read((err, msg) => { 43 | if (err) { 44 | log.err(err) 45 | return 46 | } 47 | 48 | let request = null 49 | try { 50 | request = proto.CircuitRelay.decode(msg) 51 | } catch (err) { 52 | return utils.writeResponse( 53 | sh, 54 | proto.CircuitRelay.Status.MALFORMED_MESSAGE) 55 | } 56 | 57 | switch (request.type) { 58 | case proto.CircuitRelay.Type.CAN_HOP: 59 | case proto.CircuitRelay.Type.HOP: { 60 | return listener.hopHandler.handle(request, sh) 61 | } 62 | 63 | case proto.CircuitRelay.Type.STOP: { 64 | return listener.stopHandler.handle(request, sh, connHandler) 65 | } 66 | 67 | default: { 68 | utils.writeResponse( 69 | sh, 70 | proto.CircuitRelay.Status.INVALID_MSG_TYPE) 71 | return sh.close() 72 | } 73 | } 74 | }) 75 | }) 76 | 77 | setImmediate(() => listener.emit('listen')) 78 | callback() 79 | } 80 | 81 | /** 82 | * Remove swarm listener 83 | * 84 | * @param {Function} cb 85 | * @return {void} 86 | */ 87 | listener.close = (cb) => { 88 | swarm.unhandle(multicodec.relay) 89 | setImmediate(() => listener.emit('close')) 90 | cb() 91 | } 92 | 93 | /** 94 | * Get fixed up multiaddrs 95 | * 96 | * NOTE: This method will grab the peers multiaddrs and expand them such that: 97 | * 98 | * a) If it's an existing /p2p-circuit address for a specific relay i.e. 99 | * `/ip4/0.0.0.0/tcp/0/ipfs/QmRelay/p2p-circuit` this method will expand the 100 | * address to `/ip4/0.0.0.0/tcp/0/ipfs/QmRelay/p2p-circuit/ipfs/QmPeer` where 101 | * `QmPeer` is this peers id 102 | * b) If it's not a /p2p-circuit address, it will encapsulate the address as a /p2p-circuit 103 | * addr, such when dialing over a relay with this address, it will create the circuit using 104 | * the encapsulated transport address. This is useful when for example, a peer should only 105 | * be dialed over TCP rather than any other transport 106 | * 107 | * @param {Function} callback 108 | * @return {void} 109 | */ 110 | listener.getAddrs = (callback) => { 111 | let addrs = swarm._peerInfo.multiaddrs.toArray() 112 | 113 | // get all the explicit relay addrs excluding self 114 | let p2pAddrs = addrs.filter((addr) => { 115 | return mafmt.Circuit.matches(addr) && 116 | !addr.toString().includes(swarm._peerInfo.id.toB58String()) 117 | }) 118 | 119 | // use the explicit relays instead of any relay 120 | if (p2pAddrs.length) { 121 | addrs = p2pAddrs 122 | } 123 | 124 | let listenAddrs = [] 125 | addrs.forEach((addr) => { 126 | const peerMa = `/p2p-circuit/ipfs/${swarm._peerInfo.id.toB58String()}` 127 | if (addr.toString() === peerMa) { 128 | listenAddrs.push(multiaddr(peerMa)) 129 | return 130 | } 131 | 132 | if (!mafmt.Circuit.matches(addr)) { 133 | if (addr.getPeerId()) { 134 | // by default we're reachable over any relay 135 | listenAddrs.push(multiaddr(`/p2p-circuit`).encapsulate(addr)) 136 | } else { 137 | const ma = `${addr}/ipfs/${swarm._peerInfo.id.toB58String()}` 138 | listenAddrs.push(multiaddr(`/p2p-circuit`).encapsulate(ma)) 139 | } 140 | } else { 141 | listenAddrs.push(addr.encapsulate(`/ipfs/${swarm._peerInfo.id.toB58String()}`)) 142 | } 143 | }) 144 | 145 | callback(null, listenAddrs) 146 | } 147 | 148 | return listener 149 | } 150 | -------------------------------------------------------------------------------- /src/multicodec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | relay: '/libp2p/circuit/relay/0.1.0' 5 | } 6 | -------------------------------------------------------------------------------- /src/protocol/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const protobuf = require('protons') 3 | module.exports = protobuf(` 4 | message CircuitRelay { 5 | 6 | enum Status { 7 | SUCCESS = 100; 8 | HOP_SRC_ADDR_TOO_LONG = 220; 9 | HOP_DST_ADDR_TOO_LONG = 221; 10 | HOP_SRC_MULTIADDR_INVALID = 250; 11 | HOP_DST_MULTIADDR_INVALID = 251; 12 | HOP_NO_CONN_TO_DST = 260; 13 | HOP_CANT_DIAL_DST = 261; 14 | HOP_CANT_OPEN_DST_STREAM = 262; 15 | HOP_CANT_SPEAK_RELAY = 270; 16 | HOP_CANT_RELAY_TO_SELF = 280; 17 | STOP_SRC_ADDR_TOO_LONG = 320; 18 | STOP_DST_ADDR_TOO_LONG = 321; 19 | STOP_SRC_MULTIADDR_INVALID = 350; 20 | STOP_DST_MULTIADDR_INVALID = 351; 21 | STOP_RELAY_REFUSED = 390; 22 | MALFORMED_MESSAGE = 400; 23 | } 24 | 25 | enum Type { // RPC identifier, either HOP, STOP or STATUS 26 | HOP = 1; 27 | STOP = 2; 28 | STATUS = 3; 29 | CAN_HOP = 4; 30 | } 31 | 32 | message Peer { 33 | required bytes id = 1; // peer id 34 | repeated bytes addrs = 2; // peer's known addresses 35 | } 36 | 37 | optional Type type = 1; // Type of the message 38 | 39 | optional Peer srcPeer = 2; // srcPeer and dstPeer are used when Type is HOP or STATUS 40 | optional Peer dstPeer = 3; 41 | 42 | optional Status code = 4; // Status code, used when Type is STATUS 43 | } 44 | `) 45 | -------------------------------------------------------------------------------- /test/dialer.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint max-nested-callbacks: ["error", 5] */ 3 | 4 | 'use strict' 5 | 6 | const Dialer = require('../src/circuit/dialer') 7 | const nodes = require('./fixtures/nodes') 8 | const Connection = require('interface-connection').Connection 9 | const multiaddr = require('multiaddr') 10 | const PeerInfo = require('peer-info') 11 | const PeerId = require('peer-id') 12 | const waterfall = require('async/waterfall') 13 | const pull = require('pull-stream/pull') 14 | const values = require('pull-stream/sources/values') 15 | const asyncMap = require('pull-stream/throughs/async-map') 16 | const pair = require('pull-pair/duplex') 17 | const pb = require('pull-protocol-buffers') 18 | 19 | const proto = require('../src/protocol') 20 | const utilsFactory = require('../src/circuit/utils') 21 | 22 | const sinon = require('sinon') 23 | const chai = require('chai') 24 | const dirtyChai = require('dirty-chai') 25 | const expect = chai.expect 26 | chai.use(dirtyChai) 27 | 28 | describe(`dialer tests`, function () { 29 | describe(`.dial`, function () { 30 | const dialer = sinon.createStubInstance(Dialer) 31 | 32 | beforeEach(function () { 33 | dialer.relayPeers = new Map() 34 | dialer.relayPeers.set(nodes.node2.id, new Connection()) 35 | dialer.relayPeers.set(nodes.node3.id, new Connection()) 36 | dialer.dial.callThrough() 37 | }) 38 | 39 | afterEach(function () { 40 | dialer._dialPeer.reset() 41 | }) 42 | 43 | it(`fail on non circuit addr`, function () { 44 | const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) 45 | expect(() => dialer.dial(dstMa, (err) => { 46 | err.to.match(/invalid circuit address/) 47 | })) 48 | }) 49 | 50 | it(`dial a peer`, function (done) { 51 | const dstMa = multiaddr(`/p2p-circuit/ipfs/${nodes.node3.id}`) 52 | dialer._dialPeer.callsFake(function (dstMa, relay, callback) { 53 | return callback(null, dialer.relayPeers.get(nodes.node3.id)) 54 | }) 55 | 56 | dialer.dial(dstMa, (err, conn) => { 57 | expect(err).to.not.exist() 58 | expect(conn).to.be.an.instanceOf(Connection) 59 | done() 60 | }) 61 | }) 62 | 63 | it(`dial a peer over the specified relay`, function (done) { 64 | const dstMa = multiaddr(`/ipfs/${nodes.node3.id}/p2p-circuit/ipfs/${nodes.node4.id}`) 65 | dialer._dialPeer.callsFake(function (dstMa, relay, callback) { 66 | expect(relay.toString()).to.equal(`/ipfs/${nodes.node3.id}`) 67 | return callback(null, new Connection()) 68 | }) 69 | 70 | dialer.dial(dstMa, (err, conn) => { 71 | expect(err).to.not.exist() 72 | expect(conn).to.be.an.instanceOf(Connection) 73 | done() 74 | }) 75 | }) 76 | }) 77 | 78 | describe(`.canHop`, function () { 79 | const dialer = sinon.createStubInstance(Dialer) 80 | 81 | let fromConn = null 82 | let peer = new PeerInfo(PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA')) 83 | 84 | let p = null 85 | beforeEach(function () { 86 | p = pair() 87 | fromConn = new Connection(p[0]) 88 | 89 | dialer.relayPeers = new Map() 90 | dialer.relayConns = new Map() 91 | dialer.utils = utilsFactory({}) 92 | dialer.canHop.callThrough() 93 | dialer._dialRelayHelper.callThrough() 94 | }) 95 | 96 | afterEach(function () { 97 | dialer._dialRelay.reset() 98 | }) 99 | 100 | it(`should handle successful CAN_HOP`, (done) => { 101 | dialer._dialRelay.callsFake((_, cb) => { 102 | pull( 103 | values([{ 104 | type: proto.CircuitRelay.type.HOP, 105 | code: proto.CircuitRelay.Status.SUCCESS 106 | }]), 107 | pb.encode(proto.CircuitRelay), 108 | p[1] 109 | ) 110 | cb(null, fromConn) 111 | }) 112 | 113 | dialer.canHop(peer, (err) => { 114 | expect(err).to.not.exist() 115 | expect(dialer.relayPeers.has(peer.id.toB58String())).to.be.ok() 116 | done() 117 | }) 118 | }) 119 | 120 | it(`should handle failed CAN_HOP`, function (done) { 121 | dialer._dialRelay.callsFake((_, cb) => { 122 | pull( 123 | values([{ 124 | type: proto.CircuitRelay.type.HOP, 125 | code: proto.CircuitRelay.Status.HOP_CANT_SPEAK_RELAY 126 | }]), 127 | pb.encode(proto.CircuitRelay), 128 | p[1] 129 | ) 130 | cb(null, fromConn) 131 | }) 132 | 133 | dialer.canHop(peer, (err) => { 134 | expect(err).to.exist() 135 | expect(dialer.relayPeers.has(peer.id.toB58String())).not.to.be.ok() 136 | done() 137 | }) 138 | }) 139 | }) 140 | 141 | describe(`._dialPeer`, function () { 142 | const dialer = sinon.createStubInstance(Dialer) 143 | 144 | beforeEach(function () { 145 | dialer.relayPeers = new Map() 146 | dialer.relayPeers.set(nodes.node1.id, new Connection()) 147 | dialer.relayPeers.set(nodes.node2.id, new Connection()) 148 | dialer.relayPeers.set(nodes.node3.id, new Connection()) 149 | dialer._dialPeer.callThrough() 150 | }) 151 | 152 | afterEach(function () { 153 | dialer._negotiateRelay.reset() 154 | }) 155 | 156 | it(`should dial a peer over any relay`, function (done) { 157 | const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) 158 | dialer._negotiateRelay.callsFake(function (conn, dstMa, callback) { 159 | if (conn === dialer.relayPeers.get(nodes.node3.id)) { 160 | return callback(null, dialer.relayPeers.get(nodes.node3.id)) 161 | } 162 | 163 | callback(new Error(`error`)) 164 | }) 165 | 166 | dialer._dialPeer(dstMa, (err, conn) => { 167 | expect(err).to.not.exist() 168 | expect(conn).to.be.an.instanceOf(Connection) 169 | expect(conn).to.deep.equal(dialer.relayPeers.get(nodes.node3.id)) 170 | done() 171 | }) 172 | }) 173 | 174 | it(`should fail dialing a peer over any relay`, function (done) { 175 | const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) 176 | dialer._negotiateRelay.callsFake(function (conn, dstMa, callback) { 177 | callback(new Error(`error`)) 178 | }) 179 | 180 | dialer._dialPeer(dstMa, (err, conn) => { 181 | expect(conn).to.be.undefined() 182 | expect(err).to.not.be.null() 183 | expect(err).to.equal(`no relay peers were found or all relays failed to dial`) 184 | done() 185 | }) 186 | }) 187 | }) 188 | 189 | describe(`._negotiateRelay`, function () { 190 | const dialer = sinon.createStubInstance(Dialer) 191 | const dstMa = multiaddr(`/ipfs/${nodes.node4.id}`) 192 | 193 | let conn = null 194 | let peer = null 195 | let p = null 196 | let callback = sinon.stub() 197 | 198 | beforeEach(function (done) { 199 | waterfall([ 200 | (cb) => PeerId.createFromJSON(nodes.node4, cb), 201 | (peerId, cb) => PeerInfo.create(peerId, cb), 202 | (peer, cb) => { 203 | peer.multiaddrs.add(`/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`) 204 | dialer.swarm = { 205 | _peerInfo: peer 206 | } 207 | cb() 208 | }, 209 | (cb) => { 210 | dialer.utils = utilsFactory({}) 211 | dialer.relayConns = new Map() 212 | dialer._negotiateRelay.callThrough() 213 | dialer._dialRelayHelper.callThrough() 214 | peer = new PeerInfo(PeerId.createFromB58String(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`)) 215 | p = pair() 216 | conn = new Connection(p[1]) 217 | cb() 218 | } 219 | ], done) 220 | }) 221 | 222 | afterEach(() => { 223 | callback.reset() 224 | }) 225 | 226 | it(`should write the correct dst addr`, function (done) { 227 | dialer._dialRelay.callsFake((_, cb) => { 228 | pull( 229 | p[0], 230 | pb.decode(proto.CircuitRelay), 231 | asyncMap((msg, cb) => { 232 | expect(msg.dstPeer.addrs[0]).to.deep.equal(dstMa.buffer) 233 | cb(null, { 234 | type: proto.CircuitRelay.Type.STATUS, 235 | code: proto.CircuitRelay.Status.SUCCESS 236 | }) 237 | }), 238 | pb.encode(proto.CircuitRelay), 239 | p[0] 240 | ) 241 | cb(null, conn) 242 | }) 243 | 244 | dialer._negotiateRelay(peer, dstMa, done) 245 | }) 246 | 247 | it(`should negotiate relay`, function (done) { 248 | dialer._dialRelay.callsFake((_, cb) => { 249 | pull( 250 | p[0], 251 | pb.decode(proto.CircuitRelay), 252 | asyncMap((msg, cb) => { 253 | expect(msg.dstPeer.addrs[0]).to.deep.equal(dstMa.buffer) 254 | cb(null, { 255 | type: proto.CircuitRelay.Type.STATUS, 256 | code: proto.CircuitRelay.Status.SUCCESS 257 | }) 258 | }), 259 | pb.encode(proto.CircuitRelay), 260 | p[0] 261 | ) 262 | cb(null, conn) 263 | }) 264 | 265 | dialer._negotiateRelay(peer, dstMa, (err, conn) => { 266 | expect(err).to.not.exist() 267 | expect(conn).to.be.instanceOf(Connection) 268 | done() 269 | }) 270 | }) 271 | 272 | it(`should fail with an invalid peer id`, function (done) { 273 | const dstMa = multiaddr('/ip4/127.0.0.1/tcp/4001') 274 | dialer._dialRelay.callsFake((_, cb) => { 275 | pull( 276 | p[0], 277 | pb.decode(proto.CircuitRelay), 278 | asyncMap((msg, cb) => { 279 | expect(msg.dstPeer.addrs[0]).to.deep.equal(dstMa.buffer) 280 | cb(null, { 281 | type: proto.CircuitRelay.Type.STATUS, 282 | code: proto.CircuitRelay.Status.SUCCESS 283 | }) 284 | }), 285 | pb.encode(proto.CircuitRelay), 286 | p[0] 287 | ) 288 | cb(null, conn) 289 | }) 290 | 291 | dialer._negotiateRelay(peer, dstMa, (err, conn) => { 292 | expect(err).to.exist() 293 | expect(conn).to.not.exist() 294 | done() 295 | }) 296 | }) 297 | 298 | it(`should handle failed relay negotiation`, function (done) { 299 | dialer._dialRelay.callsFake((_, cb) => { 300 | cb(null, conn) 301 | pull( 302 | values([{ 303 | type: proto.CircuitRelay.Type.STATUS, 304 | code: proto.CircuitRelay.Status.MALFORMED_MESSAGE 305 | }]), 306 | pb.encode(proto.CircuitRelay), 307 | p[0] 308 | ) 309 | }) 310 | 311 | dialer._negotiateRelay(peer, dstMa, (err, conn) => { 312 | expect(err).to.not.be.null() 313 | expect(err).to.be.an.instanceOf(Error) 314 | expect(err.message).to.be.equal(`Got 400 error code trying to dial over relay`) 315 | done() 316 | }) 317 | }) 318 | }) 319 | }) 320 | -------------------------------------------------------------------------------- /test/fixtures/nodes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.node1 = { 4 | id: 'QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE', 5 | privKey: 'CAASpwkwggSjAgEAAoIBAQDJwzJPar4nylKY71Mm5q2BOED8uPf1ILvIi15VwVZWqter6flnlii/RKEcBypPbFqJHHa56MvybgQgrFmHKwDjnJvq4jyOZfR+o/D/99Ft1p2FAEBjImSXAgNpK4YsbyV5r0Q1+Avcj++aWWlLu6enUrL9WGzeUkf0U5L6XwXEPRUQdEojAIQi241P1hyqXX5gKAZVGqcPtKb6p1db3fcXodkS1G6JR90TopJHCqTCECp3SB9c6LlG7KXU92sIHJBlhOEEzGkEI1pM1SWnNnW5VLEypU7P56ifzzp4QxPNiJeC+cmE5SrgR3cXP44iKOuNVRJwBpCh5oNYqECzgqJ9AgMBAAECggEBAJpCdqXHrAmKJCqv2HiGqCODGhTfax1s4IYNIJwaTOPIjUrwgfKUGSVb2H4wcEX3RyVLsO6lMcFyIg/FFlJFK9HavE8SmFAbXZqxx6I9HE+JZjf5IEFrW1Mlg+wWDejNNe7adSF6O79wATaWo+32VNGWZilTQTGd4UvJ1jc9DZCh8zZeNhm4C6exXD45gMB0HI1t2ZNl47scsBEE4rV+s7F7y8Yk/tIsf0wSI/H8KSXS5I9aFxr3Z9c3HOfbVwhnIfNUDqcFTeU5BnhByYNLJ4v9xGj7puidcabVXkt2zLmm/LHbKVeGzec9LW5D+KkuB/pKaslsCXN6bVlu+SbVr9UCgYEA7MXfzZw36vDyfn4LPCN0wgzz11uh3cm31QzOPlWpA7hIsL/eInpvc8wa9yBRC1sRk41CedPHn913MR6EJi0Ne6/B1QOmRYBUjr60VPRNdTXCAiLykjXg6+TZ+AKnxlUGK1hjTo8krhpWq7iD/JchVlLoqDAXGFHvSxN0H3WEUm8CgYEA2iWC9w1v+YHfT2PXcLxYde9EuLVkIS4TM7Kb0N3wr/4+K4xWjVXuaJJLJoAbihNAZw0Y+2s1PswDUEpSG0jXeNXLs6XcQxYSEAu/pFdvHFeg2BfwVQoeEFlWyTJR29uti9/APaXMo8FSVAPPR5lKZLStJDM9hEfAPfUaHyic39MCgYAKQbwjNQw7Ejr+/cjQzxxkt5jskFyftfhPs2FP0/ghYB9OANHHnpQraQEWCYFZQ5WsVac2jdUM+NQL/a1t1e/Klt+HscPHKPsAwAQh1f9w/2YrH4ZwjQL0VRKYKs1HyzEcOZT7tzm4jQ2KHNEi5Q0dpzPK7WJivFHoZ6xVHIsh4wKBgAQq20mk9BKsLHvzyFXbA0WdgI6WyIbpvmwqaVegJcz26nEiiTTCA3/z64OcxunoXD6bvXJwJeBBPX73LIJg7dzdGLsh3AdcEJRF5S9ajEDaW7RFIM4/FzvwuPu2/mFY3QPjDmUfGb23H7+DIx6XCxjJatVaNT6lsEJ+wDUALZ8JAoGAO0YJSEziA7y0dXPK5azkJUMJ5yaN+zRDmoBnEggza34rQW0s16NnIR0EBzKGwbpNyePlProv4dQEaLF1kboKsSYvV2rW2ftLVdNqBHEUYFRC9ofPctCxwM1YU21TI2/k1squ+swApg2EHMev2+WKd+jpVPIbCIvJ3AjiAKZtiGQ=', 6 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJwzJPar4nylKY71Mm5q2BOED8uPf1ILvIi15VwVZWqter6flnlii/RKEcBypPbFqJHHa56MvybgQgrFmHKwDjnJvq4jyOZfR+o/D/99Ft1p2FAEBjImSXAgNpK4YsbyV5r0Q1+Avcj++aWWlLu6enUrL9WGzeUkf0U5L6XwXEPRUQdEojAIQi241P1hyqXX5gKAZVGqcPtKb6p1db3fcXodkS1G6JR90TopJHCqTCECp3SB9c6LlG7KXU92sIHJBlhOEEzGkEI1pM1SWnNnW5VLEypU7P56ifzzp4QxPNiJeC+cmE5SrgR3cXP44iKOuNVRJwBpCh5oNYqECzgqJ9AgMBAAE=' 7 | } 8 | 9 | exports.node2 = { 10 | id: 'QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe', 11 | privKey: 'CAASpgkwggSiAgEAAoIBAQDt7YgUeBQsoN/lrgo690mB7yEh8G9iXhZiDecgZCLRRSl3v2cH9w4WjhoW9erfnVbdoTqkCK+se8uK01ySi/ubQQDPcrjacXTa6wAuRTbCG/0bUR9RxKtxZZBS1HaY7L923ulgGDTiVaRQ3JQqhzmQkaU0ikNcluSGaw0kmhXP6JmcL+wndKgW5VD9etcp2Qlk8uUFC/GAO90cOAuER3wnI3ocHGm9on9zyb97g4TDzIfjSaTW4Wanmx2yVbURQxmCba16X3LT9IMPqQaGOzq3+EewMLeCESbUm/uJaJLdqWrWRK4oNzxcMgmUkzav+s476HdA9CRo72am+g3Vdq+lAgMBAAECggEAcByKD6MZVoIjnlVo6qoVUA1+3kAuK/rLrz5/1wp4QYXGaW+eO+mVENm6v3D3UJESGnLbb+nL5Ymbunmn2EHvuBNkL1wOcJgfiPxM5ICmscaAeHu8N0plwpQp8m28yIheG8Qj0az2VmQmfhfCFVwMquuGHgC8hwdu/Uu6MLIObx1xjtaGbY9kk7nzAeXHeJ4RDeuNN0QrYuQVKwrIz1NtPNDR/cli298ZXJcm+HEhBCIHVIYpAq6BHSuiXVqPGEOYWYXo+yVhEtDJ8BmNqlN1Y1s6bnfu/tFkKUN6iQQ46vYnQEGTGR9lg7J/c6tqfRs9FcywWb9J1SX6HxPO8184zQKBgQD6vDYl20UT4ZtrzhFfMyV/1QUqFM/TdwNuiOsIewHBol9o7aOjrxrrbYVa1HOxETyBjmFsW+iIfOVl61SG2HcU4CG+O2s9WBo4JdRlOm4YQ8/83xO3YfbXzuTx8BMCyP/i1uPIZTKQFFAN0HiL96r4L60xHoWB7tQsbZiEbIO/2wKBgQDy7HnkgVeTld6o0+sT84FYRUotjDB00oUWiSeGtj0pFC4yIxhMhD8QjKiWoJyJItcoCsQ/EncuuwwRtuXi83793lJQR1DBYd+TSPg0M8J1pw97fUIPi/FU+jHtrsx7Vn/7Bk9voictsYVLAfbi68tYdsZpAaYOWYMY9NUfVuAmfwKBgCYZDwk1hgt9TkZVK2KRvPLthTldrC5veQAEoeHJ/vxTFbg105V9d9Op8odYnLOc8NqmrbrvRCfpAlo4JcHPhliPrdDf6m2Jw4IgjWNMO4pIU4QSyUYmBoHIGBWC6wCTVf47tKSwa7xkub0/nfF2km3foKtD/fk+NtMBXBlS+7ndAoGAJo6GIlCtN82X07AfJcGGjB4jUetoXYJ0gUkvruAKARUk5+xOFQcAg33v3EiNz+5pu/9JesFRjWc+2Sjwf/8p7t10ry1Ckg8Yz2XLj22PteDYQj91VsZdfaFgf1s5NXJbSdqMjSltkoEUqP0c1JOcaOQhRdVvJ+PpPPLPSPQfC70CgYBvJE1I06s7BEM1DOli3VyfNaJDI4k9W2dCJOU6Bh2MNmbdRjM3xnpOKH5SqRlCz/oI9pn4dxgbX6WPg331MD9CNYy2tt5KBQRrSuDj8p4jlzMIpX36hsyTTrzYU6WWSIPz6jXW8IexXKvXEmr8TVb78ZPiQfbG012cdUhAJniNgg==', 12 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDt7YgUeBQsoN/lrgo690mB7yEh8G9iXhZiDecgZCLRRSl3v2cH9w4WjhoW9erfnVbdoTqkCK+se8uK01ySi/ubQQDPcrjacXTa6wAuRTbCG/0bUR9RxKtxZZBS1HaY7L923ulgGDTiVaRQ3JQqhzmQkaU0ikNcluSGaw0kmhXP6JmcL+wndKgW5VD9etcp2Qlk8uUFC/GAO90cOAuER3wnI3ocHGm9on9zyb97g4TDzIfjSaTW4Wanmx2yVbURQxmCba16X3LT9IMPqQaGOzq3+EewMLeCESbUm/uJaJLdqWrWRK4oNzxcMgmUkzav+s476HdA9CRo72am+g3Vdq+lAgMBAAE=' 13 | } 14 | 15 | exports.node3 = { 16 | id: 'QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA', 17 | privKey: 'CAASpwkwggSjAgEAAoIBAQDdnGp0X7Pix5dIawfyuffVryRDRS5JXdyjayKUkgikJLYoiijB5TakrFKhx1SDKpmVLxxqAGz8m5iA2cHwetIQXTZvdYx7XXxv332En3ji8TiGRUiEFM8KQ5WCJ5G7yw8R2pv/pYdnMrPd04QbtSCn0cFVCiiA2Zkl5KnwBo/lf+sVI/TEeiwmVD9nxi13qWgBTmCysqH8Ppyu8fq+bQgqRZSlalVDswyIhgWlepPkD0uYakJJhhOxY+2RlbNhGY0qjRyMTYou2uR/hfd6j8uR++WdB0v3+DYWG2Kc3sWa4BLYb5r4trvQGO1Iagnwuk3AVoi7PldsaInekzWEVljDAgMBAAECggEAXx0jE49/xXWkmJBXePYYSL5C8hxfIV4HtJvm251R2CFpjTy/AXk/Wq4bSRQkUaeXA1CVAWntXP3rFmJfurb8McnP80agZNJa9ikV1jYbzEt71yUlWosT0XPwV0xkYBVnAmKxUafZ1ZENYcfGi53RxjVgpP8XIzZBZOIfjcVDPVw9NAOzQmq4i3DJEz5xZAkaeSM8mn5ZFl1JMBUOgyOHB7d4BWd3zuLyvnn0/08HlsaSUl0mZa3f2Lm2NlsjOiNfMCJTOIT+xDEP9THm5n2cqieSjvtpAZzV4kcoD0rB8OsyHQlFAEXzkgELDr5dVXji0rrIdVz8stYAKGfi996OAQKBgQDuviV1sc+ClJQA59vqbBiKxWqcuCKMzvmL4Yk1e/AkQeRt+JX9kALWzBx65fFmHTj4Lus8AIQoiruPxa0thtqh/m3SlucWnrdaW410xbz3KqQWS7bx+0sFWZIEi4N+PESrIYhtVbFuRiabYgliqdSU9shxtXXnvfhjl+9quZltiwKBgQDtoUCKqrZbm0bmzLvpnKdNodg1lUHaKGgEvWgza2N1t3b/GE07iha2KO3hBDta3bdfIEEOagY8o13217D0VIGsYNKpiEGLEeNIjfcXBEqAKiTfa/sXUfTprpWBZQ/7ZS+eZIYtQjq14EHa7ifAby1v3yDrMIuxphz5JfKdXFgYqQKBgHr47FikPwu2tkmFJCyqgzWvnEufOQSoc7eOc1tePIKggiX2/mM+M4gqWJ0hJeeAM+D6YeZlKa2sUBItMxeZN7JrWGw5mEx5cl4TfFhipgP2LdDiLRiVZL4bte+rYQ67wm8XdatDkYIIlkhBBi6Q5dPZDcQsQNAedPvvvb2OXi4jAoGBAKp06FpP+L2fle2LYSRDlhNvDCvrpDA8mdkEkRGJb/AKKdb09LnH5WDH3VNy+KzGrHoVJfWUAmNPAOFHeYzabaZcUeEAd5utui7afytIjbSABrEpwRTKWneiH2aROzSnMdBZ5ZHjlz/N3Q+RlHxKg/piwTdUPHCzasch/HX6vsr5AoGAGvhCNPKyCwpu8Gg5GQdx0yN6ZPar9wieD345cLanDZWKkLRQbo4SfkfoS+PDfOLzDbWFdPRnWQ0qhdPm3D/N1YD/nudHqbeDlx0dj/6lEHmmPKFFO2kiNFEhn8DycNGbvWyVBKksacuRXav21+LvW+TatUkRMhi8fgRoypnbJjg=', 18 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdnGp0X7Pix5dIawfyuffVryRDRS5JXdyjayKUkgikJLYoiijB5TakrFKhx1SDKpmVLxxqAGz8m5iA2cHwetIQXTZvdYx7XXxv332En3ji8TiGRUiEFM8KQ5WCJ5G7yw8R2pv/pYdnMrPd04QbtSCn0cFVCiiA2Zkl5KnwBo/lf+sVI/TEeiwmVD9nxi13qWgBTmCysqH8Ppyu8fq+bQgqRZSlalVDswyIhgWlepPkD0uYakJJhhOxY+2RlbNhGY0qjRyMTYou2uR/hfd6j8uR++WdB0v3+DYWG2Kc3sWa4BLYb5r4trvQGO1Iagnwuk3AVoi7PldsaInekzWEVljDAgMBAAE=' 19 | } 20 | 21 | exports.node4 = { 22 | id: 'QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy', 23 | privKey: 'CAASqAkwggSkAgEAAoIBAQC6pg6LYWbY+49SOYdYap6RPqKqZxg80IXeo3hiTUbiGtTruxVYZpnz3UbernL9J9mwlXJGRUQJUKmXmi1yePTQiyclpH0KyPefaWLbpxJQdCBI1TPZpDWo2hutWSPqhKBU1QyH2FLKQPWLdxuIX1cNFPtIlSl5gCxN6oiDIwh7++kxNM1G+d5XgJX6iHLlLoNv3Wn6XYX+VtYdyZRFk8gYyT2BrISbxmsrSjSOodwUUzF8TYTjsqW6ksL2x0mrRm2cMM9evULktqwU+I8l9ulASDbFWBXUToXaZSL9M+Oq5JvZO0WIjPeYVpAgWladtayhdxg5dBv8aTbDaM5DZvyRAgMBAAECggEAR65YbZz1k6Vg0HI5kXI4/YzxicHYJBrtHqjnJdGJxHILjZCmzPFydJ5phkG29ZRlXRS381bMn0s0Jn3WsFzVoHWgjitSvl6aAsXFapgKR42hjHcc15vh47wH3xYZ3gobTRkZG96vRO+XnX0bvM7orqR9MM3gRMI9wZqt3LcKnhpiqSlyEZ3Zehu7ZZ8B+XcUw42H6ZTXgmg5mCFEjS/1rVt+EsdZl7Ll7jHigahPA6qMjyRiZB6T20qQ0FFYfmaNuRuuC6cWUXf8DOgnEjMB/Mi/Feoip9bTqNBrVYn2XeDxdMv5pDznNKXpalsMkZwx5FpNOMKnIMdQFyAGtkeQ9QKBgQD3rjTiulitpbbQBzF8VXeymtMJAbR1TAqNv2yXoowhL3JZaWICM7nXHjjsJa3UzJygbi8bO0KWrw7tY0nUbPy5SmHtNYhmUsEjiTjqEnNRrYN68tEKr0HlgX+9rArsjOcwucl2svFSfk+rTYDHU5neZkDDhu1QmnZm/pQI92Lo4wKBgQDA6wpMd53fmX9DhWegs3xelRStcqBTw1ucWVRyPgY1hO1cJ0oReYIXKEw9CHNLW0RHvnVM26kRnqCl+dTcg7dhLuqrckuyQyY1KcRYG1ryJnz3euucaSF2UCsZCHvFNV7Vz8dszUMUVCogWmroVP6HE/BoazUCNh25s/dNwE+i+wKBgEfa1WL1luaBzgCaJaQhk4FQY2sYgIcLEYDACTwQn0C9aBpCdXmYEhEzpmX0JHM5DTOJ48atsYrPrK/3/yJOoB8NUk2kGzc8SOYLWGSoB6aphRx1N2o3IBH6ONoJAH5R/nxnWehCz7oUBP74lCS/v0MDPUS8bzrUJQeKUd4sDxjrAoGBAIRO7rJA+1qF+J1DWi4ByxNHJXZLfh/UhPj23w628SU1dGDWZVsUvZ7KOXdGW2RcRLj7q5E5uXtnEoCillViVJtnRPSun7Gzkfm2Gn3ezQH0WZKVkA+mnpd5JgW2JsS69L6pEPnS0OWZT4b+3AFZgXL8vs2ucR2CJeLdxYdilHuPAoGBAPLCzBkAboXZZtvEWqzqtVNqdMrjLHihFrpg4TXSsk8+ZQZCVN+sRyTGTvBX8+Jvx4at6ClaSgT3eJ/412fEH6CHvrFXjUE9W9y6X0axxaT63y1OXmFiB/hU3vjLWZKZWSDGNS7St02fYri4tWmGtJDjYG1maLRhMSzcoj4fP1xz', 24 | pubKey: 'CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6pg6LYWbY+49SOYdYap6RPqKqZxg80IXeo3hiTUbiGtTruxVYZpnz3UbernL9J9mwlXJGRUQJUKmXmi1yePTQiyclpH0KyPefaWLbpxJQdCBI1TPZpDWo2hutWSPqhKBU1QyH2FLKQPWLdxuIX1cNFPtIlSl5gCxN6oiDIwh7++kxNM1G+d5XgJX6iHLlLoNv3Wn6XYX+VtYdyZRFk8gYyT2BrISbxmsrSjSOodwUUzF8TYTjsqW6ksL2x0mrRm2cMM9evULktqwU+I8l9ulASDbFWBXUToXaZSL9M+Oq5JvZO0WIjPeYVpAgWladtayhdxg5dBv8aTbDaM5DZvyRAgMBAAE=' 25 | } 26 | -------------------------------------------------------------------------------- /test/helpers/test-node.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Libp2p = require('libp2p') 4 | const secio = require('libp2p-secio') 5 | 6 | class TestNode extends Libp2p { 7 | constructor (peerInfo, transports, muxer, options) { 8 | options = options || {} 9 | 10 | const modules = { 11 | transport: transports, 12 | connection: { 13 | muxer: [muxer], 14 | crypto: options.isCrypto ? [secio] : null 15 | }, 16 | discovery: [] 17 | } 18 | super(modules, peerInfo, null, options) 19 | } 20 | } 21 | 22 | module.exports = TestNode 23 | -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const TestNode = require('./test-node') 4 | const PeerInfo = require('peer-info') 5 | const PeerId = require('peer-id') 6 | const eachAsync = require('async/each') 7 | 8 | exports.createNodes = function createNodes (configNodes, callback) { 9 | const nodes = {} 10 | eachAsync(Object.keys(configNodes), (key, cb1) => { 11 | let config = configNodes[key] 12 | 13 | const setup = (err, peer) => { 14 | if (err) { 15 | callback(err) 16 | } 17 | 18 | eachAsync(config.addrs, (addr, cb2) => { 19 | peer.multiaddrs.add(addr) 20 | cb2() 21 | }, (err) => { 22 | if (err) { 23 | return callback(err) 24 | } 25 | 26 | nodes[key] = new TestNode(peer, config.transports, config.muxer, config.config) 27 | cb1() 28 | }) 29 | } 30 | 31 | if (config.id) { 32 | PeerId.createFromJSON(config.id, (err, peerId) => { 33 | if (err) return callback(err) 34 | PeerInfo.create(peerId, setup) 35 | }) 36 | } else { 37 | PeerInfo.create(setup) 38 | } 39 | }, (err) => { 40 | if (err) { 41 | return callback(err) 42 | } 43 | 44 | startNodes(nodes, (err) => { 45 | if (err) { 46 | callback(err) 47 | } 48 | 49 | callback(null, nodes) 50 | }) 51 | }) 52 | } 53 | 54 | function startNodes (nodes, callback) { 55 | eachAsync(Object.keys(nodes), 56 | (key, cb) => { 57 | nodes[key].start(cb) 58 | }, 59 | (err) => { 60 | if (err) { 61 | return callback(err) 62 | } 63 | callback(null) 64 | }) 65 | } 66 | 67 | exports.stopNodes = function stopNodes (nodes, callback) { 68 | eachAsync(Object.keys(nodes), 69 | (key, cb) => { 70 | nodes[key].stop(cb) 71 | }, 72 | (err) => { 73 | if (err) { 74 | return callback(err) 75 | } 76 | callback() 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /test/hop.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | /* eslint max-nested-callbacks: ["error", 5] */ 3 | 'use strict' 4 | 5 | const Hop = require('../src/circuit/hop') 6 | const nodes = require('./fixtures/nodes') 7 | const Connection = require('interface-connection').Connection 8 | const handshake = require('pull-handshake') 9 | const waterfall = require('async/waterfall') 10 | const PeerInfo = require('peer-info') 11 | const PeerId = require('peer-id') 12 | const multiaddr = require('multiaddr') 13 | const pull = require('pull-stream/pull') 14 | const values = require('pull-stream/sources/values') 15 | const collect = require('pull-stream/sinks/collect') 16 | const lp = require('pull-length-prefixed') 17 | const proto = require('../src/protocol') 18 | const StreamHandler = require('../src/circuit/stream-handler') 19 | 20 | const sinon = require('sinon') 21 | const chai = require('chai') 22 | const dirtyChai = require('dirty-chai') 23 | const expect = chai.expect 24 | chai.use(dirtyChai) 25 | 26 | describe('relay', () => { 27 | describe(`.handle`, () => { 28 | let relay 29 | let swarm 30 | let fromConn 31 | let stream 32 | let shake 33 | 34 | beforeEach((done) => { 35 | stream = handshake({ timeout: 1000 * 60 }) 36 | shake = stream.handshake 37 | fromConn = new Connection(stream) 38 | const peerInfo = new PeerInfo(PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA')) 39 | fromConn.setPeerInfo(peerInfo) 40 | 41 | let peers = { 42 | QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: 43 | new PeerInfo(PeerId.createFromB58String(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`)), 44 | QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA: 45 | new PeerInfo(PeerId.createFromB58String(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`)), 46 | QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy: 47 | new PeerInfo(PeerId.createFromB58String(`QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`)) 48 | } 49 | 50 | Object.keys(peers).forEach((key) => { peers[key]._connectedMultiaddr = true }) // make it truthy 51 | 52 | waterfall([ 53 | (cb) => PeerId.createFromJSON(nodes.node4, cb), 54 | (peerId, cb) => PeerInfo.create(peerId, cb), 55 | (peer, cb) => { 56 | peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') 57 | swarm = { 58 | _peerInfo: peer, 59 | conns: { 60 | QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection(), 61 | QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA: new Connection(), 62 | QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy: new Connection() 63 | }, 64 | _peerBook: { 65 | get: (peer) => { 66 | if (!peers[peer]) { 67 | throw new Error() 68 | } 69 | 70 | return peers[peer] 71 | } 72 | } 73 | } 74 | 75 | cb() 76 | } 77 | ], () => { 78 | relay = new Hop(swarm, { enabled: true }) 79 | relay._circuit = sinon.stub() 80 | relay._circuit.callsArgWith(2, null, new Connection()) 81 | done() 82 | }) 83 | }) 84 | 85 | afterEach(() => { 86 | relay._circuit.reset() 87 | }) 88 | 89 | it(`should handle a valid circuit request`, (done) => { 90 | let relayMsg = { 91 | type: proto.CircuitRelay.Type.HOP, 92 | srcPeer: { 93 | id: PeerId.createFromB58String(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).id, 94 | addrs: [multiaddr(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).buffer] 95 | }, 96 | dstPeer: { 97 | id: PeerId.createFromB58String(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).id, 98 | addrs: [multiaddr(`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).buffer] 99 | } 100 | } 101 | 102 | relay.on('circuit:success', () => { 103 | expect(relay._circuit.calledWith(sinon.match.any, relayMsg)).to.be.ok() 104 | done() 105 | }) 106 | 107 | relay.handle(relayMsg, new StreamHandler(fromConn)) 108 | }) 109 | 110 | it(`should handle a request to passive circuit`, (done) => { 111 | let relayMsg = { 112 | type: proto.CircuitRelay.Type.HOP, 113 | srcPeer: { 114 | id: PeerId.createFromB58String(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).id, 115 | addrs: [multiaddr(`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).buffer] 116 | }, 117 | dstPeer: { 118 | id: PeerId.createFromB58String(`QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe`).id, 119 | addrs: [multiaddr(`/ipfs/QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe`).buffer] 120 | } 121 | } 122 | 123 | relay.active = false 124 | lp.decodeFromReader( 125 | shake, 126 | (err, msg) => { 127 | expect(err).to.not.exist() 128 | 129 | const response = proto.CircuitRelay.decode(msg) 130 | expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_NO_CONN_TO_DST) 131 | expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) 132 | done() 133 | }) 134 | 135 | relay.handle(relayMsg, new StreamHandler(fromConn)) 136 | }) 137 | 138 | it(`should handle a request to active circuit`, (done) => { 139 | let relayMsg = { 140 | type: proto.CircuitRelay.Type.HOP, 141 | srcPeer: { 142 | id: PeerId.createFromB58String(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).id, 143 | addrs: [multiaddr(`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).buffer] 144 | }, 145 | dstPeer: { 146 | id: PeerId.createFromB58String(`QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe`).id, 147 | addrs: [multiaddr(`/ipfs/QmYJjAri5soV8RbeQcHaYYcTAYTET17QTvcoFMyKvRDTXe`).buffer] 148 | } 149 | } 150 | 151 | relay.active = true 152 | relay.on('circuit:success', () => { 153 | expect(relay._circuit.calledWith(sinon.match.any, relayMsg)).to.be.ok() 154 | done() 155 | }) 156 | 157 | relay.on('circuit:error', (err) => { 158 | done(err) 159 | }) 160 | 161 | relay.handle(relayMsg, new StreamHandler(fromConn)) 162 | }) 163 | 164 | it(`not dial to self`, (done) => { 165 | let relayMsg = { 166 | type: proto.CircuitRelay.Type.HOP, 167 | srcPeer: { 168 | id: PeerId.createFromB58String(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).id, 169 | addrs: [multiaddr(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).buffer] 170 | }, 171 | dstPeer: { 172 | id: PeerId.createFromB58String(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).id, 173 | addrs: [multiaddr(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).buffer] 174 | } 175 | } 176 | 177 | lp.decodeFromReader( 178 | shake, 179 | (err, msg) => { 180 | expect(err).to.not.exist() 181 | 182 | const response = proto.CircuitRelay.decode(msg) 183 | expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_CANT_RELAY_TO_SELF) 184 | expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) 185 | done() 186 | }) 187 | 188 | relay.handle(relayMsg, new StreamHandler(fromConn)) 189 | }) 190 | 191 | it(`fail on invalid src address`, (done) => { 192 | let relayMsg = { 193 | type: proto.CircuitRelay.Type.HOP, 194 | srcPeer: { 195 | id: `sdfkjsdnfkjdsb`, 196 | addrs: [`sdfkjsdnfkjdsb`] 197 | }, 198 | dstPeer: { 199 | id: PeerId.createFromB58String(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).id, 200 | addrs: [multiaddr(`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).buffer] 201 | } 202 | } 203 | 204 | lp.decodeFromReader( 205 | shake, 206 | (err, msg) => { 207 | expect(err).to.not.exist() 208 | 209 | const response = proto.CircuitRelay.decode(msg) 210 | expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_SRC_MULTIADDR_INVALID) 211 | expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) 212 | done() 213 | }) 214 | 215 | relay.handle(relayMsg, new StreamHandler(fromConn)) 216 | }) 217 | 218 | it(`fail on invalid dst address`, (done) => { 219 | let relayMsg = { 220 | type: proto.CircuitRelay.Type.HOP, 221 | srcPeer: { 222 | id: PeerId.createFromB58String(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).id, 223 | addrs: [multiaddr(`/ipfs/QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`).buffer] 224 | }, 225 | dstPeer: { 226 | id: PeerId.createFromB58String(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).id, 227 | addrs: [`sdfkjsdnfkjdsb`] 228 | } 229 | } 230 | 231 | lp.decodeFromReader( 232 | shake, 233 | (err, msg) => { 234 | expect(err).to.not.exist() 235 | 236 | const response = proto.CircuitRelay.decode(msg) 237 | expect(response.code).to.equal(proto.CircuitRelay.Status.HOP_DST_MULTIADDR_INVALID) 238 | expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) 239 | done() 240 | }) 241 | 242 | relay.handle(relayMsg, new StreamHandler(fromConn)) 243 | }) 244 | }) 245 | 246 | describe(`._circuit`, () => { 247 | let relay 248 | let swarm 249 | let srcConn 250 | let dstConn 251 | let srcStream 252 | let dstStream 253 | let srcShake 254 | let dstShake 255 | 256 | before((done) => { 257 | srcStream = handshake({ timeout: 1000 * 60 }) 258 | srcShake = srcStream.handshake 259 | srcConn = new Connection(srcStream) 260 | dstStream = handshake({ timeout: 1000 * 60 }) 261 | dstShake = dstStream.handshake 262 | dstConn = new Connection(dstStream) 263 | const peerInfo = new PeerInfo(PeerId.createFromB58String('QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA')) 264 | srcConn.setPeerInfo(peerInfo) 265 | 266 | let peers = { 267 | QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: 268 | new PeerInfo(PeerId.createFromB58String(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`)), 269 | QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA: 270 | new PeerInfo(PeerId.createFromB58String(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`)), 271 | QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy: 272 | new PeerInfo(PeerId.createFromB58String(`QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`)) 273 | } 274 | 275 | Object.keys(peers).forEach((key) => { peers[key]._connectedMultiaddr = true }) // make it truthy 276 | 277 | waterfall([ 278 | (cb) => PeerId.createFromJSON(nodes.node4, cb), 279 | (peerId, cb) => PeerInfo.create(peerId, cb), 280 | (peer, cb) => { 281 | peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') 282 | swarm = { 283 | _peerInfo: peer, 284 | conns: { 285 | QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection(), 286 | QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA: new Connection(), 287 | QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy: new Connection() 288 | }, 289 | _peerBook: { 290 | get: (peer) => { 291 | if (!peers[peer]) { 292 | throw new Error() 293 | } 294 | 295 | return peers[peer] 296 | } 297 | } 298 | } 299 | 300 | cb() 301 | } 302 | ], () => { 303 | relay = new Hop(swarm, { enabled: true }) 304 | relay._dialPeer = sinon.stub() 305 | relay._dialPeer.callsArgWith(1, null, dstConn) 306 | 307 | done() 308 | }) 309 | }) 310 | 311 | after(() => relay._dialPeer.reset()) 312 | 313 | describe('should correctly dial destination node', () => { 314 | let msg = { 315 | type: proto.CircuitRelay.Type.STOP, 316 | srcPeer: { 317 | id: Buffer.from(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`), 318 | addrs: [Buffer.from(`dsfsdfsdf`)] 319 | }, 320 | dstPeer: { 321 | id: Buffer.from(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`), 322 | addrs: [Buffer.from(`sdflksdfndsklfnlkdf`)] 323 | } 324 | } 325 | 326 | before(() => { 327 | relay._circuit( 328 | new StreamHandler(srcConn), 329 | msg, 330 | (err) => { 331 | expect(err).to.not.exist() 332 | }) 333 | }) 334 | 335 | it('should respond with SUCCESS to source node', (done) => { 336 | lp.decodeFromReader( 337 | srcShake, 338 | (err, msg) => { 339 | expect(err).to.not.exist() 340 | 341 | const response = proto.CircuitRelay.decode(msg) 342 | expect(response.type).to.equal(proto.CircuitRelay.Type.STATUS) 343 | expect(response.code).to.equal(proto.CircuitRelay.Status.SUCCESS) 344 | done() 345 | }) 346 | }) 347 | 348 | it('should send STOP message to destination node', (done) => { 349 | lp.decodeFromReader( 350 | dstShake, 351 | (err, _msg) => { 352 | expect(err).to.not.exist() 353 | 354 | const response = proto.CircuitRelay.decode(_msg) 355 | expect(response.type).to.deep.equal(msg.type) 356 | expect(response.srcPeer).to.deep.equal(msg.srcPeer) 357 | expect(response.dstPeer).to.deep.equal(msg.dstPeer) 358 | done() 359 | }) 360 | }) 361 | 362 | it('should create circuit', (done) => { 363 | pull( 364 | values([proto.CircuitRelay.encode({ 365 | type: proto.CircuitRelay.Type.STATUS, 366 | code: proto.CircuitRelay.Status.SUCCESS 367 | })]), 368 | lp.encode(), 369 | collect((err, encoded) => { 370 | expect(err).to.not.exist() 371 | 372 | encoded.forEach((e) => dstShake.write(e)) 373 | pull( 374 | values([Buffer.from('hello')]), 375 | lp.encode(), 376 | collect((err, encoded) => { 377 | expect(err).to.not.exist() 378 | 379 | encoded.forEach((e) => srcShake.write(e)) 380 | lp.decodeFromReader( 381 | dstShake, 382 | (err, _msg) => { 383 | expect(err).to.not.exist() 384 | expect(_msg.toString()).to.equal('hello') 385 | 386 | done() 387 | }) 388 | }) 389 | ) 390 | }) 391 | ) 392 | }) 393 | }) 394 | 395 | describe('should fail creating circuit', () => { 396 | let msg = { 397 | type: proto.CircuitRelay.Type.STOP, 398 | srcPeer: { 399 | id: Buffer.from(`QmQWqGdndSpAkxfk8iyiJyz3XXGkrDNujvc8vEst3baubA`), 400 | addrs: [Buffer.from(`dsfsdfsdf`)] 401 | }, 402 | dstPeer: { 403 | id: Buffer.from(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`), 404 | addrs: [Buffer.from(`sdflksdfndsklfnlkdf`)] 405 | } 406 | } 407 | 408 | it('should not create circuit', (done) => { 409 | relay._circuit( 410 | new StreamHandler(srcConn), 411 | msg, 412 | (err) => { 413 | expect(err).to.exist() 414 | expect(err).to.match(/Unable to create circuit!/) 415 | done() 416 | }) 417 | 418 | pull( 419 | values([proto.CircuitRelay.encode({ 420 | type: proto.CircuitRelay.Type.STATUS, 421 | code: proto.CircuitRelay.Status.STOP_RELAY_REFUSED 422 | })]), 423 | lp.encode(), 424 | collect((err, encoded) => { 425 | expect(err).to.not.exist() 426 | 427 | encoded.forEach((e) => dstShake.write(e)) 428 | }) 429 | ) 430 | }) 431 | }) 432 | }) 433 | }) 434 | -------------------------------------------------------------------------------- /test/listener.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const Listener = require('../src/listener') 5 | const nodes = require('./fixtures/nodes') 6 | const waterfall = require('async/waterfall') 7 | const PeerInfo = require('peer-info') 8 | const PeerId = require('peer-id') 9 | const multiaddr = require('multiaddr') 10 | const handshake = require('pull-handshake') 11 | const Connection = require('interface-connection').Connection 12 | const proto = require('../src/protocol') 13 | const lp = require('pull-length-prefixed') 14 | const pull = require('pull-stream/pull') 15 | const values = require('pull-stream/sources/values') 16 | const collect = require('pull-stream/sinks/collect') 17 | const multicodec = require('../src/multicodec') 18 | 19 | const chai = require('chai') 20 | const dirtyChai = require('dirty-chai') 21 | const expect = chai.expect 22 | chai.use(dirtyChai) 23 | const sinon = require('sinon') 24 | 25 | describe('listener', function () { 26 | describe(`listen`, function () { 27 | let swarm = null 28 | let handlerSpy = null 29 | let listener = null 30 | let stream = null 31 | let shake = null 32 | let conn = null 33 | 34 | beforeEach(function (done) { 35 | stream = handshake({ timeout: 1000 * 60 }) 36 | shake = stream.handshake 37 | conn = new Connection(stream) 38 | conn.setPeerInfo(new PeerInfo(PeerId 39 | .createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE'))) 40 | 41 | waterfall([ 42 | (cb) => PeerId.createFromJSON(nodes.node4, cb), 43 | (peerId, cb) => PeerInfo.create(peerId, cb), 44 | (peer, cb) => { 45 | swarm = { 46 | _peerInfo: peer, 47 | handle: sinon.spy((proto, h) => { 48 | handlerSpy = sinon.spy(h) 49 | }), 50 | conns: { 51 | QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() 52 | } 53 | } 54 | 55 | listener = Listener(swarm, {}, () => {}) 56 | listener.listen() 57 | cb() 58 | } 59 | ], done) 60 | }) 61 | 62 | afterEach(() => { 63 | listener = null 64 | }) 65 | 66 | it(`should handle HOP`, function (done) { 67 | handlerSpy(multicodec.relay, conn) 68 | 69 | let relayMsg = { 70 | type: proto.CircuitRelay.Type.HOP, 71 | srcPeer: { 72 | id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, 73 | addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] 74 | }, 75 | dstPeer: { 76 | id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, 77 | addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] 78 | } 79 | } 80 | 81 | listener.hopHandler.handle = (message, conn) => { 82 | expect(message.type).to.equal(proto.CircuitRelay.Type.HOP) 83 | 84 | expect(message.srcPeer.id.toString()).to.equal(relayMsg.srcPeer.id) 85 | expect(message.srcPeer.addrs[0].toString()).to.equal(relayMsg.srcPeer.addrs[0]) 86 | 87 | expect(message.dstPeer.id.toString()).to.equal(relayMsg.dstPeer.id) 88 | expect(message.dstPeer.addrs[0].toString()).to.equal(relayMsg.dstPeer.addrs[0]) 89 | 90 | done() 91 | } 92 | 93 | pull( 94 | values([proto.CircuitRelay.encode(relayMsg)]), 95 | lp.encode(), 96 | collect((err, encoded) => { 97 | expect(err).to.not.exist() 98 | encoded.forEach((e) => shake.write(e)) 99 | }) 100 | ) 101 | }) 102 | 103 | it(`should handle STOP`, function (done) { 104 | handlerSpy(multicodec.relay, conn) 105 | 106 | let relayMsg = { 107 | type: proto.CircuitRelay.Type.STOP, 108 | srcPeer: { 109 | id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, 110 | addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] 111 | }, 112 | dstPeer: { 113 | id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, 114 | addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] 115 | } 116 | } 117 | 118 | listener.stopHandler.handle = (message, conn) => { 119 | expect(message.type).to.equal(proto.CircuitRelay.Type.STOP) 120 | 121 | expect(message.srcPeer.id.toString()).to.equal(relayMsg.srcPeer.id) 122 | expect(message.srcPeer.addrs[0].toString()).to.equal(relayMsg.srcPeer.addrs[0]) 123 | 124 | expect(message.dstPeer.id.toString()).to.equal(relayMsg.dstPeer.id) 125 | expect(message.dstPeer.addrs[0].toString()).to.equal(relayMsg.dstPeer.addrs[0]) 126 | 127 | done() 128 | } 129 | 130 | pull( 131 | values([proto.CircuitRelay.encode(relayMsg)]), 132 | lp.encode(), 133 | collect((err, encoded) => { 134 | expect(err).to.not.exist() 135 | encoded.forEach((e) => shake.write(e)) 136 | }) 137 | ) 138 | }) 139 | 140 | it(`should emit 'connection'`, function (done) { 141 | handlerSpy(multicodec.relay, conn) 142 | 143 | let relayMsg = { 144 | type: proto.CircuitRelay.Type.STOP, 145 | srcPeer: { 146 | id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, 147 | addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] 148 | }, 149 | dstPeer: { 150 | id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, 151 | addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] 152 | } 153 | } 154 | 155 | listener.stopHandler.handle = (message, sh) => { 156 | const newConn = new Connection(sh.rest()) 157 | listener.stopHandler.emit('connection', newConn) 158 | } 159 | 160 | listener.on('connection', (conn) => { 161 | expect(conn).to.be.instanceof(Connection) 162 | done() 163 | }) 164 | 165 | pull( 166 | values([proto.CircuitRelay.encode(relayMsg)]), 167 | lp.encode(), 168 | collect((err, encoded) => { 169 | expect(err).to.not.exist() 170 | encoded.forEach((e) => shake.write(e)) 171 | }) 172 | ) 173 | }) 174 | 175 | it(`should handle CAN_HOP`, function (done) { 176 | handlerSpy(multicodec.relay, conn) 177 | 178 | let relayMsg = { 179 | type: proto.CircuitRelay.Type.CAN_HOP, 180 | srcPeer: { 181 | id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, 182 | addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] 183 | }, 184 | dstPeer: { 185 | id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, 186 | addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] 187 | } 188 | } 189 | 190 | listener.hopHandler.handle = (message, conn) => { 191 | expect(message.type).to.equal(proto.CircuitRelay.Type.CAN_HOP) 192 | 193 | expect(message.srcPeer.id.toString()).to.equal(relayMsg.srcPeer.id) 194 | expect(message.srcPeer.addrs[0].toString()).to.equal(relayMsg.srcPeer.addrs[0]) 195 | 196 | expect(message.dstPeer.id.toString()).to.equal(relayMsg.dstPeer.id) 197 | expect(message.dstPeer.addrs[0].toString()).to.equal(relayMsg.dstPeer.addrs[0]) 198 | 199 | done() 200 | } 201 | 202 | pull( 203 | values([proto.CircuitRelay.encode(relayMsg)]), 204 | lp.encode(), 205 | collect((err, encoded) => { 206 | expect(err).to.not.exist() 207 | encoded.forEach((e) => shake.write(e)) 208 | }) 209 | ) 210 | }) 211 | 212 | it(`should handle invalid message correctly`, function (done) { 213 | handlerSpy(multicodec.relay, conn) 214 | 215 | let relayMsg = { 216 | type: 100000, 217 | srcPeer: { 218 | id: Buffer.from(`QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`), 219 | addrs: [multiaddr(`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`).buffer] 220 | }, 221 | dstPeer: { 222 | id: Buffer.from(`QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`), 223 | addrs: [multiaddr(`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`).buffer] 224 | } 225 | } 226 | 227 | pull( 228 | values([Buffer.from([relayMsg])]), 229 | lp.encode(), 230 | collect((err, encoded) => { 231 | expect(err).to.not.exist() 232 | encoded.forEach((e) => shake.write(e)) 233 | }), 234 | lp.decodeFromReader(shake, { maxLength: this.maxLength }, (err, msg) => { 235 | expect(err).to.not.exist() 236 | expect(proto.CircuitRelay.decode(msg).type).to.equal(proto.CircuitRelay.Type.STATUS) 237 | expect(proto.CircuitRelay.decode(msg).code).to.equal(proto.CircuitRelay.Status.MALFORMED_MESSAGE) 238 | done() 239 | }) 240 | ) 241 | }) 242 | }) 243 | 244 | describe(`getAddrs`, function () { 245 | let swarm = null 246 | let listener = null 247 | let peerInfo = null 248 | 249 | beforeEach(function (done) { 250 | waterfall([ 251 | (cb) => PeerId.createFromJSON(nodes.node4, cb), 252 | (peerId, cb) => PeerInfo.create(peerId, cb), 253 | (peer, cb) => { 254 | swarm = { 255 | _peerInfo: peer 256 | } 257 | 258 | peerInfo = peer 259 | listener = Listener(swarm, {}, () => {}) 260 | cb() 261 | } 262 | ], done) 263 | }) 264 | 265 | afterEach(() => { 266 | peerInfo = null 267 | }) 268 | 269 | it(`should return correct addrs`, function () { 270 | peerInfo.multiaddrs.add(`/ip4/0.0.0.0/tcp/4002`) 271 | peerInfo.multiaddrs.add(`/ip4/127.0.0.1/tcp/4003/ws`) 272 | 273 | listener.getAddrs((err, addrs) => { 274 | expect(err).to.not.exist() 275 | expect(addrs).to.deep.equal([ 276 | multiaddr(`/p2p-circuit/ip4/0.0.0.0/tcp/4002/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`), 277 | multiaddr(`/p2p-circuit/ip4/127.0.0.1/tcp/4003/ws/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`)]) 278 | }) 279 | }) 280 | 281 | it(`don't return default addrs in an explicit p2p-circuit addres`, function () { 282 | peerInfo.multiaddrs.add(`/ip4/127.0.0.1/tcp/4003/ws`) 283 | peerInfo.multiaddrs.add(`/p2p-circuit/ip4/0.0.0.0/tcp/4002`) 284 | listener.getAddrs((err, addrs) => { 285 | expect(err).to.not.exist() 286 | expect(addrs[0] 287 | .toString()) 288 | .to.equal(`/p2p-circuit/ip4/0.0.0.0/tcp/4002/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`) 289 | }) 290 | }) 291 | }) 292 | }) 293 | -------------------------------------------------------------------------------- /test/proto.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const chai = require('chai') 5 | const dirtyChai = require('dirty-chai') 6 | const expect = chai.expect 7 | chai.use(dirtyChai) 8 | 9 | const multiaddr = require('multiaddr') 10 | 11 | const proto = require('../src/protocol') 12 | 13 | describe('protocol', function () { 14 | let msgObject = null 15 | let message = null 16 | 17 | before(() => { 18 | msgObject = { 19 | type: proto.CircuitRelay.Type.HOP, 20 | srcPeer: { 21 | id: Buffer.from('QmSource'), 22 | addrs: [ 23 | multiaddr('/p2p-circuit/ipfs/QmSource').buffer, 24 | multiaddr('/p2p-circuit/ip4/0.0.0.0/tcp/9000/ipfs/QmSource').buffer, 25 | multiaddr('/ip4/0.0.0.0/tcp/9000/ipfs/QmSource').buffer 26 | ] 27 | }, 28 | dstPeer: { 29 | id: Buffer.from('QmDest'), 30 | addrs: [ 31 | multiaddr('/p2p-circuit/ipfs/QmDest').buffer, 32 | multiaddr('/p2p-circuit/ip4/1.1.1.1/tcp/9000/ipfs/QmDest').buffer, 33 | multiaddr('/ip4/1.1.1.1/tcp/9000/ipfs/QmDest').buffer 34 | ] 35 | } 36 | } 37 | 38 | let buff = proto.CircuitRelay.encode(msgObject) 39 | message = proto.CircuitRelay.decode(buff) 40 | }) 41 | 42 | it(`should source and dest`, () => { 43 | expect(message.srcPeer).to.deep.equal(msgObject.srcPeer) 44 | expect(message.dstPeer).to.deep.equal(msgObject.dstPeer) 45 | }) 46 | 47 | it(`should encode message`, () => { 48 | expect(message.message).to.deep.equal(msgObject.message) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/stop.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | 'use strict' 3 | 4 | const Stop = require('../src/circuit/stop') 5 | const nodes = require('./fixtures/nodes') 6 | const Connection = require('interface-connection').Connection 7 | const handshake = require('pull-handshake') 8 | const waterfall = require('async/waterfall') 9 | const PeerInfo = require('peer-info') 10 | const PeerId = require('peer-id') 11 | const StreamHandler = require('../src/circuit/stream-handler') 12 | const proto = require('../src/protocol') 13 | 14 | const chai = require('chai') 15 | const dirtyChai = require('dirty-chai') 16 | const expect = chai.expect 17 | chai.use(dirtyChai) 18 | 19 | describe('stop', function () { 20 | describe(`handle relayed connections`, function () { 21 | let stopHandler 22 | 23 | let swarm 24 | let conn 25 | let stream 26 | 27 | beforeEach(function (done) { 28 | stream = handshake({ timeout: 1000 * 60 }) 29 | conn = new Connection(stream) 30 | const peerId = PeerId.createFromB58String('QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') 31 | conn.setPeerInfo(new PeerInfo(peerId)) 32 | 33 | waterfall([ 34 | (cb) => PeerId.createFromJSON(nodes.node4, cb), 35 | (peerId, cb) => PeerInfo.create(peerId, cb), 36 | (peer, cb) => { 37 | peer.multiaddrs.add('/p2p-circuit/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE') 38 | swarm = { 39 | _peerInfo: peer, 40 | conns: { 41 | QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE: new Connection() 42 | } 43 | } 44 | 45 | stopHandler = new Stop(swarm) 46 | cb() 47 | } 48 | ], done) 49 | }) 50 | 51 | it(`handle request with a valid multiaddr`, function (done) { 52 | stopHandler.handle({ 53 | type: proto.CircuitRelay.Type.STOP, 54 | srcPeer: { 55 | id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, 56 | addrs: [`/ipfs/QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`] 57 | }, 58 | dstPeer: { 59 | id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, 60 | addrs: [`/ipfs/QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`] 61 | } 62 | }, new StreamHandler(conn), (conn) => { // multistream handler doesn't expect errors... 63 | expect(conn).to.be.instanceOf(Connection) 64 | done() 65 | }) 66 | }) 67 | 68 | it(`handle request with invalid multiaddr`, function (done) { 69 | stopHandler.handle({ 70 | type: proto.CircuitRelay.Type.STOP, 71 | srcPeer: { 72 | id: `QmSswe1dCFRepmhjAMR5VfHeokGLcvVggkuDJm7RMfJSrE`, 73 | addrs: [`dsfsdfsdf`] 74 | }, 75 | dstPeer: { 76 | id: `QmQvM2mpqkjyXWbTHSUidUAWN26GgdMphTh9iGDdjgVXCy`, 77 | addrs: [`sdflksdfndsklfnlkdf`] 78 | } 79 | }, new StreamHandler(conn), (conn) => { 80 | expect(conn).to.not.exist() 81 | done() 82 | }) 83 | }) 84 | }) 85 | }) 86 | --------------------------------------------------------------------------------