fix(xo-server/proxy-console): fallback on TCP if WS not available (#6191)

Introduced by c99120bd2

Co-authored-by: Julien Fontanet <julien.fontanet@isonoe.net>
This commit is contained in:
Florent BEAUCHAMP 2022-04-21 14:51:33 +02:00 committed by GitHub
parent 89c72fdbad
commit b176780527
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 160 additions and 41 deletions

View File

@ -14,6 +14,8 @@
> Users must be able to say: “I had this issue, happy to know it's fixed”
- [VM/Host Console] Fix support of older versions of XCP-ng/XS, please not that HTTP proxies are note supported in that case [#6191](https://github.com/vatesfr/xen-orchestra/pull/6191)
### Packages to release
> Packages will be released in the order they are here, therefore, they should

View File

@ -32,6 +32,7 @@
"@vates/compose": "^2.1.0",
"@vates/decorate-with": "^2.0.0",
"@vates/disposable": "^0.1.1",
"@vates/event-listeners-manager": "^0.0.0",
"@vates/multi-key-map": "^0.1.0",
"@vates/parse-duration": "^0.1.1",
"@vates/predicates": "^1.0.0",

View File

@ -1,31 +1,49 @@
import { connect } from 'tls'
import { createLogger } from '@xen-orchestra/log'
import { EventListenersManager } from '@vates/event-listeners-manager'
import { URL } from 'url'
import fromEvent from 'promise-toolbox/fromEvent'
import WebSocket from 'ws'
import partialStream from 'partial-stream'
const log = createLogger('xo:proxy-console')
export default function proxyConsole(ws, vmConsole, sessionId, agent) {
function close(socket) {
const { readyState } = socket
if (readyState !== WebSocket.CLOSED && readyState !== WebSocket.CLOSING) {
socket.close()
}
}
// create a function that will
// - send data in binary
// - not error if the socket is closed
// - properly log any errors
function createSend(socket, name) {
function onSend(error) {
if (error != null) {
log.debug('error sending to the ' + name, { error })
}
}
return function send(data) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(data, { binary: true }, onSend)
}
}
}
export default async function proxyConsole(clientSocket, 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
url.hostname = address
if (url.hostname === '') {
url.hostname = vmConsole.$VM.$resident_on.address
log.warn(
`host is missing in console (${vmConsole.uuid}) URI (${vmConsole.location}) using host address (${address}) as fallback`
`host is missing in console (${vmConsole.uuid}) URI (${vmConsole.location}) using host address (${url.hostname}) as fallback`
)
}
let closed = false
const onSend = error => {
if (error) {
log.debug('error sending to the XO client:', { error })
}
}
const socket = new WebSocket(url, ['binary'], {
const serverSocket = new WebSocket(url, ['binary'], {
agent,
rejectUnauthorized: false,
headers: {
@ -33,34 +51,132 @@ export default function proxyConsole(ws, vmConsole, sessionId, agent) {
},
})
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()
const serverEvents = new EventListenersManager(serverSocket)
const clientEvents = new EventListenersManager(clientSocket)
// create a function that will enqueue data before the socket is open
//
// necessary to avoid losing messages from the client
let sendToServer = (() => {
let queue = []
serverEvents.add('open', () => {
sendToServer = createSend(serverSocket, 'server')
queue.forEach(sendToServer)
queue = undefined
})
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 })
}
return data => {
queue.push(data)
}
})()
clientEvents
.add('close', (code, reason) => {
log.debug('disconnected from the client', { code, reason })
close(serverSocket)
})
.on('close', () => {
if (!closed) {
closed = true
log.debug('disconnected from the XO client')
}
.add('error', error => {
log.debug('error from the client', { error })
})
.add('message', data => sendToServer(data))
serverEvents
.add('close', (code, reason) => {
log.debug('disconnected from the server', { code, reason })
close(clientSocket)
})
.add('error', error => {
log.debug('error from the server', { error })
})
.add('message', createSend(clientSocket, 'client'))
try {
await fromEvent(serverSocket, 'open')
} catch (error) {
clientEvents.removeAll()
serverEvents.removeAll()
log.debug('failing to open the server socket, fallback to legacy implementation', { error })
return proxyConsoleLegacy(clientSocket, url, sessionId)
}
await fromEvent(serverSocket, 'close').catch(log.warn)
}
function proxyConsoleLegacy(ws, url, sessionId) {
let closed = false
const socket = connect(
{
host: url.hostname,
port: url.port || 443,
rejectUnauthorized: false,
// Support XS <= 6.5 with Node => 12
minVersion: 'TLSv1',
},
() => {
// Write headers.
socket.write(
[
`CONNECT ${url.pathname + url.search} HTTP/1.0`,
`Host: ${url.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()
})
}
).on('error', error => {
closed = true
log.debug('error from the console:', { error })
ws.close()
})
}