refactor: rewrite the net module to simplify state tracking (#21244)
This commit is contained in:
parent
4149d76890
commit
d25256dcf5
14 changed files with 1092 additions and 1033 deletions
|
@ -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<String, String[]>
|
||||
|
||||
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()`
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@
|
|||
|
||||
#include "shell/browser/api/atom_api_net.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#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<v8::FunctionTemplate> prototype) {
|
||||
prototype->SetClassName(gin::StringToV8(isolate, "Net"));
|
||||
gin_helper::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
|
||||
.SetProperty("URLRequest", &Net::URLRequest);
|
||||
.SetProperty("URLLoader", &Net::URLLoader);
|
||||
}
|
||||
|
||||
v8::Local<v8::Value> Net::URLRequest(v8::Isolate* isolate) {
|
||||
v8::Local<v8::Value> Net::URLLoader(v8::Isolate* isolate) {
|
||||
v8::Local<v8::FunctionTemplate> constructor;
|
||||
constructor = URLRequest::GetConstructor(isolate);
|
||||
constructor = SimpleURLLoaderWrapper::GetConstructor(isolate);
|
||||
return constructor->GetFunction(isolate->GetCurrentContext())
|
||||
.ToLocalChecked();
|
||||
}
|
||||
|
@ -48,8 +50,16 @@ v8::Local<v8::Value> 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<v8::Object> exports,
|
||||
v8::Local<v8::Value> unused,
|
||||
|
@ -57,12 +67,15 @@ void Initialize(v8::Local<v8::Object> 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
|
||||
|
|
|
@ -18,7 +18,7 @@ class Net : public mate::Wrappable<Net> {
|
|||
static void BuildPrototype(v8::Isolate* isolate,
|
||||
v8::Local<v8::FunctionTemplate> prototype);
|
||||
|
||||
v8::Local<v8::Value> URLRequest(v8::Isolate* isolate);
|
||||
v8::Local<v8::Value> URLLoader(v8::Isolate* isolate);
|
||||
|
||||
protected:
|
||||
explicit Net(v8::Isolate* isolate);
|
||||
|
|
448
shell/browser/api/atom_api_url_loader.cc
Normal file
448
shell/browser/api/atom_api_url_loader.cc
Normal file
|
@ -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 <algorithm>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<char> 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<char> 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<char> buffer_;
|
||||
};
|
||||
|
||||
class JSChunkedDataPipeGetter : public gin::Wrappable<JSChunkedDataPipeGetter>,
|
||||
public network::mojom::ChunkedDataPipeGetter {
|
||||
public:
|
||||
static gin::Handle<JSChunkedDataPipeGetter> Create(
|
||||
v8::Isolate* isolate,
|
||||
v8::Local<v8::Function> body_func,
|
||||
mojo::PendingReceiver<network::mojom::ChunkedDataPipeGetter>
|
||||
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<JSChunkedDataPipeGetter>::GetObjectTemplateBuilder(
|
||||
isolate)
|
||||
.SetMethod("write", &JSChunkedDataPipeGetter::WriteChunk)
|
||||
.SetMethod("done", &JSChunkedDataPipeGetter::Done);
|
||||
}
|
||||
|
||||
static gin::WrapperInfo kWrapperInfo;
|
||||
~JSChunkedDataPipeGetter() override = default;
|
||||
|
||||
private:
|
||||
JSChunkedDataPipeGetter(
|
||||
v8::Isolate* isolate,
|
||||
v8::Local<v8::Function> body_func,
|
||||
mojo::PendingReceiver<network::mojom::ChunkedDataPipeGetter>
|
||||
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<mojo::DataPipeProducer>(std::move(pipe));
|
||||
|
||||
v8::HandleScope handle_scope(isolate_);
|
||||
v8::MicrotasksScope script_scope(isolate_,
|
||||
v8::MicrotasksScope::kRunMicrotasks);
|
||||
auto maybe_wrapper = GetWrapper(isolate_);
|
||||
v8::Local<v8::Value> wrapper;
|
||||
if (!maybe_wrapper.ToLocal(&wrapper)) {
|
||||
return;
|
||||
}
|
||||
v8::Local<v8::Value> 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<v8::Promise> WriteChunk(v8::Local<v8::Value> buffer_val) {
|
||||
gin_helper::Promise<void> promise(isolate_);
|
||||
v8::Local<v8::Promise> 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<v8::ArrayBufferView>();
|
||||
is_writing_ = true;
|
||||
bytes_written_ += buffer->ByteLength();
|
||||
auto backing_store = buffer->Buffer()->GetBackingStore();
|
||||
auto buffer_span = base::make_span(
|
||||
static_cast<char*>(backing_store->Data()) + buffer->ByteOffset(),
|
||||
buffer->ByteLength());
|
||||
auto buffer_source = std::make_unique<BufferDataSource>(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<void> 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<network::mojom::ChunkedDataPipeGetter> receiver_{this};
|
||||
std::unique_ptr<mojo::DataPipeProducer> data_producer_;
|
||||
bool is_writing_ = false;
|
||||
uint64_t bytes_written_ = 0;
|
||||
|
||||
v8::Isolate* isolate_;
|
||||
v8::Global<v8::Function> 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<SimpleURLLoaderWrapper*>& GetAllRequests() {
|
||||
static base::NoDestructor<base::IDMap<SimpleURLLoaderWrapper*>>
|
||||
s_all_requests;
|
||||
return *s_all_requests;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SimpleURLLoaderWrapper::SimpleURLLoaderWrapper(
|
||||
std::unique_ptr<network::ResourceRequest> 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<v8::Value> 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<network::mojom::AuthChallengeResponder>
|
||||
auth_challenge_responder) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
mojo::Remote<network::mojom::AuthChallengeResponder> 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<network::mojom::AuthChallengeResponder> 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<network::ResourceRequest>();
|
||||
opts.Get("method", &request->method);
|
||||
opts.Get("url", &request->url);
|
||||
std::map<std::string, std::string> 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<v8::Value> body;
|
||||
v8::Local<v8::Value> chunk_pipe_getter;
|
||||
if (opts.Get("body", &body)) {
|
||||
if (body->IsArrayBufferView()) {
|
||||
auto buffer_body = body.As<v8::ArrayBufferView>();
|
||||
auto backing_store = buffer_body->Buffer()->GetBackingStore();
|
||||
request->request_body = network::ResourceRequestBody::CreateFromBytes(
|
||||
static_cast<char*>(backing_store->Data()) + buffer_body->ByteOffset(),
|
||||
buffer_body->ByteLength());
|
||||
} else if (body->IsFunction()) {
|
||||
auto body_func = body.As<v8::Function>();
|
||||
|
||||
mojo::PendingRemote<network::mojom::ChunkedDataPipeGetter>
|
||||
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> 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<std::string>* 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<v8::FunctionTemplate> prototype) {
|
||||
prototype->SetClassName(gin::StringToV8(isolate, "SimpleURLLoaderWrapper"));
|
||||
gin_helper::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
|
||||
.SetMethod("cancel", &SimpleURLLoaderWrapper::Cancel);
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace electron
|
93
shell/browser/api/atom_api_url_loader.h
Normal file
93
shell/browser/api/atom_api_url_loader.h
Normal file
|
@ -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 <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<SimpleURLLoaderWrapper>,
|
||||
public network::SimpleURLLoaderStreamConsumer {
|
||||
public:
|
||||
~SimpleURLLoaderWrapper() override;
|
||||
static mate::WrappableBase* New(gin::Arguments* args);
|
||||
|
||||
static void BuildPrototype(v8::Isolate* isolate,
|
||||
v8::Local<v8::FunctionTemplate> 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<network::mojom::AuthChallengeResponder>
|
||||
auth_challenge_responder);
|
||||
|
||||
void Cancel();
|
||||
|
||||
private:
|
||||
SimpleURLLoaderWrapper(std::unique_ptr<network::ResourceRequest> 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<std::string>* removed_headers);
|
||||
void OnUploadProgress(uint64_t position, uint64_t total);
|
||||
void OnDownloadProgress(uint64_t current);
|
||||
|
||||
void Start();
|
||||
void Pin();
|
||||
void PinBodyGetter(v8::Local<v8::Value>);
|
||||
|
||||
uint32_t id_;
|
||||
std::unique_ptr<network::SimpleURLLoader> loader_;
|
||||
v8::Global<v8::Value> pinned_wrapper_;
|
||||
v8::Global<v8::Value> pinned_chunk_pipe_getter_;
|
||||
|
||||
base::WeakPtrFactory<SimpleURLLoaderWrapper> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_API_ATOM_API_URL_LOADER_H_
|
|
@ -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 <utility>
|
||||
|
||||
#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<network::mojom::RedirectMode> {
|
||||
static bool FromV8(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> 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<URLRequest*>& GetAllRequests() {
|
||||
static base::NoDestructor<base::IDMap<URLRequest*>> 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<mojo::DataPipeProducer>(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<network::mojom::DataPipeGetter> 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<network::mojom::DataPipeGetter> receiver) override {
|
||||
receivers_.Add(this, std::move(receiver));
|
||||
}
|
||||
|
||||
mojo::ReceiverSet<network::mojom::DataPipeGetter> 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<network::mojom::ChunkedDataPipeGetter>
|
||||
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<network::mojom::ChunkedDataPipeGetter> receiver_set_;
|
||||
};
|
||||
|
||||
URLRequest::URLRequest(gin::Arguments* args)
|
||||
: id_(GetAllRequests().Add(this)), weak_factory_(this) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
request_ = std::make_unique<network::ResourceRequest>();
|
||||
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<api::Session> 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<network::mojom::AuthChallengeResponder>
|
||||
auth_challenge_responder) {
|
||||
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
|
||||
mojo::Remote<network::mojom::AuthChallengeResponder> 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<network::mojom::AuthChallengeResponder> 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<v8::Value> 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<ChunkedDataPipeGetter>(this);
|
||||
else
|
||||
data_pipe_getter_ = std::make_unique<MultipartDataPipeGetter>(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<v8::Value> 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<std::string>* 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<mojo::StringDataSource>(
|
||||
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 <typename... Args>
|
||||
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>(args)...);
|
||||
}
|
||||
|
||||
// static
|
||||
mate::WrappableBase* URLRequest::New(gin::Arguments* args) {
|
||||
return new URLRequest(args);
|
||||
}
|
||||
|
||||
// static
|
||||
void URLRequest::BuildPrototype(v8::Isolate* isolate,
|
||||
v8::Local<v8::FunctionTemplate> 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
|
|
@ -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 <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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<URLRequest>,
|
||||
public network::SimpleURLLoaderStreamConsumer {
|
||||
public:
|
||||
static mate::WrappableBase* New(gin::Arguments* args);
|
||||
|
||||
static void BuildPrototype(v8::Isolate* isolate,
|
||||
v8::Local<v8::FunctionTemplate> 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<network::mojom::AuthChallengeResponder>
|
||||
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<v8::Value> 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<std::string>* 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 <typename... Args>
|
||||
void EmitEvent(EventType type, Args&&... args);
|
||||
|
||||
std::unique_ptr<network::ResourceRequest> request_;
|
||||
std::unique_ptr<network::SimpleURLLoader> loader_;
|
||||
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
|
||||
scoped_refptr<net::HttpResponseHeaders> 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<UploadDataPipeGetter> data_pipe_getter_;
|
||||
|
||||
// Passed from DataPipeGetter for streaming data.
|
||||
network::mojom::DataPipeGetter::ReadCallback size_callback_;
|
||||
std::unique_ptr<mojo::DataPipeProducer> 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<std::string> pending_writes_;
|
||||
|
||||
// Used by pin/unpin to manage lifetime.
|
||||
v8::Global<v8::Object> wrapper_;
|
||||
|
||||
uint32_t id_;
|
||||
|
||||
base::WeakPtrFactory<URLRequest> weak_factory_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(URLRequest);
|
||||
};
|
||||
|
||||
} // namespace api
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_API_ATOM_API_URL_REQUEST_H_
|
|
@ -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<network::mojom::AuthChallengeResponder>
|
||||
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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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<scoped_refptr<network::ResourceRequestBody>>::FromV8(
|
|||
v8::Local<v8::Value> Converter<network::ResourceRequest>::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<v8::Value> Converter<electron::VerifyRequestParams>::ToV8(
|
|||
return ConvertToV8(isolate, dict);
|
||||
}
|
||||
|
||||
// static
|
||||
v8::Local<v8::Value> Converter<net::HttpVersion>::ToV8(
|
||||
v8::Isolate* isolate,
|
||||
const net::HttpVersion& val) {
|
||||
gin::Dictionary dict = gin::Dictionary::CreateEmpty(isolate);
|
||||
dict.Set("major", static_cast<uint32_t>(val.major_value()));
|
||||
dict.Set("minor", static_cast<uint32_t>(val.minor_value()));
|
||||
return ConvertToV8(isolate, dict);
|
||||
}
|
||||
|
||||
// static
|
||||
v8::Local<v8::Value> Converter<net::RedirectInfo>::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
|
||||
|
|
|
@ -5,7 +5,10 @@
|
|||
#ifndef SHELL_COMMON_GIN_CONVERTERS_NET_CONVERTER_H_
|
||||
#define SHELL_COMMON_GIN_CONVERTERS_NET_CONVERTER_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#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> {
|
|||
electron::VerifyRequestParams val);
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Converter<net::HttpVersion> {
|
||||
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
|
||||
const net::HttpVersion& val);
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Converter<net::RedirectInfo> {
|
||||
static v8::Local<v8::Value> ToV8(v8::Isolate* isolate,
|
||||
const net::RedirectInfo& val);
|
||||
};
|
||||
|
||||
} // namespace gin
|
||||
|
||||
#endif // SHELL_COMMON_GIN_CONVERTERS_NET_CONVERTER_H_
|
||||
|
|
|
@ -67,7 +67,7 @@ void InvokeIpcCallback(v8::Local<v8::Context> context,
|
|||
.ToLocalChecked();
|
||||
auto callback_value = ipcNative->Get(context, callback_key).ToLocalChecked();
|
||||
DCHECK(callback_value->IsFunction()); // set by init.ts
|
||||
auto callback = v8::Local<v8::Function>::Cast(callback_value);
|
||||
auto callback = callback_value.As<v8::Function>();
|
||||
ignore_result(callback->Call(context, ipcNative, args.size(), args.data()));
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string> {
|
||||
return new Promise((resolve) => {
|
||||
const server = http.createServer((request, response) => {
|
||||
|
@ -32,6 +39,12 @@ function respondOnce (fn: http.RequestListener): Promise<string> {
|
|||
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))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue