'use strict' const url = require('url') const {EventEmitter} = require('events') const util = require('util') const Readable = require('stream').Readable const binding = process.atomBinding('net') const {net, Net} = binding const {URLRequest} = net Object.setPrototypeOf(Net.prototype, EventEmitter.prototype) Object.setPrototypeOf(URLRequest.prototype, EventEmitter.prototype) let kSupportedProtocols = new Set() kSupportedProtocols.add('http:') kSupportedProtocols.add('https:') class IncomingMessage extends Readable { constructor (urlRequest) { super() this._url_request = urlRequest this._shouldPush = false; this._data = []; this._url_request.on('data', (event, chunk) => { this._storeInternalData(chunk) this._pushInternalData() }) this._url_request.on('end', () => { this._storeInternalData(null) this._pushInternalData() }) } get statusCode () { return this._url_request.statusCode } get statusMessage () { return this._url_request.statusMessage } get headers () { return this._url_request.rawResponseHeaders } get httpVersion () { return `${this.httpVersionMajor}.${this.httpVersionMinor}` } get httpVersionMajor () { return this._url_request.httpVersionMajor } get httpVersionMinor () { return this._url_request.httpVersionMinor } get rawHeaders () { return this._url_request.rawResponseHeaders } _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 (async, ...rest) { if (async) { process.nextTick(() => { this._request.emit.apply(this._request, rest) }) } else { this._request.emit.apply(this._request, rest) } } URLRequest.prototype._emitResponseEvent = function (async, ...rest) { if (async) { process.nextTick(() => { this._request.emit.apply(this._request, rest) }) } else { this._response.emit.apply(this._response, rest) } } class ClientRequest extends EventEmitter { constructor (options, callback) { super() if (typeof options === 'string') { options = url.parse(options) } else { options = util._extend({}, options) } const method = (options.method || 'GET').toUpperCase() let urlStr = options.url if (!urlStr) { let 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 } } const path = options.path || '/' 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.') } urlObj.path = path urlStr = url.format(urlObj) } const sessionName = options.session || '' let urlRequest = new URLRequest({ method: method, url: urlStr, session: sessionName }) // Set back and forward links. this._url_request = urlRequest urlRequest._request = this if (options.headers) { const keys = Object.keys(options.headers) for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] 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._chunkedEncoding = false // 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._extra_headers = {} urlRequest.on('response', () => { const response = new IncomingMessage(urlRequest) urlRequest._response = response this.emit('response', response) }) if (callback) { this.once('response', callback) } } get chunkedEncoding () { return this._chunkedEncoding } set chunkedEncoding (value) { if (!this._url_request.notStarted) { throw new Error('Can\'t set the transfer encoding, headers have been sent.') } this._chunkedEncoding = value } setHeader (name, value) { if (typeof name !== 'string') { throw new TypeError('`name` should be a string in setHeader(name, value).') } if (value === undefined) { throw new Error('`value` required in setHeader("' + name + '", value).') } if (!this._url_request.notStarted) { throw new Error('Can\'t set headers after they are sent.') } const key = name.toLowerCase() this._extra_headers[key] = value this._url_request.setExtraHeader(name, value) } getHeader (name) { if (arguments.length < 1) { throw new Error('`name` is required for getHeader(name).') } if (!this._extra_headers) { return } const key = name.toLowerCase() return this._extra_headers[key] } removeHeader (name) { if (arguments.length < 1) { throw new Error('`name` is required for removeHeader(name).') } if (!this._url_request.notStarted) { throw new Error('Can\'t remove headers after they are sent.') } const key = name.toLowerCase() delete this._extra_headers[key] this._url_request.removeExtraHeader(name) } _write (chunk, encoding, callback, isLast) { let chunkIsString = typeof chunk === 'string' let 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) } // Headers are assumed to be sent on first call to _writeBuffer, // i.e. after the first call to write or end. let result = this._url_request.write(chunk, isLast) // 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._url_request.notStarted) { this._url_request.setChunkedUpload(this.chunkedEncoding) } // The write callback is fired asynchronously to mimic Node.js. if (callback) { process.nextTick(callback) } return result } write (data, encoding, callback) { if (this._url_request.finished) { let 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._url_request.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) } abort () { this._url_request.cancel() } } 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