'use strict'; const url = require('url'); const { EventEmitter } = require('events'); const { Readable } = require('stream'); const { app } = require('electron'); const { Session } = process.electronBinding('session'); const { net, Net } = process.electronBinding('net'); const { URLRequest } = net; // Net is an EventEmitter. Object.setPrototypeOf(Net.prototype, EventEmitter.prototype); EventEmitter.call(net); Object.setPrototypeOf(URLRequest.prototype, EventEmitter.prototype); const kSupportedProtocols = new Set(['http:', 'https:']); // set of headers that Node.js discards duplicates for // see https://nodejs.org/api/http.html#http_message_headers const discardableDuplicateHeaders = new Set([ 'content-type', 'content-length', 'user-agent', 'referer', 'host', 'authorization', 'proxy-authorization', 'if-modified-since', 'if-unmodified-since', 'from', 'location', 'max-forwards', 'retry-after', 'etag', 'last-modified', 'server', 'age', 'expires' ]); class IncomingMessage extends Readable { constructor(urlRequest) { super(); this.urlRequest = urlRequest; this.shouldPush = false; this.data = []; this.urlRequest.on('data', (event, chunk) => { this._storeInternalData(chunk); this._pushInternalData(); }); this.urlRequest.on('end', () => { this._storeInternalData(null); this._pushInternalData(); }); } get statusCode() { return this.urlRequest.statusCode; } get statusMessage() { return this.urlRequest.statusMessage; } get headers() { const filteredHeaders = {}; const rawHeaders = this.urlRequest.rawResponseHeaders; Object.keys(rawHeaders).forEach(header => { if (header in filteredHeaders && discardableDuplicateHeaders.has(header)) { // do nothing with discardable duplicate headers } else { if (header === 'set-cookie') { // keep set-cookie as an array per Node.js rules // see https://nodejs.org/api/http.html#http_message_headers filteredHeaders[header] = rawHeaders[header]; } else { // for non-cookie headers, the values are joined together with ', ' filteredHeaders[header] = rawHeaders[header].join(', '); } } }); return filteredHeaders; } get httpVersion() { return `${this.httpVersionMajor}.${this.httpVersionMinor}`; } get httpVersionMajor() { return this.urlRequest.httpVersionMajor; } get httpVersionMinor() { return this.urlRequest.httpVersionMinor; } get rawTrailers() { throw new Error('HTTP trailers are not supported.'); } get trailers() { throw new Error('HTTP trailers are not supported.'); } _storeInternalData(chunk) { this.data.push(chunk); } _pushInternalData() { while (this.shouldPush && this.data.length > 0) { const chunk = this.data.shift(); this.shouldPush = this.push(chunk); } } _read() { this.shouldPush = true; this._pushInternalData(); } } URLRequest.prototype._emitRequestEvent = function (isAsync, ...rest) { if (isAsync) { process.nextTick(() => { this.clientRequest.emit(...rest); }); } else { this.clientRequest.emit(...rest); } }; URLRequest.prototype._emitResponseEvent = function (isAsync, ...rest) { if (isAsync) { process.nextTick(() => { this._response.emit(...rest); }); } else { this._response.emit(...rest); } }; class ClientRequest extends EventEmitter { constructor(options, callback) { super(); if (!app.isReady()) { throw new Error('net module can only be used after app is ready'); } if (typeof options === 'string') { options = url.parse(options); } else { options = Object.assign({}, options); } const method = (options.method || 'GET').toUpperCase(); let urlStr = options.url; if (!urlStr) { const urlObj = {}; const protocol = options.protocol || 'http:'; if (!kSupportedProtocols.has(protocol)) { throw new Error('Protocol "' + protocol + '" not supported. '); } urlObj.protocol = protocol; if (options.host) { urlObj.host = options.host; } else { if (options.hostname) { urlObj.hostname = options.hostname; } else { urlObj.hostname = 'localhost'; } if (options.port) { urlObj.port = options.port; } } if (options.path && / /.test(options.path)) { // The actual regex is more like /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/ // with an additional rule for ignoring percentage-escaped characters // but that's a) hard to capture in a regular expression that performs // well, and b) possibly too restrictive for real-world usage. That's // why it only scans for spaces because those are guaranteed to create // an invalid request. throw new TypeError('Request path contains unescaped characters.'); } const pathObj = url.parse(options.path || '/'); urlObj.pathname = pathObj.pathname; urlObj.search = pathObj.search; urlObj.hash = pathObj.hash; urlStr = url.format(urlObj); } const redirectPolicy = options.redirect || 'follow'; if (!['follow', 'error', 'manual'].includes(redirectPolicy)) { throw new Error('redirect mode should be one of follow, error or manual'); } const urlRequestOptions = { method: method, url: urlStr, redirect: redirectPolicy }; if (options.session) { if (options.session instanceof Session) { urlRequestOptions.session = options.session; } else { throw new TypeError('`session` should be an instance of the Session class.'); } } else if (options.partition) { if (typeof options.partition === 'string') { urlRequestOptions.partition = options.partition; } else { throw new TypeError('`partition` should be an a string.'); } } const urlRequest = new URLRequest(urlRequestOptions); // Set back and forward links. this.urlRequest = urlRequest; urlRequest.clientRequest = this; // This is a copy of the extra headers structure held by the native // net::URLRequest. The main reason is to keep the getHeader API synchronous // after the request starts. this.extraHeaders = {}; if (options.headers) { for (const key in options.headers) { this.setHeader(key, options.headers[key]); } } // Set when the request uses chunked encoding. Can be switched // to true only once and never set back to false. this.chunkedEncodingEnabled = false; urlRequest.on('response', () => { const response = new IncomingMessage(urlRequest); urlRequest._response = response; this.emit('response', response); }); urlRequest.on('login', (event, authInfo, callback) => { this.emit('login', authInfo, (username, password) => { // If null or undefined username/password, force to empty string. if (username === null || username === undefined) { username = ''; } if (typeof username !== 'string') { throw new Error('username must be a string'); } if (password === null || password === undefined) { password = ''; } if (typeof password !== 'string') { throw new Error('password must be a string'); } callback(username, password); }); }); if (callback) { this.once('response', callback); } } get chunkedEncoding() { return this.chunkedEncodingEnabled; } set chunkedEncoding(value) { if (!this.urlRequest.notStarted) { throw new Error('Can\'t set the transfer encoding, headers have been sent.'); } this.chunkedEncodingEnabled = value; } setHeader(name, value) { if (typeof name !== 'string') { throw new TypeError('`name` should be a string in setHeader(name, value).'); } if (value == null) { throw new Error('`value` required in setHeader("' + name + '", value).'); } if (!this.urlRequest.notStarted) { throw new Error('Can\'t set headers after they are sent.'); } const key = name.toLowerCase(); this.extraHeaders[key] = value; this.urlRequest.setExtraHeader(name, value.toString()); } getHeader(name) { if (name == null) { throw new Error('`name` is required for getHeader(name).'); } if (!this.extraHeaders) { return; } const key = name.toLowerCase(); return this.extraHeaders[key]; } removeHeader(name) { if (name == null) { throw new Error('`name` is required for removeHeader(name).'); } if (!this.urlRequest.notStarted) { throw new Error('Can\'t remove headers after they are sent.'); } const key = name.toLowerCase(); delete this.extraHeaders[key]; this.urlRequest.removeExtraHeader(name); } _write(chunk, encoding, callback, isLast) { const chunkIsString = typeof chunk === 'string'; const chunkIsBuffer = chunk instanceof Buffer; if (!chunkIsString && !chunkIsBuffer) { throw new TypeError('First argument must be a string or Buffer.'); } if (chunkIsString) { // We convert all strings into binary buffers. chunk = Buffer.from(chunk, encoding); } // Since writing to the network is asynchronous, we conservatively // assume that request headers are written after delivering the first // buffer to the network IO thread. if (this.urlRequest.notStarted) { this.urlRequest.setChunkedUpload(this.chunkedEncoding); } // Headers are assumed to be sent on first call to _writeBuffer, // i.e. after the first call to write or end. const result = this.urlRequest.write(chunk, isLast); // The write callback is fired asynchronously to mimic Node.js. if (callback) { process.nextTick(callback); } return result; } write(data, encoding, callback) { if (this.urlRequest.finished) { const error = new Error('Write after end.'); process.nextTick(writeAfterEndNT, this, error, callback); return true; } return this._write(data, encoding, callback, false); } end(data, encoding, callback) { if (this.urlRequest.finished) { return false; } if (typeof data === 'function') { callback = data; encoding = null; data = null; } else if (typeof encoding === 'function') { callback = encoding; encoding = null; } data = data || ''; return this._write(data, encoding, callback, true); } followRedirect() { this.urlRequest.followRedirect(); } abort() { this.urlRequest.cancel(); } getUploadProgress() { return this.urlRequest.getUploadProgress(); } } function writeAfterEndNT(self, error, callback) { self.emit('error', error); if (callback) callback(error); } Net.prototype.request = function (options, callback) { return new ClientRequest(options, callback); }; net.ClientRequest = ClientRequest; module.exports = net;