From 1723ffd0f2b0cb75fafeec65ee7125c3da1a508c Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 13 Jun 2018 18:02:03 +0100 Subject: [PATCH 01/34] feat: wip --- package.json | 2 + src/cli/commands/name.js | 20 ++++ src/cli/commands/name/publish.js | 25 +++++ src/cli/commands/name/resolve.js | 25 +++++ src/core/components/index.js | 1 + src/core/components/name.js | 163 ++++++++++++++++++++++++++++++ src/core/index.js | 3 + src/core/namesys/index.js | 58 +++++++++++ src/core/namesys/pb/ipns.proto.js | 30 ++++++ src/core/namesys/pb/ipnsEntry.js | 31 ++++++ src/core/namesys/publisher.js | 138 +++++++++++++++++++++++++ src/core/namesys/resolver.js | 53 ++++++++++ src/core/namesys/utils.js | 9 ++ src/http/api/resources/index.js | 1 + src/http/api/resources/name.js | 62 ++++++++++++ src/http/api/routes/index.js | 1 + src/http/api/routes/name.js | 25 +++++ 17 files changed, 647 insertions(+) create mode 100644 src/cli/commands/name.js create mode 100644 src/cli/commands/name/publish.js create mode 100644 src/cli/commands/name/resolve.js create mode 100644 src/core/components/name.js create mode 100644 src/core/namesys/index.js create mode 100644 src/core/namesys/pb/ipns.proto.js create mode 100644 src/core/namesys/pb/ipnsEntry.js create mode 100644 src/core/namesys/publisher.js create mode 100644 src/core/namesys/resolver.js create mode 100644 src/core/namesys/utils.js create mode 100644 src/http/api/resources/name.js create mode 100644 src/http/api/routes/name.js diff --git a/package.json b/package.json index 66d2c0dd5c..4137f160b7 100644 --- a/package.json +++ b/package.json @@ -152,6 +152,7 @@ "peer-info": "~0.14.1", "progress": "^2.0.0", "promisify-es6": "^1.0.3", + "protons": "^1.0.1", "pull-abortable": "^4.1.1", "pull-defer": "~0.2.2", "pull-file": "^1.1.0", @@ -162,6 +163,7 @@ "pull-stream": "^3.6.8", "pull-stream-to-stream": "^1.3.4", "pull-zip": "^2.0.1", + "quick-lru": "^1.1.0", "read-pkg-up": "^4.0.0", "readable-stream": "2.3.6", "stream-to-pull-stream": "^1.7.2", diff --git a/src/cli/commands/name.js b/src/cli/commands/name.js new file mode 100644 index 0000000000..2ce7734d3e --- /dev/null +++ b/src/cli/commands/name.js @@ -0,0 +1,20 @@ +'use strict' + +/* +IPNS is a PKI namespace, where names are the hashes of public keys, and +the private key enables publishing new (signed) values. In both publish +and resolve, the default name used is the node's own PeerID, +which is the hash of its public key. +*/ +module.exports = { + command: 'name ', + + description: 'Publish and resolve IPNS names.', + + builder (yargs) { + return yargs.commandDir('name') + }, + + handler (argv) { + } +} diff --git a/src/cli/commands/name/publish.js b/src/cli/commands/name/publish.js new file mode 100644 index 0000000000..860e62c49b --- /dev/null +++ b/src/cli/commands/name/publish.js @@ -0,0 +1,25 @@ +'use strict' + +const print = require('../../utils').print + +module.exports = { + command: 'publish ', + + describe: 'Publish IPNS names.', + + builder: { + format: { + type: 'string' + } + }, + + handler (argv) { + argv.ipfs.name.publish(argv['ipfsPath'], (err, result) => { + if (err) { + throw err + } + + print(`Published to ${result.value}: /ipfs/${result.name}`) + }) + } +} diff --git a/src/cli/commands/name/resolve.js b/src/cli/commands/name/resolve.js new file mode 100644 index 0000000000..b34a5213e0 --- /dev/null +++ b/src/cli/commands/name/resolve.js @@ -0,0 +1,25 @@ +'use strict' + +const print = require('../../utils').print + +module.exports = { + command: 'resolve []', + + describe: 'Resolve IPNS names.', + + builder: { + format: { + type: 'string' + } + }, + + handler (argv) { + argv.ipfs.name.resolve(argv['name'], (err, result) => { + if (err) { + throw err + } + + print(`result: ${result}`) + }) + } +} diff --git a/src/core/components/index.js b/src/core/components/index.js index 6fc833499d..cf6506e6dd 100644 --- a/src/core/components/index.js +++ b/src/core/components/index.js @@ -28,3 +28,4 @@ exports.key = require('./key') exports.stats = require('./stats') exports.mfs = require('./mfs') exports.resolve = require('./resolve') +exports.name = require('./name') diff --git a/src/core/components/name.js b/src/core/components/name.js new file mode 100644 index 0000000000..489fe9aa59 --- /dev/null +++ b/src/core/components/name.js @@ -0,0 +1,163 @@ +'use strict' + +const promisify = require('promisify-es6') +const OFFLINE_ERROR = require('../utils').OFFLINE_ERROR +const human = require('human-to-milliseconds') + +const keyLookup = (ipfsNode, kname, cb) => { + if (kname === 'self') { + return cb(null, ipfsNode._peerInfo.id.privKey) + } + // TODO validate - jsipfs daemon --pass 123456sddadesfgefrsfesfefsfeesfe + ipfsNode._keychain.findKeyByName(kname, (err, key) => { + if (err) { + return cb(err) + } + + return cb(null, key) + }) +} + +const publish = (ipfsNode, privateKey, ipfsPath, publishOptions, callback) => { + // Should verify if exists ? + if (publishOptions.verifyIfExists) { + // TODO resolve + // https://github.com/ipfs/go-ipfs/blob/master/core/commands/publish.go#L172 + } + + // Add pubValidTime + + // Publish + const eol = new Date(Date.now()) + + ipfsNode._namesys.publishWithEOL(privateKey, ipfsPath, eol, (err, res) => { + if (err) { + callback(err) + } + + // TODO HERE HERE HERE + + callback(null, res) + }) +} + +module.exports = function name (self) { + return { + /** + * IPNS is a PKI namespace, where names are the hashes of public keys, and + * the private key enables publishing new (signed) values. In both publish + * and resolve, the default name used is the node's own PeerID, + * which is the hash of its public key. + * + * Examples: TODO such as in go + * + * @param {String} ipfsPath + * @param {Object} options + * @param {function(Error)} [callback] + * @returns {Promise|void} + */ + publish: promisify((ipfsPath, callback) => { + // https://github.com/ipfs/go-ipfs/blob/master/core/commands/publish.go + if (!self.isOnline()) { + return callback(new Error(OFFLINE_ERROR)) + } + + // TODO Validate Mounts IPNS - cannot manually publish while IPNS is mounted + + // TODO Validate Node identity not validated + + // TODO Parse options and create object + const options = { + resolve: true, + d: '24h', + ttl: undefined, + key: 'self' + } + + // TODO Create waterfall + /* waterfall([ + (cb) => human(options.d || '1s', cb), + ], callback) */ + + human(options.d || '1s', (err, value) => { + if (err) { + return callback(new Error('Error parsing lifetime option')) + } + + const publishOptions = { + verifyIfExists: options.resolve, + pubValidTime: value + } + + // TODO Date.now() + value + + // TODO TTL integration + + // Get Key + keyLookup(self, options.key, (err, key) => { + if (err) { + return callback(err) + } + + // TODO ParsePath + // https://github.com/ipfs/go-ipfs/blob/master/path/path.go + + publish(self, key, ipfsPath, publishOptions, (err, result) => { + if (err) { + callback(err) + } + + return callback(null, result) + }) + }) + }) + }), + + /** + * Given a key, query the DHT for its best value. + * + * @param {String} name ipns name to resolve. Defaults to your node's peerID. + * @param {boolean} nocache do not use cached entries. + * @param {boolean} recursive resolve until the result is not an IPNS name. + * @param {function(Error)} [callback] + * @returns {Promise|void} + */ + resolve: promisify((name, nocache, recursive, callback) => { + const local = true + + if (typeof name === 'function') { + callback = name + name = undefined + } + + if (!self.isOnline()) { + return callback(new Error(OFFLINE_ERROR)) + } + + // let resolver = self._namesys.ipnsResolver + + if (local && nocache) { + return callback(new Error('Cannot specify both local and nocache')) + } + + // Set node id as name for being resolved, if it is not received + if (!name) { + name = self._peerInfo.id.toB58String() + } + + if (!name.startsWith('/ipns/')) { + name = `/ipns/${name}` + } + + const pubKey = self._peerInfo.id.pubKey + + self._namesys.resolve(name, pubKey, (err, result) => { + if (err) { + return callback(err) + } + + return callback(null, result) + }) + }) + } +} diff --git a/src/core/index.js b/src/core/index.js index f9b29c5145..2562a86c1e 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -20,6 +20,7 @@ const EventEmitter = require('events') const config = require('./config') const boot = require('./boot') const components = require('./components') +const Namesys = require('./namesys') // replaced by repo-browser when running in the browser const defaultRepo = require('./runtime/repo-nodejs') const preload = require('./preload') @@ -89,6 +90,7 @@ class IPFS extends EventEmitter { this._pubsub = undefined this._preload = preload(this) this._mfsPreload = mfsPreload(this) + this._namesys = new Namesys(null, this._repo) // IPFS Core exposed components // - for booting up a node @@ -110,6 +112,7 @@ class IPFS extends EventEmitter { this.libp2p = components.libp2p(this) this.swarm = components.swarm(this) this.files = components.files(this) + this.name = components.name(this) this.bitswap = components.bitswap(this) this.pin = components.pin(this) this.ping = components.ping(this) diff --git a/src/core/namesys/index.js b/src/core/namesys/index.js new file mode 100644 index 0000000000..770836b41c --- /dev/null +++ b/src/core/namesys/index.js @@ -0,0 +1,58 @@ +'use strict' + +const peerId = require('peer-id') +const series = require('async/series') +// const QuickLRU = require('quick-lru'); + +const IpnsPublisher = require('./publisher') +const IpnsResolver = require('./resolver') + +// const defaultRecordTtl = 60 * 1000 + +class Namesys { + constructor (routing, repo, peerInfo) { + this.ipnsPublisher = new IpnsPublisher(routing, repo) + this.ipnsResolver = new IpnsResolver(repo) + // this.cache = new QuickLRU({maxSize: 1000}); + } + + // Resolve + resolve (name, pubKey, callback) { + // this.ipnsResolver.resolve() + + this.ipnsResolver.resolve(name, pubKey, (err, result) => { + if (err) { + return callback(err) + } + + return callback(null, result) + }) + } + + // publish (value = ipfsPath) + publish (privKey, value) { + // TODO https://github.com/ipfs/go-ipfs/blob/master/namesys/namesys.go#L111 + } + + // publish with EOL (value = ipfsPath) + publishWithEOL (privKey, value, eol, callback) { + series([ + (cb) => peerId.createFromPrivKey(privKey.bytes.toString('base64'), cb), + (cb) => this.ipnsPublisher.publishWithEOL(privKey, value, eol, cb) + ], (err, results) => { + if (err) { + return callback(err) + } + + // TODO Add to cache + // this.cache.set(id.toB58String(), { + // val: value, + // eol: Date.now() + ttl + // }) + + callback(null, results[0].toB58String()) + }) + } +} + +exports = module.exports = Namesys diff --git a/src/core/namesys/pb/ipns.proto.js b/src/core/namesys/pb/ipns.proto.js new file mode 100644 index 0000000000..4d12b43852 --- /dev/null +++ b/src/core/namesys/pb/ipns.proto.js @@ -0,0 +1,30 @@ +'use strict' + +const protons = require('protons') + +/* eslint-disable no-tabs */ +const message = ` +message IpnsEntry { + enum ValidityType { + EOL = 0; // setting an EOL says "this record is valid until..." + } + + required bytes value = 1; + required bytes signature = 2; + + optional ValidityType validityType = 3; + optional bytes validity = 4; + + optional uint64 sequence = 5; + + optional uint64 ttl = 6; + + // in order for nodes to properly validate a record upon receipt, they need the public + // key associated with it. For old RSA keys, its easiest if we just send this as part of + // the record itself. For newer ed25519 keys, the public key can be embedded in the + // peerID, making this field unnecessary. + optional bytes pubKey = 7; +} +` + +module.exports = protons(message).IpnsEntry diff --git a/src/core/namesys/pb/ipnsEntry.js b/src/core/namesys/pb/ipnsEntry.js new file mode 100644 index 0000000000..0ea7c39ebe --- /dev/null +++ b/src/core/namesys/pb/ipnsEntry.js @@ -0,0 +1,31 @@ +'use strict' + +const ipnsEntryProto = require('./ipns.proto') + +module.exports = { + // Create ipns data format + create: (value, signature, validityType, validity, sequence, ttl, pubKey) => { + // Handle validity type + if (validityType !== undefined) { + validityType = ipnsEntryProto.ValidityType.EOL + } + + return { + value: value, + signature: signature, + validityType: validityType, + validity: validity, + sequence: sequence, + ttl: ttl, + pubKey: pubKey + } + }, + // Marshal + marshal: (ipnsEntry) => { + return ipnsEntryProto.encode(ipnsEntry) + }, + // Unmarshal + unmarshal: (marsheled) => { + return ipnsEntryProto.decode(marsheled) + } +} diff --git a/src/core/namesys/publisher.js b/src/core/namesys/publisher.js new file mode 100644 index 0000000000..5e0debf967 --- /dev/null +++ b/src/core/namesys/publisher.js @@ -0,0 +1,138 @@ +'use strict' + +const peerId = require('peer-id') + +const IpnsEntry = require('./pb/ipnsEntry') +const utils = require('./utils') + +const defaultRecordTtl = 60 * 60 * 1000 + +/* + IpnsPublisher is capable of publishing and resolving names to the IPFS + routing system. + */ +class IpnsPublisher { + constructor (routing, repo) { + this.routing = routing + this.repo = repo + } + + // publish record with a eol + publishWithEOL (privKey, value, eol, callback) { + this.updateRecord(privKey, value, eol, (err, record) => { + if (err) { + return callback(err) + } + + // TODO FUTURE Put record to routing + callback(null, record) + }) + } + + // Accepts a keypair, as well as a value (ipfsPath), and publishes it out to the routing system + publish (privKey, value, callback) { + const eol = new Date(Date.now() + defaultRecordTtl) + + this.publishWithEOL(privKey, value, eol, (err, res) => { + if (err) { + return callback(err) + } + + callback(res) + }) + } + + // Returns the record this node has published corresponding to the given peer ID. + // If `checkRouting` is true and we have no existing record, this method will check the routing system for any existing records. + getPublished (peerId, checkRouting, callback) { + // TODO https://github.com/ipfs/go-ipfs/blob/master/namesys/publisher.go#L117 + this.repo.datastore.get(utils.generateIpnsDsKey(peerId), (err, dsVal) => { + let result + + if (!err) { + if (Buffer.isBuffer(dsVal)) { + result = dsVal + } else { + return callback(new Error('found ipns record that we couldn\'t convert to a value')) + } + } else if (err.notFound) { + if (!checkRouting) { + return callback(null, null) + } + // TODO Implement Routing + } else { + return callback(err) + } + + // unmarshal data + result = IpnsEntry.unmarshal(dsVal) + + return callback(null, result) + }) + } + + createEntryData (privKey, value, seqNumber, eol, callback) { + const valueBytes = Buffer.from(value) + const validityType = 0 + const sequence = seqNumber + const validity = Buffer.from(eol.toISOString()) + + const dataForSignature = this.getIpnsEntryDataForSig(valueBytes, validityType, validity) + privKey.sign(dataForSignature, (err, signature) => { + if (err) { + return callback(err) + } + + // Create IPNS entry record + const ipnsEntry = IpnsEntry.create(valueBytes, signature, validityType, validity, sequence) + + callback(null, ipnsEntry) + }) + } + + getIpnsEntryDataForSig (valueBuffer, validityType, validity) { + return Buffer.concat([valueBuffer, Buffer.from(validityType.toString()), validity]) + } + + updateRecord (privKey, value, eol, callback) { + peerId.createFromPrivKey(privKey.bytes.toString('base64'), (error, id) => { + if (error) { + callback(error) + } + + this.getPublished(id, false, (error, record) => { + if (error) { + callback(error) + } + + // Determinate the record sequence number + let seqNumber = 0 + if (record && record.sequence !== undefined) { + seqNumber = record.value.toString() !== value ? record.sequence + 1 : record.sequence + } + + // Create record + this.createEntryData(privKey, value, seqNumber, eol, (err, entryData) => { + if (err) { + return callback(err) + } + // TODO set ttl (still experimental feature for go) + + // Marshal record + const data = IpnsEntry.marshal(entryData) + + // Store the new record + this.repo.datastore.put(utils.generateIpnsDsKey(id), data, (err, res) => { + if (err) { + return callback(err) + } + + return callback(null, entryData) + }) + }) + }) + }) + } +} + +exports = module.exports = IpnsPublisher diff --git a/src/core/namesys/resolver.js b/src/core/namesys/resolver.js new file mode 100644 index 0000000000..76e0486601 --- /dev/null +++ b/src/core/namesys/resolver.js @@ -0,0 +1,53 @@ +'use strict' + +const IpnsEntry = require('./pb/ipnsEntry') +const utils = require('./utils') + +class IpnsResolver { + constructor (repo) { + this.repo = repo + } + + resolve (name, pubKey, callback) { + this.repo.datastore.get(name, (err, dsVal) => { + if (err) { + return callback(err) + } + + if (!Buffer.isBuffer(dsVal)) { + return callback(new Error('found ipns record that we couldn\'t convert to a value')) + } + + const ipnsEntry = IpnsEntry.unmarshal(dsVal) + + console.log('entry', ipnsEntry) + + return callback(null, 'entry') + + /* + let result + + if (!err) { + if (Buffer.isBuffer(dsVal)) { + result = dsVal + } else { + return callback(new Error('found ipns record that we couldn\'t convert to a value')) + } + } else if (err.notFound) { + if (!checkRouting) { + return callback(null, null) + } + // TODO Implement Routing + } else { + return callback(err) + } + + // unmarshal data + result = IpnsEntry.unmarshal(dsVal) + + return callback(null, result) */ + }) + } +} + +exports = module.exports = IpnsResolver diff --git a/src/core/namesys/utils.js b/src/core/namesys/utils.js new file mode 100644 index 0000000000..745520fdae --- /dev/null +++ b/src/core/namesys/utils.js @@ -0,0 +1,9 @@ +'use strict' + +module.exports = { + generateIpnsDsKey: (peerId) => `/ipns/${peerId.toB58String()}`, // TODO I think it should be base 32, according to go + buildIpnsKeysForId: (peerId) => ({ + nameKey: `/pk/${peerId.toB58String()}`, + ipnsKey: `/ipns/${peerId.toB58String()}` + }) +} diff --git a/src/http/api/resources/index.js b/src/http/api/resources/index.js index f937bf2e1b..58b68962cf 100644 --- a/src/http/api/resources/index.js +++ b/src/http/api/resources/index.js @@ -19,3 +19,4 @@ exports.dns = require('./dns') exports.key = require('./key') exports.stats = require('./stats') exports.resolve = require('./resolve') +exports.name = require('./name') diff --git a/src/http/api/resources/name.js b/src/http/api/resources/name.js new file mode 100644 index 0000000000..2231563ef1 --- /dev/null +++ b/src/http/api/resources/name.js @@ -0,0 +1,62 @@ +'use strict' + +const Joi = require('joi') + +exports = module.exports + +exports.resolve = { + validate: { + query: Joi.object().keys({ + arg: Joi.string(), + nocache: Joi.boolean().default(false), + recursive: Joi.boolean().default(false) + }).unknown() + }, + handler: (request, reply) => { + const ipfs = request.server.app.ipfs + const { name, nocache, recursive } = request.query + + ipfs.name.resolve(name, nocache, recursive, (err, res) => { + if (err) { + return reply({ + Message: err.toString(), + Code: 0 + }).code(500) + } + + return reply({ + Path: res + }).code(200) + }) + } +} + +exports.publish = { + validate: { + query: Joi.object().keys({ + arg: Joi.string().required(), + resolve: Joi.boolean().default(true), + lifetime: Joi.string().default('24h'), + ttl: Joi.string(), + key: Joi.string().default('self') + }).unknown() + }, + handler: (request, reply) => { + const ipfs = request.server.app.ipfs + const path = request.query.arg + + ipfs.name.publish(path, (err, res) => { + if (err) { + return reply({ + Message: err.toString(), + Code: 0 + }).code(500) + } + + return reply({ + Name: path, + Value: res + }).code(200) + }) + } +} diff --git a/src/http/api/routes/index.js b/src/http/api/routes/index.js index 4087299ecd..450df4e4a5 100644 --- a/src/http/api/routes/index.js +++ b/src/http/api/routes/index.js @@ -22,4 +22,5 @@ module.exports = (server) => { require('./key')(server) require('./stats')(server) require('./resolve')(server) + require('./name')(server) } diff --git a/src/http/api/routes/name.js b/src/http/api/routes/name.js new file mode 100644 index 0000000000..44a6501d3b --- /dev/null +++ b/src/http/api/routes/name.js @@ -0,0 +1,25 @@ +'use strict' + +const resources = require('./../resources') + +module.exports = (server) => { + const api = server.select('API') + + api.route({ + method: '*', + path: '/api/v0/name/resolve', + config: { + handler: resources.name.resolve.handler, + validate: resources.name.resolve.validate + } + }) + + api.route({ + method: '*', + path: '/api/v0/name/publish', + config: { + handler: resources.name.publish.handler, + handler: resources.name.publish.handler + } + }) +} From 70e5b0a8809afd913309c4b28a9961e5ac4027f5 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 13 Jun 2018 18:03:28 +0100 Subject: [PATCH 02/34] feat: wip --- src/core/namesys/resolver.js | 2 +- src/http/api/routes/name.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/namesys/resolver.js b/src/core/namesys/resolver.js index 76e0486601..0cb6a7d149 100644 --- a/src/core/namesys/resolver.js +++ b/src/core/namesys/resolver.js @@ -1,7 +1,7 @@ 'use strict' const IpnsEntry = require('./pb/ipnsEntry') -const utils = require('./utils') +// const utils = require('./utils') class IpnsResolver { constructor (repo) { diff --git a/src/http/api/routes/name.js b/src/http/api/routes/name.js index 44a6501d3b..8df794f222 100644 --- a/src/http/api/routes/name.js +++ b/src/http/api/routes/name.js @@ -18,7 +18,6 @@ module.exports = (server) => { method: '*', path: '/api/v0/name/publish', config: { - handler: resources.name.publish.handler, handler: resources.name.publish.handler } }) From ad27451c608bac4dfa250c750e2c7558e1ecf7c0 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 19 Jun 2018 17:14:52 +0100 Subject: [PATCH 03/34] wip --- package.json | 6 + src/cli/commands/name/publish.js | 2 +- src/core/components/name.js | 114 ++++++---------- src/core/index.js | 4 +- src/core/{namesys => ipns}/index.js | 24 ++-- src/core/ipns/path.js | 71 ++++++++++ src/core/{namesys => ipns}/pb/ipns.proto.js | 0 src/core/{namesys => ipns}/pb/ipnsEntry.js | 15 ++- src/core/ipns/publisher.js | 140 ++++++++++++++++++++ src/core/ipns/resolver.js | 66 +++++++++ src/core/ipns/utils.js | 15 +++ src/core/ipns/validator.js | 50 +++++++ src/core/namesys/publisher.js | 138 ------------------- src/core/namesys/resolver.js | 53 -------- src/core/namesys/utils.js | 9 -- src/http/api/resources/name.js | 12 +- src/http/api/routes/name.js | 3 +- 17 files changed, 424 insertions(+), 298 deletions(-) rename src/core/{namesys => ipns}/index.js (68%) create mode 100644 src/core/ipns/path.js rename src/core/{namesys => ipns}/pb/ipns.proto.js (100%) rename src/core/{namesys => ipns}/pb/ipnsEntry.js (70%) create mode 100644 src/core/ipns/publisher.js create mode 100644 src/core/ipns/resolver.js create mode 100644 src/core/ipns/utils.js create mode 100644 src/core/ipns/validator.js delete mode 100644 src/core/namesys/publisher.js delete mode 100644 src/core/namesys/resolver.js delete mode 100644 src/core/namesys/utils.js diff --git a/package.json b/package.json index 4137f160b7..da3bf039b7 100644 --- a/package.json +++ b/package.json @@ -84,8 +84,14 @@ }, "dependencies": { "@nodeutils/defaults-deep": "^1.1.0", +<<<<<<< HEAD "async": "^2.6.1", "big.js": "^5.1.2", +======= + "async": "^2.6.0", + "base32-encode": "^1.0.0", + "big.js": "^5.0.3", +>>>>>>> wip "binary-querystring": "~0.1.2", "bl": "^2.0.1", "boom": "^7.2.0", diff --git a/src/cli/commands/name/publish.js b/src/cli/commands/name/publish.js index 860e62c49b..c725eb5827 100644 --- a/src/cli/commands/name/publish.js +++ b/src/cli/commands/name/publish.js @@ -19,7 +19,7 @@ module.exports = { throw err } - print(`Published to ${result.value}: /ipfs/${result.name}`) + print(`Published to ${result.name}: ${result.value}`) }) } } diff --git a/src/core/components/name.js b/src/core/components/name.js index 489fe9aa59..abbe9042f8 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -1,8 +1,11 @@ 'use strict' const promisify = require('promisify-es6') -const OFFLINE_ERROR = require('../utils').OFFLINE_ERROR +const series = require('async/series') const human = require('human-to-milliseconds') +const path = require('../ipns/path') + +const OFFLINE_ERROR = require('../utils').OFFLINE_ERROR const keyLookup = (ipfsNode, kname, cb) => { if (kname === 'self') { @@ -18,29 +21,6 @@ const keyLookup = (ipfsNode, kname, cb) => { }) } -const publish = (ipfsNode, privateKey, ipfsPath, publishOptions, callback) => { - // Should verify if exists ? - if (publishOptions.verifyIfExists) { - // TODO resolve - // https://github.com/ipfs/go-ipfs/blob/master/core/commands/publish.go#L172 - } - - // Add pubValidTime - - // Publish - const eol = new Date(Date.now()) - - ipfsNode._namesys.publishWithEOL(privateKey, ipfsPath, eol, (err, res) => { - if (err) { - callback(err) - } - - // TODO HERE HERE HERE - - callback(null, res) - }) -} - module.exports = function name (self) { return { /** @@ -51,64 +31,55 @@ module.exports = function name (self) { * * Examples: TODO such as in go * - * @param {String} ipfsPath - * @param {Object} options + * @param {String} value ipfs path of the object to be published. + * @param {boolean} resolve resolve given path before publishing. + * @param {String} lifetime time duration that the record will be valid for. + This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are + "ns", "us" (or "µs"), "ms", "s", "m", "h". + * @param {String} ttl time duration this record should be cached for (caution: experimental). + * @param {String} key name of the key to be used or a valid PeerID, as listed by 'ipfs key list -l'. * @param {function(Error)} [callback] * @returns {Promise|void} */ - publish: promisify((ipfsPath, callback) => { - // https://github.com/ipfs/go-ipfs/blob/master/core/commands/publish.go + publish: promisify((value, resolve = true, lifetime = '24h', ttl, key = 'self', callback) => { if (!self.isOnline()) { return callback(new Error(OFFLINE_ERROR)) } - // TODO Validate Mounts IPNS - cannot manually publish while IPNS is mounted - - // TODO Validate Node identity not validated - - // TODO Parse options and create object - const options = { - resolve: true, - d: '24h', - ttl: undefined, - key: 'self' + // Parse ipfs path value + try { + value = path.parsePath(value) + } catch (err) { + return callback(err) } - // TODO Create waterfall - /* waterfall([ - (cb) => human(options.d || '1s', cb), - ], callback) */ - - human(options.d || '1s', (err, value) => { + series([ + (cb) => human(lifetime || '1s', cb), + // (cb) => ttl ? human(ttl, cb) : cb(), + (cb) => keyLookup(self, key, cb), + (cb) => resolve ? path.resolvePath(self, value, cb) : cb() // if not resolved, and error will stop the execution + ], (err, results) => { if (err) { - return callback(new Error('Error parsing lifetime option')) + return callback(err) } - const publishOptions = { - verifyIfExists: options.resolve, - pubValidTime: value - } + const pubValidTime = results[0] + const privateKey = results[1] - // TODO Date.now() + value + // TODO IMPROVEMENT - Handle ttl for cache + // const ttl = results[1] + // const privateKey = results[2] - // TODO TTL integration + // Calculate eol + const eol = new Date(Date.now() + pubValidTime) - // Get Key - keyLookup(self, options.key, (err, key) => { + // Start publishing process + self._ipns.publish(privateKey, value, eol, (err, res) => { if (err) { - return callback(err) + callback(err) } - // TODO ParsePath - // https://github.com/ipfs/go-ipfs/blob/master/path/path.go - - publish(self, key, ipfsPath, publishOptions, (err, result) => { - if (err) { - callback(err) - } - - return callback(null, result) - }) + callback(null, res) }) }) }), @@ -125,17 +96,10 @@ module.exports = function name (self) { resolve: promisify((name, nocache, recursive, callback) => { const local = true - if (typeof name === 'function') { - callback = name - name = undefined - } - if (!self.isOnline()) { return callback(new Error(OFFLINE_ERROR)) } - // let resolver = self._namesys.ipnsResolver - if (local && nocache) { return callback(new Error('Cannot specify both local and nocache')) } @@ -149,9 +113,15 @@ module.exports = function name (self) { name = `/ipns/${name}` } + // TODO local public key? const pubKey = self._peerInfo.id.pubKey + const options = { + local: local, + nocache: nocache, + recursive: recursive + } - self._namesys.resolve(name, pubKey, (err, result) => { + self._ipns.resolve(name, pubKey, options, (err, result) => { if (err) { return callback(err) } diff --git a/src/core/index.js b/src/core/index.js index 2562a86c1e..a5d5f5c346 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -20,7 +20,7 @@ const EventEmitter = require('events') const config = require('./config') const boot = require('./boot') const components = require('./components') -const Namesys = require('./namesys') +const IPNS = require('./ipns') // replaced by repo-browser when running in the browser const defaultRepo = require('./runtime/repo-nodejs') const preload = require('./preload') @@ -90,7 +90,7 @@ class IPFS extends EventEmitter { this._pubsub = undefined this._preload = preload(this) this._mfsPreload = mfsPreload(this) - this._namesys = new Namesys(null, this._repo) + this._ipns = new IPNS(null, this._repo) // IPFS Core exposed components // - for booting up a node diff --git a/src/core/namesys/index.js b/src/core/ipns/index.js similarity index 68% rename from src/core/namesys/index.js rename to src/core/ipns/index.js index 770836b41c..d70e1e4da6 100644 --- a/src/core/namesys/index.js +++ b/src/core/ipns/index.js @@ -3,13 +3,15 @@ const peerId = require('peer-id') const series = require('async/series') // const QuickLRU = require('quick-lru'); +// Consider using https://github.com/dominictarr/hashlru const IpnsPublisher = require('./publisher') const IpnsResolver = require('./resolver') +const path = require('./path') // const defaultRecordTtl = 60 * 1000 -class Namesys { +class IPNS { constructor (routing, repo, peerInfo) { this.ipnsPublisher = new IpnsPublisher(routing, repo) this.ipnsResolver = new IpnsResolver(repo) @@ -17,10 +19,8 @@ class Namesys { } // Resolve - resolve (name, pubKey, callback) { - // this.ipnsResolver.resolve() - - this.ipnsResolver.resolve(name, pubKey, (err, result) => { + resolve (name, pubKey, options, callback) { + this.ipnsResolver.resolve(name, pubKey, options, (err, result) => { if (err) { return callback(err) } @@ -29,13 +29,8 @@ class Namesys { }) } - // publish (value = ipfsPath) - publish (privKey, value) { - // TODO https://github.com/ipfs/go-ipfs/blob/master/namesys/namesys.go#L111 - } - - // publish with EOL (value = ipfsPath) - publishWithEOL (privKey, value, eol, callback) { + // Publish + publish (privKey, value, eol, callback) { series([ (cb) => peerId.createFromPrivKey(privKey.bytes.toString('base64'), cb), (cb) => this.ipnsPublisher.publishWithEOL(privKey, value, eol, cb) @@ -44,7 +39,7 @@ class Namesys { return callback(err) } - // TODO Add to cache + // TODO IMPROVEMENT - Add to cache // this.cache.set(id.toB58String(), { // val: value, // eol: Date.now() + ttl @@ -55,4 +50,5 @@ class Namesys { } } -exports = module.exports = Namesys +exports = module.exports = IPNS +exports.path = path diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js new file mode 100644 index 0000000000..c7cb1fcff5 --- /dev/null +++ b/src/core/ipns/path.js @@ -0,0 +1,71 @@ +'use strict' + +const CID = require('cids') + +const BAD_PATH_ERROR = new Error('invalid \'ipfs ref\' path') +const NO_COMPONENTS_ERROR = new Error('path must contain at least one component') + +// Should verify if the value exists before publishing it +const resolvePath = (ipfsNode, value, callback) => { + if (value.startsWith('/ipns/')) { + // TODO resolve local? + // TODO Resolve from DHT + return callback(new Error('not implemented yet')) + } + + ipfsNode.dag.get(value.substring('/ipfs/'.length), (err, value) => { + if (err) { + return callback(err) + } + + return callback(null, value) + }) +} + +// parsePath returns a well-formed ipfs Path. +// The returned path will always be prefixed with /ipfs/ or /ipns/. +// If the received string is not a valid ipfs path, an error will be returned +const parsePath = (pathStr) => { + const parts = pathStr.split('/') + + if (parts.length === 1) { + return parseCidToPath(pathStr) + } + + // if the path does not begin with a slash, we expect this to start with a hash and be an ipfs path + if (parts[0] !== '') { + if (parseCidToPath(parts[0])) { + return `/ipfs/${pathStr}` + } + } + + if (parts.length < 3) { + throw BAD_PATH_ERROR + } + + if (parts[1] === 'ipfs') { + if (!parseCidToPath(parts[2])) { + throw BAD_PATH_ERROR + } + } else if (parts[1] !== 'ipns') { + throw BAD_PATH_ERROR + } + return pathStr +} + +// parseCidToPath takes a CID in string form and returns a valid ipfs Path. +const parseCidToPath = (value) => { + if (value === '') { + throw NO_COMPONENTS_ERROR + } + + const cid = new CID(value) + CID.validateCID(cid) + + return `/ipfs/${value}` +} + +module.exports = { + resolvePath, + parsePath +} diff --git a/src/core/namesys/pb/ipns.proto.js b/src/core/ipns/pb/ipns.proto.js similarity index 100% rename from src/core/namesys/pb/ipns.proto.js rename to src/core/ipns/pb/ipns.proto.js diff --git a/src/core/namesys/pb/ipnsEntry.js b/src/core/ipns/pb/ipnsEntry.js similarity index 70% rename from src/core/namesys/pb/ipnsEntry.js rename to src/core/ipns/pb/ipnsEntry.js index 0ea7c39ebe..b11afb0a32 100644 --- a/src/core/namesys/pb/ipnsEntry.js +++ b/src/core/ipns/pb/ipnsEntry.js @@ -10,7 +10,7 @@ module.exports = { validityType = ipnsEntryProto.ValidityType.EOL } - return { + const entry = { value: value, signature: signature, validityType: validityType, @@ -19,6 +19,16 @@ module.exports = { ttl: ttl, pubKey: pubKey } + + return Object.keys(entry).reduce((acc, key) => { + const reducedEntry = acc + + if (entry[key] !== undefined) { + reducedEntry[key] = entry[key] + } + + return reducedEntry + }, {}) }, // Marshal marshal: (ipnsEntry) => { @@ -27,5 +37,6 @@ module.exports = { // Unmarshal unmarshal: (marsheled) => { return ipnsEntryProto.decode(marsheled) - } + }, + validityType: ipnsEntryProto.ValidityType } diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js new file mode 100644 index 0000000000..18be87177c --- /dev/null +++ b/src/core/ipns/publisher.js @@ -0,0 +1,140 @@ +'use strict' + +const peerId = require('peer-id') +const waterfall = require('async/waterfall') + +const IpnsEntry = require('./pb/ipnsEntry') +const utils = require('./utils') +const validator = require('./validator') + +const defaultRecordTtl = 60 * 60 * 1000 + +/* + IpnsPublisher is capable of publishing and resolving names to the IPFS + routing system. + */ +class IpnsPublisher { + constructor (routing, repo) { + this.routing = routing + this.repo = repo + } + + // publish record with a eol + publishWithEOL (privKey, value, eol, callback) { + this.updateOrCreateRecord(privKey, value, eol, (err, record) => { + if (err) { + return callback(err) + } + + // TODO ROUTING - Add record + callback(null, record) + }) + } + + // Accepts a keypair, as well as a value (ipfsPath), and publishes it out to the routing system + publish (privKey, value, callback) { + const eol = new Date(Date.now() + defaultRecordTtl) + + this.publishWithEOL(privKey, value, eol, (err, res) => { + if (err) { + return callback(err) + } + + callback(res) + }) + } + + // Returns the record this node has published corresponding to the given peer ID. + // If `checkRouting` is true and we have no existing record, this method will check the routing system for any existing records. + getPublished (peerId, checkRouting, callback) { + this.repo.datastore.get(utils.generateIpnsDsKey(peerId.id), (err, dsVal) => { + let result + + if (!err) { + if (Buffer.isBuffer(dsVal)) { + result = dsVal + } else { + return callback(new Error('found ipns record that we couldn\'t convert to a value')) + } + } else if (err.notFound) { + if (!checkRouting) { + return callback(null, { + peerId: peerId + }) + } + // TODO ROUTING + } else { + return callback(err) + } + + // unmarshal data + result = IpnsEntry.unmarshal(dsVal) + + return callback(null, { + peerId: peerId, + record: result + }) + }) + } + + createEntryRecord (privKey, value, seqNumber, eol, callback) { + const validity = eol.toISOString() + const validityType = IpnsEntry.validityType.EOL + const sequence = seqNumber + + validator.sign(privKey, value, validityType, validity, (err, signature) => { + if (err) { + return callback(err) + } + + // TODO confirm private key format compliance with go-ipfs + + // Create IPNS entry record + const ipnsEntry = IpnsEntry.create(value, signature, validityType, validity, sequence) + + return callback(null, ipnsEntry) + }) + } + + updateOrCreateRecord (privKey, value, eol, callback) { + waterfall([ + (cb) => peerId.createFromPrivKey(privKey.bytes.toString('base64'), cb), + (id, cb) => this.getPublished(id, false, cb) + ], (err, result) => { + if (err) { + callback(err) + } + + const { peerId, record } = result + + // Determinate the record sequence number + let seqNumber = 0 + if (record && record.sequence !== undefined) { + seqNumber = record.value.toString() !== value ? record.sequence + 1 : record.sequence + } + + // Create record + this.createEntryRecord(privKey, value, seqNumber, eol, (err, entryData) => { + if (err) { + return callback(err) + } + + // TODO IMPROVEMENT - set ttl (still experimental feature for go) + + // Marshal record + const data = IpnsEntry.marshal(entryData) + + // Store the new record + this.repo.datastore.put(utils.generateIpnsDsKey(peerId.id), data, (err, res) => { + if (err) { + return callback(err) + } + + return callback(null, entryData) + }) + }) + }) + } +} + +exports = module.exports = IpnsPublisher diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js new file mode 100644 index 0000000000..3bde9b32ba --- /dev/null +++ b/src/core/ipns/resolver.js @@ -0,0 +1,66 @@ +'use strict' + +const IpnsEntry = require('./pb/ipnsEntry') +const utils = require('./utils') +const validator = require('./validator') + +const multihash = require('multihashes') + +class IpnsResolver { + constructor (repo) { + this.repo = repo + } + + resolve (name, publicKey, options, callback) { + const nameSegments = name.split('/') + + if (nameSegments.length !== 3 || nameSegments[0] !== '') { + return callback(new Error(`invalid name syntax for ${name}`)) + } + + const key = nameSegments[2] + + // TODO recursive + // TODO nocache + + if (options.local) { + this.resolveLocal(key, publicKey, (err, res) => { + if (err) { + return callback(err) + } + + return callback(null, res) + }) + } else { + return callback(new Error('not implemented yet')) + } + } + + // https://github.com/ipfs/go-ipfs-routing/blob/master/offline/offline.go + resolveLocal (name, publicKey, callback) { + const ipnsKey = utils.generateIpnsDsKey(multihash.fromB58String(name)) + + this.repo.datastore.get(ipnsKey, (err, dsVal) => { + if (err) { + return callback(err) + } + + if (!Buffer.isBuffer(dsVal)) { + return callback(new Error('found ipns record that we couldn\'t convert to a value')) + } + + const ipnsEntry = IpnsEntry.unmarshal(dsVal) + + // Record validation + validator.verify(publicKey, ipnsEntry, (err) => { + if (err) { + return callback(err) + } + + return callback(null, ipnsEntry.value.toString()) + }) + }) + } +} + +exports = module.exports = IpnsResolver diff --git a/src/core/ipns/utils.js b/src/core/ipns/utils.js new file mode 100644 index 0000000000..9664a38335 --- /dev/null +++ b/src/core/ipns/utils.js @@ -0,0 +1,15 @@ +'use strict' + +const base32Encode = require('base32-encode') + +// rawStdEncoding as go +// Created issue for allowing this inside base32-encode https://github.com/LinusU/base32-encode/issues/2 +const rawStdEncoding = (key) => base32Encode(key, 'RFC4648').replace('=', '') + +module.exports = { + generateIpnsDsKey: (key) => `/ipns/${rawStdEncoding(key)}`, + buildIpnsKeysForId: (peerId) => ({ + nameKey: `/pk/${peerId.toB58String()}`, + ipnsKey: `/ipns/${peerId.toB58String()}` + }) +} diff --git a/src/core/ipns/validator.js b/src/core/ipns/validator.js new file mode 100644 index 0000000000..8425e20973 --- /dev/null +++ b/src/core/ipns/validator.js @@ -0,0 +1,50 @@ +'use strict' + +// Create IPNS Entry data for being signed +const getDataForSignature = (value, validityType, validity) => { + const valueBuffer = Buffer.from(value) + const validityTypeBuffer = Buffer.from(validityType.toString()) + const validityBuffer = Buffer.from(validity) + + return Buffer.concat([valueBuffer, validityTypeBuffer, validityBuffer]) +} + +// Sign IPNS Entry for publish +const sign = (privateKey, value, validityType, validity, callback) => { + const dataForSignature = getDataForSignature(value, validityType, validity) + + privateKey.sign(dataForSignature, (err, signature) => { + if (err) { + return callback(err) + } + return callback(null, signature) + }) +} + +// Verify IPNS entry on resolve +const verify = (publicKey, entry, callback) => { + const { value, validityType, validity } = entry + const dataForSignature = getDataForSignature(value, validityType, validity) + + // Validate Signature + publicKey.verify(dataForSignature, entry.signature, (err, result) => { + if (err) { + return callback(err) + } + + // Validate EOL + const validityDate = Date.parse(validity.toString()) + + if (validityDate < Date.now()) { + return callback(new Error('record has expired')) + } + + return callback(null, null) + }) +} + +module.exports = { + getDataForSignature, + sign, + verify +} diff --git a/src/core/namesys/publisher.js b/src/core/namesys/publisher.js deleted file mode 100644 index 5e0debf967..0000000000 --- a/src/core/namesys/publisher.js +++ /dev/null @@ -1,138 +0,0 @@ -'use strict' - -const peerId = require('peer-id') - -const IpnsEntry = require('./pb/ipnsEntry') -const utils = require('./utils') - -const defaultRecordTtl = 60 * 60 * 1000 - -/* - IpnsPublisher is capable of publishing and resolving names to the IPFS - routing system. - */ -class IpnsPublisher { - constructor (routing, repo) { - this.routing = routing - this.repo = repo - } - - // publish record with a eol - publishWithEOL (privKey, value, eol, callback) { - this.updateRecord(privKey, value, eol, (err, record) => { - if (err) { - return callback(err) - } - - // TODO FUTURE Put record to routing - callback(null, record) - }) - } - - // Accepts a keypair, as well as a value (ipfsPath), and publishes it out to the routing system - publish (privKey, value, callback) { - const eol = new Date(Date.now() + defaultRecordTtl) - - this.publishWithEOL(privKey, value, eol, (err, res) => { - if (err) { - return callback(err) - } - - callback(res) - }) - } - - // Returns the record this node has published corresponding to the given peer ID. - // If `checkRouting` is true and we have no existing record, this method will check the routing system for any existing records. - getPublished (peerId, checkRouting, callback) { - // TODO https://github.com/ipfs/go-ipfs/blob/master/namesys/publisher.go#L117 - this.repo.datastore.get(utils.generateIpnsDsKey(peerId), (err, dsVal) => { - let result - - if (!err) { - if (Buffer.isBuffer(dsVal)) { - result = dsVal - } else { - return callback(new Error('found ipns record that we couldn\'t convert to a value')) - } - } else if (err.notFound) { - if (!checkRouting) { - return callback(null, null) - } - // TODO Implement Routing - } else { - return callback(err) - } - - // unmarshal data - result = IpnsEntry.unmarshal(dsVal) - - return callback(null, result) - }) - } - - createEntryData (privKey, value, seqNumber, eol, callback) { - const valueBytes = Buffer.from(value) - const validityType = 0 - const sequence = seqNumber - const validity = Buffer.from(eol.toISOString()) - - const dataForSignature = this.getIpnsEntryDataForSig(valueBytes, validityType, validity) - privKey.sign(dataForSignature, (err, signature) => { - if (err) { - return callback(err) - } - - // Create IPNS entry record - const ipnsEntry = IpnsEntry.create(valueBytes, signature, validityType, validity, sequence) - - callback(null, ipnsEntry) - }) - } - - getIpnsEntryDataForSig (valueBuffer, validityType, validity) { - return Buffer.concat([valueBuffer, Buffer.from(validityType.toString()), validity]) - } - - updateRecord (privKey, value, eol, callback) { - peerId.createFromPrivKey(privKey.bytes.toString('base64'), (error, id) => { - if (error) { - callback(error) - } - - this.getPublished(id, false, (error, record) => { - if (error) { - callback(error) - } - - // Determinate the record sequence number - let seqNumber = 0 - if (record && record.sequence !== undefined) { - seqNumber = record.value.toString() !== value ? record.sequence + 1 : record.sequence - } - - // Create record - this.createEntryData(privKey, value, seqNumber, eol, (err, entryData) => { - if (err) { - return callback(err) - } - // TODO set ttl (still experimental feature for go) - - // Marshal record - const data = IpnsEntry.marshal(entryData) - - // Store the new record - this.repo.datastore.put(utils.generateIpnsDsKey(id), data, (err, res) => { - if (err) { - return callback(err) - } - - return callback(null, entryData) - }) - }) - }) - }) - } -} - -exports = module.exports = IpnsPublisher diff --git a/src/core/namesys/resolver.js b/src/core/namesys/resolver.js deleted file mode 100644 index 0cb6a7d149..0000000000 --- a/src/core/namesys/resolver.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict' - -const IpnsEntry = require('./pb/ipnsEntry') -// const utils = require('./utils') - -class IpnsResolver { - constructor (repo) { - this.repo = repo - } - - resolve (name, pubKey, callback) { - this.repo.datastore.get(name, (err, dsVal) => { - if (err) { - return callback(err) - } - - if (!Buffer.isBuffer(dsVal)) { - return callback(new Error('found ipns record that we couldn\'t convert to a value')) - } - - const ipnsEntry = IpnsEntry.unmarshal(dsVal) - - console.log('entry', ipnsEntry) - - return callback(null, 'entry') - - /* - let result - - if (!err) { - if (Buffer.isBuffer(dsVal)) { - result = dsVal - } else { - return callback(new Error('found ipns record that we couldn\'t convert to a value')) - } - } else if (err.notFound) { - if (!checkRouting) { - return callback(null, null) - } - // TODO Implement Routing - } else { - return callback(err) - } - - // unmarshal data - result = IpnsEntry.unmarshal(dsVal) - - return callback(null, result) */ - }) - } -} - -exports = module.exports = IpnsResolver diff --git a/src/core/namesys/utils.js b/src/core/namesys/utils.js deleted file mode 100644 index 745520fdae..0000000000 --- a/src/core/namesys/utils.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -module.exports = { - generateIpnsDsKey: (peerId) => `/ipns/${peerId.toB58String()}`, // TODO I think it should be base 32, according to go - buildIpnsKeysForId: (peerId) => ({ - nameKey: `/pk/${peerId.toB58String()}`, - ipnsKey: `/ipns/${peerId.toB58String()}` - }) -} diff --git a/src/http/api/resources/name.js b/src/http/api/resources/name.js index 2231563ef1..8159234873 100644 --- a/src/http/api/resources/name.js +++ b/src/http/api/resources/name.js @@ -14,9 +14,9 @@ exports.resolve = { }, handler: (request, reply) => { const ipfs = request.server.app.ipfs - const { name, nocache, recursive } = request.query + const { arg, nocache, recursive } = request.query - ipfs.name.resolve(name, nocache, recursive, (err, res) => { + ipfs.name.resolve(arg, nocache, recursive, (err, res) => { if (err) { return reply({ Message: err.toString(), @@ -43,9 +43,9 @@ exports.publish = { }, handler: (request, reply) => { const ipfs = request.server.app.ipfs - const path = request.query.arg + const { arg, resolve, lifetime, ttl, key } = request.query - ipfs.name.publish(path, (err, res) => { + ipfs.name.publish(arg, resolve, lifetime, ttl, key, (err, res) => { if (err) { return reply({ Message: err.toString(), @@ -54,8 +54,8 @@ exports.publish = { } return reply({ - Name: path, - Value: res + Name: res, + Value: arg }).code(200) }) } diff --git a/src/http/api/routes/name.js b/src/http/api/routes/name.js index 8df794f222..6ac5d155d0 100644 --- a/src/http/api/routes/name.js +++ b/src/http/api/routes/name.js @@ -18,7 +18,8 @@ module.exports = (server) => { method: '*', path: '/api/v0/name/publish', config: { - handler: resources.name.publish.handler + handler: resources.name.publish.handler, + validate: resources.name.resolve.validate } }) } From d819d1056aa26c923ce319764c7b53fca8a5d5aa Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 20 Jun 2018 17:31:30 +0100 Subject: [PATCH 04/34] code review fixes --- src/core/components/name.js | 12 ++++---- src/core/ipns/index.js | 4 +-- src/core/ipns/pb/ipnsEntry.js | 58 +++++++++++++++++------------------ src/core/ipns/publisher.js | 14 ++++----- src/core/ipns/resolver.js | 4 +-- src/core/ipns/validator.js | 10 ++++-- 6 files changed, 52 insertions(+), 50 deletions(-) diff --git a/src/core/components/name.js b/src/core/components/name.js index abbe9042f8..cac7273e9d 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -3,9 +3,9 @@ const promisify = require('promisify-es6') const series = require('async/series') const human = require('human-to-milliseconds') -const path = require('../ipns/path') -const OFFLINE_ERROR = require('../utils').OFFLINE_ERROR +const errors = require('../utils') +const path = require('../ipns/path') const keyLookup = (ipfsNode, kname, cb) => { if (kname === 'self') { @@ -35,7 +35,7 @@ module.exports = function name (self) { * @param {boolean} resolve resolve given path before publishing. * @param {String} lifetime time duration that the record will be valid for. This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are - "ns", "us" (or "µs"), "ms", "s", "m", "h". + "ms", "s", "m", "h". * @param {String} ttl time duration this record should be cached for (caution: experimental). * @param {String} key name of the key to be used or a valid PeerID, as listed by 'ipfs key list -l'. * @param {function(Error)} [callback] @@ -43,10 +43,10 @@ module.exports = function name (self) { */ publish: promisify((value, resolve = true, lifetime = '24h', ttl, key = 'self', callback) => { if (!self.isOnline()) { - return callback(new Error(OFFLINE_ERROR)) + return callback(new Error(errors.OFFLINE_ERROR)) } - // Parse ipfs path value + // Parse path value try { value = path.parsePath(value) } catch (err) { @@ -97,7 +97,7 @@ module.exports = function name (self) { const local = true if (!self.isOnline()) { - return callback(new Error(OFFLINE_ERROR)) + return callback(new Error(errors.OFFLINE_ERROR)) } if (local && nocache) { diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index d70e1e4da6..a1989e30f1 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -1,6 +1,6 @@ 'use strict' -const peerId = require('peer-id') +const { createFromPrivKey } = require('peer-id') const series = require('async/series') // const QuickLRU = require('quick-lru'); // Consider using https://github.com/dominictarr/hashlru @@ -32,7 +32,7 @@ class IPNS { // Publish publish (privKey, value, eol, callback) { series([ - (cb) => peerId.createFromPrivKey(privKey.bytes.toString('base64'), cb), + (cb) => createFromPrivKey(privKey.bytes.toString('base64'), cb), (cb) => this.ipnsPublisher.publishWithEOL(privKey, value, eol, cb) ], (err, results) => { if (err) { diff --git a/src/core/ipns/pb/ipnsEntry.js b/src/core/ipns/pb/ipnsEntry.js index b11afb0a32..e18aed6624 100644 --- a/src/core/ipns/pb/ipnsEntry.js +++ b/src/core/ipns/pb/ipnsEntry.js @@ -2,41 +2,39 @@ const ipnsEntryProto = require('./ipns.proto') -module.exports = { - // Create ipns data format - create: (value, signature, validityType, validity, sequence, ttl, pubKey) => { - // Handle validity type - if (validityType !== undefined) { - validityType = ipnsEntryProto.ValidityType.EOL - } +const create = (value, signature, validityType, validity, sequence, ttl, pubKey) => { + // Handle validity type + if (validityType !== undefined) { + validityType = ipnsEntryProto.ValidityType.EOL + } - const entry = { - value: value, - signature: signature, - validityType: validityType, - validity: validity, - sequence: sequence, - ttl: ttl, - pubKey: pubKey - } + const entry = { + value: value, + signature: signature, + validityType: validityType, + validity: validity, + sequence: sequence, + ttl: ttl, + pubKey: pubKey + } - return Object.keys(entry).reduce((acc, key) => { - const reducedEntry = acc + return Object.keys(entry).reduce((acc, key) => { + const reducedEntry = acc - if (entry[key] !== undefined) { - reducedEntry[key] = entry[key] - } + if (entry[key] !== undefined) { + reducedEntry[key] = entry[key] + } - return reducedEntry - }, {}) - }, + return reducedEntry + }, {}) +} + +module.exports = { + // Create ipns data format + create, // Marshal - marshal: (ipnsEntry) => { - return ipnsEntryProto.encode(ipnsEntry) - }, + marshal: ipnsEntryProto.encode, // Unmarshal - unmarshal: (marsheled) => { - return ipnsEntryProto.decode(marsheled) - }, + unmarshal: ipnsEntryProto.decode, validityType: ipnsEntryProto.ValidityType } diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index 18be87177c..ef2a7d854c 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -4,7 +4,7 @@ const peerId = require('peer-id') const waterfall = require('async/waterfall') const IpnsEntry = require('./pb/ipnsEntry') -const utils = require('./utils') +const { generateIpnsDsKey } = require('./utils') const validator = require('./validator') const defaultRecordTtl = 60 * 60 * 1000 @@ -46,8 +46,8 @@ class IpnsPublisher { // Returns the record this node has published corresponding to the given peer ID. // If `checkRouting` is true and we have no existing record, this method will check the routing system for any existing records. - getPublished (peerId, checkRouting, callback) { - this.repo.datastore.get(utils.generateIpnsDsKey(peerId.id), (err, dsVal) => { + getPublished (peerIdResult, checkRouting, callback) { + this.repo.datastore.get(generateIpnsDsKey(peerIdResult.id), (err, dsVal) => { let result if (!err) { @@ -59,7 +59,7 @@ class IpnsPublisher { } else if (err.notFound) { if (!checkRouting) { return callback(null, { - peerId: peerId + peerIdResult: peerIdResult }) } // TODO ROUTING @@ -71,7 +71,7 @@ class IpnsPublisher { result = IpnsEntry.unmarshal(dsVal) return callback(null, { - peerId: peerId, + peerIdResult: peerIdResult, record: result }) }) @@ -105,7 +105,7 @@ class IpnsPublisher { callback(err) } - const { peerId, record } = result + const { peerIdResult, record } = result // Determinate the record sequence number let seqNumber = 0 @@ -125,7 +125,7 @@ class IpnsPublisher { const data = IpnsEntry.marshal(entryData) // Store the new record - this.repo.datastore.put(utils.generateIpnsDsKey(peerId.id), data, (err, res) => { + this.repo.datastore.put(generateIpnsDsKey(peerIdResult.id), data, (err, res) => { if (err) { return callback(err) } diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index 3bde9b32ba..42b8f45e2e 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -4,7 +4,7 @@ const IpnsEntry = require('./pb/ipnsEntry') const utils = require('./utils') const validator = require('./validator') -const multihash = require('multihashes') +const { fromB58String } = require('multihashes') class IpnsResolver { constructor (repo) { @@ -38,7 +38,7 @@ class IpnsResolver { // https://github.com/ipfs/go-ipfs-routing/blob/master/offline/offline.go resolveLocal (name, publicKey, callback) { - const ipnsKey = utils.generateIpnsDsKey(multihash.fromB58String(name)) + const ipnsKey = utils.generateIpnsDsKey(fromB58String(name)) this.repo.datastore.get(ipnsKey, (err, dsVal) => { if (err) { diff --git a/src/core/ipns/validator.js b/src/core/ipns/validator.js index 8425e20973..bc8b3f1f24 100644 --- a/src/core/ipns/validator.js +++ b/src/core/ipns/validator.js @@ -1,5 +1,7 @@ 'use strict' +const IpnsEntry = require('./pb/ipnsEntry') + // Create IPNS Entry data for being signed const getDataForSignature = (value, validityType, validity) => { const valueBuffer = Buffer.from(value) @@ -33,10 +35,12 @@ const verify = (publicKey, entry, callback) => { } // Validate EOL - const validityDate = Date.parse(validity.toString()) + if (validityType === IpnsEntry.validityType.EOL) { + const validityDate = Date.parse(validity.toString()) - if (validityDate < Date.now()) { - return callback(new Error('record has expired')) + if (validityDate < Date.now()) { + return callback(new Error('record has expired')) + } } return callback(null, null) From 8a060243fd0ca38ebd67a002c7c6bc8a411185bf Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Sat, 30 Jun 2018 18:33:44 +0100 Subject: [PATCH 05/34] added ipns package, cli tests and logs --- package.json | 8 +--- src/cli/commands/name/publish.js | 28 +++++++++-- src/core/components/name.js | 55 ++++++++++++++++------ src/core/ipns/index.js | 8 +++- src/core/ipns/path.js | 14 +++++- src/core/ipns/pb/ipns.proto.js | 30 ------------ src/core/ipns/pb/ipnsEntry.js | 40 ---------------- src/core/ipns/publisher.js | 62 +++++++++++-------------- src/core/ipns/resolver.js | 37 +++++++++------ src/core/ipns/utils.js | 15 ------ src/core/ipns/validator.js | 54 ---------------------- test/cli/name.js | 79 ++++++++++++++++++++++++++++++++ 12 files changed, 214 insertions(+), 216 deletions(-) delete mode 100644 src/core/ipns/pb/ipns.proto.js delete mode 100644 src/core/ipns/pb/ipnsEntry.js delete mode 100644 src/core/ipns/utils.js delete mode 100644 src/core/ipns/validator.js create mode 100644 test/cli/name.js diff --git a/package.json b/package.json index da3bf039b7..0ecc19aa32 100644 --- a/package.json +++ b/package.json @@ -84,14 +84,8 @@ }, "dependencies": { "@nodeutils/defaults-deep": "^1.1.0", -<<<<<<< HEAD "async": "^2.6.1", "big.js": "^5.1.2", -======= - "async": "^2.6.0", - "base32-encode": "^1.0.0", - "big.js": "^5.0.3", ->>>>>>> wip "binary-querystring": "~0.1.2", "bl": "^2.0.1", "boom": "^7.2.0", @@ -124,6 +118,7 @@ "ipld": "~0.17.3", "ipld-dag-cbor": "~0.12.1", "ipld-dag-pb": "~0.14.6", + "ipns": "^0.1.1", "is-ipfs": "~0.4.2", "is-pull-stream": "~0.0.0", "is-stream": "^1.1.0", @@ -158,7 +153,6 @@ "peer-info": "~0.14.1", "progress": "^2.0.0", "promisify-es6": "^1.0.3", - "protons": "^1.0.1", "pull-abortable": "^4.1.1", "pull-defer": "~0.2.2", "pull-file": "^1.1.0", diff --git a/src/cli/commands/name/publish.js b/src/cli/commands/name/publish.js index c725eb5827..5bcc6e5eaa 100644 --- a/src/cli/commands/name/publish.js +++ b/src/cli/commands/name/publish.js @@ -8,13 +8,35 @@ module.exports = { describe: 'Publish IPNS names.', builder: { - format: { - type: 'string' + resolve: { + describe: 'Resolve given path before publishing. Default: true.', + default: true + }, + lifetime: { + alias: 't', + describe: 'Time duration that the record will be valid for. Default: 24h.', + default: '24h' + }, + key: { + alias: 'k', + describe: 'Name of the key to be used or a valid PeerID, as listed by "ipfs key list -l". Default: self.', + default: 'self' + }, + ttl: { + describe: 'Time duration this record should be cached for (caution: experimental).', + default: '' } }, handler (argv) { - argv.ipfs.name.publish(argv['ipfsPath'], (err, result) => { + const opts = { + resolve: argv.resolve, + lifetime: argv.lifetime, + key: argv.key, + ttl: argv.ttl + } + + argv.ipfs.name.publish(argv['ipfsPath'], opts, (err, result) => { if (err) { throw err } diff --git a/src/core/components/name.js b/src/core/components/name.js index cac7273e9d..8e870038bd 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -1,9 +1,16 @@ 'use strict' +const debug = require('debug') const promisify = require('promisify-es6') const series = require('async/series') +const waterfall = require('async/waterfall') const human = require('human-to-milliseconds') +const crypto = require('libp2p-crypto') + +const log = debug('jsipfs:name') +log.error = debug('jsipfs:name:error') + const errors = require('../utils') const path = require('../ipns/path') @@ -11,13 +18,22 @@ const keyLookup = (ipfsNode, kname, cb) => { if (kname === 'self') { return cb(null, ipfsNode._peerInfo.id.privKey) } - // TODO validate - jsipfs daemon --pass 123456sddadesfgefrsfesfefsfeesfe - ipfsNode._keychain.findKeyByName(kname, (err, key) => { + + // jsipfs key gen --type=rsa --size=2048 mykey --pass 12345678901234567890 + // jsipfs daemon --pass 12345678901234567890 + // jsipfs name publish QmPao1o1nEdDYAToEDf34CovQHaycmhr7sagbD3DZAEW9L --key mykey + const pass = ipfsNode._options.pass + + waterfall([ + (cb) => ipfsNode._keychain.exportKey(kname, pass, cb), + (pem, cb) => crypto.keys.import(pem, pass, cb) + ], (err, privateKey) => { if (err) { + // TODO add log return cb(err) } - return cb(null, key) + return cb(null, privateKey) }) } @@ -35,7 +51,7 @@ module.exports = function name (self) { * @param {boolean} resolve resolve given path before publishing. * @param {String} lifetime time duration that the record will be valid for. This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are - "ms", "s", "m", "h". + "ns", "ms", "s", "m", "h". Default is 24h. * @param {String} ttl time duration this record should be cached for (caution: experimental). * @param {String} key name of the key to be used or a valid PeerID, as listed by 'ipfs key list -l'. * @param {function(Error)} [callback] @@ -43,13 +59,17 @@ module.exports = function name (self) { */ publish: promisify((value, resolve = true, lifetime = '24h', ttl, key = 'self', callback) => { if (!self.isOnline()) { - return callback(new Error(errors.OFFLINE_ERROR)) + const error = errors.OFFLINE_ERROR + + log.error(error) + return callback(new Error(error)) } // Parse path value try { value = path.parsePath(value) } catch (err) { + log.error(err) return callback(err) } @@ -57,25 +77,26 @@ module.exports = function name (self) { (cb) => human(lifetime || '1s', cb), // (cb) => ttl ? human(ttl, cb) : cb(), (cb) => keyLookup(self, key, cb), - (cb) => resolve ? path.resolvePath(self, value, cb) : cb() // if not resolved, and error will stop the execution + // verify if the path exists, if not, an error will stop the execution + (cb) => resolve ? path.resolvePath(self, value, cb) : cb() ], (err, results) => { if (err) { + log.error(err) return callback(err) } - const pubValidTime = results[0] + // Calculate eol with nanoseconds precision + const pubLifetime = results[0].toFixed(6) const privateKey = results[1] // TODO IMPROVEMENT - Handle ttl for cache // const ttl = results[1] // const privateKey = results[2] - // Calculate eol - const eol = new Date(Date.now() + pubValidTime) - // Start publishing process - self._ipns.publish(privateKey, value, eol, (err, res) => { + self._ipns.publish(privateKey, value, pubLifetime, (err, res) => { if (err) { + log.error(err) callback(err) } @@ -97,10 +118,14 @@ module.exports = function name (self) { const local = true if (!self.isOnline()) { - return callback(new Error(errors.OFFLINE_ERROR)) + const error = errors.OFFLINE_ERROR + + log.error(error) + return callback(new Error(error)) } if (local && nocache) { + log.error('Cannot specify both local and nocache') return callback(new Error('Cannot specify both local and nocache')) } @@ -113,16 +138,16 @@ module.exports = function name (self) { name = `/ipns/${name}` } - // TODO local public key? - const pubKey = self._peerInfo.id.pubKey + const localPublicKey = self._peerInfo.id.pubKey const options = { local: local, nocache: nocache, recursive: recursive } - self._ipns.resolve(name, pubKey, options, (err, result) => { + self._ipns.resolve(name, localPublicKey, options, (err, result) => { if (err) { + log.error(err) return callback(err) } diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index a1989e30f1..dacea80ce3 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -5,6 +5,10 @@ const series = require('async/series') // const QuickLRU = require('quick-lru'); // Consider using https://github.com/dominictarr/hashlru +const debug = require('debug') +const log = debug('jsipfs:ipns') +log.error = debug('jsipfs:ipns:error') + const IpnsPublisher = require('./publisher') const IpnsResolver = require('./resolver') const path = require('./path') @@ -30,10 +34,10 @@ class IPNS { } // Publish - publish (privKey, value, eol, callback) { + publish (privKey, value, lifetime, callback) { series([ (cb) => createFromPrivKey(privKey.bytes.toString('base64'), cb), - (cb) => this.ipnsPublisher.publishWithEOL(privKey, value, eol, cb) + (cb) => this.ipnsPublisher.publishWithEOL(privKey, value, lifetime, cb) ], (err, results) => { if (err) { return callback(err) diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index c7cb1fcff5..be4fc7d815 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -5,11 +5,23 @@ const CID = require('cids') const BAD_PATH_ERROR = new Error('invalid \'ipfs ref\' path') const NO_COMPONENTS_ERROR = new Error('path must contain at least one component') -// Should verify if the value exists before publishing it +// https://github.com/ipfs/go-ipfs/blob/master/core/pathresolver.go#L25 +// https://github.com/ipfs/go-ipfs/blob/master/path/path.go +// https://github.com/ipfs/go-ipfs/blob/master/namesys/namesys.go +// https://github.com/ipfs/go-ipfs/blob/master/namesys/routing.go +// resolves the given path by parsing out protocol-specific entries +// (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node const resolvePath = (ipfsNode, value, callback) => { if (value.startsWith('/ipns/')) { + const parts = value.split('/') // caution, I still have the first entry of the array empty + + if (parts.length < 3 || parts[2] === '') { + return callback(NO_COMPONENTS_ERROR) + } + // TODO resolve local? // TODO Resolve from DHT + return callback(new Error('not implemented yet')) } diff --git a/src/core/ipns/pb/ipns.proto.js b/src/core/ipns/pb/ipns.proto.js deleted file mode 100644 index 4d12b43852..0000000000 --- a/src/core/ipns/pb/ipns.proto.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' - -const protons = require('protons') - -/* eslint-disable no-tabs */ -const message = ` -message IpnsEntry { - enum ValidityType { - EOL = 0; // setting an EOL says "this record is valid until..." - } - - required bytes value = 1; - required bytes signature = 2; - - optional ValidityType validityType = 3; - optional bytes validity = 4; - - optional uint64 sequence = 5; - - optional uint64 ttl = 6; - - // in order for nodes to properly validate a record upon receipt, they need the public - // key associated with it. For old RSA keys, its easiest if we just send this as part of - // the record itself. For newer ed25519 keys, the public key can be embedded in the - // peerID, making this field unnecessary. - optional bytes pubKey = 7; -} -` - -module.exports = protons(message).IpnsEntry diff --git a/src/core/ipns/pb/ipnsEntry.js b/src/core/ipns/pb/ipnsEntry.js deleted file mode 100644 index e18aed6624..0000000000 --- a/src/core/ipns/pb/ipnsEntry.js +++ /dev/null @@ -1,40 +0,0 @@ -'use strict' - -const ipnsEntryProto = require('./ipns.proto') - -const create = (value, signature, validityType, validity, sequence, ttl, pubKey) => { - // Handle validity type - if (validityType !== undefined) { - validityType = ipnsEntryProto.ValidityType.EOL - } - - const entry = { - value: value, - signature: signature, - validityType: validityType, - validity: validity, - sequence: sequence, - ttl: ttl, - pubKey: pubKey - } - - return Object.keys(entry).reduce((acc, key) => { - const reducedEntry = acc - - if (entry[key] !== undefined) { - reducedEntry[key] = entry[key] - } - - return reducedEntry - }, {}) -} - -module.exports = { - // Create ipns data format - create, - // Marshal - marshal: ipnsEntryProto.encode, - // Unmarshal - unmarshal: ipnsEntryProto.decode, - validityType: ipnsEntryProto.ValidityType -} diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index ef2a7d854c..b2b9ea0c99 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -3,9 +3,14 @@ const peerId = require('peer-id') const waterfall = require('async/waterfall') -const IpnsEntry = require('./pb/ipnsEntry') -const { generateIpnsDsKey } = require('./utils') -const validator = require('./validator') +const debug = require('debug') +const log = debug('jsipfs:ipns:publisher') +log.error = debug('jsipfs:ipns:publisher:error') + +const ERR_INVALID_IPNS_RECORD = 'ERR_INVALID_IPNS_RECORD' +const ERR_STORING_IN_DATASTORE = 'ERR_STORING_IN_DATASTORE' + +const ipns = require('ipns') const defaultRecordTtl = 60 * 60 * 1000 @@ -20,26 +25,27 @@ class IpnsPublisher { } // publish record with a eol - publishWithEOL (privKey, value, eol, callback) { - this.updateOrCreateRecord(privKey, value, eol, (err, record) => { + publishWithEOL (privKey, value, lifetime, callback) { + this.updateOrCreateRecord(privKey, value, lifetime, (err, record) => { if (err) { return callback(err) } // TODO ROUTING - Add record + + log(`${value} was published correctly with EOL`) callback(null, record) }) } // Accepts a keypair, as well as a value (ipfsPath), and publishes it out to the routing system publish (privKey, value, callback) { - const eol = new Date(Date.now() + defaultRecordTtl) - - this.publishWithEOL(privKey, value, eol, (err, res) => { + this.publishWithEOL(privKey, value, defaultRecordTtl, (err, res) => { if (err) { return callback(err) } + log(`${value} was published correctly`) callback(res) }) } @@ -47,14 +53,15 @@ class IpnsPublisher { // Returns the record this node has published corresponding to the given peer ID. // If `checkRouting` is true and we have no existing record, this method will check the routing system for any existing records. getPublished (peerIdResult, checkRouting, callback) { - this.repo.datastore.get(generateIpnsDsKey(peerIdResult.id), (err, dsVal) => { + this.repo.datastore.get(ipns.getLocalKey(peerIdResult.id), (err, dsVal) => { let result if (!err) { if (Buffer.isBuffer(dsVal)) { result = dsVal } else { - return callback(new Error('found ipns record that we couldn\'t convert to a value')) + log.error(`found ipns record that we couldn't convert to a value`) + return callback(Object.assign(new Error('found ipns record that we couldn\'t convert to a value'), { code: ERR_INVALID_IPNS_RECORD })) } } else if (err.notFound) { if (!checkRouting) { @@ -64,11 +71,12 @@ class IpnsPublisher { } // TODO ROUTING } else { + log.error(`unexpected error getting the ipns record from datastore`) return callback(err) } // unmarshal data - result = IpnsEntry.unmarshal(dsVal) + result = ipns.unmarshal(dsVal) return callback(null, { peerIdResult: peerIdResult, @@ -77,26 +85,7 @@ class IpnsPublisher { }) } - createEntryRecord (privKey, value, seqNumber, eol, callback) { - const validity = eol.toISOString() - const validityType = IpnsEntry.validityType.EOL - const sequence = seqNumber - - validator.sign(privKey, value, validityType, validity, (err, signature) => { - if (err) { - return callback(err) - } - - // TODO confirm private key format compliance with go-ipfs - - // Create IPNS entry record - const ipnsEntry = IpnsEntry.create(value, signature, validityType, validity, sequence) - - return callback(null, ipnsEntry) - }) - } - - updateOrCreateRecord (privKey, value, eol, callback) { + updateOrCreateRecord (privKey, value, validity, callback) { waterfall([ (cb) => peerId.createFromPrivKey(privKey.bytes.toString('base64'), cb), (id, cb) => this.getPublished(id, false, cb) @@ -114,22 +103,25 @@ class IpnsPublisher { } // Create record - this.createEntryRecord(privKey, value, seqNumber, eol, (err, entryData) => { + ipns.create(privKey, value, seqNumber, validity, (err, entryData) => { if (err) { + log.error(`ipns record for ${value} could not be created`) return callback(err) } // TODO IMPROVEMENT - set ttl (still experimental feature for go) // Marshal record - const data = IpnsEntry.marshal(entryData) + const data = ipns.marshal(entryData) // Store the new record - this.repo.datastore.put(generateIpnsDsKey(peerIdResult.id), data, (err, res) => { + this.repo.datastore.put(ipns.getLocalKey(peerIdResult.id), data, (err, res) => { if (err) { - return callback(err) + log.error(`ipns record for ${value} could not be stored in the datastore`) + return callback(Object.assign(new Error(`ipns record for ${value} could not be stored in the datastore`), { code: ERR_STORING_IN_DATASTORE })) } + log(`ipns record for ${value} was stored in the datastore`) return callback(null, entryData) }) }) diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index 42b8f45e2e..14146fb3f8 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -1,21 +1,27 @@ 'use strict' -const IpnsEntry = require('./pb/ipnsEntry') -const utils = require('./utils') -const validator = require('./validator') - +const ipns = require('ipns') const { fromB58String } = require('multihashes') +const debug = require('debug') +const log = debug('jsipfs:ipns:resolver') +log.error = debug('jsipfs:ipns:resolver:error') + +const ERR_INVALID_NAME_SYNTAX = 'ERR_INVALID_NAME_SYNTAX' +const ERR_INVALID_RECORD_RECEIVED = 'ERR_INVALID_RECORD_RECEIVED' +const ERR_NO_LOCAL_RECORD_FOUND = 'ERR_NO_LOCAL_RECORD_FOUND' + class IpnsResolver { constructor (repo) { this.repo = repo } - resolve (name, publicKey, options, callback) { + resolve (name, localPublicKey, options, callback) { const nameSegments = name.split('/') if (nameSegments.length !== 3 || nameSegments[0] !== '') { - return callback(new Error(`invalid name syntax for ${name}`)) + log.error(`invalid name syntax for ${name}`) + return callback(Object.assign(new Error(`invalid name syntax for ${name}`), { code: ERR_INVALID_NAME_SYNTAX })) } const key = nameSegments[2] @@ -24,11 +30,12 @@ class IpnsResolver { // TODO nocache if (options.local) { - this.resolveLocal(key, publicKey, (err, res) => { + this.resolveLocal(key, localPublicKey, (err, res) => { if (err) { return callback(err) } + log(`${name} was locally resolved correctly`) return callback(null, res) }) } else { @@ -36,23 +43,25 @@ class IpnsResolver { } } - // https://github.com/ipfs/go-ipfs-routing/blob/master/offline/offline.go - resolveLocal (name, publicKey, callback) { - const ipnsKey = utils.generateIpnsDsKey(fromB58String(name)) + // resolve ipns entries locally using the datastore + resolveLocal (name, localPublicKey, callback) { + const ipnsKey = ipns.getLocalKey(fromB58String(name)) this.repo.datastore.get(ipnsKey, (err, dsVal) => { if (err) { - return callback(err) + log.error('local record requested was not found') + return callback(Object.assign(new Error('local record requested was not found'), { code: ERR_NO_LOCAL_RECORD_FOUND })) } if (!Buffer.isBuffer(dsVal)) { - return callback(new Error('found ipns record that we couldn\'t convert to a value')) + log.error('found ipns record that we couldn\'t convert to a value') + return callback(Object.assign(new Error('found ipns record that we couldn\'t convert to a value'), { code: ERR_INVALID_RECORD_RECEIVED })) } - const ipnsEntry = IpnsEntry.unmarshal(dsVal) + const ipnsEntry = ipns.unmarshal(dsVal) // Record validation - validator.verify(publicKey, ipnsEntry, (err) => { + ipns.validate(localPublicKey, ipnsEntry, (err) => { if (err) { return callback(err) } diff --git a/src/core/ipns/utils.js b/src/core/ipns/utils.js deleted file mode 100644 index 9664a38335..0000000000 --- a/src/core/ipns/utils.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict' - -const base32Encode = require('base32-encode') - -// rawStdEncoding as go -// Created issue for allowing this inside base32-encode https://github.com/LinusU/base32-encode/issues/2 -const rawStdEncoding = (key) => base32Encode(key, 'RFC4648').replace('=', '') - -module.exports = { - generateIpnsDsKey: (key) => `/ipns/${rawStdEncoding(key)}`, - buildIpnsKeysForId: (peerId) => ({ - nameKey: `/pk/${peerId.toB58String()}`, - ipnsKey: `/ipns/${peerId.toB58String()}` - }) -} diff --git a/src/core/ipns/validator.js b/src/core/ipns/validator.js deleted file mode 100644 index bc8b3f1f24..0000000000 --- a/src/core/ipns/validator.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict' - -const IpnsEntry = require('./pb/ipnsEntry') - -// Create IPNS Entry data for being signed -const getDataForSignature = (value, validityType, validity) => { - const valueBuffer = Buffer.from(value) - const validityTypeBuffer = Buffer.from(validityType.toString()) - const validityBuffer = Buffer.from(validity) - - return Buffer.concat([valueBuffer, validityTypeBuffer, validityBuffer]) -} - -// Sign IPNS Entry for publish -const sign = (privateKey, value, validityType, validity, callback) => { - const dataForSignature = getDataForSignature(value, validityType, validity) - - privateKey.sign(dataForSignature, (err, signature) => { - if (err) { - return callback(err) - } - return callback(null, signature) - }) -} - -// Verify IPNS entry on resolve -const verify = (publicKey, entry, callback) => { - const { value, validityType, validity } = entry - const dataForSignature = getDataForSignature(value, validityType, validity) - - // Validate Signature - publicKey.verify(dataForSignature, entry.signature, (err, result) => { - if (err) { - return callback(err) - } - - // Validate EOL - if (validityType === IpnsEntry.validityType.EOL) { - const validityDate = Date.parse(validity.toString()) - - if (validityDate < Date.now()) { - return callback(new Error('record has expired')) - } - } - - return callback(null, null) - }) -} - -module.exports = { - getDataForSignature, - sign, - verify -} diff --git a/test/cli/name.js b/test/cli/name.js new file mode 100644 index 0000000000..dc302d44c6 --- /dev/null +++ b/test/cli/name.js @@ -0,0 +1,79 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) + +const ipfsExec = require('../utils/ipfs-exec') + +const DaemonFactory = require('ipfsd-ctl') +const df = DaemonFactory.create({ type: 'js' }) + +const checkAll = (bits) => string => bits.every(bit => string.includes(bit)) + +describe.only('name', () => { + let ipfs + let cidAdded + let nodeId + + before(function (done) { + this.timeout(80 * 1000) + + df.spawn({ + exec: `./src/cli/bin.js`, + config: {}, + initOptions: { bits: 512 } + }, (err, node) => { + expect(err).to.not.exist() + ipfs = ipfsExec(node.repoPath) + + ipfs('id').then((res) => { + const id = JSON.parse(res) + expect(id).to.have.property('id') + + nodeId = id.id + + ipfs('files add src/init-files/init-docs/readme') + .then((out) => { + cidAdded = out.split(' ')[1] + done() + }) + }) + }) + }) + + it('name publish should publish correctly when the file was already added', function () { + this.timeout(60 * 1000) + + return ipfs(`name publish ${cidAdded}`).then((res) => { + expect(res).to.exist() + expect(res).to.satisfy(checkAll([cidAdded, nodeId])) + }) + }) + + /* TODO resolve unexistant file does not resolve error + it('name publish should return an error when the file was not already added', function () { + this.timeout(60 * 1000) + + const notAddedCid = 'QmPFVLPmp9zv5Z5KUqLhe2EivAGccQW2r7M7jhVJGLZoZU' + + return ipfs(`name publish ${notAddedCid}`).then((res) => { + expect(res).to.exist() + expect(res).to.satisfy(checkAll([cidAdded, nodeId])) + }) + }) */ + + it('name resolve should get the entry correctly', function () { + this.timeout(60 * 1000) + + return ipfs(`name publish ${cidAdded}`).then((res) => { + expect(res).to.exist() + return ipfs('name resolve').then((res) => { + expect(res).to.exist() + expect(res).to.satisfy(checkAll([cidAdded])) + }) + }) + }) +}) From 72dbb61ada68fd4073146098191d39f3518e87b2 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 2 Jul 2018 15:27:45 +0100 Subject: [PATCH 06/34] added resolve recursive parameter --- src/cli/bin.js | 5 +++ src/cli/commands/name/resolve.js | 18 +++++++-- src/core/components/name.js | 5 ++- src/core/config.js | 1 + src/core/ipns/path.js | 2 +- src/core/ipns/resolver.js | 67 ++++++++++++++++++++++++++------ src/http/index.js | 1 + test/cli/name.js | 1 + 8 files changed, 82 insertions(+), 18 deletions(-) diff --git a/src/cli/bin.js b/src/cli/bin.js index 3307b6e9ef..29cb05cd55 100755 --- a/src/cli/bin.js +++ b/src/cli/bin.js @@ -31,6 +31,11 @@ const cli = yargs type: 'string', default: '' }) + .option('local', { + desc: 'Run the command locally, instead of using the daemon', + type: 'boolean', + default: false + }) .epilog(utils.ipfsPathHelp) .demandCommand(1) .fail((msg, err, yargs) => { diff --git a/src/cli/commands/name/resolve.js b/src/cli/commands/name/resolve.js index b34a5213e0..e248ac09d7 100644 --- a/src/cli/commands/name/resolve.js +++ b/src/cli/commands/name/resolve.js @@ -8,13 +8,25 @@ module.exports = { describe: 'Resolve IPNS names.', builder: { - format: { - type: 'string' + nocache: { + alias: 'n', + describe: 'Do not use cached entries. Default: false.', + default: false + }, + recursive: { + alias: 'r', + recursive: 'Resolve until the result is not an IPNS name. Default: false.', + default: false } }, handler (argv) { - argv.ipfs.name.resolve(argv['name'], (err, result) => { + const opts = { + nocache: argv.nocache, + recursive: argv.recursive + } + + argv.ipfs.name.resolve(argv['name'], opts, (err, result) => { if (err) { throw err } diff --git a/src/core/components/name.js b/src/core/components/name.js index 8e870038bd..f6e28d599a 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -114,8 +114,8 @@ module.exports = function name (self) { * @param {function(Error)} [callback] * @returns {Promise|void} */ - resolve: promisify((name, nocache, recursive, callback) => { - const local = true + resolve: promisify((name, nocache = false, recursive = false, callback) => { + const local = true // self._options.local if (!self.isOnline()) { const error = errors.OFFLINE_ERROR @@ -138,6 +138,7 @@ module.exports = function name (self) { name = `/ipns/${name}` } + // TODO Public key const localPublicKey = self._peerInfo.id.pubKey const options = { local: local, diff --git a/src/core/config.js b/src/core/config.js index 7b16c17d06..937d27ac2e 100644 --- a/src/core/config.js +++ b/src/core/config.js @@ -17,6 +17,7 @@ const schema = Joi.object().keys({ Joi.object().keys({ bits: Joi.number().integer() }) ).allow(null), start: Joi.boolean(), + local: Joi.boolean(), pass: Joi.string().allow(''), relay: Joi.object().keys({ enabled: Joi.boolean(), diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index be4fc7d815..be50b4c4f2 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -22,7 +22,7 @@ const resolvePath = (ipfsNode, value, callback) => { // TODO resolve local? // TODO Resolve from DHT - return callback(new Error('not implemented yet')) + return callback(null) } ipfsNode.dag.get(value.substring('/ipfs/'.length), (err, value) => { diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index 14146fb3f8..5519cbe327 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -10,13 +10,17 @@ log.error = debug('jsipfs:ipns:resolver:error') const ERR_INVALID_NAME_SYNTAX = 'ERR_INVALID_NAME_SYNTAX' const ERR_INVALID_RECORD_RECEIVED = 'ERR_INVALID_RECORD_RECEIVED' const ERR_NO_LOCAL_RECORD_FOUND = 'ERR_NO_LOCAL_RECORD_FOUND' +const ERR_RESOLVE_RECURSION = 'ERR_RESOLVE_RECURSION' + +const defaultMaximumRecursiveDepth = 32 class IpnsResolver { constructor (repo) { this.repo = repo + this._resolver = undefined // Add Router resolver } - resolve (name, localPublicKey, options, callback) { + resolve (name, publicKey, options, callback) { const nameSegments = name.split('/') if (nameSegments.length !== 3 || nameSegments[0] !== '') { @@ -25,26 +29,65 @@ class IpnsResolver { } const key = nameSegments[2] + let depth + let resolverFn + + // Define a maximum depth if recursive option + if (options.recursive) { + depth = defaultMaximumRecursiveDepth + } - // TODO recursive // TODO nocache + // TODO set default resolverFn if (options.local) { - this.resolveLocal(key, localPublicKey, (err, res) => { - if (err) { - return callback(err) - } + resolverFn = this.resolveLocal + } - log(`${name} was locally resolved correctly`) - return callback(null, res) - }) - } else { + if (!resolverFn) { return callback(new Error('not implemented yet')) } + + this.resolver(key, publicKey, depth, resolverFn, (err, res) => { + if (err) { + return callback(err) + } + + log(`${name} was locally resolved correctly`) + return callback(null, res) + }) + } + + // Recursive resolver according to the specified depth + resolver (name, publicKey, depth, resolverFn, callback) { + this._resolver = resolverFn + + // Exceeded recursive maximum depth + if (depth === 0) { + const errorStr = `could not resolve name (recursion limit of ${defaultMaximumRecursiveDepth} exceeded)` + + log.error(errorStr) + return callback(Object.assign(new Error(errorStr), { code: ERR_RESOLVE_RECURSION })) + } + + this._resolver(name, publicKey, (err, res) => { + if (err) { + return callback(err) + } + + const nameSegments = res.split('/') + + // If obtained a ipfs cid or recursive option is disabled + if (nameSegments[1] === 'ipfs' || !depth) { + return callback(null, res) + } + + return this.resolver(nameSegments[2], publicKey, depth - 1, resolverFn, callback) + }) } // resolve ipns entries locally using the datastore - resolveLocal (name, localPublicKey, callback) { + resolveLocal (name, publicKey, callback) { const ipnsKey = ipns.getLocalKey(fromB58String(name)) this.repo.datastore.get(ipnsKey, (err, dsVal) => { @@ -61,7 +104,7 @@ class IpnsResolver { const ipnsEntry = ipns.unmarshal(dsVal) // Record validation - ipns.validate(localPublicKey, ipnsEntry, (err) => { + ipns.validate(publicKey, ipnsEntry, (err) => { if (err) { return callback(err) } diff --git a/src/http/index.js b/src/http/index.js index 17e2b80cb6..ed1edce219 100644 --- a/src/http/index.js +++ b/src/http/index.js @@ -71,6 +71,7 @@ function HttpApi (repo, config, cliArgs) { init: init, start: true, config: config, + local: cliArgs && cliArgs.local, pass: cliArgs && cliArgs.pass, EXPERIMENTAL: { pubsub: cliArgs && cliArgs.enablePubsubExperiment, diff --git a/test/cli/name.js b/test/cli/name.js index dc302d44c6..92142c7f1a 100644 --- a/test/cli/name.js +++ b/test/cli/name.js @@ -1,3 +1,4 @@ +/* eslint max-nested-callbacks: ["error", 5] */ /* eslint-env mocha */ 'use strict' From d9c8836ab1ed46645e57419aa0fe6a6b66ba48b4 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 3 Jul 2018 10:41:34 +0100 Subject: [PATCH 07/34] added cache --- package.json | 1 + src/core/components/name.js | 4 +++- src/core/ipns/index.js | 30 ++++++++++++++++++++---------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 0ecc19aa32..ab7e480fcf 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ "quick-lru": "^1.1.0", "read-pkg-up": "^4.0.0", "readable-stream": "2.3.6", + "receptacle": "^1.3.2", "stream-to-pull-stream": "^1.7.2", "tar-stream": "^1.6.1", "temp": "~0.8.3", diff --git a/src/core/components/name.js b/src/core/components/name.js index f6e28d599a..645c78fde0 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -52,7 +52,9 @@ module.exports = function name (self) { * @param {String} lifetime time duration that the record will be valid for. This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are "ns", "ms", "s", "m", "h". Default is 24h. - * @param {String} ttl time duration this record should be cached for (caution: experimental). + * @param {String} ttl time duration this record should be cached for (NOT IMPLEMENTED YET). + * This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are + "ns", "ms", "s", "m", "h" (caution: experimental). * @param {String} key name of the key to be used or a valid PeerID, as listed by 'ipfs key list -l'. * @param {function(Error)} [callback] * @returns {Promise|void} diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index dacea80ce3..007c7fab1d 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -2,8 +2,7 @@ const { createFromPrivKey } = require('peer-id') const series = require('async/series') -// const QuickLRU = require('quick-lru'); -// Consider using https://github.com/dominictarr/hashlru +const Receptacle = require('receptacle') const debug = require('debug') const log = debug('jsipfs:ipns') @@ -13,17 +12,27 @@ const IpnsPublisher = require('./publisher') const IpnsResolver = require('./resolver') const path = require('./path') -// const defaultRecordTtl = 60 * 1000 +const defaultRecordTtl = 60 * 1000 class IPNS { constructor (routing, repo, peerInfo) { this.ipnsPublisher = new IpnsPublisher(routing, repo) this.ipnsResolver = new IpnsResolver(repo) - // this.cache = new QuickLRU({maxSize: 1000}); + this.cache = new Receptacle({ max: 1000 }) // Create an LRU cache with max 1000 items } // Resolve resolve (name, pubKey, options, callback) { + if (!options.nocache) { + // Try to get the record from cache + const id = name.split('/')[2] + const result = this.cache.get(id) + + if (result) { + return callback(null, result) + } + } + this.ipnsResolver.resolve(name, pubKey, options, (err, result) => { if (err) { return callback(err) @@ -43,13 +52,14 @@ class IPNS { return callback(err) } - // TODO IMPROVEMENT - Add to cache - // this.cache.set(id.toB58String(), { - // val: value, - // eol: Date.now() + ttl - // }) + // Add to cache + const id = results[0].toB58String() + const ttEol = parseFloat(lifetime) + const ttl = (ttEol < defaultRecordTtl) ? ttEol : defaultRecordTtl + + this.cache.set(id, value, { ttl: ttl }) - callback(null, results[0].toB58String()) + callback(null, id) }) } } From 22e1bc8f10d80e8bd5fae66ac10120935e1790af Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 3 Jul 2018 16:40:14 +0100 Subject: [PATCH 08/34] added proper logging, errors and code refactor --- src/core/components/name.js | 36 ++++++++++++------------- src/core/ipns/index.js | 51 ++++++++++++++++++++--------------- src/core/ipns/path.js | 54 ++++++++++++++++++++++++------------- src/core/ipns/publisher.js | 45 +++++++++++++++---------------- src/core/ipns/resolver.js | 41 ++++++++++++++++++---------- 5 files changed, 130 insertions(+), 97 deletions(-) diff --git a/src/core/components/name.js b/src/core/components/name.js index 645c78fde0..ad3099f9ab 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -14,9 +14,12 @@ log.error = debug('jsipfs:name:error') const errors = require('../utils') const path = require('../ipns/path') -const keyLookup = (ipfsNode, kname, cb) => { +const ERR_CANNOT_GET_KEY = 'ERR_CANNOT_GET_KEY' +const ERR_NOCACHE_AND_LOCAL = 'ERR_NOCACHE_AND_LOCAL' + +const keyLookup = (ipfsNode, kname, callback) => { if (kname === 'self') { - return cb(null, ipfsNode._peerInfo.id.privKey) + return callback(null, ipfsNode._peerInfo.id.privKey) } // jsipfs key gen --type=rsa --size=2048 mykey --pass 12345678901234567890 @@ -29,11 +32,13 @@ const keyLookup = (ipfsNode, kname, cb) => { (pem, cb) => crypto.keys.import(pem, pass, cb) ], (err, privateKey) => { if (err) { - // TODO add log - return cb(err) + const error = `cannot get the specified key` + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_CANNOT_GET_KEY })) } - return cb(null, privateKey) + return callback(null, privateKey) }) } @@ -45,8 +50,6 @@ module.exports = function name (self) { * and resolve, the default name used is the node's own PeerID, * which is the hash of its public key. * - * Examples: TODO such as in go - * * @param {String} value ipfs path of the object to be published. * @param {boolean} resolve resolve given path before publishing. * @param {String} lifetime time duration that the record will be valid for. @@ -117,7 +120,7 @@ module.exports = function name (self) { * @returns {Promise|void} */ resolve: promisify((name, nocache = false, recursive = false, callback) => { - const local = true // self._options.local + const local = true // TODO ROUTING - use self._options.local if (!self.isOnline()) { const error = errors.OFFLINE_ERROR @@ -127,8 +130,10 @@ module.exports = function name (self) { } if (local && nocache) { - log.error('Cannot specify both local and nocache') - return callback(new Error('Cannot specify both local and nocache')) + const error = 'Cannot specify both local and nocache' + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_NOCACHE_AND_LOCAL })) } // Set node id as name for being resolved, if it is not received @@ -140,7 +145,7 @@ module.exports = function name (self) { name = `/ipns/${name}` } - // TODO Public key + // TODO ROUTING - public key from network instead const localPublicKey = self._peerInfo.id.pubKey const options = { local: local, @@ -148,14 +153,7 @@ module.exports = function name (self) { recursive: recursive } - self._ipns.resolve(name, localPublicKey, options, (err, result) => { - if (err) { - log.error(err) - return callback(err) - } - - return callback(null, result) - }) + self._ipns.resolve(name, localPublicKey, options, callback) }) } } diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index 007c7fab1d..fc5f7594a7 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -17,10 +17,35 @@ const defaultRecordTtl = 60 * 1000 class IPNS { constructor (routing, repo, peerInfo) { this.ipnsPublisher = new IpnsPublisher(routing, repo) - this.ipnsResolver = new IpnsResolver(repo) + this.ipnsResolver = new IpnsResolver(routing, repo) this.cache = new Receptacle({ max: 1000 }) // Create an LRU cache with max 1000 items } + // Publish + publish (privKey, value, lifetime, callback) { + series([ + (cb) => createFromPrivKey(privKey.bytes.toString('base64'), cb), + (cb) => this.ipnsPublisher.publishWithEOL(privKey, value, lifetime, cb) + ], (err, results) => { + if (err) { + log.error(err) + return callback(err) + } + + log(`IPNS value ${value} was published correctly`) + + // Add to cache + const id = results[0].toB58String() + const ttEol = parseFloat(lifetime) + const ttl = (ttEol < defaultRecordTtl) ? ttEol : defaultRecordTtl + + log(`IPNS value ${value} was cached correctly`) + + this.cache.set(id, value, { ttl: ttl }) + callback(null, id) + }) + } + // Resolve resolve (name, pubKey, options, callback) { if (!options.nocache) { @@ -35,31 +60,13 @@ class IPNS { this.ipnsResolver.resolve(name, pubKey, options, (err, result) => { if (err) { + log.error(err) return callback(err) } - return callback(null, result) - }) - } - - // Publish - publish (privKey, value, lifetime, callback) { - series([ - (cb) => createFromPrivKey(privKey.bytes.toString('base64'), cb), - (cb) => this.ipnsPublisher.publishWithEOL(privKey, value, lifetime, cb) - ], (err, results) => { - if (err) { - return callback(err) - } + log(`IPNS record from ${name} was resolved correctly`) - // Add to cache - const id = results[0].toB58String() - const ttEol = parseFloat(lifetime) - const ttl = (ttEol < defaultRecordTtl) ? ttEol : defaultRecordTtl - - this.cache.set(id, value, { ttl: ttl }) - - callback(null, id) + return callback(null, result) }) } } diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index be50b4c4f2..36714d9311 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -2,30 +2,41 @@ const CID = require('cids') -const BAD_PATH_ERROR = new Error('invalid \'ipfs ref\' path') -const NO_COMPONENTS_ERROR = new Error('path must contain at least one component') +const debug = require('debug') +const log = debug('jsipfs:ipns:path') +log.error = debug('jsipfs:ipns:path:error') + +const ERR_BAD_PATH = 'ERR_BAD_PATH' +const ERR_NO_COMPONENTS = 'ERR_NO_COMPONENTS' -// https://github.com/ipfs/go-ipfs/blob/master/core/pathresolver.go#L25 -// https://github.com/ipfs/go-ipfs/blob/master/path/path.go -// https://github.com/ipfs/go-ipfs/blob/master/namesys/namesys.go -// https://github.com/ipfs/go-ipfs/blob/master/namesys/routing.go // resolves the given path by parsing out protocol-specific entries // (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node -const resolvePath = (ipfsNode, value, callback) => { - if (value.startsWith('/ipns/')) { - const parts = value.split('/') // caution, I still have the first entry of the array empty +const resolvePath = (ipfsNode, name, callback) => { + // ipns path + if (name.startsWith('/ipns/')) { + log(`resolve ipns path ${name}`) + + const local = true // TODO ROUTING - use self._options.local + const parts = name.split('/') if (parts.length < 3 || parts[2] === '') { - return callback(NO_COMPONENTS_ERROR) + const error = 'path must contain at least one component' + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_NO_COMPONENTS })) } - // TODO resolve local? - // TODO Resolve from DHT + // TODO ROUTING - public key from network instead + const localPublicKey = ipfsNode._peerInfo.id.pubKey + const options = { + local: local + } - return callback(null) + return ipfsNode._ipns.resolve(name, localPublicKey, options, callback) } - ipfsNode.dag.get(value.substring('/ipfs/'.length), (err, value) => { + // ipfs path + ipfsNode.dag.get(name.substring('/ipfs/'.length), (err, value) => { if (err) { return callback(err) } @@ -38,6 +49,7 @@ const resolvePath = (ipfsNode, value, callback) => { // The returned path will always be prefixed with /ipfs/ or /ipns/. // If the received string is not a valid ipfs path, an error will be returned const parsePath = (pathStr) => { + const badPathError = `invalid 'ipfs ref' path` const parts = pathStr.split('/') if (parts.length === 1) { @@ -52,15 +64,18 @@ const parsePath = (pathStr) => { } if (parts.length < 3) { - throw BAD_PATH_ERROR + log.error(badPathError) + throw Object.assign(new Error(badPathError), { code: ERR_BAD_PATH }) } if (parts[1] === 'ipfs') { if (!parseCidToPath(parts[2])) { - throw BAD_PATH_ERROR + log.error(badPathError) + throw Object.assign(new Error(badPathError), { code: ERR_BAD_PATH }) } } else if (parts[1] !== 'ipns') { - throw BAD_PATH_ERROR + log.error(badPathError) + throw Object.assign(new Error(badPathError), { code: ERR_BAD_PATH }) } return pathStr } @@ -68,7 +83,10 @@ const parsePath = (pathStr) => { // parseCidToPath takes a CID in string form and returns a valid ipfs Path. const parseCidToPath = (value) => { if (value === '') { - throw NO_COMPONENTS_ERROR + const error = 'path must contain at least one component' + + log.error(error) + throw Object.assign(new Error(error), { code: ERR_NO_COMPONENTS }) } const cid = new CID(value) diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index b2b9ea0c99..769868516d 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -7,17 +7,16 @@ const debug = require('debug') const log = debug('jsipfs:ipns:publisher') log.error = debug('jsipfs:ipns:publisher:error') +const ipns = require('ipns') + +const ERR_CREATING_IPNS_RECORD = 'ERR_CREATING_IPNS_RECORD' const ERR_INVALID_IPNS_RECORD = 'ERR_INVALID_IPNS_RECORD' const ERR_STORING_IN_DATASTORE = 'ERR_STORING_IN_DATASTORE' - -const ipns = require('ipns') +const ERR_UNEXPECTED_DATASTORE_RESPONSE = 'ERR_UNEXPECTED_DATASTORE_RESPONSE' const defaultRecordTtl = 60 * 60 * 1000 -/* - IpnsPublisher is capable of publishing and resolving names to the IPFS - routing system. - */ +// IpnsPublisher is capable of publishing and resolving names to the IPFS routing system. class IpnsPublisher { constructor (routing, repo) { this.routing = routing @@ -31,23 +30,15 @@ class IpnsPublisher { return callback(err) } - // TODO ROUTING - Add record + // TODO ROUTING - Add record (with public key) - log(`${value} was published correctly with EOL`) callback(null, record) }) } // Accepts a keypair, as well as a value (ipfsPath), and publishes it out to the routing system publish (privKey, value, callback) { - this.publishWithEOL(privKey, value, defaultRecordTtl, (err, res) => { - if (err) { - return callback(err) - } - - log(`${value} was published correctly`) - callback(res) - }) + this.publishWithEOL(privKey, value, defaultRecordTtl, callback) } // Returns the record this node has published corresponding to the given peer ID. @@ -60,8 +51,10 @@ class IpnsPublisher { if (Buffer.isBuffer(dsVal)) { result = dsVal } else { - log.error(`found ipns record that we couldn't convert to a value`) - return callback(Object.assign(new Error('found ipns record that we couldn\'t convert to a value'), { code: ERR_INVALID_IPNS_RECORD })) + const error = `found ipns record that we couldn't convert to a value` + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_INVALID_IPNS_RECORD })) } } else if (err.notFound) { if (!checkRouting) { @@ -69,10 +62,12 @@ class IpnsPublisher { peerIdResult: peerIdResult }) } - // TODO ROUTING + // TODO ROUTING - get } else { - log.error(`unexpected error getting the ipns record from datastore`) - return callback(err) + const error = `unexpected error getting the ipns record ${peerIdResult.id} from datastore` + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_UNEXPECTED_DATASTORE_RESPONSE })) } // unmarshal data @@ -88,7 +83,7 @@ class IpnsPublisher { updateOrCreateRecord (privKey, value, validity, callback) { waterfall([ (cb) => peerId.createFromPrivKey(privKey.bytes.toString('base64'), cb), - (id, cb) => this.getPublished(id, false, cb) + (id, cb) => this.getPublished(id, false, cb) // TODO ROUTING - change to true ], (err, result) => { if (err) { callback(err) @@ -105,8 +100,10 @@ class IpnsPublisher { // Create record ipns.create(privKey, value, seqNumber, validity, (err, entryData) => { if (err) { - log.error(`ipns record for ${value} could not be created`) - return callback(err) + const error = `ipns record for ${value} could not be created` + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_CREATING_IPNS_RECORD })) } // TODO IMPROVEMENT - set ttl (still experimental feature for go) diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index 5519cbe327..1853a7f90a 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -10,12 +10,13 @@ log.error = debug('jsipfs:ipns:resolver:error') const ERR_INVALID_NAME_SYNTAX = 'ERR_INVALID_NAME_SYNTAX' const ERR_INVALID_RECORD_RECEIVED = 'ERR_INVALID_RECORD_RECEIVED' const ERR_NO_LOCAL_RECORD_FOUND = 'ERR_NO_LOCAL_RECORD_FOUND' -const ERR_RESOLVE_RECURSION = 'ERR_RESOLVE_RECURSION' +const ERR_RESOLVE_RECURSION_LIMIT = 'ERR_RESOLVE_RECURSION_LIMIT' const defaultMaximumRecursiveDepth = 32 class IpnsResolver { - constructor (repo) { + constructor (routing, repo) { + this.routing = routing this.repo = repo this._resolver = undefined // Add Router resolver } @@ -24,22 +25,26 @@ class IpnsResolver { const nameSegments = name.split('/') if (nameSegments.length !== 3 || nameSegments[0] !== '') { - log.error(`invalid name syntax for ${name}`) - return callback(Object.assign(new Error(`invalid name syntax for ${name}`), { code: ERR_INVALID_NAME_SYNTAX })) + const error = `invalid name syntax for ${name}` + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_INVALID_NAME_SYNTAX })) } const key = nameSegments[2] + + // Define a maximum depth if recursive option enabled let depth - let resolverFn - // Define a maximum depth if recursive option if (options.recursive) { depth = defaultMaximumRecursiveDepth } - // TODO nocache + // Get the intended resoulver function // TODO set default resolverFn + let resolverFn + if (options.local) { resolverFn = this.resolveLocal } @@ -48,6 +53,8 @@ class IpnsResolver { return callback(new Error('not implemented yet')) } + // TODO Routing - Get public key from routing + this.resolver(key, publicKey, depth, resolverFn, (err, res) => { if (err) { return callback(err) @@ -60,14 +67,15 @@ class IpnsResolver { // Recursive resolver according to the specified depth resolver (name, publicKey, depth, resolverFn, callback) { + // bind resolver function this._resolver = resolverFn // Exceeded recursive maximum depth if (depth === 0) { - const errorStr = `could not resolve name (recursion limit of ${defaultMaximumRecursiveDepth} exceeded)` + const error = `could not resolve name (recursion limit of ${defaultMaximumRecursiveDepth} exceeded)` - log.error(errorStr) - return callback(Object.assign(new Error(errorStr), { code: ERR_RESOLVE_RECURSION })) + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_RESOLVE_RECURSION_LIMIT })) } this._resolver(name, publicKey, (err, res) => { @@ -82,6 +90,7 @@ class IpnsResolver { return callback(null, res) } + // continue recursively until depth equals 0 return this.resolver(nameSegments[2], publicKey, depth - 1, resolverFn, callback) }) } @@ -92,13 +101,17 @@ class IpnsResolver { this.repo.datastore.get(ipnsKey, (err, dsVal) => { if (err) { - log.error('local record requested was not found') - return callback(Object.assign(new Error('local record requested was not found'), { code: ERR_NO_LOCAL_RECORD_FOUND })) + const error = 'local record requested was not found' + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_NO_LOCAL_RECORD_FOUND })) } if (!Buffer.isBuffer(dsVal)) { - log.error('found ipns record that we couldn\'t convert to a value') - return callback(Object.assign(new Error('found ipns record that we couldn\'t convert to a value'), { code: ERR_INVALID_RECORD_RECEIVED })) + const error = `found ipns record that we couldn't convert to a value` + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_INVALID_RECORD_RECEIVED })) } const ipnsEntry = ipns.unmarshal(dsVal) From 7e19583552e52e989c134cf5ea2e2743861ad3da Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 5 Jul 2018 10:58:56 +0100 Subject: [PATCH 09/34] added cli and http tests --- src/core/components/name.js | 2 +- src/core/ipns/index.js | 3 +- src/core/ipns/path.js | 1 + src/core/ipns/publisher.js | 11 ++- test/cli/name.js | 140 ++++++++++++++++++++++++++++++----- test/core/ipns.spec.js | 46 ++++++++++++ test/http-api/inject/name.js | 53 +++++++++++++ 7 files changed, 234 insertions(+), 22 deletions(-) create mode 100644 test/core/ipns.spec.js create mode 100644 test/http-api/inject/name.js diff --git a/src/core/components/name.js b/src/core/components/name.js index ad3099f9ab..72b87f5764 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -83,7 +83,7 @@ module.exports = function name (self) { // (cb) => ttl ? human(ttl, cb) : cb(), (cb) => keyLookup(self, key, cb), // verify if the path exists, if not, an error will stop the execution - (cb) => resolve ? path.resolvePath(self, value, cb) : cb() + (cb) => resolve === true || resolve === 'true' ? path.resolvePath(self, value, cb) : cb() ], (err, results) => { if (err) { log.error(err) diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index fc5f7594a7..bbf084f5d5 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -48,7 +48,8 @@ class IPNS { // Resolve resolve (name, pubKey, options, callback) { - if (!options.nocache) { + // If recursive, we should not try to get the cached value + if (!options.nocache && !options.recursive) { // Try to get the record from cache const id = name.split('/')[2] const result = this.cache.get(id) diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index 36714d9311..4d18066552 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -12,6 +12,7 @@ const ERR_NO_COMPONENTS = 'ERR_NO_COMPONENTS' // resolves the given path by parsing out protocol-specific entries // (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node const resolvePath = (ipfsNode, name, callback) => { + console.log('resolve path'); // ipns path if (name.startsWith('/ipns/')) { log(`resolve ipns path ${name}`) diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index 769868516d..3aa1071107 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -71,7 +71,16 @@ class IpnsPublisher { } // unmarshal data - result = ipns.unmarshal(dsVal) + try { + result = ipns.unmarshal(dsVal) + } catch (err) { + const error = `found ipns record that we couldn't convert to a value` + + log.error(error) + return callback(null, { + peerIdResult: peerIdResult + }) + } return callback(null, { peerIdResult: peerIdResult, diff --git a/test/cli/name.js b/test/cli/name.js index 92142c7f1a..45802fdf2a 100644 --- a/test/cli/name.js +++ b/test/cli/name.js @@ -1,4 +1,4 @@ -/* eslint max-nested-callbacks: ["error", 5] */ +/* eslint max-nested-callbacks: ["error", 6] */ /* eslint-env mocha */ 'use strict' @@ -7,6 +7,7 @@ const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) +const hat = require('hat') const ipfsExec = require('../utils/ipfs-exec') const DaemonFactory = require('ipfsd-ctl') @@ -15,9 +16,16 @@ const df = DaemonFactory.create({ type: 'js' }) const checkAll = (bits) => string => bits.every(bit => string.includes(bit)) describe.only('name', () => { + const passPhrase = hat() + const pass = '--pass ' + passPhrase + const name = 'test-key-' + hat() + let ipfs + let ipfsd + let cidAdded let nodeId + let keyId before(function (done) { this.timeout(80 * 1000) @@ -25,55 +33,149 @@ describe.only('name', () => { df.spawn({ exec: `./src/cli/bin.js`, config: {}, + args: pass.split(' '), initOptions: { bits: 512 } - }, (err, node) => { + }, (err, _ipfsd) => { expect(err).to.not.exist() - ipfs = ipfsExec(node.repoPath) - ipfs('id').then((res) => { - const id = JSON.parse(res) - expect(id).to.have.property('id') + ipfsd = _ipfsd + ipfs = ipfsExec(_ipfsd.repoPath) + + ipfs(`${pass} key gen ${name} --type rsa --size 2048`).then((out) => { + expect(out).to.include(name) + + keyId = out.split(' ')[1] - nodeId = id.id + ipfs('id').then((res) => { + const id = JSON.parse(res) - ipfs('files add src/init-files/init-docs/readme') - .then((out) => { + expect(id).to.have.property('id') + nodeId = id.id + + ipfs('files add src/init-files/init-docs/readme').then((out) => { cidAdded = out.split(' ')[1] done() }) + }) }) }) }) - it('name publish should publish correctly when the file was already added', function () { + after(function (done) { + if (ipfsd) { + ipfsd.stop(() => done()) + } else { + done() + } + }) + + it('name publish should publish correctly when the file was already added', function (done) { this.timeout(60 * 1000) - return ipfs(`name publish ${cidAdded}`).then((res) => { + ipfs(`name publish ${cidAdded}`).then((res) => { expect(res).to.exist() expect(res).to.satisfy(checkAll([cidAdded, nodeId])) + + done() }) }) - /* TODO resolve unexistant file does not resolve error - it('name publish should return an error when the file was not already added', function () { + it('name resolve should get the entry correctly', function (done) { + this.timeout(60 * 1000) + + ipfs(`name publish ${cidAdded}`).then((res) => { + expect(res).to.exist() + + ipfs('name resolve').then((res) => { + expect(res).to.exist() + expect(res).to.satisfy(checkAll([cidAdded])) + + done() + }) + }) + }) + + it('name publish should publish correctly when the file was not added but resolve is disabled', function (done) { this.timeout(60 * 1000) const notAddedCid = 'QmPFVLPmp9zv5Z5KUqLhe2EivAGccQW2r7M7jhVJGLZoZU' - return ipfs(`name publish ${notAddedCid}`).then((res) => { + ipfs(`name publish ${notAddedCid} --resolve false`).then((res) => { + expect(res).to.exist() + expect(res).to.satisfy(checkAll([notAddedCid, nodeId])) + + done() + }) + }) + + it('name resolve should not get the entry correctly if its validity time expired', function (done) { + this.timeout(60 * 1000) + + ipfs(`name publish ${cidAdded} --lifetime 10ns`).then((res) => { + expect(res).to.exist() + + setTimeout(function () { + ipfs('name resolve') + .then((res) => { + expect(res).to.not.exist() + }) + .catch((err) => { + expect(err).to.exist() + done() + }) + }, 1) + }) + }) + + it('name publish should publish correctly when a new key is used', function (done) { + this.timeout(60 * 1000) + + ipfs(`name publish ${cidAdded} --key ${name}`).then((res) => { + expect(res).to.exist() + expect(res).to.satisfy(checkAll([cidAdded, keyId])) + + done() + }) + }) + + it('name resolve should return the immediate pointing record, unless using the recursive parameter', function (done) { + this.timeout(60 * 1000) + + ipfs(`name publish ${cidAdded}`).then((res) => { expect(res).to.exist() expect(res).to.satisfy(checkAll([cidAdded, nodeId])) + + ipfs(`name publish /ipns/${nodeId} --key ${name}`).then((res) => { + expect(res).to.exist() + expect(res).to.satisfy(checkAll([nodeId, keyId])) + + ipfs(`name resolve ${keyId}`).then((res) => { + expect(res).to.exist() + expect(res).to.satisfy(checkAll([nodeId])) + + done() + }) + }) }) - }) */ + }) - it('name resolve should get the entry correctly', function () { + it('name resolve should go recursively until finding an ipfs hash', function (done) { this.timeout(60 * 1000) - return ipfs(`name publish ${cidAdded}`).then((res) => { + ipfs(`name publish ${cidAdded}`).then((res) => { expect(res).to.exist() - return ipfs('name resolve').then((res) => { + expect(res).to.satisfy(checkAll([cidAdded, nodeId])) + + ipfs(`name publish /ipns/${nodeId} --key ${name}`).then((res) => { expect(res).to.exist() - expect(res).to.satisfy(checkAll([cidAdded])) + expect(res).to.satisfy(checkAll([nodeId, keyId])) + + ipfs(`name resolve ${keyId} --recursive`).then((res) => { + expect(res).to.exist() + expect(res).to.satisfy(checkAll([cidAdded])) + + done() + }) }) }) }) diff --git a/test/core/ipns.spec.js b/test/core/ipns.spec.js new file mode 100644 index 0000000000..a6c6235ab9 --- /dev/null +++ b/test/core/ipns.spec.js @@ -0,0 +1,46 @@ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) + +const isNode = require('detect-node') +const IPFS = require('../../src') + +const DaemonFactory = require('ipfsd-ctl') +const df = DaemonFactory.create({ type: 'proc' }) + +describe.only('ipns', () => { + if (!isNode) { + return + } + + let node + let ipfsd + + before(function (done) { + this.timeout(40 * 1000) + df.spawn({ + exec: IPFS + }, (err, _ipfsd) => { + expect(err).to.not.exist() + ipfsd = _ipfsd + node = _ipfsd.api + done() + }) + }) + + after((done) => ipfsd.stop(done)) + + it('get bootstrap list', (done) => { + console.log('node', node) + done() + // node.bootstrap.list((err, list) => { + // expect(err).to.not.exist() + // expect(list.Peers).to.deep.equal(defaultList) + // done() + // }) + }) +}) diff --git a/test/http-api/inject/name.js b/test/http-api/inject/name.js new file mode 100644 index 0000000000..f4b566be9e --- /dev/null +++ b/test/http-api/inject/name.js @@ -0,0 +1,53 @@ +/* eslint max-nested-callbacks: ["error", 8] */ +/* eslint-env mocha */ +'use strict' + +const chai = require('chai') +const dirtyChai = require('dirty-chai') + +const expect = chai.expect +chai.use(dirtyChai) + +const checkAll = (bits) => string => bits.every(bit => string.includes(bit)) + +module.exports = (http) => { + describe.only('/name', function () { + const cid = 'QmbndGRXYRyfU41TUvc52gMrwq87JJg18QsDPcCeaMcM61' + let api + + before(() => { + api = http.api.server.select('API') + }) + + it('publish a record correctly', (done) => { + api.inject({ + method: 'GET', + url: `/api/v0/name/publish?arg=${cid}&resolve=false` + }, (res) => { + expect(res).to.exist() + expect(res.result.Value).to.equal(cid) + done() + }) + }) + + it('resolve a record correctly', (done) => { + api.inject({ + method: 'GET', + url: `/api/v0/name/publish?arg=${cid}&resolve=false` + }, (res) => { + expect(res).to.exist() + expect(res.result.Value).to.equal(cid) + + api.inject({ + method: 'GET', + url: `/api/v0/name/resolve` + }, (res) => { + expect(res).to.exist() + expect(res.result.Path).to.satisfy(checkAll([cid])) + + done() + }) + }) + }) + }) +} From 7bcc6483f09134bb95ad7fb47d7a358e8f8f3048 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 16 Jul 2018 15:31:10 +0100 Subject: [PATCH 10/34] updated tests --- src/core/components/name.js | 9 +------- src/core/ipns/path.js | 1 - src/core/ipns/resolver.js | 2 +- test/cli/commands.js | 3 +-- test/cli/name.js | 2 +- test/core/interface.spec.js | 8 +++++++ test/core/ipns.spec.js | 42 +++++++++++++++++++++++++++--------- test/http-api/inject/name.js | 2 +- 8 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/core/components/name.js b/src/core/components/name.js index 72b87f5764..7d1c554bee 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -99,14 +99,7 @@ module.exports = function name (self) { // const privateKey = results[2] // Start publishing process - self._ipns.publish(privateKey, value, pubLifetime, (err, res) => { - if (err) { - log.error(err) - callback(err) - } - - callback(null, res) - }) + self._ipns.publish(privateKey, value, pubLifetime, callback) }) }), diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index 4d18066552..36714d9311 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -12,7 +12,6 @@ const ERR_NO_COMPONENTS = 'ERR_NO_COMPONENTS' // resolves the given path by parsing out protocol-specific entries // (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node const resolvePath = (ipfsNode, name, callback) => { - console.log('resolve path'); // ipns path if (name.startsWith('/ipns/')) { log(`resolve ipns path ${name}`) diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index 1853a7f90a..a66722e681 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -101,7 +101,7 @@ class IpnsResolver { this.repo.datastore.get(ipnsKey, (err, dsVal) => { if (err) { - const error = 'local record requested was not found' + const error = `local record requested was not found for ${name} (${ipnsKey})` log.error(error) return callback(Object.assign(new Error(error), { code: ERR_NO_LOCAL_RECORD_FOUND })) diff --git a/test/cli/commands.js b/test/cli/commands.js index 2c52e23d74..e6ae40498a 100644 --- a/test/cli/commands.js +++ b/test/cli/commands.js @@ -4,8 +4,7 @@ const expect = require('chai').expect const runOnAndOff = require('../utils/on-and-off') -const commandCount = 78 - +const commandCount = 81 describe('commands', () => runOnAndOff((thing) => { let ipfs diff --git a/test/cli/name.js b/test/cli/name.js index 45802fdf2a..5ae06c985a 100644 --- a/test/cli/name.js +++ b/test/cli/name.js @@ -15,7 +15,7 @@ const df = DaemonFactory.create({ type: 'js' }) const checkAll = (bits) => string => bits.every(bit => string.includes(bit)) -describe.only('name', () => { +describe('name', () => { const passPhrase = hat() const pass = '--pass ' + passPhrase const name = 'test-key-' + hat() diff --git a/test/core/interface.spec.js b/test/core/interface.spec.js index 8ac1655b97..ad23671103 100644 --- a/test/core/interface.spec.js +++ b/test/core/interface.spec.js @@ -65,6 +65,14 @@ describe('interface-ipfs-core tests', () => { ] }) + /* TODO uncomment once interface-ipfs-core PR get merged + tests.name(CommonFactory.create({ + spawnOptions: { + args: ['--pass ipfs-is-awesome-software'], + initOptions: { bits: 512 } + } + })) */ + tests.object(defaultCommonFactory) tests.pin(defaultCommonFactory) diff --git a/test/core/ipns.spec.js b/test/core/ipns.spec.js index a6c6235ab9..7ccd4b97e0 100644 --- a/test/core/ipns.spec.js +++ b/test/core/ipns.spec.js @@ -12,12 +12,15 @@ const IPFS = require('../../src') const DaemonFactory = require('ipfsd-ctl') const df = DaemonFactory.create({ type: 'proc' }) -describe.only('ipns', () => { +const ipfsRef = '/ipfs/QmPFVLPmp9zv5Z5KUqLhe2EivAGccQW2r7M7jhVJGLZoZU' + +describe('ipns', () => { if (!isNode) { return } let node + let nodeId let ipfsd before(function (done) { @@ -28,19 +31,38 @@ describe.only('ipns', () => { expect(err).to.not.exist() ipfsd = _ipfsd node = _ipfsd.api - done() + + node.id().then((res) => { + expect(res.id).to.exist() + + nodeId = res.id + done() + }) }) }) after((done) => ipfsd.stop(done)) - it('get bootstrap list', (done) => { - console.log('node', node) - done() - // node.bootstrap.list((err, list) => { - // expect(err).to.not.exist() - // expect(list.Peers).to.deep.equal(defaultList) - // done() - // }) + it('name publish should publish correctly', (done) => { + node.name.publish(ipfsRef, false, '5m', '1m', 'self', (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res).to.equal(nodeId) + done() + }) + }) + + it('name resolve should be performed correctly', (done) => { + node.name.publish(ipfsRef, false, '5m', '1m', 'self', (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + + node.name.resolve(nodeId, false, false, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res).to.equal(ipfsRef) + done() + }) + }) }) }) diff --git a/test/http-api/inject/name.js b/test/http-api/inject/name.js index f4b566be9e..d95a601d22 100644 --- a/test/http-api/inject/name.js +++ b/test/http-api/inject/name.js @@ -11,7 +11,7 @@ chai.use(dirtyChai) const checkAll = (bits) => string => bits.every(bit => string.includes(bit)) module.exports = (http) => { - describe.only('/name', function () { + describe('/name', function () { const cid = 'QmbndGRXYRyfU41TUvc52gMrwq87JJg18QsDPcCeaMcM61' let api From fab0b7654dd25f39c08dfb79fdb203c8926ffead Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 20 Jul 2018 17:40:35 +0100 Subject: [PATCH 11/34] fix: updated core arguments to an options object --- src/core/components/name.js | 42 +++++++++++++++++++++++----------- src/http/api/resources/name.js | 8 +++---- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/core/components/name.js b/src/core/components/name.js index 7d1c554bee..ae14636bc4 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -51,18 +51,26 @@ module.exports = function name (self) { * which is the hash of its public key. * * @param {String} value ipfs path of the object to be published. - * @param {boolean} resolve resolve given path before publishing. - * @param {String} lifetime time duration that the record will be valid for. + * @param {Object} options ipfs publish options. + * @param {boolean} options.resolve resolve given path before publishing. + * @param {String} options.lifetime time duration that the record will be valid for. This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are "ns", "ms", "s", "m", "h". Default is 24h. - * @param {String} ttl time duration this record should be cached for (NOT IMPLEMENTED YET). + * @param {String} options.ttl time duration this record should be cached for (NOT IMPLEMENTED YET). * This accepts durations such as "300s", "1.5h" or "2h45m". Valid time units are "ns", "ms", "s", "m", "h" (caution: experimental). - * @param {String} key name of the key to be used or a valid PeerID, as listed by 'ipfs key list -l'. + * @param {String} options.key name of the key to be used or a valid PeerID, as listed by 'ipfs key list -l'. * @param {function(Error)} [callback] * @returns {Promise|void} */ - publish: promisify((value, resolve = true, lifetime = '24h', ttl, key = 'self', callback) => { + publish: promisify((value, options, callback) => { + if (typeof options === 'function') { + callback = options + options = {} + } + + const { resolve = true, lifetime = '24h', key = 'self' } = options; + if (!self.isOnline()) { const error = errors.OFFLINE_ERROR @@ -107,12 +115,20 @@ module.exports = function name (self) { * Given a key, query the DHT for its best value. * * @param {String} name ipns name to resolve. Defaults to your node's peerID. - * @param {boolean} nocache do not use cached entries. - * @param {boolean} recursive resolve until the result is not an IPNS name. + * @param {Object} options ipfs resolve options. + * @param {boolean} options.nocache do not use cached entries. + * @param {boolean} options.recursive resolve until the result is not an IPNS name. * @param {function(Error)} [callback] * @returns {Promise|void} */ - resolve: promisify((name, nocache = false, recursive = false, callback) => { + resolve: promisify((name, options, callback) => { + if (typeof options === 'function') { + callback = options + options = {} + } + + const { nocache = false, recursive = false } = options; + const local = true // TODO ROUTING - use self._options.local if (!self.isOnline()) { @@ -140,13 +156,13 @@ module.exports = function name (self) { // TODO ROUTING - public key from network instead const localPublicKey = self._peerInfo.id.pubKey - const options = { - local: local, - nocache: nocache, - recursive: recursive + const resolveOptions = { + nocache, + recursive, + local } - self._ipns.resolve(name, localPublicKey, options, callback) + self._ipns.resolve(name, localPublicKey, resolveOptions, callback) }) } } diff --git a/src/http/api/resources/name.js b/src/http/api/resources/name.js index 8159234873..8d819bc00c 100644 --- a/src/http/api/resources/name.js +++ b/src/http/api/resources/name.js @@ -14,9 +14,9 @@ exports.resolve = { }, handler: (request, reply) => { const ipfs = request.server.app.ipfs - const { arg, nocache, recursive } = request.query + const { arg } = request.query - ipfs.name.resolve(arg, nocache, recursive, (err, res) => { + ipfs.name.resolve(arg, request.query, (err, res) => { if (err) { return reply({ Message: err.toString(), @@ -43,9 +43,9 @@ exports.publish = { }, handler: (request, reply) => { const ipfs = request.server.app.ipfs - const { arg, resolve, lifetime, ttl, key } = request.query + const { arg } = request.query - ipfs.name.publish(arg, resolve, lifetime, ttl, key, (err, res) => { + ipfs.name.publish(arg, request.query, (err, res) => { if (err) { return reply({ Message: err.toString(), From bc4a9eeb6a8802b9beb3f29ffeddf2b35a4f9b3f Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Sat, 21 Jul 2018 15:35:27 +0100 Subject: [PATCH 12/34] fix: core returns and refactor tests --- src/core/components/name.js | 4 ++-- src/core/ipns/index.js | 14 +++++++++++--- src/http/api/resources/name.js | 9 ++------- test/cli/name.js | 14 +++++++------- test/core/ipns.spec.js | 14 +++++++------- test/http-api/inject/name.js | 10 +++++----- 6 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/core/components/name.js b/src/core/components/name.js index ae14636bc4..81dc4554c7 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -69,7 +69,7 @@ module.exports = function name (self) { options = {} } - const { resolve = true, lifetime = '24h', key = 'self' } = options; + const { resolve = true, lifetime = '24h', key = 'self' } = options if (!self.isOnline()) { const error = errors.OFFLINE_ERROR @@ -127,7 +127,7 @@ module.exports = function name (self) { options = {} } - const { nocache = false, recursive = false } = options; + const { nocache = false, recursive = false } = options const local = true // TODO ROUTING - use self._options.local diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index bbf084f5d5..82e5928b75 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -42,7 +42,11 @@ class IPNS { log(`IPNS value ${value} was cached correctly`) this.cache.set(id, value, { ttl: ttl }) - callback(null, id) + + callback(null, { + Name: id, + Value: value + }) }) } @@ -55,7 +59,9 @@ class IPNS { const result = this.cache.get(id) if (result) { - return callback(null, result) + return callback(null, { + Path: result + }) } } @@ -67,7 +73,9 @@ class IPNS { log(`IPNS record from ${name} was resolved correctly`) - return callback(null, result) + return callback(null, { + Path: result + }) }) } } diff --git a/src/http/api/resources/name.js b/src/http/api/resources/name.js index 8d819bc00c..5eaa0bf239 100644 --- a/src/http/api/resources/name.js +++ b/src/http/api/resources/name.js @@ -24,9 +24,7 @@ exports.resolve = { }).code(500) } - return reply({ - Path: res - }).code(200) + return reply(res).code(200) }) } } @@ -53,10 +51,7 @@ exports.publish = { }).code(500) } - return reply({ - Name: res, - Value: arg - }).code(200) + return reply(res).code(200) }) } } diff --git a/test/cli/name.js b/test/cli/name.js index 5ae06c985a..be1c7fea98 100644 --- a/test/cli/name.js +++ b/test/cli/name.js @@ -69,7 +69,7 @@ describe('name', () => { } }) - it('name publish should publish correctly when the file was already added', function (done) { + it('should publish correctly when the file was already added', function (done) { this.timeout(60 * 1000) ipfs(`name publish ${cidAdded}`).then((res) => { @@ -80,7 +80,7 @@ describe('name', () => { }) }) - it('name resolve should get the entry correctly', function (done) { + it('should get the entry correctly', function (done) { this.timeout(60 * 1000) ipfs(`name publish ${cidAdded}`).then((res) => { @@ -95,7 +95,7 @@ describe('name', () => { }) }) - it('name publish should publish correctly when the file was not added but resolve is disabled', function (done) { + it('should publish correctly when the file was not added but resolve is disabled', function (done) { this.timeout(60 * 1000) const notAddedCid = 'QmPFVLPmp9zv5Z5KUqLhe2EivAGccQW2r7M7jhVJGLZoZU' @@ -108,7 +108,7 @@ describe('name', () => { }) }) - it('name resolve should not get the entry correctly if its validity time expired', function (done) { + it('should not get the entry correctly if its validity time expired', function (done) { this.timeout(60 * 1000) ipfs(`name publish ${cidAdded} --lifetime 10ns`).then((res) => { @@ -127,7 +127,7 @@ describe('name', () => { }) }) - it('name publish should publish correctly when a new key is used', function (done) { + it('should publish correctly when a new key is used', function (done) { this.timeout(60 * 1000) ipfs(`name publish ${cidAdded} --key ${name}`).then((res) => { @@ -138,7 +138,7 @@ describe('name', () => { }) }) - it('name resolve should return the immediate pointing record, unless using the recursive parameter', function (done) { + it('should return the immediate pointing record, unless using the recursive parameter', function (done) { this.timeout(60 * 1000) ipfs(`name publish ${cidAdded}`).then((res) => { @@ -159,7 +159,7 @@ describe('name', () => { }) }) - it('name resolve should go recursively until finding an ipfs hash', function (done) { + it('should go recursively until finding an ipfs hash', function (done) { this.timeout(60 * 1000) ipfs(`name publish ${cidAdded}`).then((res) => { diff --git a/test/core/ipns.spec.js b/test/core/ipns.spec.js index 7ccd4b97e0..df1769f598 100644 --- a/test/core/ipns.spec.js +++ b/test/core/ipns.spec.js @@ -43,24 +43,24 @@ describe('ipns', () => { after((done) => ipfsd.stop(done)) - it('name publish should publish correctly', (done) => { - node.name.publish(ipfsRef, false, '5m', '1m', 'self', (err, res) => { + it('should publish correctly with the default params', (done) => { + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { expect(err).to.not.exist() expect(res).to.exist() - expect(res).to.equal(nodeId) + expect(res.Name).to.equal(nodeId) done() }) }) - it('name resolve should be performed correctly', (done) => { - node.name.publish(ipfsRef, false, '5m', '1m', 'self', (err, res) => { + it('name resolve should return the ipfs path', (done) => { + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { expect(err).to.not.exist() expect(res).to.exist() - node.name.resolve(nodeId, false, false, (err, res) => { + node.name.resolve(nodeId, (err, res) => { expect(err).to.not.exist() expect(res).to.exist() - expect(res).to.equal(ipfsRef) + expect(res.Path).to.equal(ipfsRef) done() }) }) diff --git a/test/http-api/inject/name.js b/test/http-api/inject/name.js index d95a601d22..ebacfc753b 100644 --- a/test/http-api/inject/name.js +++ b/test/http-api/inject/name.js @@ -19,31 +19,31 @@ module.exports = (http) => { api = http.api.server.select('API') }) - it('publish a record correctly', (done) => { + it('should publish a record correctly', (done) => { api.inject({ method: 'GET', url: `/api/v0/name/publish?arg=${cid}&resolve=false` }, (res) => { expect(res).to.exist() - expect(res.result.Value).to.equal(cid) + expect(res.result.Value).to.equal(`/ipfs/${cid}`) done() }) }) - it('resolve a record correctly', (done) => { + it('should resolve a record correctly', (done) => { api.inject({ method: 'GET', url: `/api/v0/name/publish?arg=${cid}&resolve=false` }, (res) => { expect(res).to.exist() - expect(res.result.Value).to.equal(cid) + expect(res.result.Value).to.equal(`/ipfs/${cid}`) api.inject({ method: 'GET', url: `/api/v0/name/resolve` }, (res) => { expect(res).to.exist() - expect(res.result.Path).to.satisfy(checkAll([cid])) + expect(res.result.Path).to.satisfy(checkAll([`/ipfs/${cid}`])) done() }) From 7f2aa3d48a7fc5eff52fd11c98a4757078193c61 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 23 Jul 2018 18:42:40 +0100 Subject: [PATCH 13/34] fix: core with camelCase responses --- src/core/ipns/index.js | 8 ++++---- src/http/api/resources/name.js | 9 +++++++-- test/cli/name.js | 2 +- test/core/interface.spec.js | 1 - test/core/ipns.spec.js | 6 +++--- test/http-api/inject/name.js | 4 ++-- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index 82e5928b75..132edbbb7d 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -44,8 +44,8 @@ class IPNS { this.cache.set(id, value, { ttl: ttl }) callback(null, { - Name: id, - Value: value + name: id, + value: value }) }) } @@ -60,7 +60,7 @@ class IPNS { if (result) { return callback(null, { - Path: result + path: result }) } } @@ -74,7 +74,7 @@ class IPNS { log(`IPNS record from ${name} was resolved correctly`) return callback(null, { - Path: result + path: result }) }) } diff --git a/src/http/api/resources/name.js b/src/http/api/resources/name.js index 5eaa0bf239..ede04ed597 100644 --- a/src/http/api/resources/name.js +++ b/src/http/api/resources/name.js @@ -24,7 +24,9 @@ exports.resolve = { }).code(500) } - return reply(res).code(200) + return reply({ + Path: res.path + }).code(200) }) } } @@ -51,7 +53,10 @@ exports.publish = { }).code(500) } - return reply(res).code(200) + return reply({ + Name: res.name, + Value: res.value + }).code(200) }) } } diff --git a/test/cli/name.js b/test/cli/name.js index be1c7fea98..4e2a875ee0 100644 --- a/test/cli/name.js +++ b/test/cli/name.js @@ -80,7 +80,7 @@ describe('name', () => { }) }) - it('should get the entry correctly', function (done) { + it('should publish and resolve an entry with the default options', function (done) { this.timeout(60 * 1000) ipfs(`name publish ${cidAdded}`).then((res) => { diff --git a/test/core/interface.spec.js b/test/core/interface.spec.js index ad23671103..2cf613f30b 100644 --- a/test/core/interface.spec.js +++ b/test/core/interface.spec.js @@ -72,7 +72,6 @@ describe('interface-ipfs-core tests', () => { initOptions: { bits: 512 } } })) */ - tests.object(defaultCommonFactory) tests.pin(defaultCommonFactory) diff --git a/test/core/ipns.spec.js b/test/core/ipns.spec.js index df1769f598..8a0af4f901 100644 --- a/test/core/ipns.spec.js +++ b/test/core/ipns.spec.js @@ -47,12 +47,12 @@ describe('ipns', () => { node.name.publish(ipfsRef, { resolve: false }, (err, res) => { expect(err).to.not.exist() expect(res).to.exist() - expect(res.Name).to.equal(nodeId) + expect(res.name).to.equal(nodeId) done() }) }) - it('name resolve should return the ipfs path', (done) => { + it('should publish and then resolve correctly', (done) => { node.name.publish(ipfsRef, { resolve: false }, (err, res) => { expect(err).to.not.exist() expect(res).to.exist() @@ -60,7 +60,7 @@ describe('ipns', () => { node.name.resolve(nodeId, (err, res) => { expect(err).to.not.exist() expect(res).to.exist() - expect(res.Path).to.equal(ipfsRef) + expect(res.path).to.equal(ipfsRef) done() }) }) diff --git a/test/http-api/inject/name.js b/test/http-api/inject/name.js index ebacfc753b..c4efe81b84 100644 --- a/test/http-api/inject/name.js +++ b/test/http-api/inject/name.js @@ -19,7 +19,7 @@ module.exports = (http) => { api = http.api.server.select('API') }) - it('should publish a record correctly', (done) => { + it('should publish a record', (done) => { api.inject({ method: 'GET', url: `/api/v0/name/publish?arg=${cid}&resolve=false` @@ -30,7 +30,7 @@ module.exports = (http) => { }) }) - it('should resolve a record correctly', (done) => { + it('should publish and resolve a record', (done) => { api.inject({ method: 'GET', url: `/api/v0/name/publish?arg=${cid}&resolve=false` From fdf3b90719765dce37b550b6f9a1218d5ea3fa77 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 24 Jul 2018 15:57:50 +0100 Subject: [PATCH 14/34] fix: add initializeKeyspace to init --- src/core/components/init.js | 10 +++++++--- src/core/ipns/index.js | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/core/components/init.js b/src/core/components/init.js index dd3471db6c..f645e1941a 100644 --- a/src/core/components/init.js +++ b/src/core/components/init.js @@ -6,6 +6,8 @@ const parallel = require('async/parallel') const promisify = require('promisify-es6') const defaultConfig = require('../runtime/config-nodejs.js') const Keychain = require('libp2p-keychain') +const Unixfs = require('ipfs-unixfs') +const { DAGNode } = require('ipld-dag-pb') const addDefaultAssets = require('./init-assets') @@ -81,8 +83,8 @@ module.exports = function init (self) { PeerID: keys.toB58String(), PrivKey: keys.privKey.bytes.toString('base64') } + privateKey = keys.privKey if (opts.pass) { - privateKey = keys.privKey config.Keychain = Keychain.generateOptions() } opts.log('done') @@ -102,14 +104,16 @@ module.exports = function init (self) { cb(null, true) } }, - (_, cb) => { + (_, cb) => DAGNode.create(new Unixfs('directory').marshal(), cb), + (emptyDirNode, cb) => { if (opts.emptyRepo) { return cb(null, true) } const tasks = [ // add empty unixfs dir object (go-ipfs assumes this exists) - (cb) => self.object.new('unixfs-dir', cb) + (cb) => self.object.new('unixfs-dir', cb), + (cb) => self._ipns.initializeKeyspace(privateKey, emptyDirNode.toJSON().multihash, cb) ] if (typeof addDefaultAssets === 'function') { diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index 132edbbb7d..9394b119de 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -78,6 +78,11 @@ class IPNS { }) }) } + + // Initialize keyspace - sets the ipns record for the given key to point to an empty directory + initializeKeyspace (privKey, value, callback) { + this.ipnsPublisher.publish(privKey, value, callback) + } } exports = module.exports = IPNS From 8204f85c88fe04c6723f9183f390b262cb5c0c26 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Sat, 28 Jul 2018 00:12:54 +0100 Subject: [PATCH 15/34] wip --- src/core/components/init.js | 9 ++-- src/core/ipns/publisher.js | 92 ++++++++++++++++++++++++++++--------- 2 files changed, 75 insertions(+), 26 deletions(-) diff --git a/src/core/components/init.js b/src/core/components/init.js index f645e1941a..cd8010afec 100644 --- a/src/core/components/init.js +++ b/src/core/components/init.js @@ -54,6 +54,7 @@ module.exports = function init (self) { opts.log = opts.log || function () {} const config = defaultConfig() let privateKey + waterfall([ // Verify repo does not yet exist. (cb) => self._repo.exists(cb), @@ -77,13 +78,13 @@ module.exports = function init (self) { peerId.create({ bits: opts.bits }, cb) } }, - (keys, cb) => { + (peerId, cb) => { self.log('identity generated') config.Identity = { - PeerID: keys.toB58String(), - PrivKey: keys.privKey.bytes.toString('base64') + PeerID: peerId.toB58String(), + PrivKey: peerId.privKey.bytes.toString('base64') } - privateKey = keys.privKey + privateKey = peerId.privKey if (opts.pass) { config.Keychain = Keychain.generateOptions() } diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index 3aa1071107..7b90f88002 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -1,7 +1,7 @@ 'use strict' const peerId = require('peer-id') -const waterfall = require('async/waterfall') +const series = require('async/series') const debug = require('debug') const log = debug('jsipfs:ipns:publisher') @@ -25,14 +25,18 @@ class IpnsPublisher { // publish record with a eol publishWithEOL (privKey, value, lifetime, callback) { - this.updateOrCreateRecord(privKey, value, lifetime, (err, record) => { + peerId.createFromPrivKey(privKey.bytes.toString('base64'), (err, peerIdResult) => { if (err) { - return callback(err) + callback(err) } - // TODO ROUTING - Add record (with public key) + this.updateOrCreateRecord(privKey, value, lifetime, peerIdResult, (err, record) => { + if (err) { + return callback(err) + } - callback(null, record) + this.putRecordToRouting(record, peerIdResult, callback) + }) }) } @@ -41,6 +45,62 @@ class IpnsPublisher { this.publishWithEOL(privKey, value, defaultRecordTtl, callback) } + putRecordToRouting(record, peerIdResult, callback) { + const publicKey = peerIdResult._pubKey + + ipns.embedPublicKey(publicKey, record, (err, embedPublicKeyRecord) => { + if (err) { + return callback(err) + } + + const { ipnsKey, pkKey } = ipns.getIdKeys(peerIdResult.id) + + series([ + (cb) => this.publishEntry(ipnsKey, record, cb), + // Publish the public key if a public key cannot be extracted from the ID + // We will be able to deprecate this part in the future, since the public keys will be only in the peerId + (cb) => embedPublicKeyRecord ? this.publishPublicKey(pkKey, publicKey, cb) : cb(), + ], (err) => { + if (err) { + return callback(err) + } + + return callback(null, embedPublicKeyRecord || record) + }) + }) + } + + publishEntry (key, entry, callback) { + // Marshal record + const data = ipns.marshal(entry) + + // TODO Routing - this should be replaced by a put to the DHT + this.repo.datastore.put(key, data, (err, res) => { + if (err) { + log.error(`ipns record for ${value} could not be stored in the routing`) + return callback(Object.assign(new Error(`ipns record for ${value} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) + } + + log(`ipns record for ${key.toString()} was stored in the routing`) + return callback(null, res) + }) + } + + publishPublicKey (key, publicKey, callback) { + console.log('publish public key'); + + // TODO Routing - this should be replaced by a put to the DHT + this.repo.datastore.put(key, publicKey.bytes, (err, res) => { + if (err) { + log.error(`public key for ${value} could not be stored in the routing`) + return callback(Object.assign(new Error(`public key for ${value} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) + } + + log(`public key for ${key.toString()} was stored in the routing`) + return callback(null, res) + }) + } + // Returns the record this node has published corresponding to the given peer ID. // If `checkRouting` is true and we have no existing record, this method will check the routing system for any existing records. getPublished (peerIdResult, checkRouting, callback) { @@ -58,9 +118,7 @@ class IpnsPublisher { } } else if (err.notFound) { if (!checkRouting) { - return callback(null, { - peerIdResult: peerIdResult - }) + return callback(null, null) } // TODO ROUTING - get } else { @@ -77,29 +135,19 @@ class IpnsPublisher { const error = `found ipns record that we couldn't convert to a value` log.error(error) - return callback(null, { - peerIdResult: peerIdResult - }) + return callback(null, null) } - return callback(null, { - peerIdResult: peerIdResult, - record: result - }) + return callback(null, result) }) } - updateOrCreateRecord (privKey, value, validity, callback) { - waterfall([ - (cb) => peerId.createFromPrivKey(privKey.bytes.toString('base64'), cb), - (id, cb) => this.getPublished(id, false, cb) // TODO ROUTING - change to true - ], (err, result) => { + updateOrCreateRecord(privKey, value, validity, peerIdResult, callback) { + this.getPublished(peerIdResult, false, (err, record) => { // TODO ROUTING - change to true if (err) { callback(err) } - const { peerIdResult, record } = result - // Determinate the record sequence number let seqNumber = 0 if (record && record.sequence !== undefined) { From 61f4d4e579a5505d3820911c42126c4978f2bc8e Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 6 Aug 2018 22:09:47 +0100 Subject: [PATCH 16/34] wip --- package.json | 1 + src/core/components/name.js | 4 +--- src/core/ipns/index.js | 4 ++-- src/core/ipns/publisher.js | 20 +++++++++++++------- src/core/ipns/resolver.js | 32 +++++++++++++++++++------------- 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index ab7e480fcf..f334a0d7b3 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "libp2p-keychain": "~0.3.1", "libp2p-mdns": "~0.12.0", "libp2p-mplex": "~0.8.0", + "libp2p-record": "~0.5.1", "libp2p-secio": "~0.10.0", "libp2p-tcp": "~0.12.0", "libp2p-webrtc-star": "~0.15.3", diff --git a/src/core/components/name.js b/src/core/components/name.js index 81dc4554c7..d4f7237d73 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -154,15 +154,13 @@ module.exports = function name (self) { name = `/ipns/${name}` } - // TODO ROUTING - public key from network instead - const localPublicKey = self._peerInfo.id.pubKey const resolveOptions = { nocache, recursive, local } - self._ipns.resolve(name, localPublicKey, resolveOptions, callback) + self._ipns.resolve(name, self._peerInfo.id, resolveOptions, callback) }) } } diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index 9394b119de..abed722b71 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -51,7 +51,7 @@ class IPNS { } // Resolve - resolve (name, pubKey, options, callback) { + resolve (name, peerId, options, callback) { // If recursive, we should not try to get the cached value if (!options.nocache && !options.recursive) { // Try to get the record from cache @@ -65,7 +65,7 @@ class IPNS { } } - this.ipnsResolver.resolve(name, pubKey, options, (err, result) => { + this.ipnsResolver.resolve(name, peerId, options, (err, result) => { if (err) { log.error(err) return callback(err) diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index 7b90f88002..1f7f074524 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -1,6 +1,7 @@ 'use strict' const peerId = require('peer-id') +const Record = require('libp2p-record').Record const series = require('async/series') const debug = require('debug') @@ -56,10 +57,10 @@ class IpnsPublisher { const { ipnsKey, pkKey } = ipns.getIdKeys(peerIdResult.id) series([ - (cb) => this.publishEntry(ipnsKey, record, cb), + (cb) => this.publishEntry(ipnsKey, embedPublicKeyRecord || record, peerIdResult, cb), // Publish the public key if a public key cannot be extracted from the ID // We will be able to deprecate this part in the future, since the public keys will be only in the peerId - (cb) => embedPublicKeyRecord ? this.publishPublicKey(pkKey, publicKey, cb) : cb(), + (cb) => embedPublicKeyRecord ? this.publishPublicKey(pkKey, publicKey, peerIdResult, cb) : cb(), ], (err) => { if (err) { return callback(err) @@ -70,12 +71,15 @@ class IpnsPublisher { }) } - publishEntry (key, entry, callback) { + publishEntry(key, entry, peerIdResult, callback) { // Marshal record - const data = ipns.marshal(entry) + const entryData = ipns.marshal(entry) + + // Marshal to libp2p record + const rec = new Record(key.toBuffer(), entryData, peerIdResult) // TODO Routing - this should be replaced by a put to the DHT - this.repo.datastore.put(key, data, (err, res) => { + this.repo.datastore.put(key, rec.serialize(), (err, res) => { if (err) { log.error(`ipns record for ${value} could not be stored in the routing`) return callback(Object.assign(new Error(`ipns record for ${value} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) @@ -86,11 +90,13 @@ class IpnsPublisher { }) } - publishPublicKey (key, publicKey, callback) { + publishPublicKey(key, publicKey, peerIdResult, callback) { console.log('publish public key'); + // Marshal to libp2p record + const rec = new Record(key.toBuffer(), publicKey.bytes, peerIdResult) // TODO Routing - this should be replaced by a put to the DHT - this.repo.datastore.put(key, publicKey.bytes, (err, res) => { + this.repo.datastore.put(key, rec.serialize(), (err, res) => { if (err) { log.error(`public key for ${value} could not be stored in the routing`) return callback(Object.assign(new Error(`public key for ${value} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index a66722e681..e481828a97 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -2,6 +2,7 @@ const ipns = require('ipns') const { fromB58String } = require('multihashes') +const Record = require('libp2p-record').Record const debug = require('debug') const log = debug('jsipfs:ipns:resolver') @@ -21,7 +22,7 @@ class IpnsResolver { this._resolver = undefined // Add Router resolver } - resolve (name, publicKey, options, callback) { + resolve (name, peerId, options, callback) { const nameSegments = name.split('/') if (nameSegments.length !== 3 || nameSegments[0] !== '') { @@ -53,9 +54,7 @@ class IpnsResolver { return callback(new Error('not implemented yet')) } - // TODO Routing - Get public key from routing - - this.resolver(key, publicKey, depth, resolverFn, (err, res) => { + this.resolver(key, depth, peerId, resolverFn, (err, res) => { if (err) { return callback(err) } @@ -66,7 +65,7 @@ class IpnsResolver { } // Recursive resolver according to the specified depth - resolver (name, publicKey, depth, resolverFn, callback) { + resolver(name, depth, peerId, resolverFn, callback) { // bind resolver function this._resolver = resolverFn @@ -78,7 +77,7 @@ class IpnsResolver { return callback(Object.assign(new Error(error), { code: ERR_RESOLVE_RECURSION_LIMIT })) } - this._resolver(name, publicKey, (err, res) => { + this._resolver(name, peerId, (err, res) => { if (err) { return callback(err) } @@ -91,13 +90,13 @@ class IpnsResolver { } // continue recursively until depth equals 0 - return this.resolver(nameSegments[2], publicKey, depth - 1, resolverFn, callback) + return this.resolver(nameSegments[2], depth - 1, peerId, resolverFn, callback) }) } // resolve ipns entries locally using the datastore - resolveLocal (name, publicKey, callback) { - const ipnsKey = ipns.getLocalKey(fromB58String(name)) + resolveLocal(name, peerId, callback) { + const { ipnsKey } = ipns.getIdKeys(fromB58String(name)) this.repo.datastore.get(ipnsKey, (err, dsVal) => { if (err) { @@ -114,15 +113,22 @@ class IpnsResolver { return callback(Object.assign(new Error(error), { code: ERR_INVALID_RECORD_RECEIVED })) } - const ipnsEntry = ipns.unmarshal(dsVal) + const record = Record.deserialize(dsVal) + const ipnsEntry = ipns.unmarshal(record.value) - // Record validation - ipns.validate(publicKey, ipnsEntry, (err) => { + ipns.extractPublicKey(peerId, ipnsEntry, (err, pubKey) => { if (err) { return callback(err) } - return callback(null, ipnsEntry.value.toString()) + // Record validation + ipns.validate(pubKey, ipnsEntry, (err) => { + if (err) { + return callback(err) + } + + return callback(null, ipnsEntry.value.toString()) + }) }) }) } From 08a952bd7851102dcf0f781f7b4089b4ab3d1f26 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 7 Aug 2018 17:42:16 +0100 Subject: [PATCH 17/34] added republisher --- package.json | 2 +- src/core/components/name.js | 7 +- src/core/components/start.js | 1 + src/core/index.js | 2 +- src/core/ipns/index.js | 18 ++--- src/core/ipns/publisher.js | 27 ++++---- src/core/ipns/republisher.js | 128 +++++++++++++++++++++++++++++++++++ src/core/ipns/resolver.js | 4 +- test/cli/commands.js | 2 +- test/cli/name.js | 2 +- 10 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 src/core/ipns/republisher.js diff --git a/package.json b/package.json index f334a0d7b3..bce46a2eab 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "ipld": "~0.17.3", "ipld-dag-cbor": "~0.12.1", "ipld-dag-pb": "~0.14.6", - "ipns": "^0.1.1", + "ipns": "~0.1.1", "is-ipfs": "~0.4.2", "is-pull-stream": "~0.0.0", "is-stream": "^1.1.0", diff --git a/src/core/components/name.js b/src/core/components/name.js index d4f7237d73..2c741ac7e1 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -22,9 +22,6 @@ const keyLookup = (ipfsNode, kname, callback) => { return callback(null, ipfsNode._peerInfo.id.privKey) } - // jsipfs key gen --type=rsa --size=2048 mykey --pass 12345678901234567890 - // jsipfs daemon --pass 12345678901234567890 - // jsipfs name publish QmPao1o1nEdDYAToEDf34CovQHaycmhr7sagbD3DZAEW9L --key mykey const pass = ipfsNode._options.pass waterfall([ @@ -91,14 +88,14 @@ module.exports = function name (self) { // (cb) => ttl ? human(ttl, cb) : cb(), (cb) => keyLookup(self, key, cb), // verify if the path exists, if not, an error will stop the execution - (cb) => resolve === true || resolve === 'true' ? path.resolvePath(self, value, cb) : cb() + (cb) => resolve.toString() === 'true' ? path.resolvePath(self, value, cb) : cb() ], (err, results) => { if (err) { log.error(err) return callback(err) } - // Calculate eol with nanoseconds precision + // Calculate lifetime with nanoseconds precision const pubLifetime = results[0].toFixed(6) const privateKey = results[1] diff --git a/src/core/components/start.js b/src/core/components/start.js index 3a7a5716ce..7b4df7cb61 100644 --- a/src/core/components/start.js +++ b/src/core/components/start.js @@ -44,6 +44,7 @@ module.exports = (self) => { self._blockService.setExchange(self._bitswap) self._preload.start() + self._ipns.republisher.start() self._mfsPreload.start(cb) } ], done) diff --git a/src/core/index.js b/src/core/index.js index a5d5f5c346..b20262459b 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -90,7 +90,7 @@ class IPFS extends EventEmitter { this._pubsub = undefined this._preload = preload(this) this._mfsPreload = mfsPreload(this) - this._ipns = new IPNS(null, this._repo) + this._ipns = new IPNS(null, this) // IPFS Core exposed components // - for booting up a node diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index abed722b71..04960f0cf3 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -9,15 +9,17 @@ const log = debug('jsipfs:ipns') log.error = debug('jsipfs:ipns:error') const IpnsPublisher = require('./publisher') +const IpnsRepublisher = require('./republisher') const IpnsResolver = require('./resolver') const path = require('./path') const defaultRecordTtl = 60 * 1000 class IPNS { - constructor (routing, repo, peerInfo) { - this.ipnsPublisher = new IpnsPublisher(routing, repo) - this.ipnsResolver = new IpnsResolver(routing, repo) + constructor (routing, ipfs) { + this.publisher = new IpnsPublisher(routing, ipfs._repo) + this.republisher = new IpnsRepublisher(this.publisher, ipfs) + this.resolver = new IpnsResolver(routing, ipfs._repo) this.cache = new Receptacle({ max: 1000 }) // Create an LRU cache with max 1000 items } @@ -25,7 +27,7 @@ class IPNS { publish (privKey, value, lifetime, callback) { series([ (cb) => createFromPrivKey(privKey.bytes.toString('base64'), cb), - (cb) => this.ipnsPublisher.publishWithEOL(privKey, value, lifetime, cb) + (cb) => this.publisher.publishWithEOL(privKey, value, lifetime, cb) ], (err, results) => { if (err) { log.error(err) @@ -39,10 +41,10 @@ class IPNS { const ttEol = parseFloat(lifetime) const ttl = (ttEol < defaultRecordTtl) ? ttEol : defaultRecordTtl - log(`IPNS value ${value} was cached correctly`) - this.cache.set(id, value, { ttl: ttl }) + log(`IPNS value ${value} was cached correctly`) + callback(null, { name: id, value: value @@ -65,7 +67,7 @@ class IPNS { } } - this.ipnsResolver.resolve(name, peerId, options, (err, result) => { + this.resolver.resolve(name, peerId, options, (err, result) => { if (err) { log.error(err) return callback(err) @@ -81,7 +83,7 @@ class IPNS { // Initialize keyspace - sets the ipns record for the given key to point to an empty directory initializeKeyspace (privKey, value, callback) { - this.ipnsPublisher.publish(privKey, value, callback) + this.publisher.publish(privKey, value, callback) } } diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index 1f7f074524..f3b72d1ae7 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -46,21 +46,21 @@ class IpnsPublisher { this.publishWithEOL(privKey, value, defaultRecordTtl, callback) } - putRecordToRouting(record, peerIdResult, callback) { + putRecordToRouting (record, peerIdResult, callback) { const publicKey = peerIdResult._pubKey ipns.embedPublicKey(publicKey, record, (err, embedPublicKeyRecord) => { if (err) { return callback(err) } - + const { ipnsKey, pkKey } = ipns.getIdKeys(peerIdResult.id) - + series([ (cb) => this.publishEntry(ipnsKey, embedPublicKeyRecord || record, peerIdResult, cb), // Publish the public key if a public key cannot be extracted from the ID // We will be able to deprecate this part in the future, since the public keys will be only in the peerId - (cb) => embedPublicKeyRecord ? this.publishPublicKey(pkKey, publicKey, peerIdResult, cb) : cb(), + (cb) => embedPublicKeyRecord ? this.publishPublicKey(pkKey, publicKey, peerIdResult, cb) : cb() ], (err) => { if (err) { return callback(err) @@ -71,18 +71,18 @@ class IpnsPublisher { }) } - publishEntry(key, entry, peerIdResult, callback) { + publishEntry (key, entry, peerIdResult, callback) { // Marshal record const entryData = ipns.marshal(entry) // Marshal to libp2p record const rec = new Record(key.toBuffer(), entryData, peerIdResult) - + // TODO Routing - this should be replaced by a put to the DHT this.repo.datastore.put(key, rec.serialize(), (err, res) => { if (err) { - log.error(`ipns record for ${value} could not be stored in the routing`) - return callback(Object.assign(new Error(`ipns record for ${value} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) + log.error(`ipns record for ${key.toString()} could not be stored in the routing`) + return callback(Object.assign(new Error(`ipns record for ${key.toString()} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) } log(`ipns record for ${key.toString()} was stored in the routing`) @@ -90,16 +90,15 @@ class IpnsPublisher { }) } - publishPublicKey(key, publicKey, peerIdResult, callback) { - console.log('publish public key'); + publishPublicKey (key, publicKey, peerIdResult, callback) { // Marshal to libp2p record const rec = new Record(key.toBuffer(), publicKey.bytes, peerIdResult) - + // TODO Routing - this should be replaced by a put to the DHT this.repo.datastore.put(key, rec.serialize(), (err, res) => { if (err) { - log.error(`public key for ${value} could not be stored in the routing`) - return callback(Object.assign(new Error(`public key for ${value} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) + log.error(`public key for ${key.toString()} could not be stored in the routing`) + return callback(Object.assign(new Error(`public key for ${key.toString()} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) } log(`public key for ${key.toString()} was stored in the routing`) @@ -148,7 +147,7 @@ class IpnsPublisher { }) } - updateOrCreateRecord(privKey, value, validity, peerIdResult, callback) { + updateOrCreateRecord (privKey, value, validity, peerIdResult, callback) { this.getPublished(peerIdResult, false, (err, record) => { // TODO ROUTING - change to true if (err) { callback(err) diff --git a/src/core/ipns/republisher.js b/src/core/ipns/republisher.js new file mode 100644 index 0000000000..5b4d96374d --- /dev/null +++ b/src/core/ipns/republisher.js @@ -0,0 +1,128 @@ +'use strict' + +const ipns = require('ipns') +const crypto = require('libp2p-crypto') +const peerId = require('peer-id') + +const debug = require('debug') +const each = require('async/each') +const waterfall = require('async/waterfall') +const log = debug('jsipfs:ipns:republisher') +log.error = debug('jsipfs:ipns:republisher:error') + +const minute = 60 * 1000 +const hour = 60 * minute + +const defaultBroadcastInterval = 4 * hour +const defaultRecordLifetime = 24 * hour + +const ERR_NO_ENTRY_FOUND = 'ERR_NO_ENTRY_FOUND' +const ERR_INVALID_IPNS_RECORD = 'ERR_INVALID_IPNS_RECORD' + +class IpnsRepublisher { + constructor (publisher, ipfs) { + this.publisher = publisher + this.ipfs = ipfs + this.repo = ipfs._repo + } + + start () { + setInterval(() => { + this.republishEntries(this.ipfs._peerInfo.id.privKey, this.ipfs._options.pass) + }, defaultBroadcastInterval) + } + + republishEntries (privateKey, pass) { + // TODO: Should use list of published entries. + // We can't currently *do* that because go uses this method for now. + this.republishEntry(privateKey, (err) => { + if (err) { + const error = 'cannot republish entry for the node\'s private key' + + log.error(error) + return + } + + if (this.ipfs._keychain && Boolean(pass)) { + this.ipfs._keychain.listKeys((err, list) => { + if (err) { + const error = 'cannot get the list of available keys in the keychain' + + log.error(error) + return + } + + each(list, (key, cb) => { + waterfall([ + (cb) => this.ipfs._keychain.exportKey(key.name, pass, cb), + (pem, cb) => crypto.keys.import(pem, pass, cb) + ], (err, privKey) => { + if (err) { + return + } + + this.republishEntry(privKey, cb) + }) + }, (err) => { + if (err) { + console.log('err', err) + const error = 'cannot republish entry from the keychain' + + log.error(error) + } + }) + }) + } + }) + } + + republishEntry (privateKey, callback) { + waterfall([ + (cb) => peerId.createFromPrivKey(privateKey.bytes.toString('base64'), cb), + (peerIdResult, cb) => this.getLastValue(peerIdResult, cb) + ], (err, value) => { + if (err) { + return callback(err.code === ERR_NO_ENTRY_FOUND ? null : err) + } + + this.publisher.publishWithEOL(privateKey, value, defaultRecordLifetime, callback) + }) + } + + getLastValue (id, callback) { + this.repo.datastore.get(ipns.getLocalKey(id.id), (err, dsVal) => { + // error handling + // no need for republish + if (err && err.notFound) { + const error = `no previous entry for record with id: ${id}` + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_NO_ENTRY_FOUND })) + } else if (err) { + return callback(err) + } + + if (!Buffer.isBuffer(dsVal)) { + const error = `found ipns record that we couldn't convert to a value` + + log.error(error) + return callback(Object.assign(new Error(error), { code: ERR_INVALID_IPNS_RECORD })) + } + + // unmarshal data + let record + try { + record = ipns.unmarshal(dsVal) + } catch (err) { + const error = `found ipns record that we couldn't convert to a value` + + log.error(error) + return callback(error) + } + + callback(null, record.value) + }) + } +} + +exports = module.exports = IpnsRepublisher diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index e481828a97..e8e1b9182e 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -65,7 +65,7 @@ class IpnsResolver { } // Recursive resolver according to the specified depth - resolver(name, depth, peerId, resolverFn, callback) { + resolver (name, depth, peerId, resolverFn, callback) { // bind resolver function this._resolver = resolverFn @@ -95,7 +95,7 @@ class IpnsResolver { } // resolve ipns entries locally using the datastore - resolveLocal(name, peerId, callback) { + resolveLocal (name, peerId, callback) { const { ipnsKey } = ipns.getIdKeys(fromB58String(name)) this.repo.datastore.get(ipnsKey, (err, dsVal) => { diff --git a/test/cli/commands.js b/test/cli/commands.js index e6ae40498a..15108bc2a5 100644 --- a/test/cli/commands.js +++ b/test/cli/commands.js @@ -4,7 +4,7 @@ const expect = require('chai').expect const runOnAndOff = require('../utils/on-and-off') -const commandCount = 81 +const commandCount = 80 describe('commands', () => runOnAndOff((thing) => { let ipfs diff --git a/test/cli/name.js b/test/cli/name.js index 4e2a875ee0..d4ab1dbcee 100644 --- a/test/cli/name.js +++ b/test/cli/name.js @@ -52,7 +52,7 @@ describe('name', () => { expect(id).to.have.property('id') nodeId = id.id - ipfs('files add src/init-files/init-docs/readme').then((out) => { + ipfs('add src/init-files/init-docs/readme').then((out) => { cidAdded = out.split(' ')[1] done() }) From ba7585dee1e1e32f3b35183ba61bab998bb59c08 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 8 Aug 2018 15:07:24 +0100 Subject: [PATCH 18/34] chore: updated readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 86e9766524..48116f505e 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ You can check the development status at the [Waffle Board](https://waffle.io/ipf - [Core API](#core-api) - [Files](#files) - [Graph](#graph) + - [Name](#name) - [Crypto and Key Management](#crypto-and-key-management) - [Network](#network) - [Node Management](#node-management) @@ -545,6 +546,12 @@ The core API is grouped into several areas: - [`ipfs.pin.ls([hash], [options], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/PIN.md#pinls) - [`ipfs.pin.rm(hash, [options], [callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/PIN.md#pinrm) +### Name + +- [name](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/NAME.md) + - [`ipfs.name.publish(value, [options, callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/NAME.md#namepublish) + - [`ipfs.name.resolve(value, [options, callback])`](https://github.com/ipfs/interface-ipfs-core/blob/master/SPEC/NAME.md#nameresolve) + #### Crypto and Key Management - [key](https://github.com/ipfs/interface-ipfs-core/tree/master/SPEC/KEY.md) @@ -837,6 +844,8 @@ Listing of the main packages used in the IPFS ecosystem. There are also three sp | [`ipld`](//github.com/ipld/js-ipld) | [![npm](https://img.shields.io/npm/v/ipld.svg?maxAge=86400&style=flat)](//github.com/ipld/js-ipld/releases) | [![Dep](https://david-dm.org/ipld/js-ipld.svg?style=flat)](https://david-dm.org/ipld/js-ipld) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipld/js-ipld/master)](https://ci.ipfs.team/job/ipld/job/js-ipld/job/master/) | [![Coverage Status](https://codecov.io/gh/ipld/js-ipld/branch/master/graph/badge.svg)](https://codecov.io/gh/ipld/js-ipld) | | [`ipld-dag-pb`](//github.com/ipld/js-ipld-dag-pb) | [![npm](https://img.shields.io/npm/v/ipld-dag-pb.svg?maxAge=86400&style=flat)](//github.com/ipld/js-ipld-dag-pb/releases) | [![Dep](https://david-dm.org/ipld/js-ipld-dag-pb.svg?style=flat)](https://david-dm.org/ipld/js-ipld-dag-pb) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipld/js-ipld-dag-pb/master)](https://ci.ipfs.team/job/ipld/job/js-ipld-dag-pb/job/master/) | [![Coverage Status](https://codecov.io/gh/ipld/js-ipld-dag-pb/branch/master/graph/badge.svg)](https://codecov.io/gh/ipld/js-ipld-dag-pb) | | [`ipld-dag-cbor`](//github.com/ipld/js-ipld-dag-cbor) | [![npm](https://img.shields.io/npm/v/ipld-dag-cbor.svg?maxAge=86400&style=flat)](//github.com/ipld/js-ipld-dag-cbor/releases) | [![Dep](https://david-dm.org/ipld/js-ipld-dag-cbor.svg?style=flat)](https://david-dm.org/ipld/js-ipld-dag-cbor) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipld/js-ipld-dag-cbor/master)](https://ci.ipfs.team/job/ipld/job/js-ipld-dag-cbor/job/master/) | [![Coverage Status](https://codecov.io/gh/ipld/js-ipld-dag-cbor/branch/master/graph/badge.svg)](https://codecov.io/gh/ipld/js-ipld-dag-cbor) | +| **Name** | +| [`ipns`](//github.com/ipfs/js-ipns) | [![npm](https://img.shields.io/npm/v/ipns.svg?maxAge=86400&style=flat-square)](//github.com/ipfs/js-ipns/releases) | [![Dep](https://david-dm.org/ipfs/js-ipns.svg?style=flat-square)](https://david-dm.org/ipfs/js-ipns) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipfs/js-ipns/master)](https://ci.ipfs.team/job/ipfs/job/js-ipns/job/master/) | [![Coverage Status](https://codecov.io/gh/ipfs/js-ipns/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/js-ipns) | | **Repo** | | [`ipfs-repo`](//github.com/ipfs/js-ipfs-repo) | [![npm](https://img.shields.io/npm/v/ipfs-repo.svg?maxAge=86400&style=flat)](//github.com/ipfs/js-ipfs-repo/releases) | [![Dep](https://david-dm.org/ipfs/js-ipfs-repo.svg?style=flat)](https://david-dm.org/ipfs/js-ipfs-repo) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipfs/js-ipfs-repo/master)](https://ci.ipfs.team/job/ipfs/job/js-ipfs-repo/job/master/) | [![Coverage Status](https://codecov.io/gh/ipfs/js-ipfs-repo/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/js-ipfs-repo) | | **Exchange** | From 6e3c4355e64af949d5f5cf12beb30bbca28545cd Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Wed, 8 Aug 2018 16:08:52 +0100 Subject: [PATCH 19/34] chore: added tests --- README.md | 2 +- src/core/components/name.js | 7 +- src/core/ipns/index.js | 5 +- src/core/ipns/publisher.js | 10 +-- src/core/ipns/republisher.js | 12 +-- src/core/ipns/resolver.js | 10 +-- test/core/ipns.spec.js | 68 ---------------- test/core/name.spec.js | 147 +++++++++++++++++++++++++++++++++++ 8 files changed, 167 insertions(+), 94 deletions(-) delete mode 100644 test/core/ipns.spec.js create mode 100644 test/core/name.spec.js diff --git a/README.md b/README.md index 48116f505e..58cabe9613 100644 --- a/README.md +++ b/README.md @@ -845,7 +845,7 @@ Listing of the main packages used in the IPFS ecosystem. There are also three sp | [`ipld-dag-pb`](//github.com/ipld/js-ipld-dag-pb) | [![npm](https://img.shields.io/npm/v/ipld-dag-pb.svg?maxAge=86400&style=flat)](//github.com/ipld/js-ipld-dag-pb/releases) | [![Dep](https://david-dm.org/ipld/js-ipld-dag-pb.svg?style=flat)](https://david-dm.org/ipld/js-ipld-dag-pb) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipld/js-ipld-dag-pb/master)](https://ci.ipfs.team/job/ipld/job/js-ipld-dag-pb/job/master/) | [![Coverage Status](https://codecov.io/gh/ipld/js-ipld-dag-pb/branch/master/graph/badge.svg)](https://codecov.io/gh/ipld/js-ipld-dag-pb) | | [`ipld-dag-cbor`](//github.com/ipld/js-ipld-dag-cbor) | [![npm](https://img.shields.io/npm/v/ipld-dag-cbor.svg?maxAge=86400&style=flat)](//github.com/ipld/js-ipld-dag-cbor/releases) | [![Dep](https://david-dm.org/ipld/js-ipld-dag-cbor.svg?style=flat)](https://david-dm.org/ipld/js-ipld-dag-cbor) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipld/js-ipld-dag-cbor/master)](https://ci.ipfs.team/job/ipld/job/js-ipld-dag-cbor/job/master/) | [![Coverage Status](https://codecov.io/gh/ipld/js-ipld-dag-cbor/branch/master/graph/badge.svg)](https://codecov.io/gh/ipld/js-ipld-dag-cbor) | | **Name** | -| [`ipns`](//github.com/ipfs/js-ipns) | [![npm](https://img.shields.io/npm/v/ipns.svg?maxAge=86400&style=flat-square)](//github.com/ipfs/js-ipns/releases) | [![Dep](https://david-dm.org/ipfs/js-ipns.svg?style=flat-square)](https://david-dm.org/ipfs/js-ipns) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipfs/js-ipns/master)](https://ci.ipfs.team/job/ipfs/job/js-ipns/job/master/) | [![Coverage Status](https://codecov.io/gh/ipfs/js-ipns/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/js-ipns) | +| [`ipns`](//github.com/ipfs/js-ipns) | [![npm](https://img.shields.io/npm/v/ipns.svg?maxAge=86400&style=flat)](//github.com/ipfs/js-ipns/releases) | [![Dep](https://david-dm.org/ipfs/js-ipns.svg?style=flat-square)](https://david-dm.org/ipfs/js-ipns) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipfs/js-ipns/master)](https://ci.ipfs.team/job/ipfs/job/js-ipns/job/master/) | [![Coverage Status](https://codecov.io/gh/ipfs/js-ipns/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/js-ipns) | | **Repo** | | [`ipfs-repo`](//github.com/ipfs/js-ipfs-repo) | [![npm](https://img.shields.io/npm/v/ipfs-repo.svg?maxAge=86400&style=flat)](//github.com/ipfs/js-ipfs-repo/releases) | [![Dep](https://david-dm.org/ipfs/js-ipfs-repo.svg?style=flat)](https://david-dm.org/ipfs/js-ipfs-repo) | [![Build Status](https://ci.ipfs.team/buildStatus/icon?job=ipfs/js-ipfs-repo/master)](https://ci.ipfs.team/job/ipfs/job/js-ipfs-repo/job/master/) | [![Coverage Status](https://codecov.io/gh/ipfs/js-ipfs-repo/branch/master/graph/badge.svg)](https://codecov.io/gh/ipfs/js-ipfs-repo) | | **Exchange** | diff --git a/src/core/components/name.js b/src/core/components/name.js index 2c741ac7e1..47efadeede 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -14,7 +14,6 @@ log.error = debug('jsipfs:name:error') const errors = require('../utils') const path = require('../ipns/path') -const ERR_CANNOT_GET_KEY = 'ERR_CANNOT_GET_KEY' const ERR_NOCACHE_AND_LOCAL = 'ERR_NOCACHE_AND_LOCAL' const keyLookup = (ipfsNode, kname, callback) => { @@ -29,10 +28,8 @@ const keyLookup = (ipfsNode, kname, callback) => { (pem, cb) => crypto.keys.import(pem, pass, cb) ], (err, privateKey) => { if (err) { - const error = `cannot get the specified key` - - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_CANNOT_GET_KEY })) + log.error(err) + return callback(Object.assign(new Error(err), { code: ERR_CANNOT_GET_KEY })) } return callback(null, privateKey) diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index 04960f0cf3..61b36dd29f 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -75,13 +75,14 @@ class IPNS { log(`IPNS record from ${name} was resolved correctly`) - return callback(null, { + callback(null, { path: result }) }) } - // Initialize keyspace - sets the ipns record for the given key to point to an empty directory + // Initialize keyspace + // sets the ipns record for the given key to point to an empty directory initializeKeyspace (privKey, value, callback) { this.publisher.publish(privKey, value, callback) } diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index f3b72d1ae7..2e5a2a1a3b 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -66,7 +66,7 @@ class IpnsPublisher { return callback(err) } - return callback(null, embedPublicKeyRecord || record) + callback(null, embedPublicKeyRecord || record) }) }) } @@ -86,7 +86,7 @@ class IpnsPublisher { } log(`ipns record for ${key.toString()} was stored in the routing`) - return callback(null, res) + callback(null, res) }) } @@ -102,7 +102,7 @@ class IpnsPublisher { } log(`public key for ${key.toString()} was stored in the routing`) - return callback(null, res) + callback(null, res) }) } @@ -143,7 +143,7 @@ class IpnsPublisher { return callback(null, null) } - return callback(null, result) + callback(null, result) }) } @@ -181,7 +181,7 @@ class IpnsPublisher { } log(`ipns record for ${value} was stored in the datastore`) - return callback(null, entryData) + callback(null, entryData) }) }) }) diff --git a/src/core/ipns/republisher.js b/src/core/ipns/republisher.js index 5b4d96374d..d3f0d901d5 100644 --- a/src/core/ipns/republisher.js +++ b/src/core/ipns/republisher.js @@ -46,9 +46,7 @@ class IpnsRepublisher { if (this.ipfs._keychain && Boolean(pass)) { this.ipfs._keychain.listKeys((err, list) => { if (err) { - const error = 'cannot get the list of available keys in the keychain' - - log.error(error) + log.error(err) return } @@ -58,6 +56,7 @@ class IpnsRepublisher { (pem, cb) => crypto.keys.import(pem, pass, cb) ], (err, privKey) => { if (err) { + log.error(err) return } @@ -65,10 +64,7 @@ class IpnsRepublisher { }) }, (err) => { if (err) { - console.log('err', err) - const error = 'cannot republish entry from the keychain' - - log.error(error) + log.error(err) } }) }) @@ -92,7 +88,7 @@ class IpnsRepublisher { getLastValue (id, callback) { this.repo.datastore.get(ipns.getLocalKey(id.id), (err, dsVal) => { // error handling - // no need for republish + // no need to republish if (err && err.notFound) { const error = `no previous entry for record with id: ${id}` diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index e8e1b9182e..fc0d0726e2 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -19,7 +19,7 @@ class IpnsResolver { constructor (routing, repo) { this.routing = routing this.repo = repo - this._resolver = undefined // Add Router resolver + this._resolver = undefined // TODO Routing - add Router resolver } resolve (name, peerId, options, callback) { @@ -42,7 +42,7 @@ class IpnsResolver { } // Get the intended resoulver function - // TODO set default resolverFn + // TODO Routing - set default resolverFn let resolverFn @@ -60,7 +60,7 @@ class IpnsResolver { } log(`${name} was locally resolved correctly`) - return callback(null, res) + callback(null, res) }) } @@ -90,7 +90,7 @@ class IpnsResolver { } // continue recursively until depth equals 0 - return this.resolver(nameSegments[2], depth - 1, peerId, resolverFn, callback) + this.resolver(nameSegments[2], depth - 1, peerId, resolverFn, callback) }) } @@ -127,7 +127,7 @@ class IpnsResolver { return callback(err) } - return callback(null, ipnsEntry.value.toString()) + callback(null, ipnsEntry.value.toString()) }) }) }) diff --git a/test/core/ipns.spec.js b/test/core/ipns.spec.js deleted file mode 100644 index 8a0af4f901..0000000000 --- a/test/core/ipns.spec.js +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const chai = require('chai') -const dirtyChai = require('dirty-chai') -const expect = chai.expect -chai.use(dirtyChai) - -const isNode = require('detect-node') -const IPFS = require('../../src') - -const DaemonFactory = require('ipfsd-ctl') -const df = DaemonFactory.create({ type: 'proc' }) - -const ipfsRef = '/ipfs/QmPFVLPmp9zv5Z5KUqLhe2EivAGccQW2r7M7jhVJGLZoZU' - -describe('ipns', () => { - if (!isNode) { - return - } - - let node - let nodeId - let ipfsd - - before(function (done) { - this.timeout(40 * 1000) - df.spawn({ - exec: IPFS - }, (err, _ipfsd) => { - expect(err).to.not.exist() - ipfsd = _ipfsd - node = _ipfsd.api - - node.id().then((res) => { - expect(res.id).to.exist() - - nodeId = res.id - done() - }) - }) - }) - - after((done) => ipfsd.stop(done)) - - it('should publish correctly with the default params', (done) => { - node.name.publish(ipfsRef, { resolve: false }, (err, res) => { - expect(err).to.not.exist() - expect(res).to.exist() - expect(res.name).to.equal(nodeId) - done() - }) - }) - - it('should publish and then resolve correctly', (done) => { - node.name.publish(ipfsRef, { resolve: false }, (err, res) => { - expect(err).to.not.exist() - expect(res).to.exist() - - node.name.resolve(nodeId, (err, res) => { - expect(err).to.not.exist() - expect(res).to.exist() - expect(res.path).to.equal(ipfsRef) - done() - }) - }) - }) -}) diff --git a/test/core/name.spec.js b/test/core/name.spec.js new file mode 100644 index 0000000000..29fee863e9 --- /dev/null +++ b/test/core/name.spec.js @@ -0,0 +1,147 @@ +/* eslint max-nested-callbacks: ["error", 6] */ +/* eslint-env mocha */ +'use strict' + +const hat = require('hat') +const chai = require('chai') +const dirtyChai = require('dirty-chai') +const expect = chai.expect +chai.use(dirtyChai) + +const isNode = require('detect-node') +const IPFS = require('../../src') + +const DaemonFactory = require('ipfsd-ctl') +const df = DaemonFactory.create({ type: 'proc' }) + +const ipfsRef = '/ipfs/QmPFVLPmp9zv5Z5KUqLhe2EivAGccQW2r7M7jhVJGLZoZU' + +describe('name', function () { + if (!isNode) { + return + } + + let node + let nodeId + let ipfsd + + before(function (done) { + this.timeout(40 * 1000) + df.spawn({ + exec: IPFS, + args: [`--pass ${hat()}`] + }, (err, _ipfsd) => { + expect(err).to.not.exist() + ipfsd = _ipfsd + node = _ipfsd.api + + node.id().then((res) => { + expect(res.id).to.exist() + + nodeId = res.id + done() + }) + }) + }) + + after((done) => ipfsd.stop(done)) + + it('should publish correctly with the default options', function (done) { + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res.name).to.equal(nodeId) + done() + }) + }) + + it('should publish and then resolve correctly with the default options', function (done) { + node.name.publish(ipfsRef, { resolve: false }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + + node.name.resolve(nodeId, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res.path).to.equal(ipfsRef) + done() + }) + }) + }) + + it('should publish correctly with the lifetime option and resolve', function (done) { + node.name.publish(ipfsRef, { resolve: false, lifetime: '2h' }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + + node.name.resolve(nodeId, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res.path).to.equal(ipfsRef) + done() + }) + }) + }) + + it('should not get the entry correctly if its validity time expired', function (done) { + node.name.publish(ipfsRef, { resolve: false, lifetime: '1ms' }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + + setTimeout(function () { + node.name.resolve(nodeId, (err) => { + expect(err).to.exist() + done() + }) + }, 2) + }) + }) + + it('should recursively resolve to an IPFS hash', function (done) { + this.timeout(80 * 1000) + const keyName = hat() + + node.key.gen(keyName, { type: 'rsa', size: 2048 }, function (err, key) { + expect(err).to.not.exist() + + node.name.publish(ipfsRef, { resolve: false }, (err) => { + expect(err).to.not.exist() + + node.name.publish(`/ipns/${nodeId}`, { resolve: false, key: keyName }, (err) => { + expect(err).to.not.exist() + + node.name.resolve(key.id, { recursive: true }, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res.path).to.equal(ipfsRef) + done() + }) + }) + }) + }) + }) + + it('should not recursively resolve to an IPFS hash if the option recursive is not provided', function (done) { + this.timeout(80 * 1000) + const keyName = hat() + + node.key.gen(keyName, { type: 'rsa', size: 2048 }, function (err, key) { + expect(err).to.not.exist() + + node.name.publish(ipfsRef, { resolve: false }, (err) => { + expect(err).to.not.exist() + + node.name.publish(`/ipns/${nodeId}`, { resolve: false, key: keyName }, (err) => { + expect(err).to.not.exist() + + node.name.resolve(key.id, (err, res) => { + expect(err).to.not.exist() + expect(res).to.exist() + expect(res.path).to.equal(`/ipns/${nodeId}`) + done() + }) + }) + }) + }) + }) +}) From b2253e1cff0c422229d085d3d4be86b0af490fe9 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 9 Aug 2018 14:19:40 +0100 Subject: [PATCH 20/34] chore: upgrade dependency of ipns --- package.json | 2 +- test/fixtures/go-ipfs-repo/version | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bce46a2eab..25d020bd51 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "ipld": "~0.17.3", "ipld-dag-cbor": "~0.12.1", "ipld-dag-pb": "~0.14.6", - "ipns": "~0.1.1", + "ipns": "~0.1.2", "is-ipfs": "~0.4.2", "is-pull-stream": "~0.0.0", "is-stream": "^1.1.0", diff --git a/test/fixtures/go-ipfs-repo/version b/test/fixtures/go-ipfs-repo/version index c7930257df..62f9457511 100644 --- a/test/fixtures/go-ipfs-repo/version +++ b/test/fixtures/go-ipfs-repo/version @@ -1 +1 @@ -7 \ No newline at end of file +6 \ No newline at end of file From 17bcdcb1e883f2619e2686f8dde1b633623a951d Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 9 Aug 2018 16:00:26 +0100 Subject: [PATCH 21/34] fix: lint --- src/core/components/name.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/components/name.js b/src/core/components/name.js index 47efadeede..5906f3d209 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -15,6 +15,7 @@ const errors = require('../utils') const path = require('../ipns/path') const ERR_NOCACHE_AND_LOCAL = 'ERR_NOCACHE_AND_LOCAL' +const ERR_CANNOT_GET_KEY = 'ERR_CANNOT_GET_KEY' const keyLookup = (ipfsNode, kname, callback) => { if (kname === 'self') { From 1523579e261ddf4461b83aac5a83dc835fbd3728 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Thu, 9 Aug 2018 22:02:25 +0100 Subject: [PATCH 22/34] fix: code review --- package.json | 1 - src/cli/commands/name/publish.js | 2 +- src/cli/commands/name/resolve.js | 8 +- src/core/components/init.js | 7 +- src/core/components/name.js | 18 ++-- src/core/components/stop.js | 1 + src/core/ipns/index.js | 2 +- src/core/ipns/path.js | 63 +++-------- src/core/ipns/publisher.js | 177 ++++++++++++++++++++++--------- src/core/ipns/republisher.js | 80 +++++++++----- src/core/ipns/resolver.js | 55 ++++++---- src/http/api/resources/name.js | 1 - src/http/api/routes/name.js | 2 +- 13 files changed, 251 insertions(+), 166 deletions(-) diff --git a/package.json b/package.json index 25d020bd51..098503851c 100644 --- a/package.json +++ b/package.json @@ -164,7 +164,6 @@ "pull-stream": "^3.6.8", "pull-stream-to-stream": "^1.3.4", "pull-zip": "^2.0.1", - "quick-lru": "^1.1.0", "read-pkg-up": "^4.0.0", "readable-stream": "2.3.6", "receptacle": "^1.3.2", diff --git a/src/cli/commands/name/publish.js b/src/cli/commands/name/publish.js index 5bcc6e5eaa..2031ddbc7b 100644 --- a/src/cli/commands/name/publish.js +++ b/src/cli/commands/name/publish.js @@ -36,7 +36,7 @@ module.exports = { ttl: argv.ttl } - argv.ipfs.name.publish(argv['ipfsPath'], opts, (err, result) => { + argv.ipfs.name.publish(argv.ipfsPath, opts, (err, result) => { if (err) { throw err } diff --git a/src/cli/commands/name/resolve.js b/src/cli/commands/name/resolve.js index e248ac09d7..7a5f49675d 100644 --- a/src/cli/commands/name/resolve.js +++ b/src/cli/commands/name/resolve.js @@ -26,12 +26,16 @@ module.exports = { recursive: argv.recursive } - argv.ipfs.name.resolve(argv['name'], opts, (err, result) => { + argv.ipfs.name.resolve(argv.name, opts, (err, result) => { if (err) { throw err } - print(`result: ${result}`) + if (result && result.path) { + print(result.path) + } else { + print(result) + } }) } } diff --git a/src/core/components/init.js b/src/core/components/init.js index cd8010afec..7061f973cb 100644 --- a/src/core/components/init.js +++ b/src/core/components/init.js @@ -6,8 +6,6 @@ const parallel = require('async/parallel') const promisify = require('promisify-es6') const defaultConfig = require('../runtime/config-nodejs.js') const Keychain = require('libp2p-keychain') -const Unixfs = require('ipfs-unixfs') -const { DAGNode } = require('ipld-dag-pb') const addDefaultAssets = require('./init-assets') @@ -105,15 +103,14 @@ module.exports = function init (self) { cb(null, true) } }, - (_, cb) => DAGNode.create(new Unixfs('directory').marshal(), cb), + // add empty unixfs dir object (go-ipfs assumes this exists) + (_, cb) => self.object.new('unixfs-dir', cb), (emptyDirNode, cb) => { if (opts.emptyRepo) { return cb(null, true) } const tasks = [ - // add empty unixfs dir object (go-ipfs assumes this exists) - (cb) => self.object.new('unixfs-dir', cb), (cb) => self._ipns.initializeKeyspace(privateKey, emptyDirNode.toJSON().multihash, cb) ] diff --git a/src/core/components/name.js b/src/core/components/name.js index 5906f3d209..4b104d65bd 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -5,7 +5,6 @@ const promisify = require('promisify-es6') const series = require('async/series') const waterfall = require('async/waterfall') const human = require('human-to-milliseconds') - const crypto = require('libp2p-crypto') const log = debug('jsipfs:name') @@ -30,7 +29,7 @@ const keyLookup = (ipfsNode, kname, callback) => { ], (err, privateKey) => { if (err) { log.error(err) - return callback(Object.assign(new Error(err), { code: ERR_CANNOT_GET_KEY })) + return callback(Object.assign(err, { code: ERR_CANNOT_GET_KEY })) } return callback(null, privateKey) @@ -64,7 +63,10 @@ module.exports = function name (self) { options = {} } - const { resolve = true, lifetime = '24h', key = 'self' } = options + options = options || {} + const resolve = !(options.resolve === false) + const lifetime = options.lifetime || '24h' + const key = options.key || 'self' if (!self.isOnline()) { const error = errors.OFFLINE_ERROR @@ -82,7 +84,7 @@ module.exports = function name (self) { } series([ - (cb) => human(lifetime || '1s', cb), + (cb) => human(lifetime, cb), // (cb) => ttl ? human(ttl, cb) : cb(), (cb) => keyLookup(self, key, cb), // verify if the path exists, if not, an error will stop the execution @@ -122,11 +124,13 @@ module.exports = function name (self) { options = {} } - const { nocache = false, recursive = false } = options + options = options || {} + const nocache = options.nocache && options.nocache.toString() === 'true' + const recursive = options.recursive && options.recursive.toString() === 'true' const local = true // TODO ROUTING - use self._options.local - if (!self.isOnline()) { + if (!self.isOnline() && !local) { const error = errors.OFFLINE_ERROR log.error(error) @@ -134,7 +138,7 @@ module.exports = function name (self) { } if (local && nocache) { - const error = 'Cannot specify both local and nocache' + const error = 'cannot specify both local and nocache' log.error(error) return callback(Object.assign(new Error(error), { code: ERR_NOCACHE_AND_LOCAL })) diff --git a/src/core/components/stop.js b/src/core/components/stop.js index cf97b6ec6a..48e17c6f53 100644 --- a/src/core/components/stop.js +++ b/src/core/components/stop.js @@ -31,6 +31,7 @@ module.exports = (self) => { self._blockService.unsetExchange() self._bitswap.stop() self._preload.stop() + self._ipns.republisher.stop() series([ (cb) => self._mfsPreload.stop(cb), diff --git a/src/core/ipns/index.js b/src/core/ipns/index.js index 61b36dd29f..8bfc7cd718 100644 --- a/src/core/ipns/index.js +++ b/src/core/ipns/index.js @@ -26,7 +26,7 @@ class IPNS { // Publish publish (privKey, value, lifetime, callback) { series([ - (cb) => createFromPrivKey(privKey.bytes.toString('base64'), cb), + (cb) => createFromPrivKey(privKey.bytes, cb), (cb) => this.publisher.publishWithEOL(privKey, value, lifetime, cb) ], (err, results) => { if (err) { diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index 36714d9311..a78bddb838 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -1,6 +1,6 @@ 'use strict' -const CID = require('cids') +const isIPFS = require('is-ipfs') const debug = require('debug') const log = debug('jsipfs:ipns:path') @@ -13,26 +13,24 @@ const ERR_NO_COMPONENTS = 'ERR_NO_COMPONENTS' // (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node const resolvePath = (ipfsNode, name, callback) => { // ipns path - if (name.startsWith('/ipns/')) { + if (isIPFS.ipnsPath(name)) { log(`resolve ipns path ${name}`) const local = true // TODO ROUTING - use self._options.local const parts = name.split('/') if (parts.length < 3 || parts[2] === '') { - const error = 'path must contain at least one component' + const errMsg = 'path must contain at least one component' - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_NO_COMPONENTS })) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_NO_COMPONENTS })) } - // TODO ROUTING - public key from network instead - const localPublicKey = ipfsNode._peerInfo.id.pubKey const options = { local: local } - return ipfsNode._ipns.resolve(name, localPublicKey, options, callback) + return ipfsNode._ipns.resolve(name, ipfsNode._peerInfo.id, options, callback) } // ipfs path @@ -49,50 +47,13 @@ const resolvePath = (ipfsNode, name, callback) => { // The returned path will always be prefixed with /ipfs/ or /ipns/. // If the received string is not a valid ipfs path, an error will be returned const parsePath = (pathStr) => { - const badPathError = `invalid 'ipfs ref' path` - const parts = pathStr.split('/') - - if (parts.length === 1) { - return parseCidToPath(pathStr) - } - - // if the path does not begin with a slash, we expect this to start with a hash and be an ipfs path - if (parts[0] !== '') { - if (parseCidToPath(parts[0])) { - return `/ipfs/${pathStr}` - } + if (isIPFS.cid(pathStr)) { + return `/ipfs/${pathStr}` + } else if (isIPFS.path(pathStr)) { + return pathStr + } else { + throw Object.assign(new Error(`invalid 'ipfs ref' path`), { code: ERR_BAD_PATH }) } - - if (parts.length < 3) { - log.error(badPathError) - throw Object.assign(new Error(badPathError), { code: ERR_BAD_PATH }) - } - - if (parts[1] === 'ipfs') { - if (!parseCidToPath(parts[2])) { - log.error(badPathError) - throw Object.assign(new Error(badPathError), { code: ERR_BAD_PATH }) - } - } else if (parts[1] !== 'ipns') { - log.error(badPathError) - throw Object.assign(new Error(badPathError), { code: ERR_BAD_PATH }) - } - return pathStr -} - -// parseCidToPath takes a CID in string form and returns a valid ipfs Path. -const parseCidToPath = (value) => { - if (value === '') { - const error = 'path must contain at least one component' - - log.error(error) - throw Object.assign(new Error(error), { code: ERR_NO_COMPONENTS }) - } - - const cid = new CID(value) - CID.validateCID(cid) - - return `/ipfs/${value}` } module.exports = { diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index 2e5a2a1a3b..4157476d1f 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -1,7 +1,8 @@ 'use strict' -const peerId = require('peer-id') +const PeerId = require('peer-id') const Record = require('libp2p-record').Record +const { Key } = require('interface-datastore') const series = require('async/series') const debug = require('debug') @@ -12,31 +13,41 @@ const ipns = require('ipns') const ERR_CREATING_IPNS_RECORD = 'ERR_CREATING_IPNS_RECORD' const ERR_INVALID_IPNS_RECORD = 'ERR_INVALID_IPNS_RECORD' +const ERR_INVALID_DATASTORE_KEY = 'ERR_INVALID_DATASTORE_KEY' +const ERR_INVALID_PEER_ID = 'ERR_INVALID_PEER_ID' const ERR_STORING_IN_DATASTORE = 'ERR_STORING_IN_DATASTORE' const ERR_UNEXPECTED_DATASTORE_RESPONSE = 'ERR_UNEXPECTED_DATASTORE_RESPONSE' +const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER' const defaultRecordTtl = 60 * 60 * 1000 // IpnsPublisher is capable of publishing and resolving names to the IPFS routing system. class IpnsPublisher { constructor (routing, repo) { - this.routing = routing - this.repo = repo + this._routing = routing + this._repo = repo } // publish record with a eol publishWithEOL (privKey, value, lifetime, callback) { - peerId.createFromPrivKey(privKey.bytes.toString('base64'), (err, peerIdResult) => { + if (!privKey || !privKey.bytes) { + const errMsg = `one or more of the provided parameters are not defined` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_UNDEFINED_PARAMETER })) + } + + PeerId.createFromPrivKey(privKey.bytes, (err, peerId) => { if (err) { callback(err) } - this.updateOrCreateRecord(privKey, value, lifetime, peerIdResult, (err, record) => { + this._updateOrCreateRecord(privKey, value, lifetime, peerId, (err, record) => { if (err) { return callback(err) } - this.putRecordToRouting(record, peerIdResult, callback) + this._putRecordToRouting(record, peerId, callback) }) }) } @@ -46,23 +57,37 @@ class IpnsPublisher { this.publishWithEOL(privKey, value, defaultRecordTtl, callback) } - putRecordToRouting (record, peerIdResult, callback) { - const publicKey = peerIdResult._pubKey + _putRecordToRouting (record, peerId, callback) { + if (!(peerId instanceof PeerId)) { + const errMsg = `peerId received is not valid` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PEER_ID })) + } + + const publicKey = peerId._pubKey ipns.embedPublicKey(publicKey, record, (err, embedPublicKeyRecord) => { if (err) { return callback(err) } - const { ipnsKey, pkKey } = ipns.getIdKeys(peerIdResult.id) + let keys + try { + keys = ipns.getIdKeys(peerId.id) + } catch (err) { + log.error(err) + return callback(err) + } series([ - (cb) => this.publishEntry(ipnsKey, embedPublicKeyRecord || record, peerIdResult, cb), + (cb) => this._publishEntry(keys.ipnsKey, embedPublicKeyRecord || record, peerId, cb), // Publish the public key if a public key cannot be extracted from the ID // We will be able to deprecate this part in the future, since the public keys will be only in the peerId - (cb) => embedPublicKeyRecord ? this.publishPublicKey(pkKey, publicKey, peerIdResult, cb) : cb() + (cb) => embedPublicKeyRecord ? this._publishPublicKey(keys.pkKey, publicKey, peerId, cb) : cb() ], (err) => { if (err) { + log.error(err) return callback(err) } @@ -71,15 +96,27 @@ class IpnsPublisher { }) } - publishEntry (key, entry, peerIdResult, callback) { - // Marshal record - const entryData = ipns.marshal(entry) - - // Marshal to libp2p record - const rec = new Record(key.toBuffer(), entryData, peerIdResult) + _publishEntry (key, entry, peerId, callback) { + if (!(key instanceof Key)) { + const errMsg = `datastore key does not have a valid format` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_DATASTORE_KEY })) + } + + let rec + try { + // Marshal record + const entryData = ipns.marshal(entry) + // Marshal to libp2p record + rec = new Record(key.toBuffer(), entryData, peerId) + } catch (err) { + log.error(err) + return callback(err) + } // TODO Routing - this should be replaced by a put to the DHT - this.repo.datastore.put(key, rec.serialize(), (err, res) => { + this._repo.datastore.put(key, rec.serialize(), (err, res) => { if (err) { log.error(`ipns record for ${key.toString()} could not be stored in the routing`) return callback(Object.assign(new Error(`ipns record for ${key.toString()} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) @@ -90,12 +127,32 @@ class IpnsPublisher { }) } - publishPublicKey (key, publicKey, peerIdResult, callback) { - // Marshal to libp2p record - const rec = new Record(key.toBuffer(), publicKey.bytes, peerIdResult) + _publishPublicKey (key, publicKey, peerId, callback) { + if (!(key instanceof Key)) { + const errMsg = `datastore key does not have a valid format` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_DATASTORE_KEY })) + } + + if (!publicKey || !publicKey.bytes) { + const errMsg = `one or more of the provided parameters are not defined` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_UNDEFINED_PARAMETER })) + } + + let rec + try { + // Marshal to libp2p record + rec = new Record(key.toBuffer(), publicKey.bytes, peerId) + } catch (err) { + log.error(err) + return callback(err) + } // TODO Routing - this should be replaced by a put to the DHT - this.repo.datastore.put(key, rec.serialize(), (err, res) => { + this._repo.datastore.put(key, rec.serialize(), (err, res) => { if (err) { log.error(`public key for ${key.toString()} could not be stored in the routing`) return callback(Object.assign(new Error(`public key for ${key.toString()} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) @@ -108,38 +165,51 @@ class IpnsPublisher { // Returns the record this node has published corresponding to the given peer ID. // If `checkRouting` is true and we have no existing record, this method will check the routing system for any existing records. - getPublished (peerIdResult, checkRouting, callback) { - this.repo.datastore.get(ipns.getLocalKey(peerIdResult.id), (err, dsVal) => { + _getPublished (peerId, options, callback) { + if (!(peerId instanceof PeerId)) { + const errMsg = `peerId received is not valid` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PEER_ID })) + } + + options = options || {} + const checkRouting = !(options.checkRouting === false) + + this._repo.datastore.get(ipns.getLocalKey(peerId.id), (err, dsVal) => { let result - if (!err) { - if (Buffer.isBuffer(dsVal)) { - result = dsVal - } else { - const error = `found ipns record that we couldn't convert to a value` + if (err) { + if (!err.notFound) { + const errMsg = `unexpected error getting the ipns record ${peerId.id} from datastore` - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_INVALID_IPNS_RECORD })) - } - } else if (err.notFound) { - if (!checkRouting) { - return callback(null, null) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_UNEXPECTED_DATASTORE_RESPONSE })) + } else { + if (!checkRouting) { + return callback(null, null) + } else { + // TODO ROUTING - get + } } - // TODO ROUTING - get + } + + if (Buffer.isBuffer(dsVal)) { + result = dsVal } else { - const error = `unexpected error getting the ipns record ${peerIdResult.id} from datastore` + const errMsg = `found ipns record that we couldn't convert to a value` - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_UNEXPECTED_DATASTORE_RESPONSE })) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_IPNS_RECORD })) } // unmarshal data try { result = ipns.unmarshal(dsVal) } catch (err) { - const error = `found ipns record that we couldn't convert to a value` + const errMsg = `found ipns record that we couldn't convert to a value` - log.error(error) + log.error(errMsg) return callback(null, null) } @@ -147,10 +217,21 @@ class IpnsPublisher { }) } - updateOrCreateRecord (privKey, value, validity, peerIdResult, callback) { - this.getPublished(peerIdResult, false, (err, record) => { // TODO ROUTING - change to true + _updateOrCreateRecord (privKey, value, validity, peerId, callback) { + if (!(peerId instanceof PeerId)) { + const errMsg = `peerId received is not valid` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PEER_ID })) + } + + const getPublishedOptions = { + checkRouting: false // TODO ROUTING - change to true + } + + this._getPublished(peerId, getPublishedOptions, (err, record) => { if (err) { - callback(err) + return callback(err) } // Determinate the record sequence number @@ -162,10 +243,10 @@ class IpnsPublisher { // Create record ipns.create(privKey, value, seqNumber, validity, (err, entryData) => { if (err) { - const error = `ipns record for ${value} could not be created` + const errMsg = `ipns record for ${value} could not be created` - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_CREATING_IPNS_RECORD })) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_CREATING_IPNS_RECORD })) } // TODO IMPROVEMENT - set ttl (still experimental feature for go) @@ -174,7 +255,7 @@ class IpnsPublisher { const data = ipns.marshal(entryData) // Store the new record - this.repo.datastore.put(ipns.getLocalKey(peerIdResult.id), data, (err, res) => { + this._repo.datastore.put(ipns.getLocalKey(peerId.id), data, (err, res) => { if (err) { log.error(`ipns record for ${value} could not be stored in the datastore`) return callback(Object.assign(new Error(`ipns record for ${value} could not be stored in the datastore`), { code: ERR_STORING_IN_DATASTORE })) diff --git a/src/core/ipns/republisher.js b/src/core/ipns/republisher.js index d3f0d901d5..52866e3a1f 100644 --- a/src/core/ipns/republisher.js +++ b/src/core/ipns/republisher.js @@ -2,7 +2,7 @@ const ipns = require('ipns') const crypto = require('libp2p-crypto') -const peerId = require('peer-id') +const PeerId = require('peer-id') const debug = require('debug') const each = require('async/each') @@ -18,33 +18,41 @@ const defaultRecordLifetime = 24 * hour const ERR_NO_ENTRY_FOUND = 'ERR_NO_ENTRY_FOUND' const ERR_INVALID_IPNS_RECORD = 'ERR_INVALID_IPNS_RECORD' +const ERR_INVALID_PEER_ID = 'ERR_INVALID_PEER_ID' +const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER' class IpnsRepublisher { constructor (publisher, ipfs) { - this.publisher = publisher - this.ipfs = ipfs - this.repo = ipfs._repo + this._publisher = publisher + this._ipfs = ipfs + this._repo = ipfs._repo + this._interval = null } start () { - setInterval(() => { - this.republishEntries(this.ipfs._peerInfo.id.privKey, this.ipfs._options.pass) + this._interval = setInterval(() => { + this._republishEntries(this._ipfs._peerInfo.id.privKey, this._ipfs._options.pass) }, defaultBroadcastInterval) } - republishEntries (privateKey, pass) { + stop () { + clearInterval(this._interval) + } + + _republishEntries (privateKey, pass) { // TODO: Should use list of published entries. // We can't currently *do* that because go uses this method for now. - this.republishEntry(privateKey, (err) => { + this._republishEntry(privateKey, (err) => { if (err) { - const error = 'cannot republish entry for the node\'s private key' + const errMsg = 'cannot republish entry for the node\'s private key' - log.error(error) + log.error(errMsg) return } - if (this.ipfs._keychain && Boolean(pass)) { - this.ipfs._keychain.listKeys((err, list) => { + // keychain needs pass to get the cryptographic keys + if (this._ipfs._keychain && Boolean(pass)) { + this._ipfs._keychain.listKeys((err, list) => { if (err) { log.error(err) return @@ -52,7 +60,7 @@ class IpnsRepublisher { each(list, (key, cb) => { waterfall([ - (cb) => this.ipfs._keychain.exportKey(key.name, pass, cb), + (cb) => this._ipfs._keychain.exportKey(key.name, pass, cb), (pem, cb) => crypto.keys.import(pem, pass, cb) ], (err, privKey) => { if (err) { @@ -60,7 +68,7 @@ class IpnsRepublisher { return } - this.republishEntry(privKey, cb) + this._republishEntry(privKey, cb) }) }, (err) => { if (err) { @@ -72,37 +80,51 @@ class IpnsRepublisher { }) } - republishEntry (privateKey, callback) { + _republishEntry (privateKey, callback) { + if (!privateKey || !privateKey.bytes) { + const errMsg = `one or more of the provided parameters are not defined` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_UNDEFINED_PARAMETER })) + } + waterfall([ - (cb) => peerId.createFromPrivKey(privateKey.bytes.toString('base64'), cb), - (peerIdResult, cb) => this.getLastValue(peerIdResult, cb) + (cb) => PeerId.createFromPrivKey(privateKey.bytes, cb), + (peerId, cb) => this._getPreviousValue(peerId, cb) ], (err, value) => { if (err) { return callback(err.code === ERR_NO_ENTRY_FOUND ? null : err) } - this.publisher.publishWithEOL(privateKey, value, defaultRecordLifetime, callback) + this._publisher.publishWithEOL(privateKey, value, defaultRecordLifetime, callback) }) } - getLastValue (id, callback) { - this.repo.datastore.get(ipns.getLocalKey(id.id), (err, dsVal) => { + _getPreviousValue (peerId, callback) { + if (!(peerId instanceof PeerId)) { + const errMsg = `peerId received is not valid` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PEER_ID })) + } + + this._repo.datastore.get(ipns.getLocalKey(peerId.id), (err, dsVal) => { // error handling // no need to republish if (err && err.notFound) { - const error = `no previous entry for record with id: ${id}` + const errMsg = `no previous entry for record with id: ${peerId.id}` - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_NO_ENTRY_FOUND })) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_NO_ENTRY_FOUND })) } else if (err) { return callback(err) } if (!Buffer.isBuffer(dsVal)) { - const error = `found ipns record that we couldn't convert to a value` + const errMsg = `found ipns record that we couldn't process` - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_INVALID_IPNS_RECORD })) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_IPNS_RECORD })) } // unmarshal data @@ -110,10 +132,10 @@ class IpnsRepublisher { try { record = ipns.unmarshal(dsVal) } catch (err) { - const error = `found ipns record that we couldn't convert to a value` + const errMsg = `found ipns record that we couldn't convert to a value` - log.error(error) - return callback(error) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_IPNS_RECORD })) } callback(null, record.value) diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index fc0d0726e2..7f5c6b9e75 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -10,6 +10,7 @@ log.error = debug('jsipfs:ipns:resolver:error') const ERR_INVALID_NAME_SYNTAX = 'ERR_INVALID_NAME_SYNTAX' const ERR_INVALID_RECORD_RECEIVED = 'ERR_INVALID_RECORD_RECEIVED' +const ERR_INVALID_PARAMETER = 'ERR_INVALID_PARAMETER' const ERR_NO_LOCAL_RECORD_FOUND = 'ERR_NO_LOCAL_RECORD_FOUND' const ERR_RESOLVE_RECURSION_LIMIT = 'ERR_RESOLVE_RECURSION_LIMIT' @@ -17,19 +18,35 @@ const defaultMaximumRecursiveDepth = 32 class IpnsResolver { constructor (routing, repo) { - this.routing = routing - this.repo = repo + this._routing = routing + this._repo = repo this._resolver = undefined // TODO Routing - add Router resolver } resolve (name, peerId, options, callback) { + if (typeof options === 'function') { + callback = options + options = {} + } + + if (typeof name !== 'string') { + const errMsg = `one or more of the provided parameters are not valid` + + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PARAMETER })) + } + + options = options || {} + const recursive = options.recursive && options.recursive.toString() === 'true' + const local = !(options.local === false) + const nameSegments = name.split('/') if (nameSegments.length !== 3 || nameSegments[0] !== '') { - const error = `invalid name syntax for ${name}` + const errMsg = `invalid name syntax for ${name}` - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_INVALID_NAME_SYNTAX })) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_NAME_SYNTAX })) } const key = nameSegments[2] @@ -37,7 +54,7 @@ class IpnsResolver { // Define a maximum depth if recursive option enabled let depth - if (options.recursive) { + if (recursive) { depth = defaultMaximumRecursiveDepth } @@ -46,8 +63,8 @@ class IpnsResolver { let resolverFn - if (options.local) { - resolverFn = this.resolveLocal + if (local) { + resolverFn = this._resolveLocal } if (!resolverFn) { @@ -71,10 +88,10 @@ class IpnsResolver { // Exceeded recursive maximum depth if (depth === 0) { - const error = `could not resolve name (recursion limit of ${defaultMaximumRecursiveDepth} exceeded)` + const errMsg = `could not resolve name (recursion limit of ${defaultMaximumRecursiveDepth} exceeded)` - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_RESOLVE_RECURSION_LIMIT })) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_RESOLVE_RECURSION_LIMIT })) } this._resolver(name, peerId, (err, res) => { @@ -95,22 +112,22 @@ class IpnsResolver { } // resolve ipns entries locally using the datastore - resolveLocal (name, peerId, callback) { + _resolveLocal (name, peerId, callback) { const { ipnsKey } = ipns.getIdKeys(fromB58String(name)) - this.repo.datastore.get(ipnsKey, (err, dsVal) => { + this._repo.datastore.get(ipnsKey, (err, dsVal) => { if (err) { - const error = `local record requested was not found for ${name} (${ipnsKey})` + const errMsg = `local record requested was not found for ${name} (${ipnsKey})` - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_NO_LOCAL_RECORD_FOUND })) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_NO_LOCAL_RECORD_FOUND })) } if (!Buffer.isBuffer(dsVal)) { - const error = `found ipns record that we couldn't convert to a value` + const errMsg = `found ipns record that we couldn't convert to a value` - log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_INVALID_RECORD_RECEIVED })) + log.error(errMsg) + return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_RECORD_RECEIVED })) } const record = Record.deserialize(dsVal) diff --git a/src/http/api/resources/name.js b/src/http/api/resources/name.js index ede04ed597..23fc8bbab1 100644 --- a/src/http/api/resources/name.js +++ b/src/http/api/resources/name.js @@ -37,7 +37,6 @@ exports.publish = { arg: Joi.string().required(), resolve: Joi.boolean().default(true), lifetime: Joi.string().default('24h'), - ttl: Joi.string(), key: Joi.string().default('self') }).unknown() }, diff --git a/src/http/api/routes/name.js b/src/http/api/routes/name.js index 6ac5d155d0..29647f3a6a 100644 --- a/src/http/api/routes/name.js +++ b/src/http/api/routes/name.js @@ -19,7 +19,7 @@ module.exports = (server) => { path: '/api/v0/name/publish', config: { handler: resources.name.publish.handler, - validate: resources.name.resolve.validate + validate: resources.name.publish.validate } }) } From 04bf892dfc884b8377a43e3e8184cebf1e2fd2a5 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 10 Aug 2018 10:11:12 +0100 Subject: [PATCH 23/34] fix: tests --- src/core/components/init.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/core/components/init.js b/src/core/components/init.js index 7061f973cb..f99e1a8fc7 100644 --- a/src/core/components/init.js +++ b/src/core/components/init.js @@ -104,14 +104,18 @@ module.exports = function init (self) { } }, // add empty unixfs dir object (go-ipfs assumes this exists) - (_, cb) => self.object.new('unixfs-dir', cb), (emptyDirNode, cb) => { if (opts.emptyRepo) { return cb(null, true) } const tasks = [ - (cb) => self._ipns.initializeKeyspace(privateKey, emptyDirNode.toJSON().multihash, cb) + (cb) => { + waterfall([ + (cb) => self.object.new('unixfs-dir', cb), + (emptyDirNode, cb) => self._ipns.initializeKeyspace(privateKey, emptyDirNode.toJSON().multihash, cb) + ], cb) + } ] if (typeof addDefaultAssets === 'function') { From 051555395a5fc2ddf238be881d09bc415a2f1d2d Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 10 Aug 2018 10:55:00 +0100 Subject: [PATCH 24/34] fix: republish cancelable --- src/core/components/stop.js | 2 +- src/core/ipns/republisher.js | 33 +++++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/core/components/stop.js b/src/core/components/stop.js index 48e17c6f53..197f06e6bf 100644 --- a/src/core/components/stop.js +++ b/src/core/components/stop.js @@ -31,9 +31,9 @@ module.exports = (self) => { self._blockService.unsetExchange() self._bitswap.stop() self._preload.stop() - self._ipns.republisher.stop() series([ + (cb) => self._ipns.republisher.stop(cb), (cb) => self._mfsPreload.stop(cb), (cb) => self.libp2p.stop(cb), (cb) => self._repo.close(cb) diff --git a/src/core/ipns/republisher.js b/src/core/ipns/republisher.js index 52866e3a1f..449f38923d 100644 --- a/src/core/ipns/republisher.js +++ b/src/core/ipns/republisher.js @@ -26,20 +26,36 @@ class IpnsRepublisher { this._publisher = publisher this._ipfs = ipfs this._repo = ipfs._repo - this._interval = null + this._timeoutId = null + this._canceled = false + this._onCancel = null } - start () { - this._interval = setInterval(() => { - this._republishEntries(this._ipfs._peerInfo.id.privKey, this._ipfs._options.pass) - }, defaultBroadcastInterval) + start() { + const periodically = (cb) => { + this._republishEntries(this._ipfs._peerInfo.id.privKey, this._ipfs._options.pass, () => { + if (this._canceled) { + return this._onCancel() + } + this._timeoutId = setTimeout(() => periodically(cb), defaultBroadcastInterval) + }) + } + + periodically() } - stop () { - clearInterval(this._interval) + stop(cb) { + this._canceled = true + if (this._timeoutId) { + // Not running + clearTimeout(this._timeoutId) + return cb() + } + + this._onCancel = cb } - _republishEntries (privateKey, pass) { + _republishEntries (privateKey, pass, callback) { // TODO: Should use list of published entries. // We can't currently *do* that because go uses this method for now. this._republishEntry(privateKey, (err) => { @@ -74,6 +90,7 @@ class IpnsRepublisher { if (err) { log.error(err) } + callback(null) }) }) } From 36e51fbd8576ec2ed07fa70633c0d730469ea884 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 10 Aug 2018 12:07:33 +0100 Subject: [PATCH 25/34] chore: added tests for path --- src/core/components/name.js | 14 ++++++--- src/core/ipns/path.js | 16 +--------- src/core/utils.js | 25 +++++++++++++++ test/core/name.spec.js | 62 +++++++++++++++++++++++++++++++++++++ test/core/utils.js | 26 ++++++++++++++++ 5 files changed, 123 insertions(+), 20 deletions(-) diff --git a/src/core/components/name.js b/src/core/components/name.js index 4b104d65bd..e3edb4edd2 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -10,7 +10,7 @@ const crypto = require('libp2p-crypto') const log = debug('jsipfs:name') log.error = debug('jsipfs:name:error') -const errors = require('../utils') +const utils = require('../utils') const path = require('../ipns/path') const ERR_NOCACHE_AND_LOCAL = 'ERR_NOCACHE_AND_LOCAL' @@ -69,15 +69,17 @@ module.exports = function name (self) { const key = options.key || 'self' if (!self.isOnline()) { - const error = errors.OFFLINE_ERROR + const error = utils.OFFLINE_ERROR log.error(error) return callback(new Error(error)) } - // Parse path value + // TODO: params related logic should be in the core implementation + + // Normalize path value try { - value = path.parsePath(value) + value = utils.normalizePath(value) } catch (err) { log.error(err) return callback(err) @@ -131,12 +133,14 @@ module.exports = function name (self) { const local = true // TODO ROUTING - use self._options.local if (!self.isOnline() && !local) { - const error = errors.OFFLINE_ERROR + const error = utils.OFFLINE_ERROR log.error(error) return callback(new Error(error)) } + // TODO: params related logic should be in the core implementation + if (local && nocache) { const error = 'cannot specify both local and nocache' diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index a78bddb838..41afe30ec0 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -43,20 +43,6 @@ const resolvePath = (ipfsNode, name, callback) => { }) } -// parsePath returns a well-formed ipfs Path. -// The returned path will always be prefixed with /ipfs/ or /ipns/. -// If the received string is not a valid ipfs path, an error will be returned -const parsePath = (pathStr) => { - if (isIPFS.cid(pathStr)) { - return `/ipfs/${pathStr}` - } else if (isIPFS.path(pathStr)) { - return pathStr - } else { - throw Object.assign(new Error(`invalid 'ipfs ref' path`), { code: ERR_BAD_PATH }) - } -} - module.exports = { - resolvePath, - parsePath + resolvePath } diff --git a/src/core/utils.js b/src/core/utils.js index 5c66f76c94..7b3b992b26 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -5,6 +5,7 @@ const map = require('async/map') const isIpfs = require('is-ipfs') const CID = require('cids') +const ERR_BAD_PATH = 'ERR_BAD_PATH' exports.OFFLINE_ERROR = 'This command must be run in online mode. Try running \'ipfs daemon\' first.' /** @@ -37,6 +38,29 @@ function parseIpfsPath (ipfsPath) { } } +/** + * Returns a well-formed ipfs Path. + * The returned path will always be prefixed with /ipfs/ or /ipns/. + * If the received string is not a valid ipfs path, an error will be returned + * examples: + * b58Hash -> { hash: 'b58Hash', links: [] } + * b58Hash/mercury/venus -> { hash: 'b58Hash', links: ['mercury', 'venus']} + * /ipfs/b58Hash/links/by/name -> { hash: 'b58Hash', links: ['links', 'by', 'name'] } + * + * @param {String} pathStr An ipfs-path, or ipns-path or a cid + * @return {String} ipfs-path or ipns-path + * @throws on an invalid @param ipfsPath + */ +const normalizePath = (pathStr) => { + if (isIpfs.cid(pathStr)) { + return `/ipfs/${pathStr}` + } else if (isIpfs.path(pathStr)) { + return pathStr + } else { + throw Object.assign(new Error(`invalid ${pathStr} path`), { code: ERR_BAD_PATH }) + } +} + /** * Resolve various styles of an ipfs-path to the hash of the target node. * Follows links in the path. @@ -190,6 +214,7 @@ function parseChunkSize (str, name) { return size } +exports.normalizePath = normalizePath exports.parseIpfsPath = parseIpfsPath exports.resolvePath = resolvePath exports.parseChunkerString = parseChunkerString diff --git a/test/core/name.spec.js b/test/core/name.spec.js index 29fee863e9..05c9eda9b1 100644 --- a/test/core/name.spec.js +++ b/test/core/name.spec.js @@ -8,8 +8,11 @@ const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) +const fs = require('fs') + const isNode = require('detect-node') const IPFS = require('../../src') +const ipnsPath = require('../../src/core/ipns/path') const DaemonFactory = require('ipfsd-ctl') const df = DaemonFactory.create({ type: 'proc' }) @@ -145,3 +148,62 @@ describe('name', function () { }) }) }) + +describe('ipns.path', function () { + const path = 'test/fixtures/planets/solar-system.md' + const fixture = { + path, + content: fs.readFileSync(path) + } + + let node + let ipfsd + let nodeId + + if (!isNode) { + return + } + + before(function (done) { + this.timeout(40 * 1000) + df.spawn({ + exec: IPFS, + args: [`--pass ${hat()}`] + }, (err, _ipfsd) => { + expect(err).to.not.exist() + node = _ipfsd.api + ipfsd = _ipfsd + + node.id().then((res) => { + expect(res.id).to.exist() + + nodeId = res.id + done() + }) + }) + }) + + after((done) => ipfsd.stop(done)) + + it('should resolve an ipfs path correctly', function (done) { + node.files.add(fixture, (err, res) => { + ipnsPath.resolvePath(node, `/ipfs/${res[0].hash}`, (err, value) => { + expect(err).to.not.exist() + expect(value).to.exist() + done() + }) + }) + }) + + it('should resolve an ipns path correctly', function (done) { + node.files.add(fixture, (err, res) => { + node.name.publish(`/ipfs/${res[0].hash}`, (err, res) => { + ipnsPath.resolvePath(node, `/ipns/${nodeId}`, (err, value) => { + expect(err).to.not.exist() + expect(value).to.exist() + done() + }) + }) + }) + }) +}) diff --git a/test/core/utils.js b/test/core/utils.js index af73c30af6..b049cd1562 100644 --- a/test/core/utils.js +++ b/test/core/utils.js @@ -56,6 +56,32 @@ describe('utils', () => { }) }) + it('normalize path with no ipfs path, nor ipns path nor cid should throw an exception', function () { + try { + let ipfsPath = utils.normalizePath(`/${rootHash}/`) + } catch (err) { + expect(err).to.exist() + } + }) + + it('normalize path should return an ipfs path, when an ipfs path is provided', function () { + const ipfsPath = `/ipfs/${rootHash}` + expect(utils.normalizePath(ipfsPath)) + .to.equal(ipfsPath) + }) + + it('normalize path should return an ipfs path, when a cid is provided', function () { + const ipfsPath = `/ipfs/${rootHash}` + expect(utils.normalizePath(rootHash)) + .to.equal(ipfsPath) + }) + + it('normalize path should return an ipns path, when an ipns path is provided', function () { + const ipnsPath = `/ipns/${rootHash}` + expect(utils.normalizePath(ipnsPath)) + .to.equal(ipnsPath) + }) + it('parses non sha2-256 paths', function () { // There are many, many hashing algorithms. Just one should be a sufficient // indicator. Used go-ipfs@0.4.13 `add --hash=keccak-512` to generate From 87e9d911b7dafd1c93053d61eb5aa439d4bcc183 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 10 Aug 2018 12:14:11 +0100 Subject: [PATCH 26/34] fix: lint --- src/core/ipns/path.js | 1 - src/core/ipns/republisher.js | 4 ++-- test/core/name.spec.js | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index 41afe30ec0..442ca2ba11 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -6,7 +6,6 @@ const debug = require('debug') const log = debug('jsipfs:ipns:path') log.error = debug('jsipfs:ipns:path:error') -const ERR_BAD_PATH = 'ERR_BAD_PATH' const ERR_NO_COMPONENTS = 'ERR_NO_COMPONENTS' // resolves the given path by parsing out protocol-specific entries diff --git a/src/core/ipns/republisher.js b/src/core/ipns/republisher.js index 449f38923d..19f2206055 100644 --- a/src/core/ipns/republisher.js +++ b/src/core/ipns/republisher.js @@ -31,7 +31,7 @@ class IpnsRepublisher { this._onCancel = null } - start() { + start () { const periodically = (cb) => { this._republishEntries(this._ipfs._peerInfo.id.privKey, this._ipfs._options.pass, () => { if (this._canceled) { @@ -44,7 +44,7 @@ class IpnsRepublisher { periodically() } - stop(cb) { + stop (cb) { this._canceled = true if (this._timeoutId) { // Not running diff --git a/test/core/name.spec.js b/test/core/name.spec.js index 05c9eda9b1..0e0187446e 100644 --- a/test/core/name.spec.js +++ b/test/core/name.spec.js @@ -187,6 +187,7 @@ describe('ipns.path', function () { it('should resolve an ipfs path correctly', function (done) { node.files.add(fixture, (err, res) => { + expect(err).to.not.exist() ipnsPath.resolvePath(node, `/ipfs/${res[0].hash}`, (err, value) => { expect(err).to.not.exist() expect(value).to.exist() @@ -197,7 +198,9 @@ describe('ipns.path', function () { it('should resolve an ipns path correctly', function (done) { node.files.add(fixture, (err, res) => { + expect(err).to.not.exist() node.name.publish(`/ipfs/${res[0].hash}`, (err, res) => { + expect(err).to.not.exist() ipnsPath.resolvePath(node, `/ipns/${nodeId}`, (err, value) => { expect(err).to.not.exist() expect(value).to.exist() From df4a276f194a1b13ecbbd1edbef00acbe030efc5 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 10 Aug 2018 12:23:26 +0100 Subject: [PATCH 27/34] fix: code review --- src/core/components/init.js | 2 +- src/core/components/name.js | 4 ++-- src/core/ipns/path.js | 7 ------- test/core/utils.js | 2 +- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/core/components/init.js b/src/core/components/init.js index f99e1a8fc7..eb91a09979 100644 --- a/src/core/components/init.js +++ b/src/core/components/init.js @@ -104,7 +104,7 @@ module.exports = function init (self) { } }, // add empty unixfs dir object (go-ipfs assumes this exists) - (emptyDirNode, cb) => { + (_, cb) => { if (opts.emptyRepo) { return cb(null, true) } diff --git a/src/core/components/name.js b/src/core/components/name.js index e3edb4edd2..16d4fbb8ae 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -2,8 +2,8 @@ const debug = require('debug') const promisify = require('promisify-es6') -const series = require('async/series') const waterfall = require('async/waterfall') +const parallel = require('async/parallel') const human = require('human-to-milliseconds') const crypto = require('libp2p-crypto') @@ -85,7 +85,7 @@ module.exports = function name (self) { return callback(err) } - series([ + parallel([ (cb) => human(lifetime, cb), // (cb) => ttl ? human(ttl, cb) : cb(), (cb) => keyLookup(self, key, cb), diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index 442ca2ba11..c0c405957e 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -18,13 +18,6 @@ const resolvePath = (ipfsNode, name, callback) => { const local = true // TODO ROUTING - use self._options.local const parts = name.split('/') - if (parts.length < 3 || parts[2] === '') { - const errMsg = 'path must contain at least one component' - - log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_NO_COMPONENTS })) - } - const options = { local: local } diff --git a/test/core/utils.js b/test/core/utils.js index b049cd1562..aa49f1a626 100644 --- a/test/core/utils.js +++ b/test/core/utils.js @@ -58,7 +58,7 @@ describe('utils', () => { it('normalize path with no ipfs path, nor ipns path nor cid should throw an exception', function () { try { - let ipfsPath = utils.normalizePath(`/${rootHash}/`) + utils.normalizePath(`/${rootHash}/`) } catch (err) { expect(err).to.exist() } From 49725ae6f0929f5d4dc24c04e330d2e3d54cf084 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 10 Aug 2018 13:56:56 +0100 Subject: [PATCH 28/34] chore: refactored errors --- src/core/components/name.js | 12 ++++----- src/core/ipns/path.js | 3 --- src/core/ipns/publisher.js | 47 ++++++++++++++++++------------------ src/core/ipns/republisher.js | 18 ++++++-------- src/core/ipns/resolver.js | 17 +++++-------- 5 files changed, 41 insertions(+), 56 deletions(-) diff --git a/src/core/components/name.js b/src/core/components/name.js index 16d4fbb8ae..f0e9cd9ef7 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -6,6 +6,7 @@ const waterfall = require('async/waterfall') const parallel = require('async/parallel') const human = require('human-to-milliseconds') const crypto = require('libp2p-crypto') +const errcode = require('err-code') const log = debug('jsipfs:name') log.error = debug('jsipfs:name:error') @@ -13,9 +14,6 @@ log.error = debug('jsipfs:name:error') const utils = require('../utils') const path = require('../ipns/path') -const ERR_NOCACHE_AND_LOCAL = 'ERR_NOCACHE_AND_LOCAL' -const ERR_CANNOT_GET_KEY = 'ERR_CANNOT_GET_KEY' - const keyLookup = (ipfsNode, kname, callback) => { if (kname === 'self') { return callback(null, ipfsNode._peerInfo.id.privKey) @@ -29,7 +27,7 @@ const keyLookup = (ipfsNode, kname, callback) => { ], (err, privateKey) => { if (err) { log.error(err) - return callback(Object.assign(err, { code: ERR_CANNOT_GET_KEY })) + return callback(errcode(err, 'ERR_CANNOT_GET_KEY')) } return callback(null, privateKey) @@ -72,7 +70,7 @@ module.exports = function name (self) { const error = utils.OFFLINE_ERROR log.error(error) - return callback(new Error(error)) + return callback(errcode(new Error(error), 'OFFLINE_ERROR')) } // TODO: params related logic should be in the core implementation @@ -136,7 +134,7 @@ module.exports = function name (self) { const error = utils.OFFLINE_ERROR log.error(error) - return callback(new Error(error)) + return callback(errcode(new Error(error), 'OFFLINE_ERROR')) } // TODO: params related logic should be in the core implementation @@ -145,7 +143,7 @@ module.exports = function name (self) { const error = 'cannot specify both local and nocache' log.error(error) - return callback(Object.assign(new Error(error), { code: ERR_NOCACHE_AND_LOCAL })) + return callback(errcode(new Error(error), 'ERR_NOCACHE_AND_LOCAL')) } // Set node id as name for being resolved, if it is not received diff --git a/src/core/ipns/path.js b/src/core/ipns/path.js index c0c405957e..1bb05a8f90 100644 --- a/src/core/ipns/path.js +++ b/src/core/ipns/path.js @@ -6,8 +6,6 @@ const debug = require('debug') const log = debug('jsipfs:ipns:path') log.error = debug('jsipfs:ipns:path:error') -const ERR_NO_COMPONENTS = 'ERR_NO_COMPONENTS' - // resolves the given path by parsing out protocol-specific entries // (e.g. /ipns/) and then going through the /ipfs/ entries and returning the final node const resolvePath = (ipfsNode, name, callback) => { @@ -16,7 +14,6 @@ const resolvePath = (ipfsNode, name, callback) => { log(`resolve ipns path ${name}`) const local = true // TODO ROUTING - use self._options.local - const parts = name.split('/') const options = { local: local diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index 4157476d1f..f6296eafa9 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -4,6 +4,7 @@ const PeerId = require('peer-id') const Record = require('libp2p-record').Record const { Key } = require('interface-datastore') const series = require('async/series') +const errcode = require('err-code') const debug = require('debug') const log = debug('jsipfs:ipns:publisher') @@ -11,14 +12,6 @@ log.error = debug('jsipfs:ipns:publisher:error') const ipns = require('ipns') -const ERR_CREATING_IPNS_RECORD = 'ERR_CREATING_IPNS_RECORD' -const ERR_INVALID_IPNS_RECORD = 'ERR_INVALID_IPNS_RECORD' -const ERR_INVALID_DATASTORE_KEY = 'ERR_INVALID_DATASTORE_KEY' -const ERR_INVALID_PEER_ID = 'ERR_INVALID_PEER_ID' -const ERR_STORING_IN_DATASTORE = 'ERR_STORING_IN_DATASTORE' -const ERR_UNEXPECTED_DATASTORE_RESPONSE = 'ERR_UNEXPECTED_DATASTORE_RESPONSE' -const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER' - const defaultRecordTtl = 60 * 60 * 1000 // IpnsPublisher is capable of publishing and resolving names to the IPFS routing system. @@ -34,7 +27,7 @@ class IpnsPublisher { const errMsg = `one or more of the provided parameters are not defined` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_UNDEFINED_PARAMETER })) + return callback(errcode(new Error(errMsg), 'ERR_UNDEFINED_PARAMETER')) } PeerId.createFromPrivKey(privKey.bytes, (err, peerId) => { @@ -62,7 +55,7 @@ class IpnsPublisher { const errMsg = `peerId received is not valid` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PEER_ID })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_PEER_ID')) } const publicKey = peerId._pubKey @@ -101,7 +94,7 @@ class IpnsPublisher { const errMsg = `datastore key does not have a valid format` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_DATASTORE_KEY })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_DATASTORE_KEY')) } let rec @@ -118,8 +111,10 @@ class IpnsPublisher { // TODO Routing - this should be replaced by a put to the DHT this._repo.datastore.put(key, rec.serialize(), (err, res) => { if (err) { - log.error(`ipns record for ${key.toString()} could not be stored in the routing`) - return callback(Object.assign(new Error(`ipns record for ${key.toString()} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) + const errMsg = `ipns record for ${key.toString()} could not be stored in the routing` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_STORING_IN_DATASTORE')) } log(`ipns record for ${key.toString()} was stored in the routing`) @@ -132,14 +127,14 @@ class IpnsPublisher { const errMsg = `datastore key does not have a valid format` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_DATASTORE_KEY })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_DATASTORE_KEY')) } if (!publicKey || !publicKey.bytes) { const errMsg = `one or more of the provided parameters are not defined` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_UNDEFINED_PARAMETER })) + return callback(errcode(new Error(errMsg), 'ERR_UNDEFINED_PARAMETER')) } let rec @@ -154,8 +149,10 @@ class IpnsPublisher { // TODO Routing - this should be replaced by a put to the DHT this._repo.datastore.put(key, rec.serialize(), (err, res) => { if (err) { - log.error(`public key for ${key.toString()} could not be stored in the routing`) - return callback(Object.assign(new Error(`public key for ${key.toString()} could not be stored in the routing`), { code: ERR_STORING_IN_DATASTORE })) + const errMsg = `public key for ${key.toString()} could not be stored in the routing` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_STORING_IN_DATASTORE')) } log(`public key for ${key.toString()} was stored in the routing`) @@ -170,7 +167,7 @@ class IpnsPublisher { const errMsg = `peerId received is not valid` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PEER_ID })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_PEER_ID')) } options = options || {} @@ -184,7 +181,7 @@ class IpnsPublisher { const errMsg = `unexpected error getting the ipns record ${peerId.id} from datastore` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_UNEXPECTED_DATASTORE_RESPONSE })) + return callback(errcode(new Error(errMsg), 'ERR_UNEXPECTED_DATASTORE_RESPONSE')) } else { if (!checkRouting) { return callback(null, null) @@ -200,7 +197,7 @@ class IpnsPublisher { const errMsg = `found ipns record that we couldn't convert to a value` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_IPNS_RECORD })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_IPNS_RECORD')) } // unmarshal data @@ -222,7 +219,7 @@ class IpnsPublisher { const errMsg = `peerId received is not valid` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PEER_ID })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_PEER_ID')) } const getPublishedOptions = { @@ -246,7 +243,7 @@ class IpnsPublisher { const errMsg = `ipns record for ${value} could not be created` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_CREATING_IPNS_RECORD })) + return callback(errcode(new Error(errMsg), 'ERR_CREATING_IPNS_RECORD')) } // TODO IMPROVEMENT - set ttl (still experimental feature for go) @@ -257,8 +254,10 @@ class IpnsPublisher { // Store the new record this._repo.datastore.put(ipns.getLocalKey(peerId.id), data, (err, res) => { if (err) { - log.error(`ipns record for ${value} could not be stored in the datastore`) - return callback(Object.assign(new Error(`ipns record for ${value} could not be stored in the datastore`), { code: ERR_STORING_IN_DATASTORE })) + const errMsg = `ipns record for ${value} could not be stored in the datastore` + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_STORING_IN_DATASTORE')) } log(`ipns record for ${value} was stored in the datastore`) diff --git a/src/core/ipns/republisher.js b/src/core/ipns/republisher.js index 19f2206055..3febdaaeca 100644 --- a/src/core/ipns/republisher.js +++ b/src/core/ipns/republisher.js @@ -3,6 +3,7 @@ const ipns = require('ipns') const crypto = require('libp2p-crypto') const PeerId = require('peer-id') +const errcode = require('err-code') const debug = require('debug') const each = require('async/each') @@ -16,11 +17,6 @@ const hour = 60 * minute const defaultBroadcastInterval = 4 * hour const defaultRecordLifetime = 24 * hour -const ERR_NO_ENTRY_FOUND = 'ERR_NO_ENTRY_FOUND' -const ERR_INVALID_IPNS_RECORD = 'ERR_INVALID_IPNS_RECORD' -const ERR_INVALID_PEER_ID = 'ERR_INVALID_PEER_ID' -const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER' - class IpnsRepublisher { constructor (publisher, ipfs) { this._publisher = publisher @@ -102,7 +98,7 @@ class IpnsRepublisher { const errMsg = `one or more of the provided parameters are not defined` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_UNDEFINED_PARAMETER })) + return callback(errcode(new Error(errMsg), 'ERR_UNDEFINED_PARAMETER')) } waterfall([ @@ -110,7 +106,7 @@ class IpnsRepublisher { (peerId, cb) => this._getPreviousValue(peerId, cb) ], (err, value) => { if (err) { - return callback(err.code === ERR_NO_ENTRY_FOUND ? null : err) + return callback(err.code === 'ERR_NO_ENTRY_FOUND' ? null : err) } this._publisher.publishWithEOL(privateKey, value, defaultRecordLifetime, callback) @@ -122,7 +118,7 @@ class IpnsRepublisher { const errMsg = `peerId received is not valid` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PEER_ID })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_PEER_ID')) } this._repo.datastore.get(ipns.getLocalKey(peerId.id), (err, dsVal) => { @@ -132,7 +128,7 @@ class IpnsRepublisher { const errMsg = `no previous entry for record with id: ${peerId.id}` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_NO_ENTRY_FOUND })) + return callback(errcode(new Error(errMsg), 'ERR_NO_ENTRY_FOUND')) } else if (err) { return callback(err) } @@ -141,7 +137,7 @@ class IpnsRepublisher { const errMsg = `found ipns record that we couldn't process` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_IPNS_RECORD })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_IPNS_RECORD')) } // unmarshal data @@ -152,7 +148,7 @@ class IpnsRepublisher { const errMsg = `found ipns record that we couldn't convert to a value` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_IPNS_RECORD })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_IPNS_RECORD')) } callback(null, record.value) diff --git a/src/core/ipns/resolver.js b/src/core/ipns/resolver.js index 7f5c6b9e75..0b945e8b5b 100644 --- a/src/core/ipns/resolver.js +++ b/src/core/ipns/resolver.js @@ -3,17 +3,12 @@ const ipns = require('ipns') const { fromB58String } = require('multihashes') const Record = require('libp2p-record').Record +const errcode = require('err-code') const debug = require('debug') const log = debug('jsipfs:ipns:resolver') log.error = debug('jsipfs:ipns:resolver:error') -const ERR_INVALID_NAME_SYNTAX = 'ERR_INVALID_NAME_SYNTAX' -const ERR_INVALID_RECORD_RECEIVED = 'ERR_INVALID_RECORD_RECEIVED' -const ERR_INVALID_PARAMETER = 'ERR_INVALID_PARAMETER' -const ERR_NO_LOCAL_RECORD_FOUND = 'ERR_NO_LOCAL_RECORD_FOUND' -const ERR_RESOLVE_RECURSION_LIMIT = 'ERR_RESOLVE_RECURSION_LIMIT' - const defaultMaximumRecursiveDepth = 32 class IpnsResolver { @@ -33,7 +28,7 @@ class IpnsResolver { const errMsg = `one or more of the provided parameters are not valid` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_PARAMETER })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_PARAMETER')) } options = options || {} @@ -46,7 +41,7 @@ class IpnsResolver { const errMsg = `invalid name syntax for ${name}` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_NAME_SYNTAX })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_NAME_SYNTAX')) } const key = nameSegments[2] @@ -91,7 +86,7 @@ class IpnsResolver { const errMsg = `could not resolve name (recursion limit of ${defaultMaximumRecursiveDepth} exceeded)` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_RESOLVE_RECURSION_LIMIT })) + return callback(errcode(new Error(errMsg), 'ERR_RESOLVE_RECURSION_LIMIT')) } this._resolver(name, peerId, (err, res) => { @@ -120,14 +115,14 @@ class IpnsResolver { const errMsg = `local record requested was not found for ${name} (${ipnsKey})` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_NO_LOCAL_RECORD_FOUND })) + return callback(errcode(new Error(errMsg), 'ERR_NO_LOCAL_RECORD_FOUND')) } if (!Buffer.isBuffer(dsVal)) { const errMsg = `found ipns record that we couldn't convert to a value` log.error(errMsg) - return callback(Object.assign(new Error(errMsg), { code: ERR_INVALID_RECORD_RECEIVED })) + return callback(errcode(new Error(errMsg), 'ERR_INVALID_RECORD_RECEIVED')) } const record = Record.deserialize(dsVal) From ca255163843b0d26cbb1bd93a3e579d67b8ceec7 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 10 Aug 2018 14:05:24 +0100 Subject: [PATCH 29/34] chore: upgrade interface-ipfs-core --- test/core/interface.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/core/interface.spec.js b/test/core/interface.spec.js index 2cf613f30b..653536687b 100644 --- a/test/core/interface.spec.js +++ b/test/core/interface.spec.js @@ -65,13 +65,13 @@ describe('interface-ipfs-core tests', () => { ] }) - /* TODO uncomment once interface-ipfs-core PR get merged tests.name(CommonFactory.create({ spawnOptions: { args: ['--pass ipfs-is-awesome-software'], initOptions: { bits: 512 } } - })) */ + })) + tests.object(defaultCommonFactory) tests.pin(defaultCommonFactory) From 23b5fbc3406703bd7ecb835aa20e3614395dee6b Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 10 Aug 2018 16:04:56 +0100 Subject: [PATCH 30/34] fix: initial rebroadcast delay --- src/core/ipns/republisher.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/ipns/republisher.js b/src/core/ipns/republisher.js index 3febdaaeca..fa55a1c53e 100644 --- a/src/core/ipns/republisher.js +++ b/src/core/ipns/republisher.js @@ -37,12 +37,14 @@ class IpnsRepublisher { }) } - periodically() + setTimeout(() => { + periodically() + }, minute) } stop (cb) { this._canceled = true - if (this._timeoutId) { + if (this._timeoutId || !this._onCancel) { // Not running clearTimeout(this._timeoutId) return cb() From 6bb6c97b51a27f318788afde1e78e7bd95c537ad Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Fri, 10 Aug 2018 16:04:56 +0100 Subject: [PATCH 31/34] fix: initial rebroadcast delay --- src/core/components/name.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/core/components/name.js b/src/core/components/name.js index f0e9cd9ef7..f53878f84e 100644 --- a/src/core/components/name.js +++ b/src/core/components/name.js @@ -67,10 +67,10 @@ module.exports = function name (self) { const key = options.key || 'self' if (!self.isOnline()) { - const error = utils.OFFLINE_ERROR + const errMsg = utils.OFFLINE_ERROR - log.error(error) - return callback(errcode(new Error(error), 'OFFLINE_ERROR')) + log.error(errMsg) + return callback(errcode(errMsg, 'OFFLINE_ERROR')) } // TODO: params related logic should be in the core implementation @@ -131,10 +131,10 @@ module.exports = function name (self) { const local = true // TODO ROUTING - use self._options.local if (!self.isOnline() && !local) { - const error = utils.OFFLINE_ERROR + const errMsg = utils.OFFLINE_ERROR - log.error(error) - return callback(errcode(new Error(error), 'OFFLINE_ERROR')) + log.error(errMsg) + return callback(errcode(errMsg, 'OFFLINE_ERROR')) } // TODO: params related logic should be in the core implementation From 7cd8fc55e8e4474fe36c04870b44c0fdc1894232 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Mon, 27 Aug 2018 10:39:00 +0100 Subject: [PATCH 32/34] chore: upgrade ipns dependency --- package.json | 2 +- test/cli/commands.js | 2 +- test/core/{name.spec.js => name.js} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename test/core/{name.spec.js => name.js} (100%) diff --git a/package.json b/package.json index 098503851c..1fc65f0b29 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "ipld": "~0.17.3", "ipld-dag-cbor": "~0.12.1", "ipld-dag-pb": "~0.14.6", - "ipns": "~0.1.2", + "ipns": "~0.1.3", "is-ipfs": "~0.4.2", "is-pull-stream": "~0.0.0", "is-stream": "^1.1.0", diff --git a/test/cli/commands.js b/test/cli/commands.js index 15108bc2a5..e6ae40498a 100644 --- a/test/cli/commands.js +++ b/test/cli/commands.js @@ -4,7 +4,7 @@ const expect = require('chai').expect const runOnAndOff = require('../utils/on-and-off') -const commandCount = 80 +const commandCount = 81 describe('commands', () => runOnAndOff((thing) => { let ipfs diff --git a/test/core/name.spec.js b/test/core/name.js similarity index 100% rename from test/core/name.spec.js rename to test/core/name.js From 84ee2c82bc12f0a57641632adda501ab96a024f2 Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 28 Aug 2018 17:15:48 +0100 Subject: [PATCH 33/34] fix: added not implemented error for routing on ipns getPublished --- src/core/ipns/publisher.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/ipns/publisher.js b/src/core/ipns/publisher.js index f6296eafa9..877d118815 100644 --- a/src/core/ipns/publisher.js +++ b/src/core/ipns/publisher.js @@ -186,7 +186,8 @@ class IpnsPublisher { if (!checkRouting) { return callback(null, null) } else { - // TODO ROUTING - get + // TODO ROUTING - get from DHT + return callback(new Error('not implemented yet')) } } } From 77caec14882a3ecbcb28c067139e42cbcac4b02c Mon Sep 17 00:00:00 2001 From: Vasco Santos Date: Tue, 28 Aug 2018 18:46:37 +0100 Subject: [PATCH 34/34] fix: republish working for starting and stopping multiple times --- src/core/ipns/republisher.js | 71 +++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/src/core/ipns/republisher.js b/src/core/ipns/republisher.js index fa55a1c53e..feeba6d665 100644 --- a/src/core/ipns/republisher.js +++ b/src/core/ipns/republisher.js @@ -22,35 +22,68 @@ class IpnsRepublisher { this._publisher = publisher this._ipfs = ipfs this._repo = ipfs._repo - this._timeoutId = null - this._canceled = false - this._onCancel = null + this._republishHandle = null } start () { - const periodically = (cb) => { - this._republishEntries(this._ipfs._peerInfo.id.privKey, this._ipfs._options.pass, () => { - if (this._canceled) { - return this._onCancel() + if (this._republishHandle) { + const errMsg = 'already running' + + log.error(errMsg) + throw errcode(new Error(errMsg), 'ERR_REPUBLISH_ALREADY_RUNNING') + } + + // TODO: this handler should be isolated in another module + const republishHandle = { + _onCancel: null, + _timeoutId: null, + runPeriodically: (fn, period) => { + republishHandle._timeoutId = setTimeout(() => { + republishHandle._timeoutId = null + + fn((nextPeriod) => { + // Was republish cancelled while fn was being called? + if (republishHandle._onCancel) { + return republishHandle._onCancel() + } + // Schedule next + republishHandle.runPeriodically(fn, nextPeriod || period) + }) + }, period) + }, + cancel: (cb) => { + // Not currently running a republish, can callback immediately + if (republishHandle._timeoutId) { + clearTimeout(republishHandle._timeoutId) + return cb() } - this._timeoutId = setTimeout(() => periodically(cb), defaultBroadcastInterval) - }) + // Wait for republish to finish then call callback + republishHandle._onCancel = cb + } } - setTimeout(() => { - periodically() + const { privKey } = this._ipfs._peerInfo.id + const { pass } = this._ipfs._options + + republishHandle.runPeriodically((done) => { + this._republishEntries(privKey, pass, () => done(defaultBroadcastInterval)) }, minute) + + this._republishHandle = republishHandle } - stop (cb) { - this._canceled = true - if (this._timeoutId || !this._onCancel) { - // Not running - clearTimeout(this._timeoutId) - return cb() + stop (callback) { + const republishHandle = this._republishHandle + + if (!republishHandle) { + const errMsg = 'not running' + + log.error(errMsg) + return callback(errcode(new Error(errMsg), 'ERR_REPUBLISH_NOT_RUNNING')) } - this._onCancel = cb + this._republishHandle = null + republishHandle.cancel(callback) } _republishEntries (privateKey, pass, callback) { @@ -91,6 +124,8 @@ class IpnsRepublisher { callback(null) }) }) + } else { + callback(null) } }) }