diff --git a/README.md b/README.md index 673c324..9c268cf 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/ - [x] service features - [x] pubkey features - [ ] Message types - - [ ] version + - [x] version - [ ] verack - [ ] addr - [ ] inv diff --git a/lib/index.js b/lib/index.js index e19c0f0..5598022 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,6 +5,9 @@ "use strict"; +/** Current protocol version. */ +exports.PROTOCOL_VERSION = 3; + /** [Common structures.]{@link module:bitmessage/structs} */ exports.structs = require("./structs"); /** [Messages.]{@link module:bitmessage/messages} */ diff --git a/lib/messages.js b/lib/messages.js index e5e0d6b..caaa920 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -3,3 +3,97 @@ * @see {@link https://bitmessage.org/wiki/Protocol_specification#Message_types} * @module bitmessage/messages */ + +"use strict"; + +var assert = require("./util").assert; +var structs = require("./structs"); +var UserAgent = require("./user-agent"); +var util = require("./util"); + +/** + * Version message. + * @see {@link https://bitmessage.org/wiki/Protocol_specification#version} + * @namespace + */ +exports.version = { + /** Random nonce used to detect connections to self. */ + NONCE: new Buffer("20bde0a3355dad78", "hex"), + + /** + * Decode version payload. + * NOTE: `nonce` is copied. + * @param {Buffer} buf - Buffer that starts with encoded version + * payload + * @return {Object} Decoded version structure. + */ + decode: function(payload) { + // 4 + 8 + 8 + 26 + 26 + 8 + (1+) + (1+) + assert(payload.length >= 82, "Message payload is too small"); + var protoVersion = payload.readUInt32BE(0, true); + var services = structs.serviceFeatures.decode(payload.slice(4, 12)); + var time = util.readTime64BE(payload, 12); + var short = {short: true}; + var addrRecv = structs.net_addr.decode(payload.slice(20, 46), short); + var addrFrom = structs.net_addr.decode(payload.slice(46, 72), short); + var nonce = new Buffer(8); + payload.copy(nonce, 0, 72, 80); + var decodedUa = UserAgent.decode(payload.slice(80)); + var decodedStreamNumbers = structs.var_int_list.decode(decodedUa.rest); + return { + version: protoVersion, + services: services, + time: time, + remoteHost: addrRecv.host, + remotePort: addrRecv.port, + port: addrFrom.port, + nonce: nonce, + software: decodedUa.software, + streamNumbers: decodedStreamNumbers.list, + // NOTE(Kagami): Real data length. It may be some gap between end + // of stream numbers list and end of payload: + // [payload..............[stream numbers]xxxx] + // We are currently ignoring that. + length: 80 + decodedUa.length + decodedStreamNumbers.length, + }; + }, + + /** + * Encode version payload. + * @param {Object} opts - Version options + * @return {Buffer} Encoded version payload. + */ + encode: function(opts) { + // Deal with default options. + var services = opts.services || [structs.serviceFeatures.NODE_NETWORK]; + var time = opts.time || new Date(); + var nonce = opts.nonce || exports.version.NONCE; + var software = opts.software || UserAgent.SELF; + var streamNumbers = opts.streamNumbers || [1]; + // Start encoding. + var protoVersion = new Buffer(4); + protoVersion.writeUInt32BE(require("./").PROTOCOL_VERSION, 0); + var addrRecv = structs.net_addr.encode({ + services: services, + host: opts.remoteHost, + port: opts.remotePort, + short: true, + }); + var addrFrom = structs.net_addr.encode({ + services: services, + host: "127.0.0.1", + port: opts.port, + short: true, + }); + return Buffer.concat([ + protoVersion, + structs.serviceFeatures.encode(services), + util.writeTime64BE(null, time), + addrRecv, + addrFrom, + nonce, + UserAgent.encode(software), + structs.var_int_list.encode(streamNumbers), + ]); + }, +}; diff --git a/lib/structs.js b/lib/structs.js index 8c60b6d..8b8d21e 100644 --- a/lib/structs.js +++ b/lib/structs.js @@ -59,13 +59,14 @@ var message = exports.message = { command = command.slice(0, firstNonNull).toString("ascii"); var payloadLength = buf.readUInt32BE(16, true); assert(payloadLength <= 262144, "Payload is too big"); - var checksum = buf.slice(20, 24); var length = 24 + payloadLength; + assert(buf.length >= length, "Truncated payload"); + var checksum = buf.slice(20, 24); // NOTE(Kagami): We do copy instead of slice to protect against // possible source buffer modification by user. var payload = new Buffer(payloadLength); buf.copy(payload, 0, 24, length); - assert(bufferEqual(checksum, getmsgchecksum(payload)), "Bad checkum"); + assert(bufferEqual(checksum, getmsgchecksum(payload)), "Bad checksum"); var rest = buf.slice(length); return {command: command, payload: payload, length: length, rest: rest}; }, @@ -393,7 +394,7 @@ exports.net_addr = { encode: function(opts) { // Be aware of `Buffer.slice` quirk in browserify: // (does not modify parent buffer's memory in - // old browsers). + // old browsers). So we use offset instead of `buf = buf.slice`. var buf, shift; if (opts.short) { buf = new Buffer(26); @@ -404,7 +405,8 @@ exports.net_addr = { 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); + var stream = opts.stream || 1; + buf.writeUInt32BE(stream, 8); shift = 12; } var services = opts.services || [serviceFeatures.NODE_NETWORK]; diff --git a/lib/util.js b/lib/util.js index ffe85a2..fad58d0 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,7 +1,58 @@ "use strict"; -exports.assert = function(condition, message) { +var assert = exports.assert = function(condition, message) { if (!condition) { throw new Error(message || "Assertion failed"); } }; + +// Missing methods to read/write 64 bits integers from/to buffers. +// TODO(Kagami): Use this helpers in structs, pow, platform. + +var MAX_SAFE_INTEGER = exports.MAX_SAFE_INTEGER = 9007199254740991; + +exports.readUInt64BE = function(buf, offset, noAssert) { + offset = offset || 0; + var hi = buf.readUInt32BE(offset, noAssert); + var lo = buf.readUInt32BE(offset + 4, noAssert); + // Max safe number = 2^53 - 1 = + // 0b0000000000011111111111111111111111111111111111111111111111111111 + // = 2097151*(2^32) + (2^32 - 1). + // So it's safe until hi <= 2097151. See + // , + // for details. + assert(noAssert || hi <= 2097151, "Unsafe integer"); + return hi * 4294967296 + lo; +}; + +var readTimestamp64BE = exports.readTimestamp64BE = function(buf, offset) { + offset = offset || 0; + var timeHi = buf.readUInt32BE(offset); + var timeLo = buf.readUInt32BE(offset + 4); + // JavaScript's Date object can't work with timestamps higher than + // 8640000000000 (~2^43, ~275760 year). Hope JavaScript will support + // 64-bit numbers up to this date. + assert(timeHi <= 2011, "Time is too high"); + assert(timeHi !== 2011 || timeLo <= 2820767744, "Time is too high"); + return timeHi * 4294967296 + timeLo; +}; + +exports.readTime64BE = function(buf, offset) { + var timestamp = readTimestamp64BE(buf, offset); + return new Date(timestamp * 1000); +}; + +exports.writeUInt64BE = function(buf, value, offset, noAssert) { + buf = buf || new Buffer(8); + offset = offset || 0; + assert(noAssert || value <= MAX_SAFE_INTEGER, "Unsafe integer"); + buf.writeUInt32BE(Math.floor(value / 4294967296), offset, noAssert); + buf.writeUInt32BE(value % 4294967296, offset + 4, noAssert); + return buf; +}; +var writeUInt64BE = exports.writeUInt64BE; + +exports.writeTime64BE = function(buf, time, offset, noAssert) { + var timestamp = Math.floor(time.getTime() / 1000); + return writeUInt64BE(buf, timestamp, offset, noAssert); +}; diff --git a/src/pow.h b/src/pow.h index 06a59d1..ff6615d 100644 --- a/src/pow.h +++ b/src/pow.h @@ -1,8 +1,8 @@ #ifndef BITCHAN_BITMESSAGE_POW_H_ #define BITCHAN_BITMESSAGE_POW_H_ -static const int MAX_POOL_SIZE = 1024; -static const int HASH_SIZE = 64; +static const size_t MAX_POOL_SIZE = 1024; +static const size_t HASH_SIZE = 64; int pow(size_t pool_size, uint64_t target, diff --git a/test.js b/test.js index c9ddb7a..e6ac178 100644 --- a/test.js +++ b/test.js @@ -16,6 +16,8 @@ var encrypted = structs.encrypted; var messageEncodings = structs.messageEncodings; var serviceFeatures = structs.serviceFeatures; var pubkeyFeatures = structs.pubkeyFeatures; +var messages = bitmessage.messages; +var version = messages.version; var WIF = bitmessage.WIF; var POW = bitmessage.POW; var Address = bitmessage.Address; @@ -72,6 +74,10 @@ describe("Common structures", function() { expect(res.command).to.equal(""); }); + it("should throw when decoding message with truncated payload", function() { + expect(message.decode.bind(null, Buffer("e9beb4d97465737400000000000000000000000770b33ce97061796c6f61", "hex"))).to.throw(Error); + }); + it("should encode", function() { expect(message.encode({command: "test", payload: Buffer("payload")}).toString("hex")).to.equal("e9beb4d97465737400000000000000000000000770b33ce97061796c6f6164"); }); @@ -275,6 +281,28 @@ describe("Common structures", function() { }); }); +describe("Message types", function() { + describe("version", function() { + it("should encode and decode", function() { + var res = version.decode(version.encode({ + remoteHost: "1.2.3.4", + remotePort: 48444, + port: 8444, + })); + expect(res.version).to.equal(3); + expect(res.services).to.deep.equal([serviceFeatures.NODE_NETWORK]); + expect(res.time).to.be.instanceof(Date); + expect(res.remoteHost).to.equal("1.2.3.4"); + expect(res.remotePort).to.equal(48444); + expect(res.port).to.equal(8444); + expect(bufferEqual(res.nonce, version.NONCE)).to.be.true; + expect(res.software).to.deep.equal(UserAgent.SELF); + expect(res.streamNumbers).to.deep.equal([1]); + expect(res.length).to.equal(101); + }); + }); +}); + describe("WIF", function() { var wifSign = "5JgQ79vTBusc61xYPtUEHYQ38AXKdDZgQ5rFp7Cbb4ZjXUKFZEV"; var wifEnc = "5K2aL8cnsEWHwHfHnUrPo8QdYyRfoYUBmhAnWY5GTpDLbeyusnE";