/* vim: set expandtab ts=4 sw=4: */ /* * You may redistribute this program and/or modify it under the terms of * the GNU General Public License as published by the Free Software Foundation, * either version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ var UDP = require('dgram'); var Bencode = require('./bencode'); var Crypto = require('crypto'); var Fs = require('fs'); var nThen = require('nthen'); var Semaphore = require('saferphore'); var TIMEOUT_MILLISECONDS = 10000; var sendmsg = function (sock, addr, port, msg, txid, callback) { var to = setTimeout(function () { callback(new Error("timeout after " + TIMEOUT_MILLISECONDS + "ms")); delete sock.handlers[txid]; }, TIMEOUT_MILLISECONDS); sock.handlers[txid] = { callback: callback, timeout: to }; sock.send(msg, 0, msg.length, port, addr, function(err, bytes) { if (err) { clearTimeout(to); delete sock.handlers[txid]; callback(err); } }); }; var callFunc = function (sock, addr, port, pass, func, args, callback) { var cookieTxid = String(sock.counter++); var cookieMsg = Buffer.from(Bencode.encode({'q':'cookie','txid':cookieTxid})); sendmsg(sock, addr, port, cookieMsg, cookieTxid, function (err, ret) { if (err) { callback(err); return; } var cookie = ret.cookie; if (typeof(cookie) !== 'string') { throw new Error("invalid cookie in [" + ret + "]"); } var json = { txid: String(sock.counter++), q: func, args: {} }; Object.keys(args).forEach(function (arg) { json.args[arg] = args[arg]; }); if (pass) { json.aq = json.q; json.q = 'auth'; json.cookie = cookie; json.hash = Crypto.createHash('sha256').update(pass + cookie).digest('hex'); json.hash = Crypto.createHash('sha256').update(Bencode.encode(json)).digest('hex'); } sendmsg(sock, addr, port, Buffer.from(Bencode.encode(json)), json.txid, callback); }); }; var getArgs = function (func) { var rArgs = []; Object.keys(func).forEach(function (name) { if (func[name].required === '1') { rArgs.push({ name: name, type: func[name].type, required: true }); } }); // be sure that the arguments are always in the same order rArgs.sort(function (a,b) { a = a.name; b = b.name; return (a !== b) ? (a < b) ? 1 : -1 : 0; }); var oArgs = []; Object.keys(func).forEach(function (name) { if (func[name].required === '0') { oArgs.push({ name: name, type: func[name].type, required: false }); } }); oArgs.sort(function (a,b) { a = a.name; b = b.name; return (a !== b) ? (a < b) ? 1 : -1 : 0; }); rArgs.push.apply(rArgs, oArgs); return rArgs; }; var makeFunctionDescription = function (funcName, func) { var args = getArgs(func); var outArgs = []; args.forEach(function (arg) { outArgs.push( ((arg.required) ? 'required ' : '') + arg.type + ' ' + arg.name ); }); return funcName + "(" + outArgs.join(', ') + ")"; }; var compatibleType = function (typeName, obj) { switch (typeName) { case 'Int': return (typeof(obj) === 'number' && Math.floor(obj) === obj); case 'String': return (typeof(obj) === 'string'); case 'Dict': return (typeof(obj) === 'object'); case 'List': return Array.isArray(obj); default: throw new Error(); } }; var makeFunction = function (sock, addr, port, pass, funcName, func) { var args = getArgs(func); return function () { var i; var argsOut = {}; for (i = 0; i < arguments.length-1; i++) { var arg = arguments[i]; if (!args[i].required && (arg === null || arg === undefined)) { continue; } if (!compatibleType(args[i].type, arg)) { throw new Error("argument [" + i + "] ([" + arguments[i] + "]) [" + args[i].type + " " + args[i].name + "]" + " is of type [" + typeof(arg) + "] which is not compatible with " + "required type " + args[i].type); } argsOut[args[i].name] = arg; } if (args.length > i && args[i].required) { throw new Error("argument [" + i + "] [" + args[i].type + " " + args[i].name + "] is " + "required and is not specified"); } var callback = arguments[arguments.length-1]; if (typeof(callback) !== 'function') { throw new Error("callback is unspecified"); } sock.semaphore.take(function (returnAfter) { callFunc(sock, addr, port, pass, funcName, argsOut, returnAfter(callback)); }); }; }; var getFunctions = function (sock, addr, port, pass, callback) { var funcs = {}; nThen(function (waitFor) { var next = function (i) { callFunc(sock, addr, port, pass, 'Admin_availableFunctions', {page:i}, waitFor(function (err, ret) { if (err) { throw err; } Object.keys(ret.availableFunctions).forEach(function (funcName) { funcs[funcName] = ret.availableFunctions[funcName]; }); if (Object.keys(ret.availableFunctions).length > 0) { next(i+1); } }) ); }; next(0); }).nThen(function (waitFor) { var funcDescriptions = []; var cjdns = {}; Object.keys(funcs).forEach(function (name) { cjdns[name] = makeFunction(sock, addr, port, pass, name, funcs[name]); funcDescriptions.push(makeFunctionDescription(name, funcs[name])); }); cjdns.functions = function (cb) { cb(undefined, funcDescriptions); }; callback(cjdns); }); }; var connect = module.exports.connect = function (addr, port, pass, callback) { var sock = UDP.createSocket((addr.indexOf(':') !== -1) ? 'udp6' : 'udp4'); sock.semaphore = Semaphore.create(4); sock.handlers = {}; sock.counter = Math.floor(Math.random() * 4000000000); sock.on('message', function (msg) { var response = Bencode.decode(msg.toString('utf8')); if (!response.txid) { throw new Error("Response [" + msg + "] with no txid"); } var handler = sock.handlers[response.txid]; if (!handler) { if (sock.defaultHandler) { sock.defaultHandler(undefined, response); } return; } clearTimeout(handler.timeout); delete sock.handlers[response.txid]; handler.callback(undefined, response); }); nThen(function (waitFor) { callFunc(sock, addr, port, pass, 'ping', {}, waitFor(function (err, ret) { if (err) { throw err; } if (ret.error) { throw new Error(`Connect Error: ${ret.error}`); } //console.log("got pong! [" + JSON.stringify(ret) + "]"); })); }).nThen(function (waitFor) { getFunctions(sock, addr, port, pass, function (cjdns) { cjdns.disconnect = function () { sock.close(); }; cjdns.setDefaultHandler = function (handler) { sock.defaultHandler = handler; }; callback(cjdns); }); }); }; var connectWithAdminInfo = module.exports.connectWithAdminInfo = function (callback) { var cjdnsAdmin = {'addr': '127.0.0.1', 'port': 11234, 'password': 'NONE'}; nThen(function (waitFor) { Fs.readFile(process.env.HOME + '/.cjdnsadmin', waitFor(function (err, ret) { if (err && err.code != 'ENOENT') { throw err; } if (!err) { cjdnsAdmin = JSON.parse(String(ret)); } })); }).nThen(function (waitFor) { connect(cjdnsAdmin.addr, cjdnsAdmin.port, cjdnsAdmin.password, callback); }); }; var connectAsAnon = module.exports.connectAsAnon = function (callback, addr, port) { var cjdnsAdmin = {'addr': addr || '127.0.0.1', 'port': port || 11234, 'password': 'NONE'}; nThen(function (waitFor) { Fs.readFile(process.env.HOME + '/.cjdnsadmin', waitFor(function (err, ret) { if (!err) { cjdnsAdmin = JSON.parse(String(ret)); } })); }).nThen(function (waitFor) { connect(cjdnsAdmin.addr, cjdnsAdmin.port, null, callback); }); };