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))
- [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))
- [Servers] Ability to use an HTTP proxy between XO and a server
### Bug fixes
@ -42,8 +43,10 @@
- xo-server-netbox patch
- vhd-lib minor
- xen-api minor
- @xen-orchestra/backup minor
- @xen-orchestra/proxy minor
- vhd-cli minor
- xapi-explore-sr minor
- xo-server patch
- xo-web minor

View File

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

View File

@ -52,6 +52,7 @@ Options:
- `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
- `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
// Force connection.

View File

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

View File

@ -1,4 +1,5 @@
import httpRequestPlus from 'http-request-plus'
import ProxyAgent from 'proxy-agent'
import { format, parse } from 'json-rpc-protocol'
import XapiError from '../_XapiError'
@ -6,7 +7,11 @@ import XapiError from '../_XapiError'
import UnsupportedTransport from './_UnsupportedTransport'
// 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) =>
httpRequestPlus
.post(url, {
@ -17,6 +22,7 @@ export default ({ secureOptions, url }) => {
'Content-Type': 'application/json',
},
path: '/jsonrpc',
agent,
})
.readAll('utf8')
.then(

View File

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

View File

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

View File

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

View File

@ -94,7 +94,7 @@ export default class {
// 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!
// Could we use tokens instead?
// TODO: use plain objects
@ -102,6 +102,7 @@ export default class {
allowUnauthorized,
enabled: true,
host,
httpProxy,
label: label || undefined,
password,
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 xapi = this._xapis[id]
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') {
throw new Error('this entry require disconnecting the server to update it')
@ -153,6 +161,10 @@ export default class {
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)
}
@ -288,6 +300,7 @@ export default class {
readOnly: server.readOnly,
...config.get('xapiOptions'),
httpProxy: server.httpProxy,
guessVhdSizeOnImport: config.get('guessVhdSizeOnImport'),
auth: {

View File

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

View File

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

View File

@ -132,6 +132,18 @@ const COLUMNS = [
itemRenderer: ({ poolId }) => poolId !== undefined && <Pool id={poolId} link />,
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 = [
{
@ -152,13 +164,13 @@ export default class Servers extends Component {
}
_addServer = async () => {
const { label, host, password, username, allowUnauthorized } = this.state
await addServer(host, username, password, label, allowUnauthorized)
const { label, host, password, username, allowUnauthorized, httpProxy } = this.state
await addServer(host, username, password, label, allowUnauthorized, httpProxy)
this.setState({
allowUnauthorized: false,
host: '',
httpProxy: '',
label: '',
password: '',
username: '',
@ -227,6 +239,15 @@ export default class Servers extends Component {
<Toggle onChange={this.linkState('allowUnauthorized')} value={state.allowUnauthorized} />
</Tooltip>
</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'>
{_('serverConnect')}
</ActionButton>