duk_debug_proxy.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. /*
  2. * JSON debug proxy written in DukLuv
  3. *
  4. * This single file JSON debug proxy implementation is an alternative to the
  5. * Node.js-based proxy in duk_debug.js. DukLuv is a much smaller dependency
  6. * than Node.js so embedding DukLuv in a debug client is easier.
  7. */
  8. 'use strict';
  9. // XXX: Code assumes uv.write() will write fully. This is not necessarily
  10. // true; should add support for partial writes (or at least failing when
  11. // a partial write occurs).
  12. var log = new Duktape.Logger('Proxy'); // default logger
  13. //log.l = 0; // enable debug and trace logging
  14. /*
  15. * Config
  16. */
  17. var serverHost = '0.0.0.0';
  18. var serverPort = 9093;
  19. var targetHost = '127.0.0.1';
  20. var targetPort = 9091;
  21. var singleConnection = false;
  22. var readableNumberValue = false;
  23. var lenientJsonParse = false;
  24. var jxParse = false;
  25. var metadataFile = null;
  26. var metadata = {};
  27. var TORTURE = false; // for manual testing of binary/json parsing robustness
  28. /*
  29. * Detect missing 'var' declarations
  30. */
  31. // Prevent new bindings on global object. This detects missing 'var'
  32. // declarations, e.g. "x = 123;" in a function without declaring it.
  33. var global = new Function('return this;')();
  34. log.debug('Preventing extensions on global object');
  35. log.debug('Global is extensible:', Object.isExtensible(global));
  36. Object.preventExtensions(global);
  37. log.debug('Global is extensible:', Object.isExtensible(global));
  38. /*
  39. * Misc helpers
  40. */
  41. function plainBufferCopy(typedarray) {
  42. // This is still pretty awkward in Duktape 1.4.x.
  43. // Argument may be a "slice" and we want a copy of the slice
  44. // (not the full underlying buffer).
  45. var u8 = new Uint8Array(typedarray.length);
  46. u8.set(typedarray); // make a copy, ensuring there's no slice offset
  47. return Duktape.Buffer(u8); // get underlying plain buffer
  48. }
  49. function isObject(x) {
  50. // Note that typeof null === 'object'.
  51. return (typeof x === 'object' && x !== null);
  52. }
  53. function readFully(filename, cb) {
  54. uv.fs_open(metadataFile, 'r', 0, function (handle, err) {
  55. var fileOff = 0;
  56. var data = new Uint8Array(256);
  57. var dataOff = 0;
  58. if (err) {
  59. return cb(null, err);
  60. }
  61. function readCb(buf, err) {
  62. var res;
  63. var newData;
  64. log.debug('Read callback:', buf.length, err);
  65. if (err) {
  66. uv.fs_close(handle);
  67. return cb(null, err);
  68. }
  69. if (buf.length == 0) {
  70. uv.fs_close(handle);
  71. res = new Uint8Array(dataOff);
  72. res.set(data.subarray(0, dataOff));
  73. res = Duktape.Buffer(res); // plain buffer
  74. log.debug('Read', res.length, 'bytes from', filename);
  75. return cb(res, null);
  76. }
  77. while (data.length - dataOff < buf.length) {
  78. log.debug('Resize file read buffer:', data.length, '->', data.length * 2);
  79. newData = new Uint8Array(data.length * 2);
  80. newData.set(data);
  81. data = newData;
  82. }
  83. data.set(new Uint8Array(buf), dataOff);
  84. dataOff += buf.length;
  85. fileOff += buf.length;
  86. uv.fs_read(handle, 4096, fileOff, readCb);
  87. }
  88. uv.fs_read(handle, 4096, fileOff, readCb);
  89. });
  90. }
  91. /*
  92. * JSON proxy server
  93. *
  94. * Accepts an incoming JSON proxy client and connects to a debug target,
  95. * tying the two connections together. Supports both a single connection
  96. * and a persistent mode.
  97. */
  98. function JsonProxyServer(host, port) {
  99. this.name = 'JsonProxyServer';
  100. this.handle = uv.new_tcp();
  101. uv.tcp_bind(this.handle, host, port);
  102. uv.listen(this.handle, 128, this.onConnection.bind(this));
  103. }
  104. JsonProxyServer.prototype.onConnection = function onConnection(err) {
  105. if (err) {
  106. log.error('JSON proxy onConnection error:', err);
  107. return;
  108. }
  109. log.info('JSON proxy client connected'); // XXX: it'd be nice to log remote peer host:port
  110. var jsonSock = new JsonConnHandler(this);
  111. var targSock = new TargetConnHandler(this);
  112. jsonSock.targetHandler = targSock;
  113. targSock.jsonHandler = jsonSock;
  114. uv.accept(this.handle, jsonSock.handle);
  115. log.info('Connecting to debug target at', targetHost + ':' + targetPort);
  116. jsonSock.writeJson({ notify: '_TargetConnecting', args: [ targetHost, targetPort ] });
  117. uv.tcp_connect(targSock.handle, targetHost, targetPort, targSock.onConnect.bind(targSock));
  118. if (singleConnection) {
  119. log.info('Single connection mode, stop listening for more connections');
  120. uv.shutdown(this.handle);
  121. uv.read_stop(this.handle); // unnecessary but just in case
  122. uv.close(this.handle);
  123. this.handle = null;
  124. }
  125. };
  126. JsonProxyServer.prototype.onProxyClientDisconnected = function onProxyClientDisconnected() {
  127. // When this is invoked the proxy connection and the target connection
  128. // have both been closed.
  129. if (singleConnection) {
  130. log.info('Proxy connection finished (single connection mode: we should be exiting now)');
  131. } else {
  132. log.info('Proxy connection finished (persistent mode: wait for more connections)');
  133. }
  134. };
  135. /*
  136. * JSON connection handler
  137. */
  138. function JsonConnHandler(server) {
  139. var i, n;
  140. this.name = 'JsonConnHandler';
  141. this.server = server;
  142. this.handle = uv.new_tcp();
  143. this.incoming = new Uint8Array(4096);
  144. this.incomingOffset = 0;
  145. this.targetHandler = null;
  146. this.commandNumberLookup = {};
  147. if (metadata && metadata.target_commands) {
  148. for (i = 0, n = metadata.target_commands.length; i < n; i++) {
  149. this.commandNumberLookup[metadata.target_commands[i]] = i;
  150. }
  151. }
  152. }
  153. JsonConnHandler.prototype.finish = function finish(msg) {
  154. var args;
  155. if (!this.handle) {
  156. log.info('JsonConnHandler already disconnected, ignore finish()');
  157. return;
  158. }
  159. log.info('JsonConnHandler finished:', msg);
  160. try {
  161. args = msg ? [ msg ] : void 0;
  162. this.writeJson({ notify: '_Disconnecting', args: args });
  163. } catch (e) {
  164. log.info('Failed to write _Disconnecting notify, ignoring:', e);
  165. }
  166. uv.shutdown(this.handle);
  167. uv.read_stop(this.handle);
  168. uv.close(this.handle);
  169. this.handle = null;
  170. this.targetHandler.finish(msg); // disconnect target too (if not already disconnected)
  171. this.server.onProxyClientDisconnected();
  172. };
  173. JsonConnHandler.prototype.onRead = function onRead(err, data) {
  174. var newIncoming;
  175. var msg;
  176. var errmsg;
  177. var tmpBuf;
  178. log.trace('Received data from JSON socket, err:', err, 'data length:', data ? data.length : 'null');
  179. if (err) {
  180. errmsg = 'Error reading data from JSON debug client: ' + err;
  181. this.finish(errmsg);
  182. return;
  183. }
  184. if (data) {
  185. // Feed the data one byte at a time when torture testing.
  186. if (TORTURE && data.length > 1) {
  187. for (var i = 0; i < data.length; i++) {
  188. tmpBuf = Duktape.Buffer(1);
  189. tmpBuf[0] = data[i];
  190. this.onRead(null, tmpBuf);
  191. }
  192. return;
  193. }
  194. // Receive data into 'incoming', resizing as necessary.
  195. while (data.length > this.incoming.length - this.incomingOffset) {
  196. newIncoming = new Uint8Array(this.incoming.length * 1.3 + 16);
  197. newIncoming.set(this.incoming);
  198. this.incoming = newIncoming;
  199. log.debug('Resize incoming JSON buffer to ' + this.incoming.length);
  200. }
  201. this.incoming.set(new Uint8Array(data), this.incomingOffset);
  202. this.incomingOffset += data.length;
  203. // Trial parse JSON message(s).
  204. while (true) {
  205. msg = this.trialParseJsonMessage();
  206. if (!msg) {
  207. break;
  208. }
  209. try {
  210. this.dispatchJsonMessage(msg);
  211. } catch (e) {
  212. errmsg = 'JSON message dispatch failed: ' + e;
  213. this.writeJson({ notify: '_Error', args: [ errmsg ] });
  214. if (lenientJsonParse) {
  215. log.warn('JSON message dispatch failed (lenient mode, ignoring):', e);
  216. } else {
  217. log.warn('JSON message dispatch failed (dropping connection):', e);
  218. this.finish(errmsg);
  219. }
  220. }
  221. }
  222. } else {
  223. this.finish('JSON proxy client disconnected');
  224. }
  225. };
  226. JsonConnHandler.prototype.writeJson = function writeJson(msg) {
  227. log.info('PROXY --> CLIENT:', JSON.stringify(msg));
  228. if (this.handle) {
  229. uv.write(this.handle, JSON.stringify(msg) + '\n');
  230. }
  231. };
  232. JsonConnHandler.prototype.handleDebugMessage = function handleDebugMessage(dvalues) {
  233. var msg = {};
  234. var idx = 0;
  235. var cmd;
  236. if (dvalues.length <= 0) {
  237. throw new Error('invalid dvalues list: length <= 0');
  238. }
  239. var x = dvalues[idx++];
  240. if (!isObject(x)) {
  241. throw new Error('invalid initial dvalue: ' + Duktape.enc('jx', dvalues));
  242. }
  243. if (x.type === 'req') {
  244. cmd = dvalues[idx++];
  245. if (typeof cmd !== 'number') {
  246. throw new Error('invalid command: ' + Duktape.enc('jx', cmd));
  247. }
  248. msg.request = this.determineCommandName(cmd) || true;
  249. msg.command = cmd;
  250. } else if (x.type === 'rep') {
  251. msg.reply = true;
  252. } else if (x.type === 'err') {
  253. msg.error = true;
  254. } else if (x.type === 'nfy') {
  255. cmd = dvalues[idx++];
  256. if (typeof cmd !== 'number') {
  257. throw new Error('invalid command: ' + Duktape.enc('jx', cmd));
  258. }
  259. msg.notify = this.determineCommandName(cmd) || true;
  260. msg.command = cmd;
  261. } else {
  262. throw new Error('invalid initial dvalue: ' + Duktape.enc('jx', dvalues));
  263. }
  264. for (; idx < dvalues.length - 1; idx++) {
  265. if (!msg.args) {
  266. msg.args = [];
  267. }
  268. msg.args.push(dvalues[idx]);
  269. }
  270. if (!isObject(dvalues[idx]) || dvalues[idx].type !== 'eom') {
  271. throw new Error('invalid final dvalue: ' + Duktape.enc('jx', dvalues));
  272. }
  273. this.writeJson(msg);
  274. };
  275. JsonConnHandler.prototype.determineCommandName = function determineCommandName(cmd) {
  276. if (!(metadata && metadata.client_commands)) {
  277. return;
  278. }
  279. return metadata.client_commands[cmd];
  280. };
  281. JsonConnHandler.prototype.trialParseJsonMessage = function trialParseJsonMessage() {
  282. var buf = this.incoming;
  283. var avail = this.incomingOffset;
  284. var i;
  285. var msg, str, errmsg;
  286. for (i = 0; i < avail; i++) {
  287. if (buf[i] == 0x0a) {
  288. str = String(plainBufferCopy(buf.subarray(0, i)));
  289. try {
  290. if (jxParse) {
  291. msg = Duktape.dec('jx', str);
  292. } else {
  293. msg = JSON.parse(str);
  294. }
  295. } catch (e) {
  296. // In lenient mode if JSON parse fails just send back an _Error
  297. // and ignore the line (useful for initial development).
  298. //
  299. // In non-lenient mode drop the connection here; if the failed line
  300. // was a request the client is expecting a reply/error message back
  301. // (otherwise it may go out of sync) but we can't send a synthetic
  302. // one (as we can't parse the request).
  303. errmsg = 'JSON parse failed for: ' + JSON.stringify(str) + ': ' + e;
  304. this.writeJson({ notify: '_Error', args: [ errmsg ] });
  305. if (lenientJsonParse) {
  306. log.warn('JSON parse failed (lenient mode, ignoring):', e);
  307. } else {
  308. log.warn('JSON parse failed (dropping connection):', e);
  309. this.finish(errmsg);
  310. }
  311. }
  312. this.incoming.set(this.incoming.subarray(i + 1));
  313. this.incomingOffset -= i + 1;
  314. return msg;
  315. }
  316. }
  317. };
  318. JsonConnHandler.prototype.dispatchJsonMessage = function dispatchJsonMessage(msg) {
  319. var cmd;
  320. var dvalues = [];
  321. var i, n;
  322. log.info('PROXY <-- CLIENT:', JSON.stringify(msg));
  323. // Parse message type, determine initial marker for binary message.
  324. if (msg.request) {
  325. cmd = this.determineCommandNumber(msg.request, msg.command);
  326. dvalues.push(new Uint8Array([ 0x01 ]));
  327. dvalues.push(this.encodeJsonDvalue(cmd));
  328. } else if (msg.reply) {
  329. dvalues.push(new Uint8Array([ 0x02 ]));
  330. } else if (msg.notify) {
  331. cmd = this.determineCommandNumber(msg.notify, msg.command);
  332. dvalues.push(new Uint8Array([ 0x04 ]));
  333. dvalues.push(this.encodeJsonDvalue(cmd));
  334. } else if (msg.error) {
  335. dvalues.push(new Uint8Array([ 0x03 ]));
  336. } else {
  337. throw new Error('invalid input JSON message: ' + JSON.stringify(msg));
  338. }
  339. // Encode arguments into dvalues.
  340. for (i = 0, n = (msg.args ? msg.args.length : 0); i < n; i++) {
  341. dvalues.push(this.encodeJsonDvalue(msg.args[i]));
  342. }
  343. // Add an EOM, and write out the dvalues to the debug target.
  344. dvalues.push(new Uint8Array([ 0x00 ]));
  345. for (i = 0, n = dvalues.length; i < n; i++) {
  346. this.targetHandler.writeBinary(dvalues[i]);
  347. }
  348. };
  349. JsonConnHandler.prototype.determineCommandNumber = function determineCommandNumber(name, val) {
  350. var res;
  351. if (typeof name === 'string') {
  352. res = this.commandNumberLookup[name];
  353. if (!res) {
  354. log.info('Unknown command name: ' + name + ', command number: ' + val);
  355. }
  356. } else if (typeof name === 'number') {
  357. res = name;
  358. } else if (name !== true) {
  359. throw new Error('invalid command name (must be string, number, or "true"): ' + name);
  360. }
  361. if (typeof res === 'undefined' && typeof val === 'undefined') {
  362. throw new Error('cannot determine command number from name: ' + name);
  363. }
  364. if (typeof val !== 'number' && typeof val !== 'undefined') {
  365. throw new Error('invalid command number: ' + val);
  366. }
  367. res = res || val;
  368. return res;
  369. };
  370. JsonConnHandler.prototype.writeDebugStringToBuffer = function writeDebugStringToBuffer(v, buf, off) {
  371. var i, n;
  372. for (i = 0, n = v.length; i < n; i++) {
  373. buf[off + i] = v.charCodeAt(i) & 0xff; // truncate higher bits
  374. }
  375. };
  376. JsonConnHandler.prototype.encodeJsonDvalue = function encodeJsonDvalue(v) {
  377. var buf, dec, len, dv;
  378. if (isObject(v)) {
  379. if (v.type === 'eom') {
  380. return new Uint8Array([ 0x00 ]);
  381. } else if (v.type === 'req') {
  382. return new Uint8Array([ 0x01 ]);
  383. } else if (v.type === 'rep') {
  384. return new Uint8Array([ 0x02 ]);
  385. } else if (v.type === 'err') {
  386. return new Uint8Array([ 0x03 ]);
  387. } else if (v.type === 'nfy') {
  388. return new Uint8Array([ 0x04 ]);
  389. } else if (v.type === 'unused') {
  390. return new Uint8Array([ 0x15 ]);
  391. } else if (v.type === 'undefined') {
  392. return new Uint8Array([ 0x16 ]);
  393. } else if (v.type === 'number') {
  394. dec = Duktape.dec('hex', v.data);
  395. len = dec.length;
  396. if (len !== 8) {
  397. throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v));
  398. }
  399. buf = new Uint8Array(1 + len);
  400. buf[0] = 0x1a;
  401. buf.set(new Uint8Array(dec), 1);
  402. return buf;
  403. } else if (v.type === 'buffer') {
  404. dec = Duktape.dec('hex', v.data);
  405. len = dec.length;
  406. if (len <= 0xffff) {
  407. buf = new Uint8Array(3 + len);
  408. buf[0] = 0x14;
  409. buf[1] = (len >> 8) & 0xff;
  410. buf[2] = (len >> 0) & 0xff;
  411. buf.set(new Uint8Arrau(dec), 3);
  412. return buf;
  413. } else {
  414. buf = new Uint8Array(5 + len);
  415. buf[0] = 0x13;
  416. buf[1] = (len >> 24) & 0xff;
  417. buf[2] = (len >> 16) & 0xff;
  418. buf[3] = (len >> 8) & 0xff;
  419. buf[4] = (len >> 0) & 0xff;
  420. buf.set(new Uint8Array(dec), 5);
  421. return buf;
  422. }
  423. } else if (v.type === 'object') {
  424. dec = Duktape.dec('hex', v.pointer);
  425. len = dec.length;
  426. buf = new Uint8Array(3 + len);
  427. buf[0] = 0x1b;
  428. buf[1] = v.class;
  429. buf[2] = len;
  430. buf.set(new Uint8Array(dec), 3);
  431. return buf;
  432. } else if (v.type === 'pointer') {
  433. dec = Duktape.dec('hex', v.pointer);
  434. len = dec.length;
  435. buf = new Uint8Array(2 + len);
  436. buf[0] = 0x1c;
  437. buf[1] = len;
  438. buf.set(new Uint8Array(dec), 2);
  439. return buf;
  440. } else if (v.type === 'lightfunc') {
  441. dec = Duktape.dec('hex', v.pointer);
  442. len = dec.length;
  443. buf = new Uint8Array(4 + len);
  444. buf[0] = 0x1d;
  445. buf[1] = (v.flags >> 8) & 0xff;
  446. buf[2] = v.flags & 0xff;
  447. buf[3] = len;
  448. buf.set(new Uint8Array(dec), 4);
  449. return buf;
  450. } else if (v.type === 'heapptr') {
  451. dec = Duktape.dec('hex', v.pointer);
  452. len = dec.length;
  453. buf = new Uint8Array(2 + len);
  454. buf[0] = 0x1e;
  455. buf[1] = len;
  456. buf.set(new Uint8Array(dec), 2);
  457. return buf;
  458. }
  459. } else if (v === null) {
  460. return new Uint8Array([ 0x17 ]);
  461. } else if (typeof v === 'boolean') {
  462. return new Uint8Array([ v ? 0x18 : 0x19 ]);
  463. } else if (typeof v === 'number') {
  464. if (Math.floor(v) === v && /* whole */
  465. (v !== 0 || 1 / v > 0) && /* not negative zero */
  466. v >= -0x80000000 && v <= 0x7fffffff) {
  467. // Represented signed 32-bit integers as plain integers.
  468. // Debugger code expects this for all fields that are not
  469. // duk_tval representations (e.g. command numbers and such).
  470. if (v >= 0x00 && v <= 0x3f) {
  471. return new Uint8Array([ 0x80 + v ]);
  472. } else if (v >= 0x0000 && v <= 0x3fff) {
  473. return new Uint8Array([ 0xc0 + (v >> 8), v & 0xff ]);
  474. } else if (v >= -0x80000000 && v <= 0x7fffffff) {
  475. return new Uint8Array([ 0x10,
  476. (v >> 24) & 0xff,
  477. (v >> 16) & 0xff,
  478. (v >> 8) & 0xff,
  479. (v >> 0) & 0xff ]);
  480. } else {
  481. throw new Error('internal error when encoding integer to dvalue: ' + v);
  482. }
  483. } else {
  484. // Represent non-integers as IEEE double dvalues.
  485. buf = new Uint8Array(1 + 8);
  486. buf[0] = 0x1a;
  487. new DataView(buf).setFloat64(1, v, false);
  488. return buf;
  489. }
  490. } else if (typeof v === 'string') {
  491. if (v.length < 0 || v.length > 0xffffffff) {
  492. // Not possible in practice.
  493. throw new TypeError('cannot convert to dvalue, invalid string length: ' + v.length);
  494. }
  495. if (v.length <= 0x1f) {
  496. buf = new Uint8Array(1 + v.length);
  497. buf[0] = 0x60 + v.length;
  498. this.writeDebugStringToBuffer(v, buf, 1);
  499. return buf;
  500. } else if (v.length <= 0xffff) {
  501. buf = new Uint8Array(3 + v.length);
  502. buf[0] = 0x12;
  503. buf[1] = (v.length >> 8) & 0xff;
  504. buf[2] = (v.length >> 0) & 0xff;
  505. this.writeDebugStringToBuffer(v, buf, 3);
  506. return buf;
  507. } else {
  508. buf = new Uint8Array(5 + v.length);
  509. buf[0] = 0x11;
  510. buf[1] = (v.length >> 24) & 0xff;
  511. buf[2] = (v.length >> 16) & 0xff;
  512. buf[3] = (v.length >> 8) & 0xff;
  513. buf[4] = (v.length >> 0) & 0xff;
  514. this.writeDebugStringToBuffer(v, buf, 5);
  515. return buf;
  516. }
  517. }
  518. throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v));
  519. };
  520. /*
  521. * Target binary connection handler
  522. */
  523. function TargetConnHandler(server) {
  524. this.name = 'TargetConnHandler';
  525. this.server = server;
  526. this.handle = uv.new_tcp();
  527. this.jsonHandler = null;
  528. this.incoming = new Uint8Array(4096);
  529. this.incomingOffset = 0;
  530. this.dvalues = [];
  531. }
  532. TargetConnHandler.prototype.finish = function finish(msg) {
  533. if (!this.handle) {
  534. log.info('TargetConnHandler already disconnected, ignore finish()');
  535. return;
  536. }
  537. log.info('TargetConnHandler finished:', msg);
  538. this.jsonHandler.writeJson({ notify: '_TargetDisconnected' });
  539. // XXX: write a notify to target?
  540. uv.shutdown(this.handle);
  541. uv.read_stop(this.handle);
  542. uv.close(this.handle);
  543. this.handle = null;
  544. this.jsonHandler.finish(msg); // disconnect JSON client too (if not already disconnected)
  545. };
  546. TargetConnHandler.prototype.onConnect = function onConnect(err) {
  547. var errmsg;
  548. if (err) {
  549. errmsg = 'Failed to connect to target: ' + err;
  550. log.warn(errmsg);
  551. this.jsonHandler.writeJson({ notify: '_Error', args: [ String(err) ] });
  552. this.finish(errmsg);
  553. return;
  554. }
  555. // Once we're connected to the target, start read both binary and JSON
  556. // input. We don't want to read JSON input before this so that we can
  557. // always translate incoming messages to dvalues and write them out
  558. // without queueing. Any pending JSON messages will be queued by the
  559. // OS instead.
  560. log.info('Connected to debug target at', targetHost + ':' + targetPort);
  561. uv.read_start(this.jsonHandler.handle, this.jsonHandler.onRead.bind(this.jsonHandler));
  562. uv.read_start(this.handle, this.onRead.bind(this));
  563. };
  564. TargetConnHandler.prototype.writeBinary = function writeBinary(buf) {
  565. var plain = plainBufferCopy(buf);
  566. log.info('PROXY --> TARGET:', Duktape.enc('jx', plain));
  567. if (this.handle) {
  568. uv.write(this.handle, plain);
  569. }
  570. };
  571. TargetConnHandler.prototype.onRead = function onRead(err, data) {
  572. var res;
  573. var errmsg;
  574. var tmpBuf;
  575. var newIncoming;
  576. log.trace('Received data from target socket, err:', err, 'data length:', data ? data.length : 'null');
  577. if (err) {
  578. errmsg = 'Error reading data from debug target: ' + err;
  579. this.finish(errmsg);
  580. return;
  581. }
  582. if (data) {
  583. // Feed the data one byte at a time when torture testing.
  584. if (TORTURE && data.length > 1) {
  585. for (var i = 0; i < data.length; i++) {
  586. tmpBuf = Duktape.Buffer(1);
  587. tmpBuf[0] = data[i];
  588. this.onRead(null, tmpBuf);
  589. }
  590. return;
  591. }
  592. // Receive data into 'incoming', resizing as necessary.
  593. while (data.length > this.incoming.length - this.incomingOffset) {
  594. newIncoming = new Uint8Array(this.incoming.length * 1.3 + 16);
  595. newIncoming.set(this.incoming);
  596. this.incoming = newIncoming;
  597. log.debug('Resize incoming binary buffer to ' + this.incoming.length);
  598. }
  599. this.incoming.set(new Uint8Array(data), this.incomingOffset);
  600. this.incomingOffset += data.length;
  601. // Trial parse handshake unless done.
  602. if (!this.handshake) {
  603. this.trialParseHandshake();
  604. }
  605. // Trial parse dvalue(s) and debug messages.
  606. if (this.handshake) {
  607. for (;;) {
  608. res = this.trialParseDvalue();
  609. if (!res) {
  610. break;
  611. }
  612. log.trace('Got dvalue:', Duktape.enc('jx', res.dvalue));
  613. this.dvalues.push(res.dvalue);
  614. if (isObject(res.dvalue) && res.dvalue.type === 'eom') {
  615. try {
  616. this.jsonHandler.handleDebugMessage(this.dvalues);
  617. this.dvalues = [];
  618. } catch (e) {
  619. errmsg = 'JSON message handling failed: ' + e;
  620. this.jsonHandler.writeJson({ notify: '_Error', args: [ errmsg ] });
  621. if (lenientJsonParse) {
  622. log.warn('JSON message handling failed (lenient mode, ignoring):', e);
  623. } else {
  624. log.warn('JSON message handling failed (dropping connection):', e);
  625. this.finish(errmsg);
  626. }
  627. }
  628. }
  629. }
  630. }
  631. } else {
  632. log.info('Target disconnected');
  633. this.finish('Target disconnected');
  634. }
  635. };
  636. TargetConnHandler.prototype.trialParseHandshake = function trialParseHandshake() {
  637. var buf = this.incoming;
  638. var avail = this.incomingOffset;
  639. var i;
  640. var msg;
  641. var m;
  642. var protocolVersion;
  643. for (i = 0; i < avail; i++) {
  644. if (buf[i] == 0x0a) {
  645. msg = String(plainBufferCopy(buf.subarray(0, i)));
  646. this.incoming.set(this.incoming.subarray(i + 1));
  647. this.incomingOffset -= i + 1;
  648. // Generic handshake format: only relies on initial version field.
  649. m = /^(\d+) (.*)$/.exec(msg) || {};
  650. protocolVersion = +m[1];
  651. this.handshake = {
  652. line: msg,
  653. protocolVersion: protocolVersion,
  654. text: m[2]
  655. };
  656. // More detailed v1 handshake line.
  657. if (protocolVersion === 1) {
  658. m = /^(\d+) (\d+) (.*?) (.*?) (.*)$/.exec(msg) || {};
  659. this.handshake.dukVersion = m[1];
  660. this.handshake.dukGitDescribe = m[2];
  661. this.handshake.targetString = m[3];
  662. }
  663. this.jsonHandler.writeJson({ notify: '_TargetConnected', args: [ msg ] });
  664. log.info('Target handshake: ' + JSON.stringify(this.handshake));
  665. return;
  666. }
  667. }
  668. };
  669. TargetConnHandler.prototype.bufferToDebugString = function bufferToDebugString(buf) {
  670. return String.fromCharCode.apply(null, buf);
  671. };
  672. TargetConnHandler.prototype.trialParseDvalue = function trialParseDvalue() {
  673. var _this = this;
  674. var buf = this.incoming;
  675. var avail = this.incomingOffset;
  676. var v;
  677. var gotValue = false; // explicit flag for e.g. v === undefined
  678. var dv = new DataView(buf);
  679. var tmp;
  680. var x;
  681. var len;
  682. function consume(n) {
  683. log.info('PROXY <-- TARGET:', Duktape.enc('jx', _this.incoming.subarray(0, n)));
  684. _this.incoming.set(_this.incoming.subarray(n));
  685. _this.incomingOffset -= n;
  686. }
  687. x = buf[0];
  688. if (avail <= 0) {
  689. ;
  690. } else if (x >= 0xc0) {
  691. // 0xc0...0xff: integers 0-16383
  692. if (avail >= 2) {
  693. v = ((x - 0xc0) << 8) + buf[1];
  694. consume(2);
  695. }
  696. } else if (x >= 0x80) {
  697. // 0x80...0xbf: integers 0-63
  698. v = x - 0x80;
  699. consume(1);
  700. } else if (x >= 0x60) {
  701. // 0x60...0x7f: strings with length 0-31
  702. len = x - 0x60;
  703. if (avail >= 1 + len) {
  704. v = new Uint8Array(len);
  705. v.set(buf.subarray(1, 1 + len));
  706. v = this.bufferToDebugString(v);
  707. consume(1 + len);
  708. }
  709. } else {
  710. switch (x) {
  711. case 0x00: consume(1); v = { type: 'eom' }; break;
  712. case 0x01: consume(1); v = { type: 'req' }; break;
  713. case 0x02: consume(1); v = { type: 'rep' }; break;
  714. case 0x03: consume(1); v = { type: 'err' }; break;
  715. case 0x04: consume(1); v = { type: 'nfy' }; break;
  716. case 0x10: // 4-byte signed integer
  717. if (avail >= 5) {
  718. v = dv.getInt32(1, false);
  719. consume(5);
  720. }
  721. break;
  722. case 0x11: // 4-byte string
  723. if (avail >= 5) {
  724. len = dv.getUint32(1, false);
  725. if (avail >= 5 + len) {
  726. v = new Uint8Array(len);
  727. v.set(buf.subarray(5, 5 + len));
  728. v = this.bufferToDebugString(v);
  729. consume(5 + len);
  730. }
  731. }
  732. break;
  733. case 0x12: // 2-byte string
  734. if (avail >= 3) {
  735. len = dv.getUint16(1, false);
  736. if (avail >= 3 + len) {
  737. v = new Uint8Array(len);
  738. v.set(buf.subarray(3, 3 + len));
  739. v = this.bufferToDebugString(v);
  740. consume(3 + len);
  741. }
  742. }
  743. break;
  744. case 0x13: // 4-byte buffer
  745. if (avail >= 5) {
  746. len = dv.getUint32(1, false);
  747. if (avail >= 5 + len) {
  748. v = new Uint8Array(len);
  749. v.set(buf.subarray(5, 5 + len));
  750. v = { type: 'buffer', data: Duktape.enc('hex', Duktape.Buffer(v)) };
  751. consume(5 + len);
  752. }
  753. }
  754. break;
  755. case 0x14: // 2-byte buffer
  756. if (avail >= 3) {
  757. len = dv.getUint16(1, false);
  758. if (avail >= 3 + len) {
  759. v = new Uint8Array(len);
  760. v.set(buf.subarray(3, 3 + len));
  761. v = { type: 'buffer', data: Duktape.enc('hex', Duktape.Buffer(v)) };
  762. consume(3 + len);
  763. }
  764. }
  765. break;
  766. case 0x15: // unused/none
  767. v = { type: 'unused' };
  768. consume(1);
  769. break;
  770. case 0x16: // undefined
  771. v = { type: 'undefined' };
  772. gotValue = true; // indicate 'v' is actually set
  773. consume(1);
  774. break;
  775. case 0x17: // null
  776. v = null;
  777. gotValue = true; // indicate 'v' is actually set
  778. consume(1);
  779. break;
  780. case 0x18: // true
  781. v = true;
  782. consume(1);
  783. break;
  784. case 0x19: // false
  785. v = false;
  786. consume(1);
  787. break;
  788. case 0x1a: // number (IEEE double), big endian
  789. if (avail >= 9) {
  790. tmp = new Uint8Array(8);
  791. tmp.set(buf.subarray(1, 9));
  792. v = { type: 'number', data: Duktape.enc('hex', Duktape.Buffer(tmp)) };
  793. if (readableNumberValue) {
  794. // The value key should not be used programmatically,
  795. // it is just there to make the dumps more readable.
  796. v.value = new DataView(tmp.buffer).getFloat64(0, false);
  797. }
  798. consume(9);
  799. }
  800. break;
  801. case 0x1b: // object
  802. if (avail >= 3) {
  803. len = buf[2];
  804. if (avail >= 3 + len) {
  805. v = new Uint8Array(len);
  806. v.set(buf.subarray(3, 3 + len));
  807. v = { type: 'object', 'class': buf[1], pointer: Duktape.enc('hex', Duktape.Buffer(v)) };
  808. consume(3 + len);
  809. }
  810. }
  811. break;
  812. case 0x1c: // pointer
  813. if (avail >= 2) {
  814. len = buf[1];
  815. if (avail >= 2 + len) {
  816. v = new Uint8Array(len);
  817. v.set(buf.subarray(2, 2 + len));
  818. v = { type: 'pointer', pointer: Duktape.enc('hex', Duktape.Buffer(v)) };
  819. consume(2 + len);
  820. }
  821. }
  822. break;
  823. case 0x1d: // lightfunc
  824. if (avail >= 4) {
  825. len = buf[3];
  826. if (avail >= 4 + len) {
  827. v = new Uint8Array(len);
  828. v.set(buf.subarray(4, 4 + len));
  829. v = { type: 'lightfunc', flags: dv.getUint16(1, false), pointer: Duktape.enc('hex', Duktape.Buffer(v)) };
  830. consume(4 + len);
  831. }
  832. }
  833. break;
  834. case 0x1e: // heapptr
  835. if (avail >= 2) {
  836. len = buf[1];
  837. if (avail >= 2 + len) {
  838. v = new Uint8Array(len);
  839. v.set(buf.subarray(2, 2 + len));
  840. v = { type: 'heapptr', pointer: Duktape.enc('hex', Duktape.Buffer(v)) };
  841. consume(2 + len);
  842. }
  843. }
  844. break;
  845. default:
  846. throw new Error('failed parse initial byte: ' + buf[0]);
  847. }
  848. }
  849. if (typeof v !== 'undefined' || gotValue) {
  850. return { dvalue: v };
  851. }
  852. };
  853. /*
  854. * Main
  855. */
  856. function main() {
  857. var argv = typeof uv.argv === 'function' ? uv.argv() : [];
  858. var i;
  859. for (i = 2; i < argv.length; i++) { // skip dukluv and script name
  860. if (argv[i] == '--help') {
  861. print('Usage: dukluv ' + argv[1] + ' [option]+');
  862. print('');
  863. print(' --server-host HOST JSON proxy server listen address');
  864. print(' --server-port PORT JSON proxy server listen port');
  865. print(' --target-host HOST Debug target address');
  866. print(' --target-port PORT Debug target port');
  867. print(' --metadata FILE Proxy metadata file (usually named duk_debug_meta.json)');
  868. print(' --log-level LEVEL Set log level, default is 2; 0=trace, 1=debug, 2=info, 3=warn, etc');
  869. print(' --single Run a single proxy connection and exit (default: persist for multiple connections)');
  870. print(' --readable-numbers Add a non-programmatic "value" key for IEEE doubles help readability');
  871. print(' --lenient Ignore (with warning) invalid JSON without dropping connection');
  872. print(' --jx-parse Parse JSON proxy input with JX, useful when testing manually');
  873. print('');
  874. return; // don't register any sockets/timers etc to exit
  875. } else if (argv[i] == '--single') {
  876. singleConnection = true;
  877. continue;
  878. } else if (argv[i] == '--readable-numbers') {
  879. readableNumberValue = true;
  880. continue;
  881. } else if (argv[i] == '--lenient') {
  882. lenientJsonParse = true;
  883. continue;
  884. } else if (argv[i] == '--jx-parse') {
  885. jxParse = true;
  886. continue;
  887. }
  888. if (i >= argv.length - 1) {
  889. throw new Error('missing option value for ' + argv[i]);
  890. }
  891. if (argv[i] == '--server-host') {
  892. serverHost = argv[i + 1];
  893. i++;
  894. } else if (argv[i] == '--server-port') {
  895. serverPort = Math.floor(+argv[i + 1]);
  896. i++;
  897. } else if (argv[i] == '--target-host') {
  898. targetHost = argv[i + 1];
  899. i++;
  900. } else if (argv[i] == '--target-port') {
  901. targetPort = Math.floor(+argv[i + 1]);
  902. i++;
  903. } else if (argv[i] == '--metadata') {
  904. metadataFile = argv[i + 1];
  905. i++;
  906. } else if (argv[i] == '--log-level') {
  907. log.l = Math.floor(+argv[i + 1]);
  908. i++;
  909. } else {
  910. throw new Error('invalid option ' + argv[i]);
  911. }
  912. }
  913. function runServer() {
  914. var serverSocket = new JsonProxyServer(serverHost, serverPort);
  915. var connMode = singleConnection ? 'single connection mode' : 'persistent connection mode';
  916. log.info('Listening for incoming JSON debug connection on ' + serverHost + ':' + serverPort +
  917. ', target is ' + targetHost + ':' + targetPort + ', ' + connMode);
  918. }
  919. if (metadataFile) {
  920. log.info('Read proxy metadata from', metadataFile);
  921. readFully(metadataFile, function (data, err) {
  922. if (err) {
  923. log.error('Failed to load metadata:', err);
  924. throw err;
  925. }
  926. try {
  927. metadata = JSON.parse(String(data));
  928. } catch (e) {
  929. log.error('Failed to parse JSON metadata from ' + metadataFile + ': ' + e);
  930. throw e;
  931. }
  932. runServer();
  933. });
  934. } else {
  935. runServer();
  936. }
  937. }
  938. main();