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:
parent
b9ff3db9b0
commit
c99120bd24
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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, {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user