feat(xen-api): add HTTP proxy support (#5958)

See #5436

Using an IP address as HTTPS proxy show this warning: `DeprecationWarning: Setting the TLS ServerName to an IP address is not permitted by RFC 6066`

The corresponding issue is there : TooTallNate/node-https-proxy-agent#127
This commit is contained in:
Florent BEAUCHAMP 2021-10-27 17:30:41 +02:00 committed by GitHub
parent 0c87dee31c
commit 2412f8b1e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 82 additions and 10 deletions

View File

@ -11,6 +11,7 @@
- [Jobs] Ability to copy a job ID (PR [#5951](https://github.com/vatesfr/xen-orchestra/pull/5951)) - [Jobs] Ability to copy a job ID (PR [#5951](https://github.com/vatesfr/xen-orchestra/pull/5951))
- [Host/advanced] Add button to enable/disable the host (PR [#5952](https://github.com/vatesfr/xen-orchestra/pull/5952)) - [Host/advanced] Add button to enable/disable the host (PR [#5952](https://github.com/vatesfr/xen-orchestra/pull/5952))
- [VM/export] Ability to copy the export URL (PR [#5948](https://github.com/vatesfr/xen-orchestra/pull/5948)) - [VM/export] Ability to copy the export URL (PR [#5948](https://github.com/vatesfr/xen-orchestra/pull/5948))
- [Servers] Ability to use an HTTP proxy between XO and a server
### Bug fixes ### Bug fixes
@ -42,8 +43,10 @@
- xo-server-netbox patch - xo-server-netbox patch
- vhd-lib minor - vhd-lib minor
- xen-api minor
- @xen-orchestra/backup minor - @xen-orchestra/backup minor
- @xen-orchestra/proxy minor - @xen-orchestra/proxy minor
- vhd-cli minor - vhd-cli minor
- xapi-explore-sr minor
- xo-server patch - xo-server patch
- xo-web minor - xo-web minor

View File

@ -3,6 +3,7 @@
import archy from 'archy' import archy from 'archy'
import chalk from 'chalk' import chalk from 'chalk'
import execPromise from 'exec-promise' import execPromise from 'exec-promise'
import firstDefined from '@xen-orchestra/defined'
import humanFormat from 'human-format' import humanFormat from 'human-format'
import pw from 'pw' import pw from 'pw'
import { createClient } from 'xen-api' import { createClient } from 'xen-api'
@ -69,11 +70,13 @@ execPromise(async args => {
url = required('Host URL'), url = required('Host URL'),
user = required('Host user'), user = required('Host user'),
password = await askPassword('Host password'), password = await askPassword('Host password'),
httpProxy = firstDefined(process.env.http_proxy, process.env.HTTP_PROXY),
] = args ] = args
const xapi = createClient({ const xapi = createClient({
allowUnauthorized: true, allowUnauthorized: true,
auth: { user, password }, auth: { user, password },
httpProxy,
readOnly: true, readOnly: true,
url, url,
watchEvents: false, watchEvents: false,

View File

@ -52,6 +52,7 @@ Options:
- `auth`: credentials used to sign in (can also be specified in the URL) - `auth`: credentials used to sign in (can also be specified in the URL)
- `readOnly = false`: if true, no methods with side-effects can be called - `readOnly = false`: if true, no methods with side-effects can be called
- `callTimeout`: number of milliseconds after which a call is considered failed (can also be a map of timeouts by methods) - `callTimeout`: number of milliseconds after which a call is considered failed (can also be a map of timeouts by methods)
- `httpProxy`: URL of the HTTP/HTTPS proxy used to reach the host, can include credentials
```js ```js
// Force connection. // Force connection.

View File

@ -115,6 +115,7 @@ export class Xapi extends EventEmitter {
} }
this._allowUnauthorized = opts.allowUnauthorized this._allowUnauthorized = opts.allowUnauthorized
this._httpProxy = opts.httpProxy
this._setUrl(url) this._setUrl(url)
this._connected = new Promise(resolve => { this._connected = new Promise(resolve => {
@ -851,6 +852,7 @@ export class Xapi extends EventEmitter {
rejectUnauthorized: !this._allowUnauthorized, rejectUnauthorized: !this._allowUnauthorized,
}, },
url, url,
httpProxy: this._httpProxy,
}) })
this._url = url this._url = url
} }

View File

@ -1,4 +1,5 @@
import httpRequestPlus from 'http-request-plus' import httpRequestPlus from 'http-request-plus'
import ProxyAgent from 'proxy-agent'
import { format, parse } from 'json-rpc-protocol' import { format, parse } from 'json-rpc-protocol'
import XapiError from '../_XapiError' import XapiError from '../_XapiError'
@ -6,7 +7,11 @@ import XapiError from '../_XapiError'
import UnsupportedTransport from './_UnsupportedTransport' import UnsupportedTransport from './_UnsupportedTransport'
// https://github.com/xenserver/xenadmin/blob/0df39a9d83cd82713f32d24704852a0fd57b8a64/XenModel/XenAPI/Session.cs#L403-L433 // https://github.com/xenserver/xenadmin/blob/0df39a9d83cd82713f32d24704852a0fd57b8a64/XenModel/XenAPI/Session.cs#L403-L433
export default ({ secureOptions, url }) => { export default ({ secureOptions, url, httpProxy }) => {
let agent
if (httpProxy !== undefined) {
agent = new ProxyAgent(httpProxy)
}
return (method, args) => return (method, args) =>
httpRequestPlus httpRequestPlus
.post(url, { .post(url, {
@ -17,6 +22,7 @@ export default ({ secureOptions, url }) => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
path: '/jsonrpc', path: '/jsonrpc',
agent,
}) })
.readAll('utf8') .readAll('utf8')
.then( .then(

View File

@ -1,5 +1,6 @@
import { createClient, createSecureClient } from 'xmlrpc' import { createClient, createSecureClient } from 'xmlrpc'
import { promisify } from 'promise-toolbox' import { promisify } from 'promise-toolbox'
import ProxyAgent from 'proxy-agent'
import XapiError from '../_XapiError' import XapiError from '../_XapiError'
@ -70,10 +71,15 @@ const parseResult = result => {
throw new UnsupportedTransport() throw new UnsupportedTransport()
} }
export default ({ secureOptions, url: { hostname, port, protocol } }) => { export default ({ secureOptions, url: { hostname, port, protocol }, httpProxy }) => {
const secure = protocol === 'https:' const secure = protocol === 'https:'
let agent
if (httpProxy !== undefined) {
agent = new ProxyAgent(httpProxy)
}
const client = (secure ? createSecureClient : createClient)({ const client = (secure ? createSecureClient : createClient)({
...(secure ? secureOptions : undefined), ...(secure ? secureOptions : undefined),
agent,
host: hostname, host: hostname,
path: '/json', path: '/json',
port, port,

View File

@ -1,5 +1,6 @@
import { createClient, createSecureClient } from 'xmlrpc' import { createClient, createSecureClient } from 'xmlrpc'
import { promisify } from 'promise-toolbox' import { promisify } from 'promise-toolbox'
import ProxyAgent from 'proxy-agent'
import XapiError from '../_XapiError' import XapiError from '../_XapiError'
@ -30,10 +31,15 @@ const parseResult = result => {
return result.Value return result.Value
} }
export default ({ secureOptions, url: { hostname, port, protocol } }) => { export default ({ secureOptions, url: { hostname, port, protocol, httpProxy } }) => {
const secure = protocol === 'https:' const secure = protocol === 'https:'
let agent
if (httpProxy !== undefined) {
agent = new ProxyAgent(httpProxy)
}
const client = (secure ? createSecureClient : createClient)({ const client = (secure ? createSecureClient : createClient)({
...(secure ? secureOptions : undefined), ...(secure ? secureOptions : undefined),
agent,
host: hostname, host: hostname,
port, port,
}) })

View File

@ -36,6 +36,10 @@ add.params = {
optional: true, optional: true,
type: 'boolean', type: 'boolean',
}, },
httpProxy: {
optional: true,
type: 'string',
},
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@ -104,6 +108,10 @@ set.params = {
optional: true, optional: true,
type: 'boolean', type: 'boolean',
}, },
httpProxy: {
optional: true,
type: ['string', 'null'],
},
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View File

@ -94,7 +94,7 @@ export default class {
// TODO: disconnect servers on stop. // TODO: disconnect servers on stop.
} }
async registerXenServer({ allowUnauthorized = false, host, label, password, readOnly = false, username }) { async registerXenServer({ allowUnauthorized = false, host, label, password, readOnly = false, username, httpProxy }) {
// FIXME: We are storing passwords which is bad! // FIXME: We are storing passwords which is bad!
// Could we use tokens instead? // Could we use tokens instead?
// TODO: use plain objects // TODO: use plain objects
@ -102,6 +102,7 @@ export default class {
allowUnauthorized, allowUnauthorized,
enabled: true, enabled: true,
host, host,
httpProxy,
label: label || undefined, label: label || undefined,
password, password,
readOnly, readOnly,
@ -119,11 +120,18 @@ export default class {
} }
} }
async updateXenServer(id, { allowUnauthorized, enabled, error, host, label, password, readOnly, username }) { async updateXenServer(
id,
{ allowUnauthorized, enabled, error, host, label, password, readOnly, username, httpProxy }
) {
const server = await this._getXenServer(id) const server = await this._getXenServer(id)
const xapi = this._xapis[id] const xapi = this._xapis[id]
const requireDisconnected = const requireDisconnected =
allowUnauthorized !== undefined || host !== undefined || password !== undefined || username !== undefined allowUnauthorized !== undefined ||
host !== undefined ||
password !== undefined ||
username !== undefined ||
httpProxy !== undefined
if (requireDisconnected && xapi !== undefined && xapi.status !== 'disconnected') { if (requireDisconnected && xapi !== undefined && xapi.status !== 'disconnected') {
throw new Error('this entry require disconnecting the server to update it') throw new Error('this entry require disconnecting the server to update it')
@ -153,6 +161,10 @@ export default class {
server.set('allowUnauthorized', allowUnauthorized) server.set('allowUnauthorized', allowUnauthorized)
} }
if (httpProxy !== undefined) {
// if value is null, pass undefined to the model , so it will delete this optionnal property from the Server object
server.set('httpProxy', httpProxy === null ? undefined : httpProxy)
}
await this._servers.update(server) await this._servers.update(server)
} }
@ -288,6 +300,7 @@ export default class {
readOnly: server.readOnly, readOnly: server.readOnly,
...config.get('xapiOptions'), ...config.get('xapiOptions'),
httpProxy: server.httpProxy,
guessVhdSizeOnImport: config.get('guessVhdSizeOnImport'), guessVhdSizeOnImport: config.get('guessVhdSizeOnImport'),
auth: { auth: {

View File

@ -1826,6 +1826,8 @@ const messages = {
serverEnabled: 'Enabled', serverEnabled: 'Enabled',
serverDisabled: 'Disabled', serverDisabled: 'Disabled',
serverDisable: 'Disable server', serverDisable: 'Disable server',
serverHttpProxy: ' HTTP proxy URL',
serverHttpProxyPlaceHolder: ' HTTP proxy URL',
// ----- Copy VM ----- // ----- Copy VM -----
copyVm: 'Copy VM', copyVm: 'Copy VM',

View File

@ -560,10 +560,11 @@ export const exportConfig = () =>
// Server ------------------------------------------------------------ // Server ------------------------------------------------------------
export const addServer = (host, username, password, label, allowUnauthorized) => export const addServer = (host, username, password, label, allowUnauthorized, httpProxy) =>
_call('server.add', { _call('server.add', {
allowUnauthorized, allowUnauthorized,
host, host,
httpProxy,
label, label,
password, password,
username, username,

View File

@ -132,6 +132,18 @@ const COLUMNS = [
itemRenderer: ({ poolId }) => poolId !== undefined && <Pool id={poolId} link />, itemRenderer: ({ poolId }) => poolId !== undefined && <Pool id={poolId} link />,
name: _('pool'), name: _('pool'),
}, },
{
itemRenderer: (server, formatMessage) => (
<Text
value={server.httpProxy || ''}
// force a null value for falsish value to ensure the value is removed from object if set to ''
onChange={httpProxy => editServer(server, { httpProxy: httpProxy || null })}
placeholder={formatMessage(messages.serverHttpProxyPlaceHolder)}
/>
),
name: _('serverHttpProxy'),
sortCriteria: _ => _.httpProxy,
},
] ]
const INDIVIDUAL_ACTIONS = [ const INDIVIDUAL_ACTIONS = [
{ {
@ -152,13 +164,13 @@ export default class Servers extends Component {
} }
_addServer = async () => { _addServer = async () => {
const { label, host, password, username, allowUnauthorized } = this.state const { label, host, password, username, allowUnauthorized, httpProxy } = this.state
await addServer(host, username, password, label, allowUnauthorized, httpProxy)
await addServer(host, username, password, label, allowUnauthorized)
this.setState({ this.setState({
allowUnauthorized: false, allowUnauthorized: false,
host: '', host: '',
httpProxy: '',
label: '', label: '',
password: '', password: '',
username: '', username: '',
@ -227,6 +239,15 @@ export default class Servers extends Component {
<Toggle onChange={this.linkState('allowUnauthorized')} value={state.allowUnauthorized} /> <Toggle onChange={this.linkState('allowUnauthorized')} value={state.allowUnauthorized} />
</Tooltip> </Tooltip>
</div>{' '} </div>{' '}
<div className='form-group'>
<input
className='form-control'
onChange={this.linkState('httpProxy')}
placeholder={formatMessage(messages.serverHttpProxy)}
type='text'
value={state.httpProxy || ''}
/>
</div>{' '}
<ActionButton btnStyle='primary' form='form-add-server' handler={this._addServer} icon='save'> <ActionButton btnStyle='primary' form='form-add-server' handler={this._addServer} icon='save'>
{_('serverConnect')} {_('serverConnect')}
</ActionButton> </ActionButton>