diff --git a/README.md b/README.md index 6d37c93..5d9a37e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/ - [x] var_int - [x] var_str - [x] var_int_list - - [ ] net_addr + - [x] net_addr - [ ] encrypted - [x] message encodings - [x] service features @@ -57,6 +57,7 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/ - [x] getRipe - [x] fromRandom - [ ] fromPassphrase + - [ ] UserAgent - [ ] Message - [ ] encrypt - [ ] decrypt diff --git a/lib/structs.js b/lib/structs.js index e01a74f..f5348a8 100644 --- a/lib/structs.js +++ b/lib/structs.js @@ -87,17 +87,17 @@ var message = exports.message = { /** - * var_int. + * Variable length integer. * @see {@link https://bitmessage.org/wiki/Protocol_specification#Variable_length_integer} * @namespace * @static */ var var_int = exports.var_int = { /** - * Decode var_int. - * @param {Buffer} buf - A buffer that starts with encoded var_int + * Decode `var_int`. + * @param {Buffer} buf - A buffer that starts with encoded `var_int` * @return {{value: number, length: number, rest: Buffer}} - * Decoded var_int structure. + * Decoded `var_int` structure. */ decode: function(buf) { var value, length; @@ -140,9 +140,9 @@ var var_int = exports.var_int = { }, /** - * Encode number into var_int. + * Encode number into `var_int`. * @param {(number|Buffer)} value - Input number - * @return {Buffer} Encoded var_int. + * @return {Buffer} Encoded `var_int`. */ encode: function(value) { var buf, targetStart; @@ -180,16 +180,16 @@ var var_int = exports.var_int = { }; /** - * var_str. + * Variable length string. * @see {@link https://bitmessage.org/wiki/Protocol_specification#Variable_length_string} * @namespace */ exports.var_str = { /** - * Decode var_str. - * @param {Buffer} buf - A buffer that starts with encoded var_str + * Decode `var_str`. + * @param {Buffer} buf - A buffer that starts with encoded `var_str` * @return {{str: string, length: number, rest: Buffer}} - * Decoded var_str structure. + * Decoded `var_str` structure. */ decode: function(buf) { var decoded = var_int.decode(buf); @@ -201,9 +201,9 @@ exports.var_str = { }, /** - * Encode string into var_str. + * Encode string into `var_str`. * @param {string} str - A string - * @return {Buffer} Encoded var_str. + * @return {Buffer} Encoded `var_str`. */ encode: function(str) { // XXX(Kagami): Spec doesn't mention encoding, using UTF-8. @@ -213,17 +213,17 @@ exports.var_str = { }; /** - * var_int_list. + * Variable length list of integers. * @see {@link https://bitmessage.org/wiki/Protocol_specification#Variable_length_list_of_integers} * @namespace */ exports.var_int_list = { /** - * Decode var_int_list. + * Decode `var_int_list`. * @param {Buffer} buf - A buffer that starts with encoded - * var_int_list + * `var_int_list` * @return {{list: number[], length: number, rest: Buffer}} - * Decoded var_int_list structure. + * Decoded `var_int_list` structure. */ decode: function(buf) { var decoded = var_int.decode(buf); @@ -241,9 +241,9 @@ exports.var_int_list = { }, /** - * Encode list of numbers into var_int_list. + * Encode list of numbers into `var_int_list`. * @param {number[]} list - A number list - * @return {Buffer} Encoded var_int_list. + * @return {Buffer} Encoded `var_int_list`. */ encode: function(list) { var listBuf = Buffer.concat(list.map(var_int.encode)); @@ -251,10 +251,164 @@ exports.var_int_list = { }, }; +// See https://en.wikipedia.org/wiki/IPv6#IPv4-mapped_IPv6_addresses +var IPv4_MAPPING = new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255]); + +// Very simple equivalent of inet_ntop(3). +function inet_ntop(buf) { + assert(buf.length === 16, "Bad buffer size"); + // IPv4 mapped to IPv6. + if (bufferEqual(buf.slice(0, 12), IPv4_MAPPING)) { + return Array.prototype.join.call(buf.slice(12), "."); + // IPv6. + } else { + var groups = []; + for (var i = 0; i < 8; i++) { + groups.push(buf.readUInt16BE(i * 2, true).toString(16)); + } + return groups.join(":"); + } +} + +// Very simple equivalent of inet_pton(3). +function inet_pton(str) { + var buf = new Buffer(16); + buf.fill(0); + // IPv4. + if (str.indexOf(":") === -1) { + IPv4_MAPPING.copy(buf); + var octets = str.split(/\./g).map(function(s) {return parseInt(s, 10);}); + // Support short form from inet_aton(3) man page. + if (octets.length === 1) { + buf.writeUInt32BE(octets[0], 12); + } else { + // To check against 1000.bad.addr + octets.forEach(function(octet) { + assert(octet >= 0, "Bad IPv4 address"); + assert(octet <= 255, "Bad IPv4 address"); + }); + if (octets.length === 2) { + buf[12] = octets[0]; + buf[15] = octets[1]; + } else if (octets.length === 3) { + buf[12] = octets[0]; + buf[13] = octets[1]; + buf[15] = octets[2]; + } else if (octets.length === 4) { + buf[12] = octets[0]; + buf[13] = octets[1]; + buf[14] = octets[2]; + buf[15] = octets[3]; + } else { + throw new Error("Bad IPv4 address"); + } + } + // IPv6. + } else { + var dgroups = str.split(/::/g); + // To check against 1::1::1 + assert(dgroups.length <= 2, "Bad IPv6 address"); + var groups = []; + var i; + if (dgroups[0]) { + groups.push.apply(groups, dgroups[0].split(/:/g)); + } + if (dgroups.length === 2) { + if (dgroups[1]) { + var splitted = dgroups[1].split(/:/g); + var fill = 8 - (groups.length + splitted.length); + // To check against 1:1:1:1::1:1:1:1 + assert(fill > 0, "Bad IPv6 address"); + for (i = 0; i < fill; i++) { + groups.push(0); + } + groups.push.apply(groups, splitted); + } else { + // To check against 1:1:1:1:1:1:1:1:: + assert(groups.length <= 7, "Bad IPv6 address"); + } + } else { + // To check against 1:1:1 + assert(groups.length === 8, "Bad IPv6 address"); + } + for (i = 0; i < Math.min(groups.length, 8); i++) { + buf.writeUInt16BE(parseInt(groups[i], 16), i * 2); + } + } + return buf; +} + +/** + * Network address. + * @see {@link https://bitmessage.org/wiki/Protocol_specification#Network_address} + * @namespace + */ +exports.net_addr = { + /** + * Decode `net_addr`. + * @param {Buffer} buf - A buffer that contains encoded `net_addr` + * @param {?Object} opts - Decode options; use `short` option to + * decode `net_addr` used in version message + * @return {Object} Decoded `net_addr` structure. + */ + decode: function(buf, opts) { + var short = !!(opts || {}).short; + var res = {}; + if (short) { + assert(buf.length === 26, "Bad buffer size"); + } else { + assert(buf.length === 38, "Bad buffer size"); + var timeHi = buf.readUInt32BE(0, true); + var timeLo = buf.readUInt32BE(4, true); + // JavaScript's Date object can't work with timestamps higher than + // 8640000000000 (~2^43, ~275760 year). + assert(timeHi <= 2011, "Time is too high"); + assert(timeHi !== 2011 || timeLo <= 2820767744, "Time is too high"); + res.time = new Date((timeHi * 4294967296 + timeLo) * 1000); + res.stream = buf.readUInt32BE(8, true); + buf = buf.slice(12); + } + res.services = serviceFeatures.decode(buf.slice(0, 8)); + res.host = inet_ntop(buf.slice(8, 24)); + res.port = buf.readUInt16BE(24, true); + return res; + }, + + /** + * Encode `net_addr`. + * @param {Object} opts - Encode options; use `short` option to encode + * `net_addr` used in version message + * @return {Buffer} Encoded `net_addr`. + */ + encode: function(opts) { + // Be aware of `Buffer.slice` quirk in browserify: + // (does not modify parent buffer's memory in + // old browsers). + var buf, shift; + if (opts.short) { + buf = new Buffer(26); + shift = 0; + } else { + buf = new Buffer(38); + var time = opts.time || new Date(); + time = Math.floor(time.getTime() / 1000); + buf.writeUInt32BE(Math.floor(time / 4294967296), 0, true); // high32 + buf.writeUInt32BE(time % 4294967296, 4, true); // low32 + buf.writeUInt32BE(opts.stream, 8); + shift = 12; + } + serviceFeatures.encode(opts.services).copy(buf, shift); + inet_pton(opts.host).copy(buf, shift + 8); + buf.writeUInt16BE(opts.port, shift + 24); + return buf; + }, +}; + /** * Message encodings. Extends {@link var_int} by adding known encoding type * constants. * @see {@link https://bitmessage.org/wiki/Protocol_specification#Message_Encodings} + * @namespace */ exports.messageEncodings = Object.assign(Object.create(var_int), { /** @@ -308,8 +462,10 @@ var bitfield = function(size) { * Service bitfield features. Implements encoding/decoding for a 8-byte * buffer object. * @see {@link https://bitmessage.org/wiki/Protocol_specification#version} + * @namespace + * @static */ -exports.serviceFeatures = Object.assign(bitfield(64), { +var serviceFeatures = exports.serviceFeatures = Object.assign(bitfield(64), { /** This is a normal network node. */ NODE_NETWORK: 0, }); @@ -318,6 +474,7 @@ exports.serviceFeatures = Object.assign(bitfield(64), { * Pubkey bitfield features. Implements encoding/decoding for a 4-byte * buffer object. * @see {@link https://bitmessage.org/wiki/Protocol_specification#Pubkey_bitfield_features} + * @namespace */ exports.pubkeyFeatures = Object.assign(bitfield(32), { /** diff --git a/test.js b/test.js index b6d9148..05c3658 100644 --- a/test.js +++ b/test.js @@ -10,6 +10,7 @@ var message = structs.message; var var_int = structs.var_int; var var_str = structs.var_str; var var_int_list = structs.var_int_list; +var net_addr = structs.net_addr; var messageEncodings = structs.messageEncodings; var serviceFeatures = structs.serviceFeatures; var pubkeyFeatures = structs.pubkeyFeatures; @@ -171,6 +172,39 @@ describe("Common structures", function() { }); }); + // FIXME(Kagami): Add more tests for inet_pton, inet_ntop; add more + // fail tests. + describe("net_addr", function() { + it("should decode", function() { + var res; + res = net_addr.decode(Buffer("0000000054aaf6c000000001000000000000000100000000000000000000ffff7f00000120fc", "hex")); + expect(res.time.getTime()).to.equal(1420490432000); + expect(res.stream).to.equal(1); + expect(res.services).to.have.members([serviceFeatures.NODE_NETWORK]); + expect(res.host).to.equal("127.0.0.1"); + expect(res.port).to.equal(8444); + + expect(net_addr.decode.bind(null, Buffer("000000000000000100000000000000000000ffff7f00000120fc", "hex"))).to.throw(Error);; + + res = net_addr.decode(Buffer("000000000000000100000000000000000000ffff7f00000120fc", "hex"), {short: true}); + expect(res.services).to.have.members([serviceFeatures.NODE_NETWORK]); + expect(res.host).to.equal("127.0.0.1"); + expect(res.port).to.equal(8444); + + res = net_addr.decode(Buffer("000000000000000100000000000000000000000000000001fde8", "hex"), {short: true}); + expect(res.services).to.have.members([serviceFeatures.NODE_NETWORK]); + expect(res.host).to.equal("0:0:0:0:0:0:0:1"); + expect(res.port).to.equal(65000); + }); + + it("should encode", function() { + var time = new Date(1420490432000); + expect(net_addr.encode({time: time, stream: 1, services: [serviceFeatures.NODE_NETWORK], host: "127.0.0.1", port: 8444}).toString("hex")).to.equal("0000000054aaf6c000000001000000000000000100000000000000000000ffff7f00000120fc"); + expect(net_addr.encode({short: true, services: [serviceFeatures.NODE_NETWORK], host: "127.0.0.1", port: 8444}).toString("hex")).to.equal("000000000000000100000000000000000000ffff7f00000120fc"); + expect(net_addr.encode({short: true, services: [serviceFeatures.NODE_NETWORK], host: "::1", port: 65000}).toString("hex")).to.equal("000000000000000100000000000000000000000000000001fde8"); + }); + }); + describe("Message encodings", function() { it("should decode", function() { expect(messageEncodings.decode(Buffer([2])).value).to.equal(messageEncodings.SIMPLE); @@ -229,6 +263,7 @@ describe("WIF", function() { }); }); +// FIXME(Kagami): Add more fail tests. describe("Address", function() { it("should decode Bitmessage address", function() { var addr = Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z")