/dom/network/src/TCPSocket.js

https://github.com/marcussaad/firefox · JavaScript · 634 lines · 471 code · 102 blank · 61 comment · 54 complexity · f55afdb85091c44d5170087db8233ed1 MD5 · raw file

  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const Cc = Components.classes;
  6. const Ci = Components.interfaces;
  7. const Cu = Components.utils;
  8. const Cr = Components.results;
  9. const CC = Components.Constructor;
  10. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  11. Cu.import("resource://gre/modules/Services.jsm");
  12. const InputStreamPump = CC(
  13. "@mozilla.org/network/input-stream-pump;1", "nsIInputStreamPump", "init"),
  14. AsyncStreamCopier = CC(
  15. "@mozilla.org/network/async-stream-copier;1", "nsIAsyncStreamCopier", "init"),
  16. ScriptableInputStream = CC(
  17. "@mozilla.org/scriptableinputstream;1", "nsIScriptableInputStream", "init"),
  18. BinaryInputStream = CC(
  19. "@mozilla.org/binaryinputstream;1", "nsIBinaryInputStream", "setInputStream"),
  20. StringInputStream = CC(
  21. '@mozilla.org/io/string-input-stream;1', 'nsIStringInputStream'),
  22. MultiplexInputStream = CC(
  23. '@mozilla.org/io/multiplex-input-stream;1', 'nsIMultiplexInputStream');
  24. const kCONNECTING = 'connecting';
  25. const kOPEN = 'open';
  26. const kCLOSING = 'closing';
  27. const kCLOSED = 'closed';
  28. const BUFFER_SIZE = 65536;
  29. /*
  30. * Debug logging function
  31. */
  32. let debug = true;
  33. function LOG(msg) {
  34. if (debug)
  35. dump("TCPSocket: " + msg + "\n");
  36. }
  37. /*
  38. * nsITCPSocketEvent object
  39. */
  40. function TCPSocketEvent(type, sock, data) {
  41. this._type = type;
  42. this._target = sock;
  43. this._data = data;
  44. }
  45. TCPSocketEvent.prototype = {
  46. __exposedProps__: {
  47. type: 'r',
  48. target: 'r',
  49. data: 'r'
  50. },
  51. get type() {
  52. return this._type;
  53. },
  54. get target() {
  55. return this._target;
  56. },
  57. get data() {
  58. return this._data;
  59. }
  60. }
  61. /*
  62. * nsIDOMTCPSocket object
  63. */
  64. function TCPSocket() {
  65. this._readyState = kCLOSED;
  66. this._onopen = null;
  67. this._ondrain = null;
  68. this._ondata = null;
  69. this._onerror = null;
  70. this._onclose = null;
  71. this._binaryType = "string";
  72. this._host = "";
  73. this._port = 0;
  74. this._ssl = false;
  75. // As a workaround for bug https://bugzilla.mozilla.org/show_bug.cgi?id=786639
  76. // we want to create any Uint8Array's off of the owning window so that there
  77. // is no need for a wrapper to exist around the typed array from the
  78. // perspective of content. (The wrapper is bad because it only lets content
  79. // see length, and forbids access to the array indices unless we excplicitly
  80. // list them all.) We will then access the array through a wrapper, but
  81. // since we are chrome-privileged, this does not pose a problem.
  82. this.useWin = null;
  83. }
  84. TCPSocket.prototype = {
  85. __exposedProps__: {
  86. open: 'r',
  87. host: 'r',
  88. port: 'r',
  89. ssl: 'r',
  90. bufferedAmount: 'r',
  91. suspend: 'r',
  92. resume: 'r',
  93. close: 'r',
  94. send: 'r',
  95. readyState: 'r',
  96. binaryType: 'r',
  97. onopen: 'rw',
  98. ondrain: 'rw',
  99. ondata: 'rw',
  100. onerror: 'rw',
  101. onclose: 'rw'
  102. },
  103. // The binary type, "string" or "arraybuffer"
  104. _binaryType: null,
  105. // Internal
  106. _hasPrivileges: null,
  107. // Raw socket streams
  108. _transport: null,
  109. _socketInputStream: null,
  110. _socketOutputStream: null,
  111. // Input stream machinery
  112. _inputStreamPump: null,
  113. _inputStreamScriptable: null,
  114. _inputStreamBinary: null,
  115. // Output stream machinery
  116. _multiplexStream: null,
  117. _multiplexStreamCopier: null,
  118. _asyncCopierActive: false,
  119. _waitingForDrain: false,
  120. _suspendCount: 0,
  121. // Reported parent process buffer
  122. _bufferedAmount: 0,
  123. // IPC socket actor
  124. _socketBridge: null,
  125. // Public accessors.
  126. get readyState() {
  127. return this._readyState;
  128. },
  129. get binaryType() {
  130. return this._binaryType;
  131. },
  132. get host() {
  133. return this._host;
  134. },
  135. get port() {
  136. return this._port;
  137. },
  138. get ssl() {
  139. return this._ssl;
  140. },
  141. get bufferedAmount() {
  142. if (this._inChild) {
  143. return this._bufferedAmount;
  144. }
  145. return this._multiplexStream.available();
  146. },
  147. get onopen() {
  148. return this._onopen;
  149. },
  150. set onopen(f) {
  151. this._onopen = f;
  152. },
  153. get ondrain() {
  154. return this._ondrain;
  155. },
  156. set ondrain(f) {
  157. this._ondrain = f;
  158. },
  159. get ondata() {
  160. return this._ondata;
  161. },
  162. set ondata(f) {
  163. this._ondata = f;
  164. },
  165. get onerror() {
  166. return this._onerror;
  167. },
  168. set onerror(f) {
  169. this._onerror = f;
  170. },
  171. get onclose() {
  172. return this._onclose;
  173. },
  174. set onclose(f) {
  175. this._onclose = f;
  176. },
  177. // Helper methods.
  178. _createTransport: function ts_createTransport(host, port, sslMode) {
  179. let options, optlen;
  180. if (sslMode) {
  181. options = [sslMode];
  182. optlen = 1;
  183. } else {
  184. options = null;
  185. optlen = 0;
  186. }
  187. return Cc["@mozilla.org/network/socket-transport-service;1"]
  188. .getService(Ci.nsISocketTransportService)
  189. .createTransport(options, optlen, host, port, null);
  190. },
  191. _ensureCopying: function ts_ensureCopying() {
  192. let self = this;
  193. if (this._asyncCopierActive) {
  194. return;
  195. }
  196. this._asyncCopierActive = true;
  197. this._multiplexStreamCopier.asyncCopy({
  198. onStartRequest: function ts_output_onStartRequest() {
  199. },
  200. onStopRequest: function ts_output_onStopRequest(request, context, status) {
  201. self._asyncCopierActive = false;
  202. self._multiplexStream.removeStream(0);
  203. if (status) {
  204. self._readyState = kCLOSED;
  205. let err = new Error("Connection closed while writing: " + status);
  206. err.status = status;
  207. self.callListener("error", err);
  208. self.callListener("close");
  209. return;
  210. }
  211. if (self._multiplexStream.count) {
  212. self._ensureCopying();
  213. } else {
  214. if (self._waitingForDrain) {
  215. self._waitingForDrain = false;
  216. self.callListener("drain");
  217. }
  218. if (self._readyState === kCLOSING) {
  219. self._socketOutputStream.close();
  220. self._readyState = kCLOSED;
  221. self.callListener("close");
  222. }
  223. }
  224. }
  225. }, null);
  226. },
  227. callListener: function ts_callListener(type, data) {
  228. if (!this["on" + type])
  229. return;
  230. this["on" + type].call(null, new TCPSocketEvent(type, this, data || ""));
  231. },
  232. /* nsITCPSocketInternal methods */
  233. callListenerError: function ts_callListenerError(type, message, filename,
  234. lineNumber, columnNumber) {
  235. this.callListener(type, new Error(message, filename, lineNumber, columnNumber));
  236. },
  237. callListenerData: function ts_callListenerString(type, data) {
  238. this.callListener(type, data);
  239. },
  240. callListenerArrayBuffer: function ts_callListenerArrayBuffer(type, data) {
  241. this.callListener(type, data);
  242. },
  243. callListenerVoid: function ts_callListenerVoid(type) {
  244. this.callListener(type);
  245. },
  246. updateReadyStateAndBuffered: function ts_setReadyState(readyState, bufferedAmount) {
  247. this._readyState = readyState;
  248. this._bufferedAmount = bufferedAmount;
  249. },
  250. /* end nsITCPSocketInternal methods */
  251. initWindowless: function ts_initWindowless() {
  252. return Services.prefs.getBoolPref("dom.mozTCPSocket.enabled");
  253. },
  254. init: function ts_init(aWindow) {
  255. if (!this.initWindowless())
  256. return null;
  257. let principal = aWindow.document.nodePrincipal;
  258. let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"]
  259. .getService(Ci.nsIScriptSecurityManager);
  260. let perm = principal == secMan.getSystemPrincipal()
  261. ? Ci.nsIPermissionManager.ALLOW_ACTION
  262. : Services.perms.testExactPermissionFromPrincipal(principal, "tcp-socket");
  263. this._hasPrivileges = perm == Ci.nsIPermissionManager.ALLOW_ACTION;
  264. let util = aWindow.QueryInterface(
  265. Ci.nsIInterfaceRequestor
  266. ).getInterface(Ci.nsIDOMWindowUtils);
  267. this.useWin = XPCNativeWrapper.unwrap(aWindow);
  268. this.innerWindowID = util.currentInnerWindowID;
  269. LOG("window init: " + this.innerWindowID);
  270. },
  271. observe: function(aSubject, aTopic, aData) {
  272. if (aTopic == "inner-window-destroyed") {
  273. let wId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
  274. if (wId == this.innerWindowID) {
  275. LOG("inner-window-destroyed: " + this.innerWindowID);
  276. // This window is now dead, so we want to clear the callbacks
  277. // so that we don't get a "can't access dead object" when the
  278. // underlying stream goes to tell us that we are closed
  279. this.onopen = null;
  280. this.ondrain = null;
  281. this.ondata = null;
  282. this.onerror = null;
  283. this.onclose = null;
  284. this.useWin = null;
  285. // Clean up our socket
  286. this.close();
  287. }
  288. }
  289. },
  290. // nsIDOMTCPSocket
  291. open: function ts_open(host, port, options) {
  292. if (!this.initWindowless())
  293. return null;
  294. this._inChild = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime)
  295. .processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
  296. LOG("content process: " + (this._inChild ? "true" : "false") + "\n");
  297. // in the testing case, init won't be called and
  298. // hasPrivileges will be null. We want to proceed to test.
  299. if (this._hasPrivileges !== true && this._hasPrivileges !== null) {
  300. throw new Error("TCPSocket does not have permission in this context.\n");
  301. }
  302. let that = new TCPSocket();
  303. that.useWin = this.useWin;
  304. that.innerWindowID = this.innerWindowID;
  305. that._inChild = this._inChild;
  306. LOG("window init: " + that.innerWindowID);
  307. Services.obs.addObserver(that, "inner-window-destroyed", true);
  308. LOG("startup called\n");
  309. LOG("Host info: " + host + ":" + port + "\n");
  310. that._readyState = kCONNECTING;
  311. that._host = host;
  312. that._port = port;
  313. if (options !== undefined) {
  314. if (options.useSSL) {
  315. that._ssl = 'ssl';
  316. } else {
  317. that._ssl = false;
  318. }
  319. that._binaryType = options.binaryType || that._binaryType;
  320. }
  321. LOG("SSL: " + that.ssl + "\n");
  322. if (this._inChild) {
  323. that._socketBridge = Cc["@mozilla.org/tcp-socket-child;1"]
  324. .createInstance(Ci.nsITCPSocketChild);
  325. that._socketBridge.open(that, host, port, !!that._ssl,
  326. that._binaryType, this.useWin, this);
  327. return that;
  328. }
  329. let transport = that._transport = this._createTransport(host, port, that._ssl);
  330. transport.setEventSink(that, Services.tm.currentThread);
  331. transport.securityCallbacks = new SecurityCallbacks(that);
  332. that._socketInputStream = transport.openInputStream(0, 0, 0);
  333. that._socketOutputStream = transport.openOutputStream(
  334. Ci.nsITransport.OPEN_UNBUFFERED, 0, 0);
  335. // If the other side is not listening, we will
  336. // get an onInputStreamReady callback where available
  337. // raises to indicate the connection was refused.
  338. that._socketInputStream.asyncWait(
  339. that, that._socketInputStream.WAIT_CLOSURE_ONLY, 0, Services.tm.currentThread);
  340. if (that._binaryType === "arraybuffer") {
  341. that._inputStreamBinary = new BinaryInputStream(that._socketInputStream);
  342. } else {
  343. that._inputStreamScriptable = new ScriptableInputStream(that._socketInputStream);
  344. }
  345. that._multiplexStream = new MultiplexInputStream();
  346. that._multiplexStreamCopier = new AsyncStreamCopier(
  347. that._multiplexStream,
  348. that._socketOutputStream,
  349. // (nsSocketTransport uses gSocketTransportService)
  350. Cc["@mozilla.org/network/socket-transport-service;1"]
  351. .getService(Ci.nsIEventTarget),
  352. /* source buffered */ true, /* sink buffered */ false,
  353. BUFFER_SIZE, /* close source*/ false, /* close sink */ false);
  354. return that;
  355. },
  356. close: function ts_close() {
  357. if (this._readyState === kCLOSED || this._readyState === kCLOSING)
  358. return;
  359. LOG("close called\n");
  360. this._readyState = kCLOSING;
  361. if (this._inChild) {
  362. this._socketBridge.close();
  363. return;
  364. }
  365. if (!this._multiplexStream.count) {
  366. this._socketOutputStream.close();
  367. }
  368. this._socketInputStream.close();
  369. },
  370. send: function ts_send(data) {
  371. if (this._readyState !== kOPEN) {
  372. throw new Error("Socket not open.");
  373. }
  374. if (this._inChild) {
  375. this._socketBridge.send(data);
  376. }
  377. let new_stream = new StringInputStream();
  378. if (this._binaryType === "arraybuffer") {
  379. // It would be really nice if there were an interface
  380. // that took an ArrayBuffer like StringInputStream takes
  381. // a string. There is one, but only in C++ and not exposed
  382. // to js as far as I can tell
  383. var dataLen = data.length;
  384. var offset = 0;
  385. var result = "";
  386. while (dataLen) {
  387. var fragmentLen = dataLen;
  388. if (fragmentLen > 32768)
  389. fragmentLen = 32768;
  390. dataLen -= fragmentLen;
  391. var fragment = data.subarray(offset, offset + fragmentLen);
  392. offset += fragmentLen;
  393. result += String.fromCharCode.apply(null, fragment);
  394. }
  395. data = result;
  396. }
  397. var newBufferedAmount = this.bufferedAmount + data.length;
  398. var bufferNotFull = newBufferedAmount < BUFFER_SIZE;
  399. if (this._inChild) {
  400. return bufferNotFull;
  401. }
  402. new_stream.setData(data, data.length);
  403. this._multiplexStream.appendStream(new_stream);
  404. if (newBufferedAmount >= BUFFER_SIZE) {
  405. // If we buffered more than some arbitrary amount of data,
  406. // (65535 right now) we should tell the caller so they can
  407. // wait until ondrain is called if they so desire. Once all the
  408. //buffered data has been written to the socket, ondrain is
  409. // called.
  410. this._waitingForDrain = true;
  411. }
  412. this._ensureCopying();
  413. return bufferNotFull;
  414. },
  415. suspend: function ts_suspend() {
  416. if (this._inChild) {
  417. this._socketBridge.suspend();
  418. return;
  419. }
  420. if (this._inputStreamPump) {
  421. this._inputStreamPump.suspend();
  422. } else {
  423. ++this._suspendCount;
  424. }
  425. },
  426. resume: function ts_resume() {
  427. if (this._inChild) {
  428. this._socketBridge.resume();
  429. return;
  430. }
  431. if (this._inputStreamPump) {
  432. this._inputStreamPump.resume();
  433. } else {
  434. --this._suspendCount;
  435. }
  436. },
  437. // nsITransportEventSink (Triggered by transport.setEventSink)
  438. onTransportStatus: function ts_onTransportStatus(
  439. transport, status, progress, max) {
  440. if (status === Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
  441. this._readyState = kOPEN;
  442. this.callListener("open");
  443. this._inputStreamPump = new InputStreamPump(
  444. this._socketInputStream, -1, -1, 0, 0, false
  445. );
  446. while (this._suspendCount--) {
  447. this._inputStreamPump.suspend();
  448. }
  449. this._inputStreamPump.asyncRead(this, null);
  450. }
  451. },
  452. // nsIAsyncInputStream (Triggered by _socketInputStream.asyncWait)
  453. // Only used for detecting connection refused
  454. onInputStreamReady: function ts_onInputStreamReady(input) {
  455. try {
  456. input.available();
  457. } catch (e) {
  458. this.callListener("error", new Error("Connection refused"));
  459. }
  460. },
  461. // nsIRequestObserver (Triggered by _inputStreamPump.asyncRead)
  462. onStartRequest: function ts_onStartRequest(request, context) {
  463. },
  464. // nsIRequestObserver (Triggered by _inputStreamPump.asyncRead)
  465. onStopRequest: function ts_onStopRequest(request, context, status) {
  466. let buffered_output = this._multiplexStream.count !== 0;
  467. this._inputStreamPump = null;
  468. if (buffered_output && !status) {
  469. // If we have some buffered output still, and status is not an
  470. // error, the other side has done a half-close, but we don't
  471. // want to be in the close state until we are done sending
  472. // everything that was buffered. We also don't want to call onclose
  473. // yet.
  474. return;
  475. }
  476. this._readyState = kCLOSED;
  477. if (status) {
  478. let err = new Error("Connection closed: " + status);
  479. err.status = status;
  480. this.callListener("error", err);
  481. }
  482. this.callListener("close");
  483. },
  484. // nsIStreamListener (Triggered by _inputStreamPump.asyncRead)
  485. onDataAvailable: function ts_onDataAvailable(request, context, inputStream, offset, count) {
  486. if (this._binaryType === "arraybuffer") {
  487. let ua = this.useWin ? new this.useWin.Uint8Array(count)
  488. : new Uint8Array(count);
  489. ua.set(this._inputStreamBinary.readByteArray(count));
  490. this.callListener("data", ua);
  491. } else {
  492. this.callListener("data", this._inputStreamScriptable.read(count));
  493. }
  494. },
  495. classID: Components.ID("{cda91b22-6472-11e1-aa11-834fec09cd0a}"),
  496. classInfo: XPCOMUtils.generateCI({
  497. classID: Components.ID("{cda91b22-6472-11e1-aa11-834fec09cd0a}"),
  498. contractID: "@mozilla.org/tcp-socket;1",
  499. classDescription: "Client TCP Socket",
  500. interfaces: [
  501. Ci.nsIDOMTCPSocket,
  502. ],
  503. flags: Ci.nsIClassInfo.DOM_OBJECT,
  504. }),
  505. QueryInterface: XPCOMUtils.generateQI([
  506. Ci.nsIDOMTCPSocket,
  507. Ci.nsITCPSocketInternal,
  508. Ci.nsIDOMGlobalPropertyInitializer,
  509. Ci.nsIObserver,
  510. Ci.nsISupportsWeakReference
  511. ])
  512. }
  513. function SecurityCallbacks(socket) {
  514. this._socket = socket;
  515. }
  516. SecurityCallbacks.prototype = {
  517. notifyCertProblem: function sc_notifyCertProblem(socketInfo, status,
  518. targetSite) {
  519. this._socket.callListener("error", status);
  520. this._socket.close();
  521. return true;
  522. },
  523. getInterface: function sc_getInterface(iid) {
  524. return this.QueryInterface(iid);
  525. },
  526. QueryInterface: XPCOMUtils.generateQI([
  527. Ci.nsIBadCertListener2,
  528. Ci.nsIInterfaceRequestor,
  529. Ci.nsISupports
  530. ])
  531. };
  532. this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TCPSocket]);