cjdnsadmin.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. /* vim: set expandtab ts=4 sw=4: */
  2. /*
  3. * You may redistribute this program and/or modify it under the terms of
  4. * the GNU General Public License as published by the Free Software Foundation,
  5. * either version 3 of the License, or (at your option) any later version.
  6. *
  7. * This program is distributed in the hope that it will be useful,
  8. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. * GNU General Public License for more details.
  11. *
  12. * You should have received a copy of the GNU General Public License
  13. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. */
  15. var UDP = require('dgram');
  16. var Bencode = require('./bencode');
  17. var Crypto = require('crypto');
  18. var Fs = require('fs');
  19. var nThen = require('nthen');
  20. var Semaphore = require('saferphore');
  21. var TIMEOUT_MILLISECONDS = 10000;
  22. var sendmsg = function (sock, addr, port, msg, txid, callback) {
  23. var to = setTimeout(function () {
  24. callback(new Error("timeout after " + TIMEOUT_MILLISECONDS + "ms"));
  25. delete sock.handlers[txid];
  26. }, TIMEOUT_MILLISECONDS);
  27. sock.handlers[txid] = {
  28. callback: callback,
  29. timeout: to
  30. };
  31. sock.send(msg, 0, msg.length, port, addr, function(err, bytes) {
  32. if (err) {
  33. clearTimeout(to);
  34. delete sock.handlers[txid];
  35. callback(err);
  36. }
  37. });
  38. };
  39. var callFunc = function (sock, addr, port, pass, func, args, callback) {
  40. var cookieTxid = String(sock.counter++);
  41. var cookieMsg = Buffer.from(Bencode.encode({'q':'cookie','txid':cookieTxid}));
  42. sendmsg(sock, addr, port, cookieMsg, cookieTxid, function (err, ret) {
  43. if (err) { callback(err); return; }
  44. var cookie = ret.cookie;
  45. if (typeof(cookie) !== 'string') { throw new Error("invalid cookie in [" + ret + "]"); }
  46. var json = {
  47. txid: String(sock.counter++),
  48. q: func,
  49. args: {}
  50. };
  51. Object.keys(args).forEach(function (arg) {
  52. json.args[arg] = args[arg];
  53. });
  54. if (pass) {
  55. json.aq = json.q;
  56. json.q = 'auth';
  57. json.cookie = cookie;
  58. json.hash = Crypto.createHash('sha256').update(pass + cookie).digest('hex');
  59. json.hash = Crypto.createHash('sha256').update(Bencode.encode(json)).digest('hex');
  60. }
  61. sendmsg(sock, addr, port, Buffer.from(Bencode.encode(json)), json.txid, callback);
  62. });
  63. };
  64. var getArgs = function (func) {
  65. var rArgs = [];
  66. Object.keys(func).forEach(function (name) {
  67. if (func[name].required === '1') {
  68. rArgs.push({ name: name, type: func[name].type, required: true });
  69. }
  70. });
  71. // be sure that the arguments are always in the same order
  72. rArgs.sort(function (a,b) { a = a.name; b = b.name; return (a !== b) ? (a < b) ? 1 : -1 : 0; });
  73. var oArgs = [];
  74. Object.keys(func).forEach(function (name) {
  75. if (func[name].required === '0') {
  76. oArgs.push({ name: name, type: func[name].type, required: false });
  77. }
  78. });
  79. oArgs.sort(function (a,b) { a = a.name; b = b.name; return (a !== b) ? (a < b) ? 1 : -1 : 0; });
  80. rArgs.push.apply(rArgs, oArgs);
  81. return rArgs;
  82. };
  83. var makeFunctionDescription = function (funcName, func) {
  84. var args = getArgs(func);
  85. var outArgs = [];
  86. args.forEach(function (arg) {
  87. outArgs.push( ((arg.required) ? 'required ' : '') + arg.type + ' ' + arg.name );
  88. });
  89. return funcName + "(" + outArgs.join(', ') + ")";
  90. };
  91. var compatibleType = function (typeName, obj) {
  92. switch (typeName) {
  93. case 'Int': return (typeof(obj) === 'number' && Math.floor(obj) === obj);
  94. case 'String': return (typeof(obj) === 'string');
  95. case 'Dict': return (typeof(obj) === 'object');
  96. case 'List': return Array.isArray(obj);
  97. default: throw new Error();
  98. }
  99. };
  100. var makeFunction = function (sock, addr, port, pass, funcName, func) {
  101. var args = getArgs(func);
  102. return function () {
  103. var i;
  104. var argsOut = {};
  105. for (i = 0; i < arguments.length-1; i++) {
  106. var arg = arguments[i];
  107. if (!args[i].required && (arg === null || arg === undefined)) { continue; }
  108. if (!compatibleType(args[i].type, arg)) {
  109. throw new Error("argument [" + i + "] ([" + arguments[i] + "]) [" +
  110. args[i].type + " " + args[i].name + "]" +
  111. " is of type [" + typeof(arg) + "] which is not compatible with " +
  112. "required type " + args[i].type);
  113. }
  114. argsOut[args[i].name] = arg;
  115. }
  116. if (args.length > i && args[i].required) {
  117. throw new Error("argument [" + i + "] [" + args[i].type + " " + args[i].name + "] is " +
  118. "required and is not specified");
  119. }
  120. var callback = arguments[arguments.length-1];
  121. if (typeof(callback) !== 'function') {
  122. throw new Error("callback is unspecified");
  123. }
  124. sock.semaphore.take(function (returnAfter) {
  125. callFunc(sock, addr, port, pass, funcName, argsOut, returnAfter(callback));
  126. });
  127. };
  128. };
  129. var getFunctions = function (sock, addr, port, pass, callback) {
  130. var funcs = {};
  131. nThen(function (waitFor) {
  132. var next = function (i) {
  133. callFunc(sock, addr, port, pass, 'Admin_availableFunctions', {page:i},
  134. waitFor(function (err, ret) {
  135. if (err) { throw err; }
  136. Object.keys(ret.availableFunctions).forEach(function (funcName) {
  137. funcs[funcName] = ret.availableFunctions[funcName];
  138. });
  139. if (Object.keys(ret.availableFunctions).length > 0) {
  140. next(i+1);
  141. }
  142. })
  143. );
  144. };
  145. next(0);
  146. }).nThen(function (waitFor) {
  147. var funcDescriptions = [];
  148. var cjdns = {};
  149. Object.keys(funcs).forEach(function (name) {
  150. cjdns[name] = makeFunction(sock, addr, port, pass, name, funcs[name]);
  151. funcDescriptions.push(makeFunctionDescription(name, funcs[name]));
  152. });
  153. cjdns.functions = function (cb) { cb(undefined, funcDescriptions); };
  154. callback(cjdns);
  155. });
  156. };
  157. var connect = module.exports.connect = function (addr, port, pass, callback) {
  158. var sock = UDP.createSocket((addr.indexOf(':') !== -1) ? 'udp6' : 'udp4');
  159. sock.semaphore = Semaphore.create(4);
  160. sock.handlers = {};
  161. sock.counter = Math.floor(Math.random() * 4000000000);
  162. sock.on('message', function (msg) {
  163. var response = Bencode.decode(msg.toString('utf8'));
  164. if (!response.txid) {
  165. throw new Error("Response [" + msg + "] with no txid");
  166. }
  167. var handler = sock.handlers[response.txid];
  168. if (!handler) {
  169. if (sock.defaultHandler) {
  170. sock.defaultHandler(undefined, response);
  171. }
  172. return;
  173. }
  174. clearTimeout(handler.timeout);
  175. delete sock.handlers[response.txid];
  176. handler.callback(undefined, response);
  177. });
  178. nThen(function (waitFor) {
  179. callFunc(sock, addr, port, pass, 'ping', {}, waitFor(function (err, ret) {
  180. if (err) { throw err; }
  181. if (ret.error) { throw new Error(`Connect Error: ${ret.error}`); }
  182. //console.log("got pong! [" + JSON.stringify(ret) + "]");
  183. }));
  184. }).nThen(function (waitFor) {
  185. getFunctions(sock, addr, port, pass, function (cjdns) {
  186. cjdns.disconnect = function () { sock.close(); };
  187. cjdns.setDefaultHandler = function (handler) { sock.defaultHandler = handler; };
  188. callback(cjdns);
  189. });
  190. });
  191. };
  192. var connectWithAdminInfo = module.exports.connectWithAdminInfo = function (callback) {
  193. var cjdnsAdmin = {'addr': '127.0.0.1', 'port': 11234, 'password': 'NONE'};
  194. nThen(function (waitFor) {
  195. Fs.readFile(process.env.HOME + '/.cjdnsadmin', waitFor(function (err, ret) {
  196. if (err && err.code != 'ENOENT') { throw err; }
  197. if (!err) { cjdnsAdmin = JSON.parse(String(ret)); }
  198. }));
  199. }).nThen(function (waitFor) {
  200. connect(cjdnsAdmin.addr, cjdnsAdmin.port, cjdnsAdmin.password, callback);
  201. });
  202. };
  203. var connectAsAnon = module.exports.connectAsAnon = function (callback, addr, port) {
  204. var cjdnsAdmin = {'addr': addr || '127.0.0.1', 'port': port || 11234, 'password': 'NONE'};
  205. nThen(function (waitFor) {
  206. Fs.readFile(process.env.HOME + '/.cjdnsadmin', waitFor(function (err, ret) {
  207. if (!err) { cjdnsAdmin = JSON.parse(String(ret)); }
  208. }));
  209. }).nThen(function (waitFor) {
  210. connect(cjdnsAdmin.addr, cjdnsAdmin.port, null, callback);
  211. });
  212. };