feat: support VM/hosts consoles behind HTTP proxy (#6133)

This is a major change in the way xo-server connect to a console, from connecting directly as a TCP socket to using a WebSocket in binary mode.

This was already the case prior c17620e but was changed due to XenServer issues with their WebSocket console implementation, it appears to be working fine now.
This commit is contained in:
Florent BEAUCHAMP 2022-03-10 13:54:32 +01:00 committed by GitHub
parent b9ff3db9b0
commit c99120bd24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 64 additions and 93 deletions

View File

@ -8,6 +8,7 @@
> Users must be able to say: “Nice enhancement, I'm eager to test it”
- [REST API] Expose networks, VBDs, VDIs and VIFs
- [Console] Supports host and VM consoles behind HTTP proxies [#6133](https://github.com/vatesfr/xen-orchestra/pull/6133)
### Bug fixes

View File

@ -3,6 +3,7 @@ import dns from 'dns'
import kindOf from 'kindof'
import ms from 'ms'
import httpRequest from 'http-request-plus'
import ProxyAgent from 'proxy-agent'
import { coalesceCalls } from '@vates/coalesce-calls'
import { Collection } from 'xo-collection'
import { EventEmitter } from 'events'
@ -119,7 +120,9 @@ export class Xapi extends EventEmitter {
}
this._allowUnauthorized = opts.allowUnauthorized
this._httpProxy = opts.httpProxy
if (opts.httpProxy !== undefined) {
this._httpAgent = new ProxyAgent(this._httpProxy)
}
this._setUrl(url)
this._connected = new Promise(resolve => {
@ -153,6 +156,10 @@ export class Xapi extends EventEmitter {
}
}
get httpAgent() {
return this._httpAgent
}
get readOnly() {
return this._readOnly
}
@ -386,6 +393,7 @@ export class Xapi extends EventEmitter {
// Support XS <= 6.5 with Node => 12
minVersion: 'TLSv1',
agent: this.httpAgent,
}),
{
when: { code: 302 },
@ -471,6 +479,7 @@ export class Xapi extends EventEmitter {
query: 'task_id' in query ? omit(query, 'task_id') : query,
maxRedirects: 0,
agent: this.httpAgent,
}).then(
response => {
response.cancel()
@ -881,7 +890,7 @@ export class Xapi extends EventEmitter {
rejectUnauthorized: !this._allowUnauthorized,
},
url,
httpProxy: this._httpProxy,
agent: this.httpAgent,
})
this._url = url
}

View File

@ -1,5 +1,4 @@
import httpRequestPlus from 'http-request-plus'
import ProxyAgent from 'proxy-agent'
import { format, parse } from 'json-rpc-protocol'
import XapiError from '../_XapiError'
@ -7,11 +6,7 @@ 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, httpProxy }) => {
let agent
if (httpProxy !== undefined) {
agent = new ProxyAgent(httpProxy)
}
export default ({ secureOptions, url, agent }) => {
return (method, args) =>
httpRequestPlus
.post(url, {

View File

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

View File

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

View File

@ -683,7 +683,7 @@ const setUpConsoleProxy = (webServer, xo) => {
// FIXME: lost connection due to VM restart is not detected.
webSocketServer.handleUpgrade(req, socket, head, connection => {
proxyConsole(connection, vmConsole, xapi.sessionId)
proxyConsole(connection, vmConsole, xapi.sessionId, xapi.httpAgent)
})
} catch (error) {
try {

View File

@ -1,16 +1,16 @@
import partialStream from 'partial-stream'
import { connect } from 'tls'
import { createLogger } from '@xen-orchestra/log'
import { parse } from 'url'
import { URL } from 'url'
import WebSocket from 'ws'
const log = createLogger('xo:proxy-console')
export default function proxyConsole(ws, vmConsole, sessionId) {
const url = parse(vmConsole.location)
let { hostname } = url
export default function proxyConsole(ws, vmConsole, sessionId, agent) {
const url = new URL(vmConsole.location)
url.protocol = 'wss:'
const { hostname } = url
if (hostname === null || hostname === '') {
const { address } = vmConsole.$VM.$resident_on
hostname = address
url.hostname = address
log.warn(
`host is missing in console (${vmConsole.uuid}) URI (${vmConsole.location}) using host address (${address}) as fallback`
@ -19,72 +19,48 @@ export default function proxyConsole(ws, vmConsole, sessionId) {
let closed = false
const socket = connect(
{
host: hostname,
port: url.port || 443,
rejectUnauthorized: false,
// Support XS <= 6.5 with Node => 12
minVersion: 'TLSv1',
},
() => {
// Write headers.
socket.write(
[`CONNECT ${url.path} HTTP/1.0`, `Host: ${hostname}`, `Cookie: session_id=${sessionId}`, '', ''].join('\r\n')
)
const onSend = error => {
if (error) {
log.debug('error sending to the XO client:', { error })
}
}
socket
.pipe(
partialStream('\r\n\r\n', headers => {
// TODO: check status code 200.
log.debug('connected')
})
)
.on('data', data => {
if (!closed) {
ws.send(data, onSend)
}
})
.on('end', () => {
if (!closed) {
closed = true
log.debug('disconnected from the console')
}
ws.close()
})
ws.on('error', error => {
closed = true
log.debug('error from the XO client:', { error })
socket.end()
})
.on('message', data => {
if (!closed) {
socket.write(data)
}
})
.on('close', () => {
if (!closed) {
closed = true
log.debug('disconnected from the XO client')
}
socket.end()
})
const onSend = error => {
if (error) {
log.debug('error sending to the XO client:', { error })
}
).on('error', error => {
closed = true
log.debug('error from the console:', { error })
}
ws.close()
const socket = new WebSocket(url, ['binary'], {
agent,
rejectUnauthorized: false,
headers: {
cookie: `session_id=${sessionId}`,
},
})
socket
.on('message', data => {
if (!closed) {
ws.send(data, onSend)
}
})
.on('error', error => {
log.warn('error,', error, socket.protocol)
})
.on('close', () => {
closed = true
ws.close()
})
ws.on('error', error => {
closed = true
log.debug('error from the XO client:', { error })
socket.end()
})
.on('message', data => {
if (!closed) {
socket.send(data, { binary: true })
}
})
.on('close', () => {
if (!closed) {
closed = true
log.debug('disconnected from the XO client')
}
})
}