diff --git a/README.md b/README.md index 928d38a..c1bc748 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/ - [x] getRipe - [x] fromRandom - [ ] fromPassphrase - - [ ] UserAgent + - [x] UserAgent - [ ] Message - [ ] encrypt - [ ] decrypt diff --git a/lib/index.js b/lib/index.js index dfcd472..e19c0f0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -19,3 +19,5 @@ exports.POW = require("./pow"); /** [Working with addresses.]{@link module:bitmessage/address} */ exports.Address = require("./address"); +/** [User Agent.]{@link module:bitmessage/user-agent} */ +exports.UserAgent = require("./user-agent"); diff --git a/lib/structs.js b/lib/structs.js index 6d3bb52..20ae40d 100644 --- a/lib/structs.js +++ b/lib/structs.js @@ -38,6 +38,7 @@ var message = exports.message = { /** * Decode message structure. + * NOTE: `payload` is copied, `rest` references input buffer. * @param {Buffer} buf - Buffer that starts with encoded message * structure * @return {{command: string, payload: Buffer, length: number, rest: Buffer}} @@ -99,6 +100,7 @@ var message = exports.message = { var var_int = exports.var_int = { /** * Decode `var_int`. + * NOTE: `rest` references input buffer. * @param {Buffer} buf - A buffer that starts with encoded `var_int` * @return {{value: number, length: number, rest: Buffer}} * Decoded `var_int` structure. @@ -191,6 +193,7 @@ var var_int = exports.var_int = { exports.var_str = { /** * Decode `var_str`. + * NOTE: `rest` references input buffer. * @param {Buffer} buf - A buffer that starts with encoded `var_str` * @return {{str: string, length: number, rest: Buffer}} * Decoded `var_str` structure. @@ -226,6 +229,7 @@ exports.var_str = { exports.var_int_list = { /** * Decode `var_int_list`. + * NOTE: `rest` references input buffer. * @param {Buffer} buf - A buffer that starts with encoded * `var_int_list` * @return {{list: number[], length: number, rest: Buffer}} @@ -421,6 +425,7 @@ var SECP256K1_TYPE = 714; exports.encrypted = { /** * Decode encrypted payload. + * NOTE: all structure members are copied. * @param {Buffer} buf - A buffer that contains encrypted payload * @return {Object} Decoded encrypted structure. */ diff --git a/lib/user-agent.js b/lib/user-agent.js new file mode 100644 index 0000000..4f893b3 --- /dev/null +++ b/lib/user-agent.js @@ -0,0 +1,84 @@ +/** + * Working with Bitmessage user agents. + * @see {@link https://bitmessage.org/wiki/User_Agent} + * @module bitmessage/user-agent + */ + +"use strict"; + +var var_str = require("./structs").var_str; +var BM_NAME = require("../package.json").name; +var BM_VERSION = require("../package.json").version; + +/** + * Decode user agent stack. + * NOTE: Decoding is rather loose and non-strict, it won't fail on bad + * user agent format because it's not that important. + * Also note that `rest` references input buffer. + * @param {Buffer} buf - A buffer that starts with encoded user agent + * @return {{software: Object[], length: number, rest: Buffer}} + * Decoded user agent structure. + */ +exports.decode = function(buf) { + var decoded = var_str.decode(buf); + var software = []; + if (decoded.str) { + software = decoded.str.slice(1, -1).split("/"); + software = software.map(function(str) { + // That's more readable than /([^:]*)(?::([^(]*)(?:\(([^)]*))?)?/ + var soft = {name: str}; + var semicolon = soft.name.indexOf(":"); + if (semicolon !== -1) { + soft.version = soft.name.slice(semicolon + 1); + soft.name = soft.name.slice(0, semicolon); + var obracket = soft.version.indexOf("("); + if (obracket !== -1) { + soft.comments = soft.version.slice(obracket + 1); + soft.version = soft.version.slice(0, obracket); + var cbracket = soft.comments.indexOf(")"); + if (cbracket !== -1) { + soft.comments = soft.comments.slice(0, cbracket); + } + } + } + return soft; + }); + } + return {software: software, length: decoded.length, rest: decoded.rest}; +}; + +/** + * Encode user agent. Most underlying software comes first. + * @param {Object[]} software - List of software to encode + * @return {Buffer} Encoded user agent. + */ +var encode = exports.encode = function(software) { + var ua = software.map(function(soft) { + var version = soft.version || "0.0.0"; + var str = soft.name + ":" + version; + if (str.comments) { + str += "(" + comments + ")"; + } + return str; + }).join("/"); + return var_str.encode("/" + ua + "/"); +}; + +/** + * Encode bitmessage's user agent. + * @return {Buffer} Encoded user agent. + */ +exports.encodeSelf = function() { + return encode([{name: BM_NAME, version: BM_VERSION}]); +}; + +/** + * Encode user agent with bitmessage's user agent underneath. Most + * underlying software comes first. + * @param {Object[]} software - List of software to encode + * @return {Buffer} Encoded user agent. + */ +exports.encodeSelfWith = function(software) { + software = [{name: BM_NAME, version: BM_VERSION}].concat(software); + return encode(software); +}; diff --git a/test.js b/test.js index 3fb16ee..f640d90 100644 --- a/test.js +++ b/test.js @@ -19,6 +19,7 @@ var pubkeyFeatures = structs.pubkeyFeatures; var WIF = bitmessage.WIF; var POW = bitmessage.POW; var Address = bitmessage.Address; +var UserAgent = bitmessage.UserAgent; describe("Crypto", function() { it("should implement SHA-512 hash", function() { @@ -377,4 +378,44 @@ describe("High-level classes", function() { }); } }); + + describe("User Agent", function() { + var pybm = {name: "PyBitmessage", version: "0.4.4"}; + var bnode = {name: "bitchan-node", version: "0.0.1"}; + var bweb = {name: "bitchan-web"}; + + it("should decode", function() { + var ua = var_str.encode("/cBitmessage:0.2(iPad; U; CPU OS 3_2_1)/AndroidBuild:0.8/"); + var res = UserAgent.decode(ua); + expect(res.software).to.deep.equal([ + {name: "cBitmessage", version: "0.2", comments: "iPad; U; CPU OS 3_2_1"}, + {name: "AndroidBuild", version: "0.8"}, + ]); + expect(res.length).to.equal(58); + expect(res.rest.toString("hex")).to.equal(""); + }); + + it("should encode", function() { + var ua = UserAgent.encode([pybm]); + expect(var_str.decode(ua).str).to.equal("/PyBitmessage:0.4.4/"); + var res = UserAgent.decode(ua); + expect(res.software).to.deep.equal([pybm]); + expect(res.length).to.equal(21); + expect(res.rest.toString("hex")).to.equal(""); + }); + + it("should encode bitmessage's user agent", function() { + var res = UserAgent.decode(UserAgent.encodeSelf()) + var software = res.software; + expect(software[0].name).to.equal("bitmessage"); + expect(software[0]).to.have.property("version"); + + res = UserAgent.decode(UserAgent.encodeSelfWith([bnode, bweb])); + software = res.software; + expect(software[0].name).to.equal("bitmessage"); + expect(software[1]).to.deep.equal(bnode); + expect(software[2].name).to.equal(bweb.name); + expect(software[2].version).to.equal("0.0.0"); + }); + }); });