diff --git a/package.json b/package.json index 5ec694b76d..6527488d7d 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "debug": "^2.2.0", "fs-blob-store": "^5.2.1", "hapi": "^13.3.0", - "ipfs-api": "github:ipfs/js-ipfs-api#f3e2d42", + "ipfs-api": "^3.0.1", "ipfs-blocks": "^0.1.0", "ipfs-data-importing": "^0.3.3", "ipfs-merkle-dag": "^0.4.0", diff --git a/src/cli/commands/swarm/addrs.js b/src/cli/commands/swarm/addrs.js new file mode 100644 index 0000000000..f61f941d9b --- /dev/null +++ b/src/cli/commands/swarm/addrs.js @@ -0,0 +1,20 @@ +const Command = require('ronin').Command +const utils = require('../../utils') +const debug = require('debug') +const log = debug('cli:object') +log.error = debug('cli:object:error') + +module.exports = Command.extend({ + desc: '', + + options: {}, + + run: () => { + utils.getIPFS((err, ipfs) => { + if (err) { + throw err + } + // TODO + }) + } +}) diff --git a/src/cli/commands/swarm/addrs/local.js b/src/cli/commands/swarm/addrs/local.js new file mode 100644 index 0000000000..7371267744 --- /dev/null +++ b/src/cli/commands/swarm/addrs/local.js @@ -0,0 +1,33 @@ +const Command = require('ronin').Command +const utils = require('../../../utils') +const debug = require('debug') +const log = debug('cli:object') +log.error = debug('cli:object:error') + +module.exports = Command.extend({ + desc: 'List local addresses', + + options: {}, + + run: () => { + utils.getIPFS((err, ipfs) => { + if (err) { + throw err + } + + if (!utils.isDaemonOn()) { + throw new Error('This command must be run in online mode. Try running \'ipfs daemon\' first.') + } + + ipfs.swarm.localAddrs((err, res) => { + if (err) { + throw err + } + + res.Strings.forEach((addr) => { + console.log(addr) + }) + }) + }) + } +}) diff --git a/src/cli/commands/swarm/connect.js b/src/cli/commands/swarm/connect.js new file mode 100644 index 0000000000..95a63aad27 --- /dev/null +++ b/src/cli/commands/swarm/connect.js @@ -0,0 +1,35 @@ +const Command = require('ronin').Command +const utils = require('../../utils') +const debug = require('debug') +const log = debug('cli:swarm') +log.error = debug('cli:swarm:error') + +module.exports = Command.extend({ + desc: 'Open connection to a given address', + + options: {}, + + run: (address) => { + if (!address) { + throw new Error("Argument 'address' is required") + } + + utils.getIPFS((err, ipfs) => { + if (err) { + throw err + } + + if (!utils.isDaemonOn()) { + throw new Error('This command must be run in online mode. Try running \'ipfs daemon\' first.') + } + + ipfs.swarm.connect(address, (err, res) => { + if (err) { + throw err + } + + console.log(res.Strings[0]) + }) + }) + } +}) diff --git a/src/cli/commands/swarm/disconnect.js b/src/cli/commands/swarm/disconnect.js new file mode 100644 index 0000000000..e87f3d54d8 --- /dev/null +++ b/src/cli/commands/swarm/disconnect.js @@ -0,0 +1,21 @@ +const Command = require('ronin').Command +const utils = require('../../utils') +// const bs58 = require('bs58') +const debug = require('debug') +const log = debug('cli:object') +log.error = debug('cli:object:error') + +module.exports = Command.extend({ + desc: '', + + options: {}, + + run: () => { + utils.getIPFS((err, ipfs) => { + if (err) { + throw err + } + // TODO + }) + } +}) diff --git a/src/cli/commands/swarm/peers.js b/src/cli/commands/swarm/peers.js new file mode 100644 index 0000000000..7d18970f08 --- /dev/null +++ b/src/cli/commands/swarm/peers.js @@ -0,0 +1,33 @@ +const Command = require('ronin').Command +const utils = require('../../utils') +const debug = require('debug') +const log = debug('cli:object') +log.error = debug('cli:object:error') + +module.exports = Command.extend({ + desc: 'List peers with open connections', + + options: {}, + + run: () => { + utils.getIPFS((err, ipfs) => { + if (err) { + throw err + } + + if (!utils.isDaemonOn()) { + throw new Error('This command must be run in online mode. Try running \'ipfs daemon\' first.') + } + + ipfs.swarm.peers((err, res) => { + if (err) { + throw err + } + + res.Strings.forEach((addr) => { + console.log(addr) + }) + }) + }) + } +}) diff --git a/src/core/index.js b/src/core/index.js index 08226abb5d..66b3bb2160 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -29,6 +29,7 @@ function IPFS (repo) { const dagS = new DAGService(blockS) var peerInfo var libp2pNode + var peerInfoBook = {} this.load = (callback) => { repo.exists((err, exists) => { @@ -308,6 +309,8 @@ function IPFS (repo) { } } + const OFFLINE_ERROR = new Error('This command must be run in online mode. Try running \'ipfs daemon\' first.') + this.libp2p = { start: (callback) => { libp2pNode = new libp2p.Node(peerInfo) @@ -323,11 +326,57 @@ function IPFS (repo) { libp2pNode.swarm.close(callback) }, swarm: { - peers: () => {}, - addrs: notImpl, - connect: notImpl, - disconnect: notImpl, - filters: notImpl + peers: (callback) => { + if (!libp2pNode) { + return callback(OFFLINE_ERROR) + } + + callback(null, peerInfoBook) + }, + // all the addrs we know + addrs: (callback) => { + if (!libp2pNode) { + return callback(OFFLINE_ERROR) + } + // TODO + notImpl() + }, + localAddrs: (callback) => { + if (!libp2pNode) { + return callback(OFFLINE_ERROR) + } + + callback(null, peerInfo.multiaddrs) + }, + connect: (ma, callback) => { + if (!libp2pNode) { + return callback(OFFLINE_ERROR) + } + + const idStr = ma.toString().match(/\/ipfs\/(.*)/) + if (!idStr) { + return callback(new Error('invalid multiaddr')) + } + const id = peerId.createFromB58String(idStr[1]) + const peer = new PeerInfo(id) + + ma = ma.toString().replace(/\/ipfs\/(.*)/, '') // FIXME remove this when multiaddr supports ipfs + + peer.multiaddr.add(multiaddr(ma)) + peerInfoBook[peer.id.toB58String()] = peer + + libp2pNode.swarm.dial(peer, (err) => { + callback(err, id) + }) + }, + disconnect: (callback) => { + if (!libp2pNode) { + return callback(OFFLINE_ERROR) + } + + notImpl() + }, + filters: notImpl // TODO }, routing: {}, records: {}, diff --git a/src/http-api/index.js b/src/http-api/index.js index e0cf86d95e..cdae357ec8 100644 --- a/src/http-api/index.js +++ b/src/http-api/index.js @@ -10,8 +10,13 @@ log.error = debug('api:error') exports = module.exports -exports.start = (callback) => { - const ipfs = exports.ipfs = new IPFS() +exports.start = (repo, callback) => { + if (typeof repo === 'function') { + callback = repo + repo = undefined + } + + const ipfs = exports.ipfs = new IPFS(repo) ipfs.load(() => { const repoPath = process.env.IPFS_PATH || os.homedir() + '/.ipfs' try { @@ -67,7 +72,9 @@ exports.start = (callback) => { exports.stop = (callback) => { const repoPath = process.env.IPFS_PATH || os.homedir() + '/.ipfs' fs.unlinkSync(repoPath + '/api') + console.log('->', 'going to stop libp2p') exports.ipfs.libp2p.stop(() => { + console.log('->', 'going to stop api server') exports.server.stop(callback) }) } diff --git a/src/http-api/resources/index.js b/src/http-api/resources/index.js index 4aecd49f28..4e8af44429 100644 --- a/src/http-api/resources/index.js +++ b/src/http-api/resources/index.js @@ -5,3 +5,4 @@ exports.repo = require('./repo') exports.object = require('./object') exports.config = require('./config') exports.block = require('./block') +exports.swarm = require('./swarm') diff --git a/src/http-api/resources/swarm.js b/src/http-api/resources/swarm.js new file mode 100644 index 0000000000..ea9b1079ad --- /dev/null +++ b/src/http-api/resources/swarm.js @@ -0,0 +1,83 @@ +'use strict' + +const ipfs = require('./../index.js').ipfs +const debug = require('debug') +const log = debug('http-api:block') +log.error = debug('http-api:block:error') + +exports = module.exports + +// common pre request handler that parses the args and returns `addr` which is assigned to `request.pre.args` +exports.parseAddrs = (request, reply) => { + if (!request.query.arg) { + return reply("Argument 'addr' is required").code(400).takeover() + } + + return reply({ + addr: request.query.arg + }) +} + +exports.peers = { + // main route handler which is called after the above `parseArgs`, but only if the args were valid + handler: (request, reply) => { + ipfs.libp2p.swarm.peers((err, peers) => { + if (err) { + log.error(err) + return reply({ + Message: err.toString(), + Code: 0 + }).code(500) + } + + return reply({ + Strings: Object.keys(peers) + .map((key) => + `${peers[key].multiaddrs[0].toString()}/ipfs/${peers[key].id.toB58String()}`) + }) + }) + } +} + +exports.localAddrs = { + // main route handler which is called after the above `parseArgs`, but only if the args were valid + handler: (request, reply) => { + ipfs.libp2p.swarm.localAddrs((err, addrs) => { + if (err) { + log.error(err) + return reply({ + Message: err.toString(), + Code: 0 + }).code(500) + } + + return reply({ + Strings: addrs.map((addr) => addr.toString()) + }) + }) + } +} + +exports.connect = { + // uses common parseAddr method that returns a `addr` + parseArgs: exports.parseAddrs, + + // main route handler which is called after the above `parseArgs`, but only if the args were valid + handler: (request, reply) => { + const addr = request.pre.args.addr + + ipfs.libp2p.swarm.connect(addr, (err, res) => { + if (err) { + log.error(err) + return reply({ + Message: err.toString(), + Code: 0 + }).code(500) + } + + return reply({ + Strings: [`connect ${res.toB58String()} success`] + }) + }) + } +} diff --git a/src/http-api/routes/index.js b/src/http-api/routes/index.js index 57702b2f25..167aa6585b 100644 --- a/src/http-api/routes/index.js +++ b/src/http-api/routes/index.js @@ -6,4 +6,5 @@ module.exports = (server) => { require('./object')(server) // require('./repo')(server) require('./config')(server) + require('./swarm')(server) } diff --git a/src/http-api/routes/swarm.js b/src/http-api/routes/swarm.js new file mode 100644 index 0000000000..bfef3f7706 --- /dev/null +++ b/src/http-api/routes/swarm.js @@ -0,0 +1,57 @@ +const resources = require('./../resources') + +module.exports = (server) => { + const api = server.select('API') + + api.route({ + method: 'GET', + path: '/api/v0/swarm/peers', + config: { + handler: resources.swarm.peers.handler + } + }) + + // api.route({ + // method: 'GET', + // path: '/api/v0/swarm/addrs', + // config: { + // handler: resources.swarm.addrs.handler + // } + // }) + + api.route({ + method: 'GET', + path: '/api/v0/swarm/addrs/local', + config: { + handler: resources.swarm.localAddrs.handler + } + }) + + api.route({ + method: 'GET', + path: '/api/v0/swarm/connect', + config: { + pre: [ + { method: resources.swarm.connect.parseArgs, assign: 'args' } + ], + handler: resources.swarm.connect.handler + } + }) + + // api.route({ + // method: 'GET', + // path: '/api/v0/swarm/disconnect', + // config: { + // handler: resources.swarm.disconnect + // } + // }) + + // TODO + // api.route({ + // method: 'GET', + // path: '/api/v0/swarm/filters', + // config: { + // handler: resources.swarm.disconnect + // } + // }) +} diff --git a/test/cli-tests/test-commands.js b/test/cli-tests/test-commands.js index 10a69fd824..d636d19bcf 100644 --- a/test/cli-tests/test-commands.js +++ b/test/cli-tests/test-commands.js @@ -9,7 +9,7 @@ describe('commands', () => { .run((err, stdout, exitcode) => { expect(err).to.not.exist expect(exitcode).to.equal(0) - expect(stdout.length).to.equal(40) + expect(stdout.length).to.equal(45) done() }) }) diff --git a/test/cli-tests/test-swarm.js b/test/cli-tests/test-swarm.js new file mode 100644 index 0000000000..b1c98d0354 --- /dev/null +++ b/test/cli-tests/test-swarm.js @@ -0,0 +1,74 @@ +/* eslint-env mocha */ + +const expect = require('chai').expect +const nexpect = require('nexpect') +const httpAPI = require('../../src/http-api') +const createTempNode = require('../utils/temp-node') + +describe('swarm', function () { + this.timeout(20000) + + var ipfs + var ipfsAddr + + before((done) => { + createTempNode(8, (err, _ipfs) => { + expect(err).to.not.exist + ipfs = _ipfs + ipfs.libp2p.start(done) + }) + }) + + before((done) => { + ipfs.id((err, res) => { + expect(err).to.not.exist + ipfsAddr = `${res.Addresses[0]}/ipfs/${res.ID}` + done() + }) + }) + + describe('api running', () => { + before((done) => { + httpAPI.start((err) => { + expect(err).to.not.exist + done() + }) + }) + + after((done) => { + httpAPI.stop((err) => { + expect(err).to.not.exist + done() + }) + }) + + it('connect', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'swarm', 'connect', ipfsAddr]) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + done() + }) + }) + + it('peers', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'swarm', 'peers']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout).to.have.length.above(0) + done() + }) + }) + + it('addrs local', (done) => { + nexpect.spawn('node', [process.cwd() + '/src/cli/bin.js', 'swarm', 'addrs', 'local']) + .run((err, stdout, exitcode) => { + expect(err).to.not.exist + expect(exitcode).to.equal(0) + expect(stdout).to.have.length.above(0) + done() + }) + }) + }) +}) diff --git a/test/core-tests/test-bootstrap.js b/test/core-tests/test-bootstrap.js index c7cc803d03..7c874f5729 100644 --- a/test/core-tests/test-bootstrap.js +++ b/test/core-tests/test-bootstrap.js @@ -2,7 +2,6 @@ const expect = require('chai').expect -process.env.IPFS_PATH = process.cwd() + '/tests/repo-example' const IPFS = require('../../src/core') describe('bootstrap', () => { diff --git a/test/core-tests/test-id.js b/test/core-tests/test-id.js index 6486a76afa..71bc38f56e 100644 --- a/test/core-tests/test-id.js +++ b/test/core-tests/test-id.js @@ -2,7 +2,6 @@ const expect = require('chai').expect -process.env.IPFS_PATH = process.cwd() + '/tests/repo-example' const IPFS = require('../../src/core') describe('id', () => { diff --git a/test/core-tests/test-swarm-node.js b/test/core-tests/test-swarm-node.js index aaf073f6d0..30f7fd5ed3 100644 --- a/test/core-tests/test-swarm-node.js +++ b/test/core-tests/test-swarm-node.js @@ -2,30 +2,73 @@ const expect = require('chai').expect -process.env.IPFS_PATH = process.cwd() + '/tests/repo-example' -const IPFS = require('../../src/core') +const createTempNode = require('../utils/temp-node') -describe('swarm', () => { - var ipfs +describe('swarm', function () { + this.timeout(20000) + + var ipfsA + var ipfsB + var ipfsAAddr + + before((done) => { + createTempNode(3, (err, ipfs) => { + expect(err).to.not.exist + ipfsA = ipfs + + createTempNode(4, (err, ipfs) => { + expect(err).to.not.exist + ipfsB = ipfs + done() + }) + }) + }) before((done) => { - ipfs = new IPFS() - ipfs.load(done) + ipfsA.id((err, res) => { + expect(err).to.not.exist + ipfsAAddr = `${res.Addresses[0]}/ipfs/${res.ID}` + done() + }) }) it('start', (done) => { - ipfs.libp2p.start((err) => { + ipfsA.libp2p.start((err) => { + expect(err).to.not.exist + ipfsB.libp2p.start((err) => { + expect(err).to.not.exist + done() + }) + }) + }) + + it('connect', (done) => { + ipfsB.libp2p.swarm.connect(ipfsAAddr, (err, res) => { + expect(err).to.not.exist + done() + }) + }) + + it('peers', (done) => { + ipfsB.libp2p.swarm.peers((err, res) => { + expect(err).to.not.exist + expect(Object.keys(res)).to.have.length(1) + done() + }) + }) + + it('localAddrs', (done) => { + ipfsB.libp2p.swarm.localAddrs((err, res) => { expect(err).to.not.exist + expect(res.length).to.equal(1) done() }) }) - it.skip('swarm peers', (done) => {}) - it.skip('swarm connect', (done) => {}) - it.skip('swarm disconnect', (done) => {}) + it.skip('disconnect', (done) => {}) - it('stop', (done) => { - ipfs.libp2p.stop((err) => { + it.skip('stop', (done) => { + ipfsA.libp2p.stop((err) => { expect(err).to.not.exist done() }) diff --git a/test/core-tests/test-version.js b/test/core-tests/test-version.js index da96ee4190..cb9f964d72 100644 --- a/test/core-tests/test-version.js +++ b/test/core-tests/test-version.js @@ -2,7 +2,6 @@ const expect = require('chai').expect -process.env.IPFS_PATH = process.cwd() + '/tests/repo-example' const IPFS = require('../../src/core') describe('version', () => { diff --git a/test/http-api-tests/index.js b/test/http-api-tests/index.js index 8f9c46f808..20834da9bc 100644 --- a/test/http-api-tests/index.js +++ b/test/http-api-tests/index.js @@ -24,6 +24,7 @@ describe('http api', () => { after((done) => { api.stop((err) => { expect(err).to.not.exist + rimraf(repoTests, (err) => { expect(err).to.not.exist done() diff --git a/test/http-api-tests/test-swarm.js b/test/http-api-tests/test-swarm.js new file mode 100644 index 0000000000..a729cdcebb --- /dev/null +++ b/test/http-api-tests/test-swarm.js @@ -0,0 +1,189 @@ +/* eslint-env mocha */ + +const expect = require('chai').expect +const APIctl = require('ipfs-api') +const createTempNode = require('../utils/temp-node') + +describe('swarm', function () { + this.timeout(20000) + + describe('api', () => { + var api + var ipfs // tmp node + var ipfsAddr + + before((done) => { + createTempNode(6, (err, _ipfs) => { + expect(err).to.not.exist + ipfs = _ipfs + ipfs.libp2p.start(done) + }) + }) + + before((done) => { + ipfs.id((err, res) => { + expect(err).to.not.exist + ipfsAddr = `${res.Addresses[0]}/ipfs/${res.ID}` + done() + }) + }) + + after((done) => { + // cause CI takes forever + var closed = false + setTimeout(() => { + if (!closed) { + closed = true + done() + } + }, 10000) + ipfs.libp2p.stop(() => { + if (!closed) { + closed = true + done() + } + }) + }) + + it('gets the api obj', (done) => { + api = require('../../src/http-api').server.select('API') + done() + }) + + it('/swarm/connect returns 400 for request without argument', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/swarm/connect' + }, (res) => { + expect(res.statusCode).to.equal(400) + expect(res.result).to.be.a('string') + done() + }) + }) + + it('/swarm/connect returns 500 for request with invalid argument', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/swarm/connect?arg=invalid' + }, (res) => { + expect(res.statusCode).to.equal(500) + expect(res.result.Code).to.equal(0) + expect(res.result.Message).to.be.a('string') + done() + }) + }) + + it('/swarm/connect returns value', (done) => { + api.inject({ + method: 'GET', + url: `/api/v0/swarm/connect?arg=${ipfsAddr}` + }, (res) => { + expect(res.statusCode).to.equal(200) + done() + }) + }) + + it('/swarm/peers returns value', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/swarm/peers' + }, (res) => { + expect(res.statusCode).to.equal(200) + expect(res.result.Strings).to.have.length.above(0) + done() + }) + }) + + it('/swarm/addrs/local returns value', (done) => { + api.inject({ + method: 'GET', + url: '/api/v0/swarm/addrs/local' + }, (res) => { + expect(res.statusCode).to.equal(200) + expect(res.result.Strings).to.have.length.above(0) + done() + }) + }) + }) + + describe('using js-ipfs-api', () => { + var ctl + var ipfs + var ipfsAddr + + before((done) => { + createTempNode(7, (err, _ipfs) => { + expect(err).to.not.exist + ipfs = _ipfs + ipfs.libp2p.start(done) + }) + }) + + before((done) => { + ipfs.id((err, res) => { + expect(err).to.not.exist + ipfsAddr = `${res.Addresses[0]}/ipfs/${res.ID}` + done() + }) + }) + + after((done) => { + // cause CI takes forever + var closed = false + setTimeout(() => { + if (!closed) { + closed = true + done() + } + }, 10000) + ipfs.libp2p.stop(() => { + if (!closed) { + closed = true + done() + } + }) + }) + + it('start IPFS API ctl', (done) => { + ctl = APIctl('/ip4/127.0.0.1/tcp/6001') + done() + }) + + it('ipfs.swarm.connect returns error for request without argument', (done) => { + ctl.swarm.connect(null, (err, result) => { + expect(err).to.exist + done() + }) + }) + + it('ipfs.swarm.connect returns error for request with invalid argument', (done) => { + ctl.swarm.connect('invalid', (err, result) => { + expect(err).to.exist + done() + }) + }) + + it('ipfs.swarm.connect returns value', (done) => { + ctl.swarm.connect(ipfsAddr, (err, result) => { + expect(err).to.not.exist + done() + }) + }) + + it('ipfs.swarm.peers returns value', (done) => { + ctl.swarm.peers((err, result) => { + expect(err).to.not.exist + expect(result.Strings).to.have.length.above(0) + done() + }) + }) + + it('ipfs.swarm.localAddrsreturns value', (done) => { + ctl.swarm.localAddrs((err, result) => { + expect(err).to.not.exist + expect(result.Strings).to.have.length.above(0) + done() + }) + }) + }) +}) diff --git a/test/utils/temp-node.js b/test/utils/temp-node.js new file mode 100644 index 0000000000..4055a15b62 --- /dev/null +++ b/test/utils/temp-node.js @@ -0,0 +1,38 @@ +/* eslint-env mocha */ + +const expect = require('chai').expect + +const IPFS = require('../../src/core') +const createTempRepo = require('./temp-repo') + +function setAddresses (repo, num, callback) { + repo.config.get((err, config) => { + expect(err).to.not.exist + config.Addresses = { + Swarm: [ + `/ip4/127.0.0.1/tcp/1000${num}` + ], + API: `/ip4/127.0.0.1/tcp/1100${num}`, + Gateway: `/ip4/127.0.0.1/tcp/1200${num}` + } + + repo.config.set(config, callback) + }) +} + +function createTempNode (num, callback) { + const repo = createTempRepo() + const ipfs = new IPFS(repo) + ipfs.init({ emptyRepo: true }, (err) => { + expect(err).to.not.exist + setAddresses(repo, num, (err) => { + expect(err).to.not.exist + + ipfs.load((err) => { + expect(err).to.not.exist + callback(null, ipfs) + }) + }) + }) +} +module.exports = createTempNode