diff --git a/@xen-orchestra/proxy-cli/index.mjs b/@xen-orchestra/proxy-cli/index.mjs index 2b9f8cbe7..47e0bd2e8 100755 --- a/@xen-orchestra/proxy-cli/index.mjs +++ b/@xen-orchestra/proxy-cli/index.mjs @@ -67,38 +67,44 @@ ${pkg.name} v${pkg.version}` // sequence path of the current call const callPath = [] + let url + let { token } = opts + if (opts.url !== '') { + url = new URL(opts.url) + const { username } = url + if (username !== '') { + token = username + url.username = '' + } + } else { + url = new URL('https://localhost/') + if (opts.host !== '') { + url.host = opts.host + } else { + const { hostname = 'localhost', port } = config?.http?.listen?.https ?? {} + url.hostname = hostname + url.port = port + } + } + + url = new URL('/api/v1', url) const baseRequest = { headers: { 'content-type': 'application/json', + cookie: `authenticationToken=${token}`, }, - pathname: '/api/v1', + method: 'POST', rejectUnauthorized: false, } - let { token } = opts - if (opts.url !== '') { - const { protocol, host, username } = new URL(opts.url) - Object.assign(baseRequest, { protocol, host }) - if (username !== '') { - token = username - } - } else { - baseRequest.protocol = 'https:' - if (opts.host !== '') { - baseRequest.host = opts.host - } else { - const { hostname = 'localhost', port } = config?.http?.listen?.https ?? {} - baseRequest.hostname = hostname - baseRequest.port = port - } - } - baseRequest.headers.cookie = `authenticationToken=${token}` const call = async ({ method, params }) => { if (callPath.length !== 0) { process.stderr.write(`\n${colors.bold(`--- call #${callPath.join('.')}`)} ---\n\n`) } - const response = await hrp.post(baseRequest, { + const response = await hrp(url, { + ...baseRequest, + body: format.request(0, method, params), }) diff --git a/@xen-orchestra/proxy-cli/package.json b/@xen-orchestra/proxy-cli/package.json index d019ef9be..ddd841224 100644 --- a/@xen-orchestra/proxy-cli/package.json +++ b/@xen-orchestra/proxy-cli/package.json @@ -32,7 +32,7 @@ "content-type": "^1.0.4", "cson-parser": "^4.0.7", "getopts": "^2.2.3", - "http-request-plus": "^0.14.0", + "http-request-plus": "github:JsCommunity/http-request-plus#v1", "json-rpc-protocol": "^0.13.1", "promise-toolbox": "^0.21.0", "pumpify": "^2.0.1", diff --git a/@xen-orchestra/upload-ova/package.json b/@xen-orchestra/upload-ova/package.json index 6db96ad62..3489503e3 100644 --- a/@xen-orchestra/upload-ova/package.json +++ b/@xen-orchestra/upload-ova/package.json @@ -35,7 +35,7 @@ "form-data": "^4.0.0", "fs-extra": "^11.1.0", "get-stream": "^6.0.0", - "http-request-plus": "^0.14.0", + "http-request-plus": "github:JsCommunity/http-request-plus#v1", "human-format": "^1.0.0", "lodash": "^4.17.4", "pretty-ms": "^7.0.0", diff --git a/@xen-orchestra/upload-ova/src/index.js b/@xen-orchestra/upload-ova/src/index.js index 065194ad0..a145429ff 100755 --- a/@xen-orchestra/upload-ova/src/index.js +++ b/@xen-orchestra/upload-ova/src/index.js @@ -230,10 +230,15 @@ export async function upload(args) { ) formData.append('file', input, { filename: 'file', knownLength: length }) try { - return await hrp.post(url.toString(), { body: formData, headers: formData.getHeaders() }).readAll('utf-8') + const response = await hrp(url.toString(), { body: formData, headers: formData.getHeaders(), method: 'POST' }) + return await response.text() } catch (e) { console.log('ERROR', e) - console.log('ERROR content', await e.response.readAll('utf-8')) + const { response } = e + if (response !== undefined) { + console.log('ERROR content', await response.text()) + } + throw e } } diff --git a/@xen-orchestra/xapi/package.json b/@xen-orchestra/xapi/package.json index 1ca0b0d3d..924d230fa 100644 --- a/@xen-orchestra/xapi/package.json +++ b/@xen-orchestra/xapi/package.json @@ -26,7 +26,7 @@ "@xen-orchestra/log": "^0.6.0", "d3-time-format": "^3.0.0", "golike-defer": "^0.5.1", - "http-request-plus": "^0.14.0", + "http-request-plus": "github:JsCommunity/http-request-plus#v1", "json-rpc-protocol": "^0.13.2", "lodash": "^4.17.15", "promise-toolbox": "^0.21.0", diff --git a/packages/xen-api/package.json b/packages/xen-api/package.json index 2dac96333..55de2d7c2 100644 --- a/packages/xen-api/package.json +++ b/packages/xen-api/package.json @@ -35,7 +35,7 @@ "bind-property-descriptor": "^2.0.0", "blocked": "^1.2.1", "debug": "^4.0.1", - "http-request-plus": "^0.14.0", + "http-request-plus": "github:JsCommunity/http-request-plus#v1", "jest-diff": "^29.0.3", "json-rpc-protocol": "^0.13.1", "kindof": "^2.0.0", diff --git a/packages/xen-api/src/index.js b/packages/xen-api/src/index.js index 78d8c956e..ca8eebc73 100644 --- a/packages/xen-api/src/index.js +++ b/packages/xen-api/src/index.js @@ -5,7 +5,6 @@ import ms from 'ms' import httpRequest from 'http-request-plus' import map from 'lodash/map' import noop from 'lodash/noop' -import omit from 'lodash/omit' import ProxyAgent from 'proxy-agent' import { coalesceCalls } from '@vates/coalesce-calls' import { Collection } from 'xo-collection' @@ -392,7 +391,7 @@ export class Xapi extends EventEmitter { const response = await this._addSyncStackTrace( pRetry( async () => - httpRequest($cancelToken, url.href, { + httpRequest(url, { rejectUnauthorized: !this._allowUnauthorized, // this is an inactivity timeout (unclear in Node doc) @@ -403,6 +402,8 @@ export class Xapi extends EventEmitter { // Support XS <= 6.5 with Node => 12 minVersion: 'TLSv1', agent: this.httpAgent, + + signal: $cancelToken, }), { when: { code: 302 }, @@ -411,7 +412,7 @@ export class Xapi extends EventEmitter { if (response === undefined) { throw error } - response.cancel() + response.destroy() url = await this._replaceHostAddressInUrl(new URL(response.headers.location, url)) }, } @@ -467,40 +468,45 @@ export class Xapi extends EventEmitter { url.search = new URLSearchParams(query) await this._setHostAddressInUrl(url, host) - const doRequest = httpRequest.put.bind(undefined, $cancelToken, { - agent: this.httpAgent, + const doRequest = (url, opts) => + httpRequest(url, { + agent: this.httpAgent, - body, - headers, - rejectUnauthorized: !this._allowUnauthorized, + body, + headers, + method: 'PUT', + rejectUnauthorized: !this._allowUnauthorized, + signal: $cancelToken, - // this is an inactivity timeout (unclear in Node doc) - timeout: this._httpInactivityTimeout, + // this is an inactivity timeout (unclear in Node doc) + timeout: this._httpInactivityTimeout, - // Support XS <= 6.5 with Node => 12 - minVersion: 'TLSv1', - }) + // Support XS <= 6.5 with Node => 12 + minVersion: 'TLSv1', + + ...opts, + }) + + const dummyUrl = new URL(url) + dummyUrl.searchParams.delete('task_id') // if body is a stream, sends a dummy request to probe for a redirection // before consuming body const response = await this._addSyncStackTrace( isStream - ? doRequest(url.href, { + ? doRequest(dummyUrl, { body: '', - // omit task_id because this request will fail on purpose - query: 'task_id' in query ? omit(query, 'task_id') : query, - maxRedirects: 0, }).then( response => { - response.cancel() - return doRequest(url.href) + response.destroy() + return doRequest(url) }, async error => { let response if (error != null && (response = error.response) != null) { - response.cancel() + response.destroy() const { headers: { location }, @@ -510,14 +516,14 @@ export class Xapi extends EventEmitter { // ensure the original query is sent const newUrl = new URL(location, url) newUrl.searchParams.set('task_id', query.task_id) - return doRequest((await this._replaceHostAddressInUrl(newUrl)).href) + return doRequest(await this._replaceHostAddressInUrl(newUrl)) } } throw error } ) - : doRequest(url.href) + : doRequest(url) ) if (pTaskResult !== undefined) { @@ -540,13 +546,13 @@ export class Xapi extends EventEmitter { const { req } = response if (!req.finished) { await new Promise((resolve, reject) => { - req.on('finish', resolve).on('error', reject) + req.on('finish', resolve) response.on('error', reject) }) } if (useHack) { - response.cancel() + response.destroy() } else { // consume the response response.resume() diff --git a/packages/xen-api/src/transports/json-rpc.js b/packages/xen-api/src/transports/json-rpc.js index 9a964936b..f8595d6b3 100644 --- a/packages/xen-api/src/transports/json-rpc.js +++ b/packages/xen-api/src/transports/json-rpc.js @@ -1,6 +1,5 @@ import httpRequestPlus from 'http-request-plus' import { format, parse } from 'json-rpc-protocol' -import { join } from 'path' import XapiError from '../_XapiError' @@ -8,41 +7,37 @@ import UnsupportedTransport from './_UnsupportedTransport' // https://github.com/xenserver/xenadmin/blob/0df39a9d83cd82713f32d24704852a0fd57b8a64/XenModel/XenAPI/Session.cs#L403-L433 export default ({ secureOptions, url, agent }) => { - return (method, args) => - httpRequestPlus - .post(url, { - ...secureOptions, - body: format.request(0, method, args), - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - pathname: join(url.pathname, 'jsonrpc'), - agent, - }) - .readAll('utf8') - .then( - text => { - let response - try { - response = parse(text) - } catch (error) { - throw new UnsupportedTransport() - } + url = new URL('./jsonrpc', Object.assign(new URL('http://localhost'), url)) - if (response.type === 'response') { - return response.result - } + return async function (method, args) { + const res = await httpRequestPlus(url, { + ...secureOptions, + body: format.request(0, method, args), + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method: 'POST', + agent, + }).catch(error => { + console.warn('xen-api/transports/json-rpc', error) - throw XapiError.wrap(response.error) - }, - error => { - if (error.response !== undefined) { - // HTTP error - throw new UnsupportedTransport() - } + throw new UnsupportedTransport() + }) - throw error - } - ) + const text = await res.text() + + let response + try { + response = parse(text) + } catch (error) { + throw new UnsupportedTransport() + } + + if (response.type === 'response') { + return response.result + } + + throw XapiError.wrap(response.error) + } } diff --git a/packages/xo-cli/index.mjs b/packages/xo-cli/index.mjs index 47f5fd1c5..86c5e58dc 100755 --- a/packages/xo-cli/index.mjs +++ b/packages/xo-cli/index.mjs @@ -473,14 +473,15 @@ async function call(args) { noop ) - return hrp - .post(url, httpOptions, { - body: input, - headers: { - 'content-length': length, - }, - }) - .readAll('utf-8') + const response = await hrp(url, { + ...httpOptions, + body: input, + headers: { + 'content-length': length, + }, + method: 'POST', + }) + return response.text() } } diff --git a/packages/xo-cli/package.json b/packages/xo-cli/package.json index d70af3704..ef2c483d0 100644 --- a/packages/xo-cli/package.json +++ b/packages/xo-cli/package.json @@ -32,7 +32,7 @@ "chalk": "^5.0.1", "fs-extra": "^11.1.0", "getopts": "^2.3.0", - "http-request-plus": "^0.14.0", + "http-request-plus": "github:JsCommunity/http-request-plus#v1", "human-format": "^1.0.0", "lodash": "^4.17.4", "micromatch": "^4.0.2", diff --git a/packages/xo-server-netbox/src/index.js b/packages/xo-server-netbox/src/index.js index 77a822c90..1c2c1c26e 100644 --- a/packages/xo-server-netbox/src/index.js +++ b/packages/xo-server-netbox/src/index.js @@ -35,11 +35,6 @@ const indexName = (name, index) => { return name.slice(0, NAME_MAX_LENGTH - suffix.length) + suffix } -const onRequest = req => { - req.setTimeout(REQUEST_TIMEOUT) - req.on('timeout', req.abort) -} - class Netbox { #allowUnauthorized #endpoint @@ -108,15 +103,15 @@ class Netbox { const options = { headers: { 'Content-Type': 'application/json', Authorization: `Token ${this.#token}` }, method, - onRequest, rejectUnauthorized: !this.#allowUnauthorized, + timeout: REQUEST_TIMEOUT, } const httpRequest = async () => { try { const response = await this.#xo.httpRequest(url, options) this.#netboxApiVersion = response.headers['api-version'] - const body = await response.readAll() + const body = await response.text() if (body.length > 0) { return JSON.parse(body) } @@ -127,7 +122,7 @@ class Netbox { body: dataDebug, } try { - const body = await error.response.readAll() + const body = await error.response.text() if (body.length > 0) { error.data.error = JSON.parse(body) } diff --git a/packages/xo-server-perf-alert/src/index.js b/packages/xo-server-perf-alert/src/index.js index ef167e218..2b3cd6620 100644 --- a/packages/xo-server-perf-alert/src/index.js +++ b/packages/xo-server-perf-alert/src/index.js @@ -689,7 +689,7 @@ ${entriesWithMissingStats.map(({ listItem }) => listItem).join('\n')}` payload.vm_uuid = xapiObject.uuid } // JSON is not well formed, can't use the default node parser - return JSON5.parse(await (await xapi.getResource('/rrd_updates', payload)).readAll()) + return JSON5.parse(await (await xapi.getResource('/rrd_updates', payload)).text()) } } diff --git a/packages/xo-server-transport-icinga2/src/index.js b/packages/xo-server-transport-icinga2/src/index.js index d1aca3164..64074959e 100644 --- a/packages/xo-server-transport-icinga2/src/index.js +++ b/packages/xo-server-transport-icinga2/src/index.js @@ -121,7 +121,7 @@ class XoServerIcinga2 { exit_status: icinga2Status, }), }) - .readAll() + .then(response => response.text()) } } diff --git a/packages/xo-server-web-hooks/src/index.js b/packages/xo-server-web-hooks/src/index.js index 40f8b1c0e..72226114f 100644 --- a/packages/xo-server-web-hooks/src/index.js +++ b/packages/xo-server-web-hooks/src/index.js @@ -38,10 +38,7 @@ class XoServerHooks { body: JSON.stringify({ ...data, type }), headers: { 'Content-Type': 'application/json' }, method: 'POST', - onRequest: req => { - req.setTimeout(1e4) - req.on('timeout', req.abort) - }, + timeout: 1e4, }) } diff --git a/packages/xo-server/package.json b/packages/xo-server/package.json index 1516f7ab5..5c87a931c 100644 --- a/packages/xo-server/package.json +++ b/packages/xo-server/package.json @@ -82,7 +82,7 @@ "helmet": "^3.9.0", "highland": "^2.11.1", "http-proxy": "^1.16.2", - "http-request-plus": "^0.14.0", + "http-request-plus": "github:JsCommunity/http-request-plus#v1", "http-server-plus": "^1.0.0", "human-format": "^1.0.0", "iterable-backoff": "^0.1.0", diff --git a/packages/xo-server/src/api/vm.mjs b/packages/xo-server/src/api/vm.mjs index bdfb8db78..5d5ebee99 100644 --- a/packages/xo-server/src/api/vm.mjs +++ b/packages/xo-server/src/api/vm.mjs @@ -1133,7 +1133,7 @@ async function handleExport(req, res, { xapi, vmRef, compress, format = 'xva' }) const stream = format === 'ova' ? await xapi.exportVmOva(vmRef) : await xapi.VM_export(FAIL_ON_QUEUE, vmRef, { compress }) - res.on('close', () => stream.cancel()) + res.on('close', () => stream.destroy()) // Remove the filename as it is already part of the URL. stream.headers['content-disposition'] = 'attachment' diff --git a/packages/xo-server/src/xapi-stats.mjs b/packages/xo-server/src/xapi-stats.mjs index a3aed3b73..4f81da821 100644 --- a/packages/xo-server/src/xapi-stats.mjs +++ b/packages/xo-server/src/xapi-stats.mjs @@ -263,7 +263,7 @@ export default class XapiStats { start: timestamp, }, }) - .then(response => response.readAll().then(JSON5.parse)) + .then(response => response.text().then(JSON5.parse)) } // To avoid multiple requests, we keep a cash for the stats and diff --git a/packages/xo-server/src/xapi/mixins/patching.mjs b/packages/xo-server/src/xapi/mixins/patching.mjs index 64a7cc963..4af34d8a0 100644 --- a/packages/xo-server/src/xapi/mixins/patching.mjs +++ b/packages/xo-server/src/xapi/mixins/patching.mjs @@ -65,11 +65,7 @@ export default { async _getXenUpdates() { const response = await this.xo.httpRequest('http://updates.xensource.com/XenServer/updates.xml') - if (response.statusCode !== 200) { - throw new Error('cannot fetch patches list from Citrix') - } - - const data = parseXml(await response.readAll()).patchdata + const data = parseXml(await response.buffer()).patchdata const patches = { __proto__: null } forEach(data.patches.patch, patch => { diff --git a/packages/xo-server/src/xo-mixins/http.mjs b/packages/xo-server/src/xo-mixins/http.mjs index efe6bebfc..feae48ca8 100644 --- a/packages/xo-server/src/xo-mixins/http.mjs +++ b/packages/xo-server/src/xo-mixins/http.mjs @@ -26,13 +26,11 @@ export default class Http { }) } - httpRequest(...args) { - return hrp( - { - agent: this._agent, - }, - ...args - ) + httpRequest(url, opts) { + return hrp(url, { + ...opts, + agent: this._agent, + }) } // Inject the proxy into the environnement, it will be automatically used by `_agent` and by most libs (e.g `axios`) diff --git a/packages/xo-server/src/xo-mixins/proxies.mjs b/packages/xo-server/src/xo-mixins/proxies.mjs index a97696715..f5b827bc0 100644 --- a/packages/xo-server/src/xo-mixins/proxies.mjs +++ b/packages/xo-server/src/xo-mixins/proxies.mjs @@ -419,6 +419,8 @@ export default class Proxy { async callProxyMethod(id, method, params, { assertType = 'scalar' } = {}) { const proxy = await this._getProxy(id) + const url = new URL('https://localhost/api/v1') + const request = { body: format.request(0, method, params), headers: { @@ -426,14 +428,12 @@ export default class Proxy { Cookie: cookie.serialize('authenticationToken', proxy.authenticationToken), }, method: 'POST', - pathname: '/api/v1', - protocol: 'https:', rejectUnauthorized: false, timeout: this._app.config.getDuration('xo-proxy.callTimeout'), } if (proxy.address !== undefined) { - request.host = proxy.address + url.host = proxy.address } else { const vm = this._app.getXapi(proxy.vmUuid).getObjectByUuid(proxy.vmUuid) @@ -444,11 +444,10 @@ export default class Proxy { throw error } - // use hostname field to avoid issues with IPv6 addresses - request.hostname = address + url.hostname = address.includes(':') ? `[${address}]` : address } - const response = await hrp(request) + const response = await hrp(url, request) const authenticationToken = parseSetCookie(response, { map: true, diff --git a/yarn.lock b/yarn.lock index ca2537f4b..9397c466d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10786,14 +10786,11 @@ http-proxy@^1.16.2, http-proxy@^1.17.0, http-proxy@^1.18.1: follow-redirects "^1.0.0" requires-port "^1.0.0" -http-request-plus@^0.14.0: +"http-request-plus@github:JsCommunity/http-request-plus#v1": version "0.14.0" - resolved "https://registry.yarnpkg.com/http-request-plus/-/http-request-plus-0.14.0.tgz#a190ece4b5b67d66ab442d2983a1a1bcd363b866" - integrity sha512-EX/OoPA2vWO7k9Whq+vOSN/nH/P1Ae9smCSzhBIIEU8WLDJyXoH9S8pY4Ieh4yQartrMB1vc2o6T4KV4oIQsDQ== + resolved "https://codeload.github.com/JsCommunity/http-request-plus/tar.gz/af641418c9a91e0285f9662e2794822045062b0c" dependencies: - is-redirect "^1.0.0" - promise-toolbox "^0.19.2" - pump "^3.0.0" + "@xen-orchestra/log" "^0.6.0" http-server-plus@^0.12.0: version "0.12.0" @@ -11621,11 +11618,6 @@ is-promise@^2.0.0, is-promise@^2.2.2: resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== -is-redirect@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" - integrity sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw== - is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@^1.1.0, is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -15879,7 +15871,7 @@ promise-polyfill@^6.0.1: resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057" integrity sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ== -promise-toolbox@^0.19.0, promise-toolbox@^0.19.2: +promise-toolbox@^0.19.0: version "0.19.2" resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.19.2.tgz#7453d117313a9afba6add0c67c46210f7b5833f8" integrity sha512-3956j2kaS4nJG1ANd4SZBQj8GrxLSlvfpVzMT4I7k7K8BkhhpAChXOI3B1VMlU7TQstShBh4D4uKt9zFjahKNg==