diff --git a/docs/api/client-request.md b/docs/api/client-request.md index f5247883082d..262b24ab30d5 100644 --- a/docs/api/client-request.md +++ b/docs/api/client-request.md @@ -32,8 +32,8 @@ the hostname and the port number 'hostname:port'. * `redirect` String (optional) - The redirect mode for this request. Should be one of `follow`, `error` or `manual`. Defaults to `follow`. When mode is `error`, any redirection will be aborted. When mode is `manual` the redirection will be -deferred until [`request.followRedirect`](#requestfollowredirect) is invoked. Listen for the [`redirect`](#event-redirect) event in -this mode to get more details about the redirect request. +cancelled unless [`request.followRedirect`](#requestfollowredirect) is invoked +synchronously during the [`redirect`](#event-redirect) event. `options` properties such as `protocol`, `host`, `hostname`, `port` and `path` strictly follow the Node.js model as described in the @@ -136,8 +136,11 @@ Returns: * `redirectUrl` String * `responseHeaders` Record -Emitted when there is redirection and the mode is `manual`. Calling -[`request.followRedirect`](#requestfollowredirect) will continue with the redirection. +Emitted when the server returns a redirect response (e.g. 301 Moved +Permanently). Calling [`request.followRedirect`](#requestfollowredirect) will +continue with the redirection. If this event is handled, +[`request.followRedirect`](#requestfollowredirect) must be called +**synchronously**, otherwise the request will be cancelled. ### Instance Properties @@ -214,7 +217,8 @@ response object,it will emit the `aborted` event. #### `request.followRedirect()` -Continues any deferred redirection request when the redirection mode is `manual`. +Continues any pending redirection. Can only be called during a `'redirect'` +event. #### `request.getUploadProgress()` diff --git a/filenames.gni b/filenames.gni index a0515ea00c72..7490e615304f 100644 --- a/filenames.gni +++ b/filenames.gni @@ -98,8 +98,8 @@ filenames = { "shell/browser/api/atom_api_top_level_window.h", "shell/browser/api/atom_api_tray.cc", "shell/browser/api/atom_api_tray.h", - "shell/browser/api/atom_api_url_request.cc", - "shell/browser/api/atom_api_url_request.h", + "shell/browser/api/atom_api_url_loader.cc", + "shell/browser/api/atom_api_url_loader.h", "shell/browser/api/atom_api_view.cc", "shell/browser/api/atom_api_view.h", "shell/browser/api/atom_api_web_contents.cc", diff --git a/lib/browser/api/net.js b/lib/browser/api/net.js index 7168c1b512f0..fe5abe465fb9 100644 --- a/lib/browser/api/net.js +++ b/lib/browser/api/net.js @@ -2,17 +2,13 @@ const url = require('url') const { EventEmitter } = require('events') -const { Readable } = require('stream') +const { Readable, Writable } = require('stream') const { app } = require('electron') const { Session } = process.electronBinding('session') -const { net, Net } = process.electronBinding('net') -const { URLRequest } = net +const { net, Net, _isValidHeaderName, _isValidHeaderValue } = process.electronBinding('net') +const { URLLoader } = net -// Net is an EventEmitter. -Object.setPrototypeOf(Net.prototype, EventEmitter.prototype) -EventEmitter.call(net) - -Object.setPrototypeOf(URLRequest.prototype, EventEmitter.prototype) +Object.setPrototypeOf(URLLoader.prototype, EventEmitter.prototype) const kSupportedProtocols = new Set(['http:', 'https:']) @@ -40,32 +36,24 @@ const discardableDuplicateHeaders = new Set([ ]) class IncomingMessage extends Readable { - constructor (urlRequest) { + constructor (responseHead) { 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() - }) + this._shouldPush = false + this._data = [] + this._responseHead = responseHead } get statusCode () { - return this.urlRequest.statusCode + return this._responseHead.statusCode } get statusMessage () { - return this.urlRequest.statusMessage + return this._responseHead.statusMessage } get headers () { const filteredHeaders = {} - const rawHeaders = this.urlRequest.rawResponseHeaders + const rawHeaders = this._responseHead.headers Object.keys(rawHeaders).forEach(header => { if (header in filteredHeaders && discardableDuplicateHeaders.has(header)) { // do nothing with discardable duplicate headers @@ -88,11 +76,11 @@ class IncomingMessage extends Readable { } get httpVersionMajor () { - return this.urlRequest.httpVersionMajor + return this._responseHead.httpVersion.major } get httpVersionMinor () { - return this.urlRequest.httpVersionMinor + return this._responseHead.httpVersion.minor } get rawTrailers () { @@ -104,170 +92,197 @@ class IncomingMessage extends Readable { } _storeInternalData (chunk) { - this.data.push(chunk) + this._data.push(chunk) + this._pushInternalData() } _pushInternalData () { - while (this.shouldPush && this.data.length > 0) { - const chunk = this.data.shift() - this.shouldPush = this.push(chunk) + while (this._shouldPush && this._data.length > 0) { + const chunk = this._data.shift() + this._shouldPush = this.push(chunk) } } _read () { - this.shouldPush = true + 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) { +/** Writable stream that buffers up everything written to it. */ +class SlurpStream extends Writable { + constructor () { super() + this._data = Buffer.alloc(0) + } + _write (chunk, encoding, callback) { + this._data = Buffer.concat([this._data, chunk]) + callback() + } + data () { return this._data } +} + +class ChunkedBodyStream extends Writable { + constructor (clientRequest) { + super() + this._clientRequest = clientRequest + } + + _write (chunk, encoding, callback) { + if (this._downstream) { + this._downstream.write(chunk).then(callback, callback) + } else { + // the contract of _write is that we won't be called again until we call + // the callback, so we're good to just save a single chunk. + this._pendingChunk = chunk + this._pendingCallback = callback + + // The first write to a chunked body stream begins the request. + this._clientRequest._startRequest() + } + } + + _final (callback) { + this._downstream.done() + callback() + } + + startReading (pipe) { + if (this._downstream) { + throw new Error('two startReading calls???') + } + this._downstream = pipe + if (this._pendingChunk) { + const doneWriting = (maybeError) => { + const cb = this._pendingCallback + delete this._pendingCallback + delete this._pendingChunk + cb(maybeError) + } + this._downstream.write(this._pendingChunk).then(doneWriting, doneWriting) + } + } +} + +function parseOptions (options) { + if (typeof options === 'string') { + options = url.parse(options) + } else { + options = { ...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') + } + + if (options.headers != null && typeof options.headers !== 'object') { + throw new TypeError('headers must be an object') + } + + const urlLoaderOptions = { + method: method, + url: urlStr, + redirectPolicy, + extraHeaders: options.headers || {} + } + for (const [name, value] of Object.entries(urlLoaderOptions.extraHeaders)) { + if (!_isValidHeaderName(name)) { + throw new Error(`Invalid header name: '${name}'`) + } + if (!_isValidHeaderValue(value.toString())) { + throw new Error(`Invalid value for header '${name}': '${value}'`) + } + } + if (options.session) { + if (options.session instanceof Session) { + urlLoaderOptions.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') { + urlLoaderOptions.partition = options.partition + } else { + throw new TypeError('`partition` should be a string') + } + } + return urlLoaderOptions +} + +class ClientRequest extends Writable { + constructor (options, callback) { + super({ autoDestroy: true }) 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 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) => { - const handled = this.emit('login', authInfo, callback) - if (!handled) { - // If there were no listeners, cancel the authentication request. - callback() - } - }) - if (callback) { this.once('response', callback) } - } - get chunkedEncoding () { - return this.chunkedEncodingEnabled + const { redirectPolicy, ...urlLoaderOptions } = parseOptions(options) + this._urlLoaderOptions = urlLoaderOptions + this._redirectPolicy = redirectPolicy + this._started = false } set chunkedEncoding (value) { - if (!this.urlRequest.notStarted) { - throw new Error('Can\'t set the transfer encoding, headers have been sent') + if (this._started) { + throw new Error('chunkedEncoding can only be set before the request is started') + } + if (typeof this._chunkedEncoding !== 'undefined') { + throw new Error('chunkedEncoding can only be set once') + } + this._chunkedEncoding = !!value + if (this._chunkedEncoding) { + this._body = new ChunkedBodyStream(this) + this._urlLoaderOptions.body = (pipe) => { + this._body.startReading(pipe) + } } - this.chunkedEncodingEnabled = value } setHeader (name, value) { @@ -277,13 +292,18 @@ class ClientRequest extends EventEmitter { if (value == null) { throw new Error('`value` required in setHeader("' + name + '", value)') } - if (!this.urlRequest.notStarted) { + if (this._started || this._firstWrite) { throw new Error('Can\'t set headers after they are sent') } + if (!_isValidHeaderName(name)) { + throw new Error(`Invalid header name: '${name}'`) + } + if (!_isValidHeaderValue(value.toString())) { + throw new Error(`Invalid value for header '${name}': '${value}'`) + } const key = name.toLowerCase() - this.extraHeaders[key] = value - this.urlRequest.setExtraHeader(name, value.toString()) + this._urlLoaderOptions.extraHeaders[key] = value } getHeader (name) { @@ -291,12 +311,8 @@ class ClientRequest extends EventEmitter { throw new Error('`name` is required for getHeader(name)') } - if (!this.extraHeaders) { - return - } - const key = name.toLowerCase() - return this.extraHeaders[key] + return this._urlLoaderOptions.extraHeaders[key] } removeHeader (name) { @@ -304,93 +320,144 @@ class ClientRequest extends EventEmitter { throw new Error('`name` is required for removeHeader(name)') } - if (!this.urlRequest.notStarted) { + if (this._started || this._firstWrite) { throw new Error('Can\'t remove headers after they are sent') } const key = name.toLowerCase() - delete this.extraHeaders[key] - this.urlRequest.removeExtraHeader(name) + delete this._urlLoaderOptions.extraHeaders[key] } - _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') + _write (chunk, encoding, callback) { + this._firstWrite = true + if (!this._body) { + this._body = new SlurpStream() + this._body.on('finish', () => { + this._urlLoaderOptions.body = this._body.data() + this._startRequest() + }) } - - 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 + // TODO: is this the right way to forward to another stream? + this._body.write(chunk, encoding, callback) } - write (data, encoding, callback) { - if (this.urlRequest.finished) { - const error = new Error('Write after end') - process.nextTick(writeAfterEndNT, this, error, callback) - return true + _final (callback) { + if (this._body) { + // TODO: is this the right way to forward to another stream? + this._body.end(callback) + } else { + // end() called without a body, go ahead and start the request + this._startRequest() + callback() } - - return this._write(data, encoding, callback, false) } - end (data, encoding, callback) { - if (this.urlRequest.finished) { - return false + _startRequest () { + this._started = true + const stringifyValues = (obj) => { + const ret = {} + for (const k in obj) { + ret[k] = obj[k].toString() + } + return ret } + const opts = { ...this._urlLoaderOptions, extraHeaders: stringifyValues(this._urlLoaderOptions.extraHeaders) } + this._urlLoader = new URLLoader(opts) + this._urlLoader.on('response-started', (event, finalUrl, responseHead) => { + const response = this._response = new IncomingMessage(responseHead) + this.emit('response', response) + }) + this._urlLoader.on('data', (event, data) => { + this._response._storeInternalData(Buffer.from(data)) + }) + this._urlLoader.on('complete', () => { + if (this._response) { this._response._storeInternalData(null) } + }) + this._urlLoader.on('error', (event, netErrorString) => { + const error = new Error(netErrorString) + if (this._response) this._response.destroy(error) + this._die(error) + }) - if (typeof data === 'function') { - callback = data - encoding = null - data = null - } else if (typeof encoding === 'function') { - callback = encoding - encoding = null - } + this._urlLoader.on('login', (event, authInfo, callback) => { + const handled = this.emit('login', authInfo, callback) + if (!handled) { + // If there were no listeners, cancel the authentication request. + callback() + } + }) - data = data || '' + this._urlLoader.on('redirect', (event, redirectInfo, headers) => { + const { statusCode, newMethod, newUrl } = redirectInfo + if (this._redirectPolicy === 'error') { + this._die(new Error(`Attempted to redirect, but redirect policy was 'error'`)) + } else if (this._redirectPolicy === 'manual') { + let _followRedirect = false + this._followRedirectCb = () => { _followRedirect = true } + try { + this.emit('redirect', statusCode, newMethod, newUrl, headers) + } finally { + this._followRedirectCb = null + if (!_followRedirect) { + this._die(new Error('Redirect was cancelled')) + } + } + } else if (this._redirectPolicy === 'follow') { + // Calling followRedirect() when the redirect policy is 'follow' is + // allowed but does nothing. (Perhaps it should throw an error + // though...? Since the redirect will happen regardless.) + try { + this._followRedirectCb = () => {} + this.emit('redirect', statusCode, newMethod, newUrl, headers) + } finally { + this._followRedirectCb = null + } + } else { + this._die(new Error(`Unexpected redirect policy '${this._redirectPolicy}'`)) + } + }) - return this._write(data, encoding, callback, true) + this._urlLoader.on('upload-progress', (event, position, total) => { + this._uploadProgress = { active: true, started: true, current: position, total } + this.emit('upload-progress', position, total) // Undocumented, for now + }) + + this._urlLoader.on('download-progress', (event, current) => { + if (this._response) { + this._response.emit('download-progress', current) // Undocumented, for now + } + }) } followRedirect () { - this.urlRequest.followRedirect() + if (this._followRedirectCb) { + this._followRedirectCb() + } else { + throw new Error('followRedirect() called, but was not waiting for a redirect') + } } abort () { - this.urlRequest.cancel() + if (!this._aborted) { + process.nextTick(() => { this.emit('abort') }) + } + this._aborted = true + this._die() + } + + _die (err) { + this.destroy(err) + if (this._urlLoader) { + this._urlLoader.cancel() + if (this._response) this._response.destroy(err) + } } getUploadProgress () { - return this.urlRequest.getUploadProgress() + return { ...this._uploadProgress } || { active: false } } } -function writeAfterEndNT (self, error, callback) { - self.emit('error', error) - if (callback) callback(error) -} - Net.prototype.request = function (options, callback) { return new ClientRequest(options, callback) } diff --git a/shell/browser/api/atom_api_net.cc b/shell/browser/api/atom_api_net.cc index 1e30d04277a7..d3b4dbe0b335 100644 --- a/shell/browser/api/atom_api_net.cc +++ b/shell/browser/api/atom_api_net.cc @@ -4,9 +4,11 @@ #include "shell/browser/api/atom_api_net.h" +#include + #include "gin/handle.h" #include "services/network/public/cpp/features.h" -#include "shell/browser/api/atom_api_url_request.h" +#include "shell/browser/api/atom_api_url_loader.h" #include "shell/common/gin_helper/dictionary.h" #include "shell/common/gin_helper/object_template_builder.h" @@ -32,12 +34,12 @@ void Net::BuildPrototype(v8::Isolate* isolate, v8::Local prototype) { prototype->SetClassName(gin::StringToV8(isolate, "Net")); gin_helper::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate()) - .SetProperty("URLRequest", &Net::URLRequest); + .SetProperty("URLLoader", &Net::URLLoader); } -v8::Local Net::URLRequest(v8::Isolate* isolate) { +v8::Local Net::URLLoader(v8::Isolate* isolate) { v8::Local constructor; - constructor = URLRequest::GetConstructor(isolate); + constructor = SimpleURLLoaderWrapper::GetConstructor(isolate); return constructor->GetFunction(isolate->GetCurrentContext()) .ToLocalChecked(); } @@ -48,8 +50,16 @@ v8::Local Net::URLRequest(v8::Isolate* isolate) { namespace { +bool IsValidHeaderName(std::string header_name) { + return net::HttpUtil::IsValidHeaderName(header_name); +} + +bool IsValidHeaderValue(std::string header_value) { + return net::HttpUtil::IsValidHeaderValue(header_value); +} + using electron::api::Net; -using electron::api::URLRequest; +using electron::api::SimpleURLLoaderWrapper; void Initialize(v8::Local exports, v8::Local unused, @@ -57,12 +67,15 @@ void Initialize(v8::Local exports, void* priv) { v8::Isolate* isolate = context->GetIsolate(); - URLRequest::SetConstructor(isolate, base::BindRepeating(URLRequest::New)); + SimpleURLLoaderWrapper::SetConstructor( + isolate, base::BindRepeating(SimpleURLLoaderWrapper::New)); gin_helper::Dictionary dict(isolate, exports); dict.Set("net", Net::Create(isolate)); dict.Set("Net", Net::GetConstructor(isolate)->GetFunction(context).ToLocalChecked()); + dict.SetMethod("_isValidHeaderName", &IsValidHeaderName); + dict.SetMethod("_isValidHeaderValue", &IsValidHeaderValue); } } // namespace diff --git a/shell/browser/api/atom_api_net.h b/shell/browser/api/atom_api_net.h index 5a7d916dd729..2b9098a48b3d 100644 --- a/shell/browser/api/atom_api_net.h +++ b/shell/browser/api/atom_api_net.h @@ -18,7 +18,7 @@ class Net : public mate::Wrappable { static void BuildPrototype(v8::Isolate* isolate, v8::Local prototype); - v8::Local URLRequest(v8::Isolate* isolate); + v8::Local URLLoader(v8::Isolate* isolate); protected: explicit Net(v8::Isolate* isolate); diff --git a/shell/browser/api/atom_api_url_loader.cc b/shell/browser/api/atom_api_url_loader.cc new file mode 100644 index 000000000000..966c5d5a3885 --- /dev/null +++ b/shell/browser/api/atom_api_url_loader.cc @@ -0,0 +1,448 @@ +// Copyright (c) 2019 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/api/atom_api_url_loader.h" + +#include +#include +#include +#include +#include +#include + +#include "base/containers/id_map.h" +#include "gin/handle.h" +#include "gin/object_template_builder.h" +#include "gin/wrappable.h" +#include "mojo/public/cpp/system/data_pipe_producer.h" +#include "services/network/public/cpp/resource_request.h" +#include "services/network/public/cpp/simple_url_loader.h" +#include "services/network/public/mojom/chunked_data_pipe_getter.mojom.h" +#include "services/network/public/mojom/url_loader_factory.mojom.h" +#include "shell/browser/api/atom_api_session.h" +#include "shell/browser/atom_browser_context.h" +#include "shell/common/gin_converters/callback_converter.h" +#include "shell/common/gin_converters/gurl_converter.h" +#include "shell/common/gin_converters/net_converter.h" +#include "shell/common/gin_helper/dictionary.h" +#include "shell/common/gin_helper/object_template_builder.h" +#include "shell/common/node_includes.h" + +class BufferDataSource : public mojo::DataPipeProducer::DataSource { + public: + explicit BufferDataSource(base::span buffer) { + buffer_.resize(buffer.size()); + memcpy(buffer_.data(), buffer.data(), buffer_.size()); + } + ~BufferDataSource() override = default; + + private: + // mojo::DataPipeProducer::DataSource: + uint64_t GetLength() const override { return buffer_.size(); } + ReadResult Read(uint64_t offset, base::span buffer) override { + ReadResult result; + if (offset <= buffer_.size()) { + size_t readable_size = buffer_.size() - offset; + size_t writable_size = buffer.size(); + size_t copyable_size = std::min(readable_size, writable_size); + memcpy(buffer.data(), &buffer_[offset], copyable_size); + result.bytes_read = copyable_size; + } else { + NOTREACHED(); + result.result = MOJO_RESULT_OUT_OF_RANGE; + } + return result; + } + + std::vector buffer_; +}; + +class JSChunkedDataPipeGetter : public gin::Wrappable, + public network::mojom::ChunkedDataPipeGetter { + public: + static gin::Handle Create( + v8::Isolate* isolate, + v8::Local body_func, + mojo::PendingReceiver + chunked_data_pipe_getter) { + return gin::CreateHandle( + isolate, new JSChunkedDataPipeGetter( + isolate, body_func, std::move(chunked_data_pipe_getter))); + } + + // gin::Wrappable + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) override { + return gin::Wrappable::GetObjectTemplateBuilder( + isolate) + .SetMethod("write", &JSChunkedDataPipeGetter::WriteChunk) + .SetMethod("done", &JSChunkedDataPipeGetter::Done); + } + + static gin::WrapperInfo kWrapperInfo; + ~JSChunkedDataPipeGetter() override = default; + + private: + JSChunkedDataPipeGetter( + v8::Isolate* isolate, + v8::Local body_func, + mojo::PendingReceiver + chunked_data_pipe_getter) + : isolate_(isolate), body_func_(isolate, body_func) { + receiver_.Bind(std::move(chunked_data_pipe_getter)); + } + + // network::mojom::ChunkedDataPipeGetter: + void GetSize(GetSizeCallback callback) override { + size_callback_ = std::move(callback); + } + + void StartReading(mojo::ScopedDataPipeProducerHandle pipe) override { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + + if (body_func_.IsEmpty()) { + LOG(ERROR) << "Tried to read twice from a JSChunkedDataPipeGetter"; + // Drop the handle on the floor. + return; + } + data_producer_ = std::make_unique(std::move(pipe)); + + v8::HandleScope handle_scope(isolate_); + v8::MicrotasksScope script_scope(isolate_, + v8::MicrotasksScope::kRunMicrotasks); + auto maybe_wrapper = GetWrapper(isolate_); + v8::Local wrapper; + if (!maybe_wrapper.ToLocal(&wrapper)) { + return; + } + v8::Local argv[] = {wrapper}; + node::Environment* env = node::Environment::GetCurrent(isolate_); + auto global = env->context()->Global(); + node::MakeCallback(isolate_, global, body_func_.Get(isolate_), + node::arraysize(argv), argv, {0, 0}); + } + + v8::Local WriteChunk(v8::Local buffer_val) { + gin_helper::Promise promise(isolate_); + v8::Local handle = promise.GetHandle(); + if (!buffer_val->IsArrayBufferView()) { + promise.RejectWithErrorMessage("Expected an ArrayBufferView"); + return handle; + } + if (is_writing_) { + promise.RejectWithErrorMessage("Only one write can be pending at a time"); + return handle; + } + if (!size_callback_) { + promise.RejectWithErrorMessage("Can't write after calling done()"); + return handle; + } + auto buffer = buffer_val.As(); + is_writing_ = true; + bytes_written_ += buffer->ByteLength(); + auto backing_store = buffer->Buffer()->GetBackingStore(); + auto buffer_span = base::make_span( + static_cast(backing_store->Data()) + buffer->ByteOffset(), + buffer->ByteLength()); + auto buffer_source = std::make_unique(buffer_span); + data_producer_->Write( + std::move(buffer_source), + base::BindOnce(&JSChunkedDataPipeGetter::OnWriteChunkComplete, + // We're OK to use Unretained here because we own + // |data_producer_|. + base::Unretained(this), std::move(promise))); + return handle; + } + + void OnWriteChunkComplete(gin_helper::Promise promise, + MojoResult result) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + is_writing_ = false; + if (result == MOJO_RESULT_OK) { + promise.Resolve(); + } else { + promise.RejectWithErrorMessage("mojo result not ok"); + Finished(); + } + } + + // TODO(nornagon): accept a net error here to allow the data provider to + // cancel the request with an error. + void Done() { + if (size_callback_) { + std::move(size_callback_).Run(net::OK, bytes_written_); + Finished(); + } + } + + void Finished() { + size_callback_.Reset(); + body_func_.Reset(); + receiver_.reset(); + data_producer_.reset(); + } + + GetSizeCallback size_callback_; + mojo::Receiver receiver_{this}; + std::unique_ptr data_producer_; + bool is_writing_ = false; + uint64_t bytes_written_ = 0; + + v8::Isolate* isolate_; + v8::Global body_func_; +}; + +gin::WrapperInfo JSChunkedDataPipeGetter::kWrapperInfo = { + gin::kEmbedderNativeGin}; + +namespace electron { + +namespace api { + +namespace { + +const net::NetworkTrafficAnnotationTag kTrafficAnnotation = + net::DefineNetworkTrafficAnnotation("electron_net_module", R"( + semantics { + sender: "Electron Net module" + description: + "Issue HTTP/HTTPS requests using Chromium's native networking " + "library." + trigger: "Using the Net module" + data: "Anything the user wants to send." + destination: OTHER + } + policy { + cookies_allowed: YES + cookies_store: "user" + setting: "This feature cannot be disabled." + })"); + +base::IDMap& GetAllRequests() { + static base::NoDestructor> + s_all_requests; + return *s_all_requests; +} + +} // namespace + +SimpleURLLoaderWrapper::SimpleURLLoaderWrapper( + std::unique_ptr request, + network::mojom::URLLoaderFactory* url_loader_factory) + : id_(GetAllRequests().Add(this)) { + // We slightly abuse the |render_frame_id| field in ResourceRequest so that + // we can correlate any authentication events that arrive with this request. + request->render_frame_id = id_; + + // SimpleURLLoader wants to control the request body itself. We have other + // ideas. + auto request_body = std::move(request->request_body); + auto* request_ref = request.get(); + loader_ = + network::SimpleURLLoader::Create(std::move(request), kTrafficAnnotation); + if (request_body) { + request_ref->request_body = std::move(request_body); + } + + loader_->SetAllowHttpErrorResults(true); + loader_->SetOnResponseStartedCallback(base::BindOnce( + &SimpleURLLoaderWrapper::OnResponseStarted, base::Unretained(this))); + loader_->SetOnRedirectCallback(base::BindRepeating( + &SimpleURLLoaderWrapper::OnRedirect, base::Unretained(this))); + loader_->SetOnUploadProgressCallback(base::BindRepeating( + &SimpleURLLoaderWrapper::OnUploadProgress, base::Unretained(this))); + loader_->SetOnDownloadProgressCallback(base::BindRepeating( + &SimpleURLLoaderWrapper::OnDownloadProgress, base::Unretained(this))); + + loader_->DownloadAsStream(url_loader_factory, this); +} + +void SimpleURLLoaderWrapper::Pin() { + // Prevent ourselves from being GC'd until the request is complete. + // Must be called after InitWithArgs(), otherwise GetWrapper() isn't + // initialized. + pinned_wrapper_.Reset(isolate(), GetWrapper()); +} + +void SimpleURLLoaderWrapper::PinBodyGetter(v8::Local body_getter) { + pinned_chunk_pipe_getter_.Reset(isolate(), body_getter); +} + +SimpleURLLoaderWrapper::~SimpleURLLoaderWrapper() { + GetAllRequests().Remove(id_); +} + +// static +SimpleURLLoaderWrapper* SimpleURLLoaderWrapper::FromID(uint32_t id) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + return GetAllRequests().Lookup(id); +} + +void SimpleURLLoaderWrapper::OnAuthRequired( + const GURL& url, + bool first_auth_attempt, + net::AuthChallengeInfo auth_info, + network::mojom::URLResponseHeadPtr head, + mojo::PendingRemote + auth_challenge_responder) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + mojo::Remote auth_responder( + std::move(auth_challenge_responder)); + // WeakPtr because if we're Cancel()ed while waiting for auth, and the + // network service also decides to cancel at the same time and kill this + // pipe, we might end up trying to call Cancel again on dead memory. + auth_responder.set_disconnect_handler(base::BindOnce( + &SimpleURLLoaderWrapper::Cancel, weak_factory_.GetWeakPtr())); + auto cb = base::BindOnce( + [](mojo::Remote auth_responder, + gin::Arguments* args) { + base::string16 username_str, password_str; + if (!args->GetNext(&username_str) || !args->GetNext(&password_str)) { + auth_responder->OnAuthCredentials(base::nullopt); + return; + } + auth_responder->OnAuthCredentials( + net::AuthCredentials(username_str, password_str)); + }, + std::move(auth_responder)); + Emit("login", auth_info, base::AdaptCallbackForRepeating(std::move(cb))); +} + +void SimpleURLLoaderWrapper::Cancel() { + loader_.reset(); + pinned_wrapper_.Reset(); + pinned_chunk_pipe_getter_.Reset(); + // This ensures that no further callbacks will be called, so there's no need + // for additional guards. +} + +// static +mate::WrappableBase* SimpleURLLoaderWrapper::New(gin::Arguments* args) { + gin_helper::Dictionary opts; + if (!args->GetNext(&opts)) { + args->ThrowTypeError("Expected a dictionary"); + return nullptr; + } + auto request = std::make_unique(); + opts.Get("method", &request->method); + opts.Get("url", &request->url); + std::map extra_headers; + if (opts.Get("extraHeaders", &extra_headers)) { + for (const auto& it : extra_headers) { + if (!net::HttpUtil::IsValidHeaderName(it.first) || + !net::HttpUtil::IsValidHeaderValue(it.second)) { + args->ThrowTypeError("Invalid header name or value"); + return nullptr; + } + request->headers.SetHeader(it.first, it.second); + } + } + + v8::Local body; + v8::Local chunk_pipe_getter; + if (opts.Get("body", &body)) { + if (body->IsArrayBufferView()) { + auto buffer_body = body.As(); + auto backing_store = buffer_body->Buffer()->GetBackingStore(); + request->request_body = network::ResourceRequestBody::CreateFromBytes( + static_cast(backing_store->Data()) + buffer_body->ByteOffset(), + buffer_body->ByteLength()); + } else if (body->IsFunction()) { + auto body_func = body.As(); + + mojo::PendingRemote + data_pipe_getter; + chunk_pipe_getter = JSChunkedDataPipeGetter::Create( + args->isolate(), body_func, + data_pipe_getter.InitWithNewPipeAndPassReceiver()) + .ToV8(); + request->request_body = new network::ResourceRequestBody(); + request->request_body->SetToChunkedDataPipe(std::move(data_pipe_getter)); + } + } + + std::string partition; + gin::Handle session; + if (!opts.Get("session", &session)) { + if (opts.Get("partition", &partition)) + session = Session::FromPartition(args->isolate(), partition); + else // default session + session = Session::FromPartition(args->isolate(), ""); + } + + auto url_loader_factory = session->browser_context()->GetURLLoaderFactory(); + + auto* ret = + new SimpleURLLoaderWrapper(std::move(request), url_loader_factory.get()); + ret->InitWithArgs(args); + ret->Pin(); + if (!chunk_pipe_getter.IsEmpty()) { + ret->PinBodyGetter(chunk_pipe_getter); + } + return ret; +} + +void SimpleURLLoaderWrapper::OnDataReceived(base::StringPiece string_piece, + base::OnceClosure resume) { + DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + v8::HandleScope handle_scope(isolate()); + auto array_buffer = v8::ArrayBuffer::New(isolate(), string_piece.size()); + auto backing_store = array_buffer->GetBackingStore(); + memcpy(backing_store->Data(), string_piece.data(), string_piece.size()); + Emit("data", array_buffer); + std::move(resume).Run(); +} + +void SimpleURLLoaderWrapper::OnComplete(bool success) { + if (success) { + Emit("complete"); + } else { + Emit("error", net::ErrorToString(loader_->NetError())); + } + loader_.reset(); + pinned_wrapper_.Reset(); + pinned_chunk_pipe_getter_.Reset(); +} + +void SimpleURLLoaderWrapper::OnRetry(base::OnceClosure start_retry) {} + +void SimpleURLLoaderWrapper::OnResponseStarted( + const GURL& final_url, + const network::mojom::URLResponseHead& response_head) { + gin::Dictionary dict = gin::Dictionary::CreateEmpty(isolate()); + dict.Set("statusCode", response_head.headers->response_code()); + dict.Set("statusMessage", response_head.headers->GetStatusText()); + dict.Set("headers", response_head.headers.get()); + dict.Set("httpVersion", response_head.headers->GetHttpVersion()); + Emit("response-started", final_url, dict); +} + +void SimpleURLLoaderWrapper::OnRedirect( + const net::RedirectInfo& redirect_info, + const network::mojom::URLResponseHead& response_head, + std::vector* removed_headers) { + Emit("redirect", redirect_info, response_head.headers.get()); +} + +void SimpleURLLoaderWrapper::OnUploadProgress(uint64_t position, + uint64_t total) { + Emit("upload-progress", position, total); +} + +void SimpleURLLoaderWrapper::OnDownloadProgress(uint64_t current) { + Emit("download-progress", current); +} + +// static +void SimpleURLLoaderWrapper::BuildPrototype( + v8::Isolate* isolate, + v8::Local prototype) { + prototype->SetClassName(gin::StringToV8(isolate, "SimpleURLLoaderWrapper")); + gin_helper::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate()) + .SetMethod("cancel", &SimpleURLLoaderWrapper::Cancel); +} + +} // namespace api + +} // namespace electron diff --git a/shell/browser/api/atom_api_url_loader.h b/shell/browser/api/atom_api_url_loader.h new file mode 100644 index 000000000000..2236c318ef6a --- /dev/null +++ b/shell/browser/api/atom_api_url_loader.h @@ -0,0 +1,93 @@ +// Copyright (c) 2019 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_API_ATOM_API_URL_LOADER_H_ +#define SHELL_BROWSER_API_ATOM_API_URL_LOADER_H_ + +#include +#include +#include + +#include "base/memory/weak_ptr.h" +#include "net/base/auth.h" +#include "services/network/public/cpp/simple_url_loader_stream_consumer.h" +#include "services/network/public/mojom/network_context.mojom.h" +#include "services/network/public/mojom/url_loader_factory.mojom-forward.h" +#include "services/network/public/mojom/url_response_head.mojom.h" +#include "shell/common/gin_helper/event_emitter.h" +#include "url/gurl.h" +#include "v8/include/v8.h" + +namespace gin { +class Arguments; +} + +namespace network { +class SimpleURLLoader; +struct ResourceRequest; +} // namespace network + +namespace electron { + +namespace api { + +/** Wraps a SimpleURLLoader to make it usable from JavaScript */ +class SimpleURLLoaderWrapper + : public gin_helper::EventEmitter, + public network::SimpleURLLoaderStreamConsumer { + public: + ~SimpleURLLoaderWrapper() override; + static mate::WrappableBase* New(gin::Arguments* args); + + static void BuildPrototype(v8::Isolate* isolate, + v8::Local prototype); + + static SimpleURLLoaderWrapper* FromID(uint32_t id); + + void OnAuthRequired( + const GURL& url, + bool first_auth_attempt, + net::AuthChallengeInfo auth_info, + network::mojom::URLResponseHeadPtr head, + mojo::PendingRemote + auth_challenge_responder); + + void Cancel(); + + private: + SimpleURLLoaderWrapper(std::unique_ptr loader, + network::mojom::URLLoaderFactory* url_loader_factory); + + // SimpleURLLoaderStreamConsumer: + void OnDataReceived(base::StringPiece string_piece, + base::OnceClosure resume) override; + void OnComplete(bool success) override; + void OnRetry(base::OnceClosure start_retry) override; + + // SimpleURLLoader callbacks + void OnResponseStarted(const GURL& final_url, + const network::mojom::URLResponseHead& response_head); + void OnRedirect(const net::RedirectInfo& redirect_info, + const network::mojom::URLResponseHead& response_head, + std::vector* removed_headers); + void OnUploadProgress(uint64_t position, uint64_t total); + void OnDownloadProgress(uint64_t current); + + void Start(); + void Pin(); + void PinBodyGetter(v8::Local); + + uint32_t id_; + std::unique_ptr loader_; + v8::Global pinned_wrapper_; + v8::Global pinned_chunk_pipe_getter_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace api + +} // namespace electron + +#endif // SHELL_BROWSER_API_ATOM_API_URL_LOADER_H_ diff --git a/shell/browser/api/atom_api_url_request.cc b/shell/browser/api/atom_api_url_request.cc deleted file mode 100644 index a50f6968bd1f..000000000000 --- a/shell/browser/api/atom_api_url_request.cc +++ /dev/null @@ -1,597 +0,0 @@ -// Copyright (c) 2019 GitHub, Inc. -// Use of this source code is governed by the MIT license that can be -// found in the LICENSE file. - -#include "shell/browser/api/atom_api_url_request.h" - -#include - -#include "base/containers/id_map.h" -#include "gin/handle.h" -#include "mojo/public/cpp/bindings/receiver_set.h" -#include "mojo/public/cpp/system/string_data_source.h" -#include "net/http/http_util.h" -#include "services/network/public/mojom/chunked_data_pipe_getter.mojom.h" -#include "shell/browser/api/atom_api_session.h" -#include "shell/browser/atom_browser_context.h" -#include "shell/common/gin_converters/callback_converter.h" -#include "shell/common/gin_converters/gurl_converter.h" -#include "shell/common/gin_converters/net_converter.h" -#include "shell/common/gin_helper/dictionary.h" -#include "shell/common/gin_helper/event_emitter_caller.h" -#include "shell/common/gin_helper/object_template_builder.h" - -#include "shell/common/node_includes.h" - -namespace gin { - -template <> -struct Converter { - static bool FromV8(v8::Isolate* isolate, - v8::Local val, - network::mojom::RedirectMode* out) { - std::string mode; - if (!ConvertFromV8(isolate, val, &mode)) - return false; - if (mode == "follow") - *out = network::mojom::RedirectMode::kFollow; - else if (mode == "error") - *out = network::mojom::RedirectMode::kError; - else if (mode == "manual") - *out = network::mojom::RedirectMode::kManual; - else - return false; - return true; - } -}; - -} // namespace gin - -namespace electron { - -namespace api { - -namespace { - -base::IDMap& GetAllRequests() { - static base::NoDestructor> s_all_requests; - return *s_all_requests; -} - -// Network state for request and response. -enum State { - STATE_STARTED = 1 << 0, - STATE_FINISHED = 1 << 1, - STATE_CANCELED = 1 << 2, - STATE_FAILED = 1 << 3, - STATE_CLOSED = 1 << 4, - STATE_ERROR = STATE_CANCELED | STATE_FAILED | STATE_CLOSED, -}; - -// Annotation tag passed to NetworkService. -const net::NetworkTrafficAnnotationTag kTrafficAnnotation = - net::DefineNetworkTrafficAnnotation("electron_net_module", R"( - semantics { - sender: "Electron Net module" - description: - "Issue HTTP/HTTPS requests using Chromium's native networking " - "library." - trigger: "Using the Net module" - data: "Anything the user wants to send." - destination: OTHER - } - policy { - cookies_allowed: YES - cookies_store: "user" - setting: "This feature cannot be disabled." - })"); - -} // namespace - -// Common class for streaming data. -class UploadDataPipeGetter { - public: - explicit UploadDataPipeGetter(URLRequest* request) : request_(request) {} - virtual ~UploadDataPipeGetter() = default; - - virtual void AttachToRequestBody(network::ResourceRequestBody* body) = 0; - - protected: - void SetCallback(network::mojom::DataPipeGetter::ReadCallback callback) { - request_->size_callback_ = std::move(callback); - } - - void SetPipe(mojo::ScopedDataPipeProducerHandle pipe) { - request_->producer_ = - std::make_unique(std::move(pipe)); - request_->StartWriting(); - } - - private: - URLRequest* request_; - - DISALLOW_COPY_AND_ASSIGN(UploadDataPipeGetter); -}; - -// Streaming multipart data to NetworkService. -class MultipartDataPipeGetter : public UploadDataPipeGetter, - public network::mojom::DataPipeGetter { - public: - explicit MultipartDataPipeGetter(URLRequest* request) - : UploadDataPipeGetter(request) {} - ~MultipartDataPipeGetter() override = default; - - void AttachToRequestBody(network::ResourceRequestBody* body) override { - mojo::PendingRemote data_pipe_getter_remote; - receivers_.Add(this, - data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); - body->AppendDataPipe(std::move(data_pipe_getter_remote)); - } - - private: - // network::mojom::DataPipeGetter: - void Read(mojo::ScopedDataPipeProducerHandle pipe, - ReadCallback callback) override { - SetCallback(std::move(callback)); - SetPipe(std::move(pipe)); - } - - void Clone( - mojo::PendingReceiver receiver) override { - receivers_.Add(this, std::move(receiver)); - } - - mojo::ReceiverSet receivers_; -}; - -// Streaming chunked data to NetworkService. -class ChunkedDataPipeGetter : public UploadDataPipeGetter, - public network::mojom::ChunkedDataPipeGetter { - public: - explicit ChunkedDataPipeGetter(URLRequest* request) - : UploadDataPipeGetter(request) {} - ~ChunkedDataPipeGetter() override = default; - - void AttachToRequestBody(network::ResourceRequestBody* body) override { - mojo::PendingRemote - data_pipe_getter_remote; - receiver_set_.Add(this, - data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); - body->SetToChunkedDataPipe(std::move(data_pipe_getter_remote)); - } - - private: - // network::mojom::ChunkedDataPipeGetter: - void GetSize(GetSizeCallback callback) override { - SetCallback(std::move(callback)); - } - - void StartReading(mojo::ScopedDataPipeProducerHandle pipe) override { - SetPipe(std::move(pipe)); - } - - mojo::ReceiverSet receiver_set_; -}; - -URLRequest::URLRequest(gin::Arguments* args) - : id_(GetAllRequests().Add(this)), weak_factory_(this) { - DCHECK_CURRENTLY_ON(content::BrowserThread::UI); - request_ = std::make_unique(); - gin_helper::Dictionary dict; - if (args->GetNext(&dict)) { - dict.Get("method", &request_->method); - dict.Get("url", &request_->url); - dict.Get("redirect", &redirect_mode_); - request_->redirect_mode = redirect_mode_; - } - - request_->render_frame_id = id_; - - std::string partition; - gin::Handle session; - if (!dict.Get("session", &session)) { - if (dict.Get("partition", &partition)) - session = Session::FromPartition(args->isolate(), partition); - else // default session - session = Session::FromPartition(args->isolate(), ""); - } - - url_loader_factory_ = session->browser_context()->GetURLLoaderFactory(); - - InitWithArgs(args); -} - -URLRequest::~URLRequest() = default; - -URLRequest* URLRequest::FromID(uint32_t id) { - DCHECK_CURRENTLY_ON(content::BrowserThread::UI); - return GetAllRequests().Lookup(id); -} - -void URLRequest::OnAuthRequired( - const GURL& url, - bool first_auth_attempt, - net::AuthChallengeInfo auth_info, - network::mojom::URLResponseHeadPtr head, - mojo::PendingRemote - auth_challenge_responder) { - DCHECK_CURRENTLY_ON(content::BrowserThread::UI); - mojo::Remote auth_responder( - std::move(auth_challenge_responder)); - auth_responder.set_disconnect_handler( - base::BindOnce(&URLRequest::Cancel, weak_factory_.GetWeakPtr())); - auto cb = base::BindOnce( - [](mojo::Remote auth_responder, - gin::Arguments* args) { - base::string16 username_str, password_str; - if (!args->GetNext(&username_str) || !args->GetNext(&password_str)) { - auth_responder->OnAuthCredentials(base::nullopt); - return; - } - auth_responder->OnAuthCredentials( - net::AuthCredentials(username_str, password_str)); - }, - std::move(auth_responder)); - Emit("login", auth_info, base::AdaptCallbackForRepeating(std::move(cb))); -} - -bool URLRequest::NotStarted() const { - return request_state_ == 0; -} - -bool URLRequest::Finished() const { - return request_state_ & STATE_FINISHED; -} - -void URLRequest::Cancel() { - // Cancel only once. - if (request_state_ & (STATE_CANCELED | STATE_CLOSED)) - return; - - // Mark as canceled. - request_state_ |= STATE_CANCELED; - EmitEvent(EventType::kRequest, true, "abort"); - - if ((response_state_ & STATE_STARTED) && !(response_state_ & STATE_FINISHED)) - EmitEvent(EventType::kResponse, true, "aborted"); - - Close(); -} - -void URLRequest::Close() { - if (!(request_state_ & STATE_CLOSED)) { - request_state_ |= STATE_CLOSED; - if (response_state_ & STATE_STARTED) { - // Emit a close event if we really have a response object. - EmitEvent(EventType::kResponse, true, "close"); - } - EmitEvent(EventType::kRequest, true, "close"); - } - Unpin(); - loader_.reset(); -} - -bool URLRequest::Write(v8::Local data, bool is_last) { - if (request_state_ & (STATE_FINISHED | STATE_ERROR)) - return false; - - size_t length = node::Buffer::Length(data); - - if (!loader_) { - // Pin on first write. - request_state_ = STATE_STARTED; - Pin(); - - // Create the loader. - network::ResourceRequest* request_ref = request_.get(); - loader_ = network::SimpleURLLoader::Create(std::move(request_), - kTrafficAnnotation); - - loader_->SetAllowHttpErrorResults(true); - loader_->SetOnResponseStartedCallback( - base::Bind(&URLRequest::OnResponseStarted, weak_factory_.GetWeakPtr())); - loader_->SetOnRedirectCallback( - base::Bind(&URLRequest::OnRedirect, weak_factory_.GetWeakPtr())); - loader_->SetOnUploadProgressCallback( - base::Bind(&URLRequest::OnUploadProgress, weak_factory_.GetWeakPtr())); - - // Create upload data pipe if we have data to write. - if (length > 0) { - request_ref->request_body = new network::ResourceRequestBody(); - if (is_chunked_upload_) - data_pipe_getter_ = std::make_unique(this); - else - data_pipe_getter_ = std::make_unique(this); - data_pipe_getter_->AttachToRequestBody(request_ref->request_body.get()); - } - - // Start downloading. - loader_->DownloadAsStream(url_loader_factory_.get(), this); - } - - if (length > 0) - pending_writes_.emplace_back(node::Buffer::Data(data), length); - - if (is_last) { - // The ElementsUploadDataStream requires the knowledge of content length - // before doing upload, while Node's stream does not give us any size - // information. So the only option left for us is to keep all the write - // data in memory and flush them after the write is done. - // - // While this looks frustrating, it is actually the behavior of the non- - // NetworkService implementation, and we are not breaking anything. - if (!pending_writes_.empty()) { - last_chunk_written_ = true; - StartWriting(); - } - - request_state_ |= STATE_FINISHED; - EmitEvent(EventType::kRequest, true, "finish"); - } - return true; -} - -void URLRequest::FollowRedirect() { - if (request_state_ & (STATE_CANCELED | STATE_CLOSED)) - return; - follow_redirect_ = true; -} - -bool URLRequest::SetExtraHeader(const std::string& name, - const std::string& value) { - if (!request_) - return false; - if (!net::HttpUtil::IsValidHeaderName(name)) - return false; - if (!net::HttpUtil::IsValidHeaderValue(value)) - return false; - request_->headers.SetHeader(name, value); - return true; -} - -void URLRequest::RemoveExtraHeader(const std::string& name) { - if (request_) - request_->headers.RemoveHeader(name); -} - -void URLRequest::SetChunkedUpload(bool is_chunked_upload) { - if (request_) - is_chunked_upload_ = is_chunked_upload; -} - -gin::Dictionary URLRequest::GetUploadProgress() { - gin::Dictionary progress = gin::Dictionary::CreateEmpty(isolate()); - if (loader_) { - if (request_) - progress.Set("started", false); - else - progress.Set("started", true); - progress.Set("current", upload_position_); - progress.Set("total", upload_total_); - progress.Set("active", true); - } else { - progress.Set("active", false); - } - return progress; -} - -int URLRequest::StatusCode() const { - if (response_headers_) - return response_headers_->response_code(); - return -1; -} - -std::string URLRequest::StatusMessage() const { - if (response_headers_) - return response_headers_->GetStatusText(); - return ""; -} - -net::HttpResponseHeaders* URLRequest::RawResponseHeaders() const { - return response_headers_.get(); -} - -uint32_t URLRequest::ResponseHttpVersionMajor() const { - if (response_headers_) - return response_headers_->GetHttpVersion().major_value(); - return 0; -} - -uint32_t URLRequest::ResponseHttpVersionMinor() const { - if (response_headers_) - return response_headers_->GetHttpVersion().minor_value(); - return 0; -} - -void URLRequest::OnDataReceived(base::StringPiece data, - base::OnceClosure resume) { - // In case we received an unexpected event from Chromium net, don't emit any - // data event after request cancel/error/close. - if (!(request_state_ & STATE_ERROR) && !(response_state_ & STATE_ERROR)) { - v8::HandleScope handle_scope(isolate()); - v8::Local buffer; - auto maybe = node::Buffer::Copy(isolate(), data.data(), data.size()); - if (maybe.ToLocal(&buffer)) - Emit("data", buffer); - } - std::move(resume).Run(); -} - -void URLRequest::OnRetry(base::OnceClosure start_retry) {} - -void URLRequest::OnComplete(bool success) { - if (success) { - // In case we received an unexpected event from Chromium net, don't emit any - // data event after request cancel/error/close. - if (!(request_state_ & STATE_ERROR) && !(response_state_ & STATE_ERROR)) { - response_state_ |= STATE_FINISHED; - Emit("end"); - } - } else { // failed - // If response is started then emit response event, else emit request error. - // - // Error is only emitted when there is no previous failure. This is to align - // with the behavior of non-NetworkService implementation. - std::string error = net::ErrorToString(loader_->NetError()); - if (response_state_ & STATE_STARTED) { - if (!(response_state_ & STATE_FAILED)) - EmitError(EventType::kResponse, error); - } else { - if (!(request_state_ & STATE_FAILED)) - EmitError(EventType::kRequest, error); - } - } - - Close(); -} - -void URLRequest::OnResponseStarted( - const GURL& final_url, - const network::mojom::URLResponseHead& response_head) { - // Don't emit any event after request cancel. - if (request_state_ & STATE_ERROR) - return; - - response_headers_ = response_head.headers; - response_state_ |= STATE_STARTED; - Emit("response"); -} - -void URLRequest::OnRedirect( - const net::RedirectInfo& redirect_info, - const network::mojom::URLResponseHead& response_head, - std::vector* to_be_removed_headers) { - if (!loader_) - return; - - if (request_state_ & (STATE_CLOSED | STATE_CANCELED)) { - NOTREACHED(); - Cancel(); - return; - } - - switch (redirect_mode_) { - case network::mojom::RedirectMode::kError: - Cancel(); - EmitError( - EventType::kRequest, - "Request cannot follow redirect with the current redirect mode"); - break; - case network::mojom::RedirectMode::kManual: - // When redirect mode is "manual", the user has to explicitly call the - // FollowRedirect method to continue redirecting, otherwise the request - // would be cancelled. - // - // Note that the SimpleURLLoader always calls FollowRedirect and does not - // provide a formal way for us to cancel redirection, we have to cancel - // the request to prevent the redirection. - follow_redirect_ = false; - EmitEvent(EventType::kRequest, false, "redirect", - redirect_info.status_code, redirect_info.new_method, - redirect_info.new_url, response_head.headers.get()); - if (!follow_redirect_) - Cancel(); - break; - case network::mojom::RedirectMode::kFollow: - EmitEvent(EventType::kRequest, false, "redirect", - redirect_info.status_code, redirect_info.new_method, - redirect_info.new_url, response_head.headers.get()); - break; - } -} - -void URLRequest::OnUploadProgress(uint64_t position, uint64_t total) { - upload_position_ = position; - upload_total_ = total; -} - -void URLRequest::OnWrite(MojoResult result) { - if (result != MOJO_RESULT_OK) - return; - - // Continue the pending writes. - pending_writes_.pop_front(); - if (!pending_writes_.empty()) - DoWrite(); -} - -void URLRequest::DoWrite() { - DCHECK(producer_); - DCHECK(!pending_writes_.empty()); - producer_->Write( - std::make_unique( - pending_writes_.front(), mojo::StringDataSource::AsyncWritingMode:: - STRING_STAYS_VALID_UNTIL_COMPLETION), - base::BindOnce(&URLRequest::OnWrite, weak_factory_.GetWeakPtr())); -} - -void URLRequest::StartWriting() { - if (!last_chunk_written_ || size_callback_.is_null()) - return; - - size_t size = 0; - for (const auto& data : pending_writes_) - size += data.size(); - std::move(size_callback_).Run(net::OK, size); - DoWrite(); -} - -void URLRequest::Pin() { - if (wrapper_.IsEmpty()) { - wrapper_.Reset(isolate(), GetWrapper()); - } -} - -void URLRequest::Unpin() { - wrapper_.Reset(); -} - -void URLRequest::EmitError(EventType type, base::StringPiece message) { - if (type == EventType::kRequest) - request_state_ |= STATE_FAILED; - else - response_state_ |= STATE_FAILED; - v8::HandleScope handle_scope(isolate()); - auto error = v8::Exception::Error(gin::StringToV8(isolate(), message)); - EmitEvent(type, false, "error", error); -} - -template -void URLRequest::EmitEvent(EventType type, Args&&... args) { - const char* method = - type == EventType::kRequest ? "_emitRequestEvent" : "_emitResponseEvent"; - v8::HandleScope handle_scope(isolate()); - gin_helper::CustomEmit(isolate(), GetWrapper(), method, - std::forward(args)...); -} - -// static -mate::WrappableBase* URLRequest::New(gin::Arguments* args) { - return new URLRequest(args); -} - -// static -void URLRequest::BuildPrototype(v8::Isolate* isolate, - v8::Local prototype) { - prototype->SetClassName(gin::StringToV8(isolate, "URLRequest")); - gin_helper::Destroyable::MakeDestroyable(isolate, prototype); - gin_helper::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate()) - .SetMethod("write", &URLRequest::Write) - .SetMethod("cancel", &URLRequest::Cancel) - .SetMethod("setExtraHeader", &URLRequest::SetExtraHeader) - .SetMethod("removeExtraHeader", &URLRequest::RemoveExtraHeader) - .SetMethod("setChunkedUpload", &URLRequest::SetChunkedUpload) - .SetMethod("followRedirect", &URLRequest::FollowRedirect) - .SetMethod("getUploadProgress", &URLRequest::GetUploadProgress) - .SetProperty("notStarted", &URLRequest::NotStarted) - .SetProperty("finished", &URLRequest::Finished) - .SetProperty("statusCode", &URLRequest::StatusCode) - .SetProperty("statusMessage", &URLRequest::StatusMessage) - .SetProperty("rawResponseHeaders", &URLRequest::RawResponseHeaders) - .SetProperty("httpVersionMajor", &URLRequest::ResponseHttpVersionMajor) - .SetProperty("httpVersionMinor", &URLRequest::ResponseHttpVersionMinor); -} - -} // namespace api - -} // namespace electron diff --git a/shell/browser/api/atom_api_url_request.h b/shell/browser/api/atom_api_url_request.h deleted file mode 100644 index 36dd349685e0..000000000000 --- a/shell/browser/api/atom_api_url_request.h +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) 2019 GitHub, Inc. -// Use of this source code is governed by the MIT license that can be -// found in the LICENSE file. - -#ifndef SHELL_BROWSER_API_ATOM_API_URL_REQUEST_H_ -#define SHELL_BROWSER_API_ATOM_API_URL_REQUEST_H_ - -#include -#include -#include -#include - -#include "gin/arguments.h" -#include "gin/dictionary.h" -#include "mojo/public/cpp/system/data_pipe_producer.h" -#include "services/network/public/cpp/shared_url_loader_factory.h" -#include "services/network/public/cpp/simple_url_loader.h" -#include "services/network/public/cpp/simple_url_loader_stream_consumer.h" -#include "services/network/public/mojom/data_pipe_getter.mojom.h" -#include "services/network/public/mojom/network_context.mojom.h" -#include "shell/common/gin_helper/event_emitter.h" - -namespace electron { - -namespace api { - -class UploadDataPipeGetter; - -class URLRequest : public gin_helper::EventEmitter, - public network::SimpleURLLoaderStreamConsumer { - public: - static mate::WrappableBase* New(gin::Arguments* args); - - static void BuildPrototype(v8::Isolate* isolate, - v8::Local prototype); - static URLRequest* FromID(uint32_t id); - - void OnAuthRequired( - const GURL& url, - bool first_auth_attempt, - net::AuthChallengeInfo auth_info, - network::mojom::URLResponseHeadPtr head, - mojo::PendingRemote - auth_challenge_responder); - - protected: - explicit URLRequest(gin::Arguments* args); - ~URLRequest() override; - - bool NotStarted() const; - bool Finished() const; - - void Cancel(); - void Close(); - - bool Write(v8::Local data, bool is_last); - void FollowRedirect(); - bool SetExtraHeader(const std::string& name, const std::string& value); - void RemoveExtraHeader(const std::string& name); - void SetChunkedUpload(bool is_chunked_upload); - gin::Dictionary GetUploadProgress(); - int StatusCode() const; - std::string StatusMessage() const; - net::HttpResponseHeaders* RawResponseHeaders() const; - uint32_t ResponseHttpVersionMajor() const; - uint32_t ResponseHttpVersionMinor() const; - - // SimpleURLLoaderStreamConsumer: - void OnDataReceived(base::StringPiece string_piece, - base::OnceClosure resume) override; - void OnComplete(bool success) override; - void OnRetry(base::OnceClosure start_retry) override; - - private: - friend class UploadDataPipeGetter; - - void OnResponseStarted(const GURL& final_url, - const network::mojom::URLResponseHead& response_head); - void OnRedirect(const net::RedirectInfo& redirect_info, - const network::mojom::URLResponseHead& response_head, - std::vector* to_be_removed_headers); - void OnUploadProgress(uint64_t position, uint64_t total); - void OnWrite(MojoResult result); - - // Write the first data of |pending_writes_|. - void DoWrite(); - - // Start streaming. - void StartWriting(); - - // Manage lifetime of wrapper. - void Pin(); - void Unpin(); - - // Emit events. - enum class EventType { - kRequest, - kResponse, - }; - void EmitError(EventType type, base::StringPiece error); - template - void EmitEvent(EventType type, Args&&... args); - - std::unique_ptr request_; - std::unique_ptr loader_; - scoped_refptr url_loader_factory_; - scoped_refptr response_headers_; - - // Redirect mode. - // - // Note that we store it ourselves instead of reading from the one stored in - // |request_|, this is because with multiple redirections, the original one - // might be modified. - network::mojom::RedirectMode redirect_mode_ = - network::mojom::RedirectMode::kFollow; - - // The DataPipeGetter passed to reader. - bool is_chunked_upload_ = false; - std::unique_ptr data_pipe_getter_; - - // Passed from DataPipeGetter for streaming data. - network::mojom::DataPipeGetter::ReadCallback size_callback_; - std::unique_ptr producer_; - - // Whether request.end() has been called. - bool last_chunk_written_ = false; - - // Whether the redirect should be followed. - bool follow_redirect_ = true; - - // Upload progress. - uint64_t upload_position_ = 0; - uint64_t upload_total_ = 0; - - // Current status. - int request_state_ = 0; - int response_state_ = 0; - - // Pending writes that not yet sent to NetworkService. - std::list pending_writes_; - - // Used by pin/unpin to manage lifetime. - v8::Global wrapper_; - - uint32_t id_; - - base::WeakPtrFactory weak_factory_; - - DISALLOW_COPY_AND_ASSIGN(URLRequest); -}; - -} // namespace api - -} // namespace electron - -#endif // SHELL_BROWSER_API_ATOM_API_URL_REQUEST_H_ diff --git a/shell/browser/atom_browser_context.cc b/shell/browser/atom_browser_context.cc index faa5fdf9c84a..11bf6d54ca99 100644 --- a/shell/browser/atom_browser_context.cc +++ b/shell/browser/atom_browser_context.cc @@ -33,7 +33,7 @@ #include "services/network/public/cpp/features.h" #include "services/network/public/cpp/wrapper_shared_url_loader_factory.h" #include "services/network/public/mojom/network_context.mojom.h" -#include "shell/browser/api/atom_api_url_request.h" +#include "shell/browser/api/atom_api_url_loader.h" #include "shell/browser/atom_browser_client.h" #include "shell/browser/atom_browser_main_parts.h" #include "shell/browser/atom_download_manager_delegate.h" @@ -362,11 +362,12 @@ class AuthResponder : public network::mojom::TrustedAuthClient { ::network::mojom::URLResponseHeadPtr head, mojo::PendingRemote auth_challenge_responder) override { - api::URLRequest* url_request = api::URLRequest::FromID(routing_id); - if (url_request) { - url_request->OnAuthRequired(url, first_auth_attempt, auth_info, - std::move(head), - std::move(auth_challenge_responder)); + api::SimpleURLLoaderWrapper* url_loader = + api::SimpleURLLoaderWrapper::FromID(routing_id); + if (url_loader) { + url_loader->OnAuthRequired(url, first_auth_attempt, auth_info, + std::move(head), + std::move(auth_challenge_responder)); } } }; diff --git a/shell/common/gin_converters/net_converter.cc b/shell/common/gin_converters/net_converter.cc index fc1c24a6bca5..bc925622263c 100644 --- a/shell/common/gin_converters/net_converter.cc +++ b/shell/common/gin_converters/net_converter.cc @@ -17,6 +17,8 @@ #include "net/cert/x509_certificate.h" #include "net/cert/x509_util.h" #include "net/http/http_response_headers.h" +#include "net/http/http_version.h" +#include "net/url_request/redirect_info.h" #include "services/network/public/cpp/resource_request.h" #include "shell/browser/api/atom_api_data_pipe_holder.h" #include "shell/common/gin_converters/gurl_converter.h" @@ -337,7 +339,7 @@ bool Converter>::FromV8( v8::Local Converter::ToV8( v8::Isolate* isolate, const network::ResourceRequest& val) { - gin::Dictionary dict(isolate, v8::Object::New(isolate)); + gin::Dictionary dict = gin::Dictionary::CreateEmpty(isolate); dict.Set("method", val.method); dict.Set("url", val.url.spec()); dict.Set("referrer", val.referrer.spec()); @@ -359,4 +361,32 @@ v8::Local Converter::ToV8( return ConvertToV8(isolate, dict); } +// static +v8::Local Converter::ToV8( + v8::Isolate* isolate, + const net::HttpVersion& val) { + gin::Dictionary dict = gin::Dictionary::CreateEmpty(isolate); + dict.Set("major", static_cast(val.major_value())); + dict.Set("minor", static_cast(val.minor_value())); + return ConvertToV8(isolate, dict); +} + +// static +v8::Local Converter::ToV8( + v8::Isolate* isolate, + const net::RedirectInfo& val) { + gin::Dictionary dict = gin::Dictionary::CreateEmpty(isolate); + + dict.Set("statusCode", val.status_code); + dict.Set("newMethod", val.new_method); + dict.Set("newUrl", val.new_url); + dict.Set("newSiteForCookies", val.new_site_for_cookies); + dict.Set("newReferrer", val.new_referrer); + dict.Set("insecureSchemeWasUpgraded", val.insecure_scheme_was_upgraded); + dict.Set("isSignedExchangeFallbackRedirect", + val.is_signed_exchange_fallback_redirect); + + return ConvertToV8(isolate, dict); +} + } // namespace gin diff --git a/shell/common/gin_converters/net_converter.h b/shell/common/gin_converters/net_converter.h index a3e9a382a32c..0d05fe5f5537 100644 --- a/shell/common/gin_converters/net_converter.h +++ b/shell/common/gin_converters/net_converter.h @@ -5,7 +5,10 @@ #ifndef SHELL_COMMON_GIN_CONVERTERS_NET_CONVERTER_H_ #define SHELL_COMMON_GIN_CONVERTERS_NET_CONVERTER_H_ +#include + #include "gin/converter.h" +#include "services/network/public/mojom/fetch_api.mojom.h" #include "shell/browser/net/cert_verifier_client.h" namespace base { @@ -19,6 +22,7 @@ class URLRequest; class X509Certificate; class HttpResponseHeaders; struct CertPrincipal; +class HttpVersion; } // namespace net namespace network { @@ -96,6 +100,18 @@ struct Converter { electron::VerifyRequestParams val); }; +template <> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, + const net::HttpVersion& val); +}; + +template <> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, + const net::RedirectInfo& val); +}; + } // namespace gin #endif // SHELL_COMMON_GIN_CONVERTERS_NET_CONVERTER_H_ diff --git a/shell/renderer/electron_api_service_impl.cc b/shell/renderer/electron_api_service_impl.cc index dbadebd23046..3c20e86d6084 100644 --- a/shell/renderer/electron_api_service_impl.cc +++ b/shell/renderer/electron_api_service_impl.cc @@ -67,7 +67,7 @@ void InvokeIpcCallback(v8::Local context, .ToLocalChecked(); auto callback_value = ipcNative->Get(context, callback_key).ToLocalChecked(); DCHECK(callback_value->IsFunction()); // set by init.ts - auto callback = v8::Local::Cast(callback_value); + auto callback = callback_value.As(); ignore_result(callback->Call(context, ipcNative, args.size(), args.data())); } diff --git a/spec-main/api-net-spec.ts b/spec-main/api-net-spec.ts index 32ea06dc5906..faf5852f3d19 100644 --- a/spec-main/api-net-spec.ts +++ b/spec-main/api-net-spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai' import { net, session, ClientRequest, BrowserWindow } from 'electron' import * as http from 'http' import * as url from 'url' -import { AddressInfo } from 'net' +import { AddressInfo, Socket } from 'net' import { emittedOnce } from './events-helpers' const kOneKiloByte = 1024 @@ -22,6 +22,13 @@ function randomString (length: number) { return buffer.toString() } +const cleanupTasks: (() => void)[] = [] + +function cleanUp () { + cleanupTasks.forEach(t => t()) + cleanupTasks.length = 0 +} + function respondOnce (fn: http.RequestListener): Promise { return new Promise((resolve) => { const server = http.createServer((request, response) => { @@ -32,6 +39,12 @@ function respondOnce (fn: http.RequestListener): Promise { server.listen(0, '127.0.0.1', () => { resolve(`http://127.0.0.1:${(server.address() as AddressInfo).port}`) }) + const sockets: Socket[] = [] + server.on('connection', s => sockets.push(s)) + cleanupTasks.push(() => { + server.close() + sockets.forEach(s => s.destroy()) + }) }) } @@ -57,6 +70,10 @@ respondOnce.toSingleURL = (fn: http.RequestListener) => { } describe('net module', () => { + afterEach(cleanUp) + afterEach(async () => { + await session.defaultSession.clearCache() + }) describe('HTTP basics', () => { it('should be able to issue a basic GET request', (done) => { respondOnce.toSingleURL((request, response) => { @@ -325,6 +342,36 @@ describe('net module', () => { request.end() }) }) + + it('should upload body when 401', async () => { + const [user, pass] = ['user', 'pass'] + const serverUrl = await respondOnce.toSingleURL((request, response) => { + if (!request.headers.authorization) { + return response.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Foo"' }).end() + } + response.writeHead(200) + request.on('data', (chunk) => { response.write(chunk) }) + request.on('end', () => { + response.end() + }) + }) + const requestData = randomString(kOneKiloByte) + const responseData = await new Promise((resolve, reject) => { + const request = net.request({ method: 'GET', url: serverUrl }) + request.on('response', (response) => { + response.on('error', reject) + let data = '' + response.on('data', (chunk) => { data += chunk.toString() }) + response.on('end', () => resolve(data)) + }) + request.on('login', (authInfo, cb) => { + cb(user, pass) + }) + request.on('error', reject) + request.end(requestData) + }) + expect(responseData).to.equal(requestData) + }) }) describe('ClientRequest API', () => { @@ -955,10 +1002,10 @@ describe('net module', () => { }) it('should follow redirect when no redirect handler is provided', (done) => { - const requestUrl = '/301' + const requestUrl = '/302' respondOnce.toRoutes({ - '/301': (request, response) => { - response.statusCode = 301 + '/302': (request, response) => { + response.statusCode = 302 response.setHeader('Location', '/200') response.end() }, @@ -981,12 +1028,12 @@ describe('net module', () => { it('should follow redirect chain when no redirect handler is provided', (done) => { respondOnce.toRoutes({ '/redirectChain': (request, response) => { - response.statusCode = 301 - response.setHeader('Location', '/301') + response.statusCode = 302 + response.setHeader('Location', '/302') response.end() }, - '/301': (request, response) => { - response.statusCode = 301 + '/302': (request, response) => { + response.statusCode = 302 response.setHeader('Location', '/200') response.end() }, @@ -1008,7 +1055,7 @@ describe('net module', () => { it('should not follow redirect when request is canceled in redirect handler', async () => { const serverUrl = await respondOnce.toSingleURL((request, response) => { - response.statusCode = 301 + response.statusCode = 302 response.setHeader('Location', '/200') response.end() }) @@ -1017,18 +1064,33 @@ describe('net module', () => { }) urlRequest.end() urlRequest.on('redirect', () => { urlRequest.abort() }) + urlRequest.on('error', () => {}) await emittedOnce(urlRequest, 'abort') }) + it('should not follow redirect when mode is error', async () => { + const serverUrl = await respondOnce.toSingleURL((request, response) => { + response.statusCode = 302 + response.setHeader('Location', '/200') + response.end() + }) + const urlRequest = net.request({ + url: serverUrl, + redirect: 'error' + }) + urlRequest.end() + await emittedOnce(urlRequest, 'error') + }) + it('should follow redirect when handler calls callback', async () => { const serverUrl = await respondOnce.toRoutes({ '/redirectChain': (request, response) => { - response.statusCode = 301 - response.setHeader('Location', '/301') + response.statusCode = 302 + response.setHeader('Location', '/302') response.end() }, - '/301': (request, response) => { - response.statusCode = 301 + '/302': (request, response) => { + response.statusCode = 302 response.setHeader('Location', '/200') response.end() }, @@ -1037,7 +1099,7 @@ describe('net module', () => { response.end() } }) - const urlRequest = net.request(`${serverUrl}/redirectChain`) + const urlRequest = net.request({ url: `${serverUrl}/redirectChain`, redirect: 'manual' }) const redirects: string[] = [] urlRequest.on('redirect', (status, method, url) => { redirects.push(url) @@ -1047,7 +1109,7 @@ describe('net module', () => { const [response] = await emittedOnce(urlRequest, 'response') expect(response.statusCode).to.equal(200) expect(redirects).to.deep.equal([ - `${serverUrl}/301`, + `${serverUrl}/302`, `${serverUrl}/200` ]) }) @@ -1276,9 +1338,9 @@ describe('net module', () => { await emittedOnce(response, 'end') }) - it('should be able to pipe a net response into a writable stream', (done) => { + it('should be able to pipe a net response into a writable stream', async () => { const bodyData = randomString(kOneKiloByte) - Promise.all([ + const [netServerUrl, nodeServerUrl] = await Promise.all([ respondOnce.toSingleURL((request, response) => response.end(bodyData)), respondOnce.toSingleURL((request, response) => { let receivedBodyData = '' @@ -1293,8 +1355,10 @@ describe('net module', () => { response.end() }) }) - ]).then(([netServerUrl, nodeServerUrl]) => { - const netRequest = net.request(netServerUrl) + ]) + const netRequest = net.request(netServerUrl) + netRequest.end() + await new Promise((resolve) => { netRequest.on('response', (netResponse) => { const serverUrl = url.parse(nodeServerUrl) const nodeOptions = { @@ -1305,7 +1369,7 @@ describe('net module', () => { const nodeRequest = http.request(nodeOptions, res => { res.on('data', () => {}) res.on('end', () => { - done() + resolve() }) }); // TODO: IncomingMessage should properly extend ReadableStream in the @@ -1360,14 +1424,11 @@ describe('net module', () => { const urlRequest = net.request(serverUrl) urlRequest.on('response', (response) => { response.on('data', () => {}) - response.on('end', () => {}) + response.on('end', () => { done() }) }) - urlRequest.on('close', () => { - process.nextTick(() => { - const v8Util = process.electronBinding('v8_util') - v8Util.requestGarbageCollectionForTesting() - done() - }) + process.nextTick(() => { + const v8Util = process.electronBinding('v8_util') + v8Util.requestGarbageCollectionForTesting() }) urlRequest.end() }) @@ -1427,5 +1488,84 @@ describe('net module', () => { urlRequest.end(randomBuffer(kOneMegaByte)) }) }) + + it('should finish sending data when urlRequest is unreferenced before close event for chunked encoding', (done) => { + respondOnce.toSingleURL((request, response) => { + let received = Buffer.alloc(0) + request.on('data', (data) => { + received = Buffer.concat([received, data]) + }) + request.on('end', () => { + response.end() + expect(received.length).to.equal(kOneMegaByte) + done() + }) + }).then(serverUrl => { + const urlRequest = net.request(serverUrl) + urlRequest.on('response', (response) => { + response.on('data', () => {}) + response.on('end', () => {}) + }) + urlRequest.chunkedEncoding = true + urlRequest.end(randomBuffer(kOneMegaByte)) + const v8Util = process.electronBinding('v8_util') + v8Util.requestGarbageCollectionForTesting() + }) + }) + + it('should finish sending data when urlRequest is unreferenced', (done) => { + respondOnce.toSingleURL((request, response) => { + let received = Buffer.alloc(0) + request.on('data', (data) => { + received = Buffer.concat([received, data]) + }) + request.on('end', () => { + response.end() + expect(received.length).to.equal(kOneMegaByte) + done() + }) + }).then(serverUrl => { + const urlRequest = net.request(serverUrl) + urlRequest.on('response', (response) => { + response.on('data', () => {}) + response.on('end', () => {}) + }) + urlRequest.on('close', () => { + process.nextTick(() => { + const v8Util = process.electronBinding('v8_util') + v8Util.requestGarbageCollectionForTesting() + }) + }) + urlRequest.end(randomBuffer(kOneMegaByte)) + }) + }) + + it('should finish sending data when urlRequest is unreferenced for chunked encoding', (done) => { + respondOnce.toSingleURL((request, response) => { + let received = Buffer.alloc(0) + request.on('data', (data) => { + received = Buffer.concat([received, data]) + }) + request.on('end', () => { + response.end() + expect(received.length).to.equal(kOneMegaByte) + done() + }) + }).then(serverUrl => { + const urlRequest = net.request(serverUrl) + urlRequest.on('response', (response) => { + response.on('data', () => {}) + response.on('end', () => {}) + }) + urlRequest.on('close', () => { + process.nextTick(() => { + const v8Util = process.electronBinding('v8_util') + v8Util.requestGarbageCollectionForTesting() + }) + }) + urlRequest.chunkedEncoding = true + urlRequest.end(randomBuffer(kOneMegaByte)) + }) + }) }) })