1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-01-20 14:03:36 +03:00

F OpenNebula/one#5763: VNC in federations (#3219)

Co-authored-by: Tino Vázquez <cvazquez@opennebula.io>
This commit is contained in:
Jorge Miguel Lobo Escalona 2024-09-09 16:10:15 +02:00 committed by GitHub
parent be6bcf877d
commit 1a1bc3c294
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 330 additions and 234 deletions

View File

@ -13,28 +13,29 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo, useMemo, useCallback, ReactElement } from 'react'
import PropTypes from 'prop-types'
import { useHistory, generatePath } from 'react-router-dom'
import { ReactElement, memo, useCallback, useMemo } from 'react'
import { generatePath, useHistory } from 'react-router-dom'
import {
AppleImac2021 as VncIcon,
TerminalOutline as SshIcon,
Windows as RdpIcon,
} from 'iconoir-react'
import { SubmitButton } from 'client/components/FormControl'
import {
Windows as RdpIcon,
TerminalOutline as SshIcon,
AppleImac2021 as VncIcon,
} from 'iconoir-react'
import { useViews } from 'client/features/Auth'
import { useGeneral } from 'client/features/General'
import { useLazyGetGuacamoleSessionQuery } from 'client/features/OneApi/vm'
import { PATH } from 'client/apps/sunstone/routes'
import { Translate } from 'client/components/HOC'
import { RESOURCE_NAMES, T, VM, VM_ACTIONS, _APPS } from 'client/constants'
import {
getHypervisor,
nicsIncludesTheConnectionType,
isAvailableAction,
nicsIncludesTheConnectionType,
} from 'client/models/VirtualMachine'
import { Translate } from 'client/components/HOC'
import { T, VM, RESOURCE_NAMES, VM_ACTIONS, _APPS } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routes'
const GUACAMOLE_BUTTONS = {
[VM_ACTIONS.VNC]: { tooltip: T.Vnc, icon: <VncIcon /> },
@ -49,22 +50,26 @@ const GuacamoleButton = memo(({ vm, connectionType, onClick }) => {
const { icon, tooltip } = GUACAMOLE_BUTTONS[connectionType]
const history = useHistory()
const [getSession, { isLoading }] = useLazyGetGuacamoleSessionQuery()
const { zone, defaultZone } = useGeneral()
const goToConsole = useCallback(
async (evt) => {
try {
evt.stopPropagation()
const params = { id: vm?.ID, type: connectionType }
zone !== defaultZone && (params.zone = zone)
if (typeof onClick === 'function') {
const session = await getSession(params).unwrap()
onClick(session)
} else {
openNewBrowserTab(generatePath(PATH.GUACAMOLE, params))
const path = `${generatePath(PATH.GUACAMOLE, params)}?zone=${zone}`
openNewBrowserTab(path)
}
} catch {}
},
[vm?.ID, connectionType, history, onClick]
[vm?.ID, connectionType, history, onClick, zone]
)
return (

View File

@ -13,19 +13,19 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useRef, useEffect, RefObject } from 'react'
import { WebSocketTunnel, Tunnel, Client } from 'guacamole-common-js'
import { Client, Tunnel, WebSocketTunnel } from 'guacamole-common-js'
import { RefObject, useEffect, useRef, useState } from 'react'
import { useGeneralApi } from 'client/features/General'
import { useGuacamole, useGuacamoleApi } from 'client/features/Guacamole'
import { getConnectString, clientStateToString } from 'client/models/Guacamole'
import { fakeDelay, isDevelopment } from 'client/utils'
import {
GUACAMOLE_STATES_STR,
GuacamoleSession, // eslint-disable-line no-unused-vars
SOCKETS,
GUACAMOLE_STATES_STR,
T,
} from 'client/constants'
import { useGeneralApi } from 'client/features/General'
import { useGuacamole, useGuacamoleApi } from 'client/features/Guacamole'
import { clientStateToString, getConnectString } from 'client/models/Guacamole'
import { fakeDelay } from 'client/utils'
const {
CONNECTING,
@ -49,38 +49,40 @@ const {
* @param {object} options - Client options
* @param {string} options.id - Session includes type and VM id. Eg: '6-vnc'
* @param {RefObject} options.display - Session display. Only exists if display plugins is enabled
* @param {string} options.zone - zone id
* @param {boolean} options.externalZone - is a external zone
* @returns {GuacamoleClientType} Guacamole client props
*/
const GuacamoleClient = ({ id, display }) => {
const guac = useRef(createGuacamoleClient()).current
const GuacamoleClient = ({ id, display, zone, externalZone }) => {
const [changeConnection, setChangeConnection] = useState(false)
const firstZone = useRef(zone).current
const guac = useRef(createGuacamoleClient(externalZone))
// Automatically update the client thumbnail
// guac.client.onsync = () => handleUpdateThumbnail()
if (`${firstZone}` !== `${zone}` && !changeConnection) {
guac.current = createGuacamoleClient(externalZone)
setChangeConnection(true)
}
const { enqueueError, enqueueInfo, enqueueSuccess } = useGeneralApi()
const { token, ...session } = useGuacamole(id)
const {
setConnectionState,
setTunnelUnstable,
setMultiTouchSupport,
// updateThumbnail,
} = useGuacamoleApi(id)
const { setConnectionState, setTunnelUnstable, setMultiTouchSupport } =
useGuacamoleApi(id)
const handleConnect = (width, height, force = false) => {
if (!session?.isUninitialized && !session.isDisconnected && !force) return
isDevelopment() && console.log(`connect ${id} 🔵`)
const options = { token, display, width, height }
zone && (options.zone = zone)
const connectString = getConnectString(options)
guac.client.connect(connectString)
guac.current.client.connect(connectString)
}
const handleDisconnect = () => {
try {
isDevelopment() && console.log(`disconnect ${id} 🔴`)
guac.client?.disconnect()
guac.current.client?.disconnect()
} catch {}
}
@ -92,64 +94,12 @@ const GuacamoleClient = ({ id, display }) => {
handleConnect(width, height, true)
}
/**
* Store the thumbnail of the given managed client within the connection
* history under its associated ID. If the client is not connected, this
* function has no effect.
*/
/* const handleUpdateThumbnail = () => {
const nowTimestamp = new Date().getTime()
const lastTimestamp = session?.thumbnail?.timestamp
if (
lastTimestamp &&
nowTimestamp - lastTimestamp < THUMBNAIL_UPDATE_FREQUENCY
)
return
const clientDisplay = guac.client?.getDisplay()
if (clientDisplay?.getWidth() <= 0 || clientDisplay?.getHeight() <= 0)
return
// Get screenshot
const canvas = clientDisplay.flatten()
// Calculate scale of thumbnail (max 320x240, max zoom 100%)
const scale = Math.min(320 / canvas.width, 240 / canvas.height, 1)
// Create thumbnail canvas
const thumbnail = document.createElement('canvas')
thumbnail.width = canvas.width * scale
thumbnail.height = canvas.height * scale
// Scale screenshot to thumbnail
const context = thumbnail.getContext('2d')
context.drawImage(
canvas,
0,
0,
canvas.width,
canvas.height,
0,
0,
thumbnail.width,
thumbnail.height
)
thumbnail.toBlob((blob) => {
const url = URL.createObjectURL(blob)
const newThumbnail = { timestamp: nowTimestamp, canvas: url }
updateThumbnail({ thumbnail: newThumbnail })
}, 'image/webp')
} */
useEffect(() => {
guac.tunnel.onerror = (status) => {
guac.current.tunnel.onerror = (status) => {
setConnectionState({ state: TUNNEL_ERROR, statusCode: status.code })
}
guac.tunnel.onstatechange = (state) => {
guac.current.tunnel.onstatechange = (state) => {
;({
[Tunnel.State.CONNECTING]: () => {
setConnectionState({ state: CONNECTING })
@ -166,7 +116,7 @@ const GuacamoleClient = ({ id, display }) => {
}[state]?.())
}
guac.client.onstatechange = (state) => {
guac.current.client.onstatechange = (state) => {
const stateString = clientStateToString(state)
const isDisconnect = [DISCONNECTING, DISCONNECTED].includes(stateString)
const isDisconnected = DISCONNECTED === stateString
@ -178,19 +128,19 @@ const GuacamoleClient = ({ id, display }) => {
!isDisconnect && setConnectionState({ state: stateString })
}
guac.client.onerror = (status) => {
guac.current.client.onerror = (status) => {
enqueueError(status.message)
setConnectionState({ state: CLIENT_ERROR, statusCode: status.code })
}
guac.client.onmultitouch = (layer, touches) => {
guac.current.client.onmultitouch = (layer, touches) => {
setMultiTouchSupport({ touches })
}
return () => {
handleDisconnect()
}
}, [id])
}, [id, zone, externalZone, changeConnection])
useEffect(() => {
session?.isError && handleDisconnect()
@ -200,13 +150,14 @@ const GuacamoleClient = ({ id, display }) => {
!session.isConnected && handleConnect()
}, [token])
return { token, ...session, ...guac, handleReconnect }
return { token, ...session, ...guac.current, handleReconnect }
}
const createGuacamoleClient = () => {
const createGuacamoleClient = (externalZone) => {
const { protocol, host } = window.location
const websocketProtocol = protocol === 'https:' ? 'wss:' : 'ws:'
const guacamoleWs = `${websocketProtocol}//${host}/fireedge/${SOCKETS.GUACAMOLE}`
const endpoint = externalZone ? SOCKETS.EXTERNAL_GUACAMOLE : SOCKETS.GUACAMOLE
const guacamoleWs = `${websocketProtocol}//${host}/fireedge/${endpoint}`
const tunnel = new WebSocketTunnel(guacamoleWs)
const client = new Client(tunnel)

View File

@ -17,7 +17,6 @@
/* eslint-disable jsdoc/valid-types */
import { useRef } from 'react'
import { useGetLatest, reducePlugin } from 'client/components/Consoles/utils'
import GuacamoleClient, {
GuacamoleClientType,
} from 'client/components/Consoles/Guacamole/client'
@ -26,6 +25,7 @@ import {
GuacamoleKeyboardPlugin,
GuacamoleMousePlugin,
} from 'client/components/Consoles/Guacamole/plugins'
import { reducePlugin, useGetLatest } from 'client/components/Consoles/utils'
/**
* Creates guacamole session.
@ -55,6 +55,6 @@ const useGuacamoleSession = (options, ...plugins) => {
return getInstance()
}
export * from 'client/components/Consoles/Guacamole/plugins'
export * from 'client/components/Consoles/Guacamole/buttons'
export * from 'client/components/Consoles/Guacamole/plugins'
export { useGuacamoleSession }

View File

@ -13,22 +13,22 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useEffect, useMemo } from 'react'
import { Avatar, Divider, Skeleton, Stack, Typography } from '@mui/material'
import PropTypes from 'prop-types'
import { ReactElement, useEffect, useMemo } from 'react'
import { useHistory } from 'react-router'
import { Stack, Typography, Divider, Skeleton, Avatar } from '@mui/material'
import { useGetVmQuery } from 'client/features/OneApi/vm'
import { useLazyGetServiceQuery } from 'client/features/OneApi/service'
import { useGeneralApi } from 'client/features/General'
import { StatusBadge } from 'client/components/Status'
import { PATH } from 'client/apps/sunstone/routes'
import { Translate } from 'client/components/HOC'
import { OpenNebulaLogo } from 'client/components/Icons'
import MultipleTags from 'client/components/MultipleTags'
import { Translate } from 'client/components/HOC'
import { getIps, getState, isVCenter } from 'client/models/VirtualMachine'
import { StatusBadge } from 'client/components/Status'
import { STATIC_FILES_URL, T, VM_ACTIONS } from 'client/constants'
import { useGeneralApi } from 'client/features/General'
import { useLazyGetServiceQuery } from 'client/features/OneApi/service'
import { useGetVmQuery } from 'client/features/OneApi/vm'
import { timeFromMilliseconds } from 'client/models/Helper'
import { PATH } from 'client/apps/sunstone/routes'
import { T, VM_ACTIONS, STATIC_FILES_URL } from 'client/constants'
import { getIps, getState, isVCenter } from 'client/models/VirtualMachine'
/**
* @param {object} props - Props

View File

@ -19,7 +19,6 @@ import { memo, useCallback, useMemo } from 'react'
import { ConsoleButton } from 'client/components/Buttons'
import { VirtualMachineCard } from 'client/components/Cards'
import { VM_ACTIONS, VM_EXTENDED_POOL } from 'client/constants'
import { useGeneral } from 'client/features/General'
import vmApi, { useUpdateUserTemplateMutation } from 'client/features/OneApi/vm'
import { jsonToXml } from 'client/models/Helper'
@ -28,9 +27,6 @@ const CONNECTION_TYPES = [VNC, RDP, SSH, VMRC]
const Row = memo(
({ original, value, onClickLabel, globalErrors, ...props }) => {
// This is for not showing VNC coneections when the user use other zone.
const { zone, defaultZone } = useGeneral()
const [update] = useUpdateUserTemplateMutation()
const state = vmApi.endpoints.getVms.useQueryState(
@ -64,14 +60,13 @@ const Row = memo(
globalErrors={globalErrors}
actions={
<>
{zone === defaultZone &&
CONNECTION_TYPES.map((connectionType) => (
<ConsoleButton
key={`${memoVm}-${connectionType}`}
connectionType={connectionType}
vm={memoVm}
/>
))}
{CONNECTION_TYPES.map((connectionType) => (
<ConsoleButton
key={`${memoVm}-${connectionType}`}
connectionType={connectionType}
vm={memoVm}
/>
))}
</>
}
/>

View File

@ -172,6 +172,7 @@ export const SOCKETS = {
HOOKS: 'hooks',
PROVISION: 'provision',
GUACAMOLE: 'guacamole',
EXTERNAL_GUACAMOLE: 'external-guacamole',
VMRC: 'vmrc',
}

View File

@ -13,33 +13,56 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement, useMemo, useRef, useEffect } from 'react'
import { useParams, useHistory } from 'react-router'
import { Box, Stack, Container, Typography } from '@mui/material'
import { Box, Container, Stack, Typography } from '@mui/material'
import { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
import { useHistory, useParams } from 'react-router'
import { useLocation } from 'react-router-dom'
import { GuacamoleLogo } from 'client/components/Icons'
import { useViews } from 'client/features/Auth'
import { useGeneral, useGeneralApi } from 'client/features/General'
import { useGetGuacamoleSessionQuery } from 'client/features/OneApi/vm'
import { PATH } from 'client/apps/sunstone/routes'
import {
HeaderVmInfo,
useGuacamoleSession,
GuacamoleDisplay,
GuacamoleKeyboard,
GuacamoleMouse,
GuacamoleClipboard,
GuacamoleCtrlAltDelButton,
GuacamoleReconnectButton,
GuacamoleDisplay,
GuacamoleFullScreenButton,
GuacamoleKeyboard,
GuacamoleMouse,
GuacamoleReconnectButton,
GuacamoleScreenshotButton,
HeaderVmInfo,
useGuacamoleSession,
} from 'client/components/Consoles'
import { GuacamoleLogo } from 'client/components/Icons'
import { PATH } from 'client/apps/sunstone/routes'
import { Tr } from 'client/components/HOC'
import { sentenceCase } from 'client/utils'
import { RESOURCE_NAMES, T } from 'client/constants'
import { sentenceCase } from 'client/utils'
/** @returns {ReactElement} Guacamole container */
const Guacamole = () => {
// set default zone for request
const [isZoneChanged, setIsZoneChanged] = useState(false)
const location = useLocation()
const searchParams = new URLSearchParams(location.search)
const zone = searchParams.get('zone')
const { zone: selectedZone, defaultZone } = useGeneral()
const { changeZone } = useGeneralApi()
useEffect(() => {
const handleChangeZone = async () => {
if (zone && zone !== selectedZone) {
await changeZone(zone)
}
setIsZoneChanged(true)
}
handleChangeZone()
}, [zone, selectedZone, changeZone])
const containerRef = useRef(null)
const headerRef = useRef(null)
@ -52,15 +75,37 @@ const Guacamole = () => {
[view]
)
const { isError: queryIsError } = useGetGuacamoleSessionQuery(
{ id, type },
{ refetchOnMountOrArgChange: false, skip: !isAvailableView }
const paramsGetGuacamoleSession = { id, type }
const { isError: queryIsError, data } = useGetGuacamoleSessionQuery(
paramsGetGuacamoleSession,
{
refetchOnMountOrArgChange: false,
skip: !isAvailableView && !isZoneChanged,
}
)
useEffect(() => {
;(queryIsError || !isAvailableView) && redirectTo(PATH.DASHBOARD)
}, [queryIsError])
const guacamoleOption = useMemo(
() => ({
id: `${id}-${type}`,
container: containerRef.current,
header: headerRef.current,
zone: selectedZone,
externalZone: `${selectedZone}` !== `${defaultZone}`,
}),
[
selectedZone,
containerRef.current?.offsetWidth,
containerRef.current?.offsetHeight,
headerRef.current?.offsetWidth,
headerRef.current?.offsetHeight,
]
)
const {
token,
clientState,
@ -69,19 +114,7 @@ const Guacamole = () => {
isConnected,
...session
} = useGuacamoleSession(
useMemo(
() => ({
id: `${id}-${type}`,
container: containerRef.current,
header: headerRef.current,
}),
[
containerRef.current?.offsetWidth,
containerRef.current?.offsetHeight,
headerRef.current?.offsetWidth,
headerRef.current?.offsetHeight,
]
),
guacamoleOption,
GuacamoleDisplay,
GuacamoleMouse,
GuacamoleKeyboard,
@ -118,7 +151,7 @@ const Guacamole = () => {
gap="1em"
padding="1em"
>
<HeaderVmInfo id={id} type={type} />
{data && <HeaderVmInfo {...paramsGetGuacamoleSession} />}
<Stack
direction={{ sm: 'row', md: 'column' }}
alignItems={{ sm: 'center', md: 'end' }}
@ -126,7 +159,7 @@ const Guacamole = () => {
flexWrap="wrap"
gap="1em"
>
{connectionState && (
{data && connectionState && (
<Stack
title={`${Tr(T.GuacamoleState)}: ${connectionState}`}
flexGrow={1}
@ -141,15 +174,17 @@ const Guacamole = () => {
<GuacamoleLogo />
</Stack>
)}
<Stack direction="row" alignItems="center" gap="1em">
<GuacamoleReconnectButton {...session} />
<GuacamoleScreenshotButton {...session} />
<GuacamoleFullScreenButton {...session} />
<GuacamoleCtrlAltDelButton {...session} />
</Stack>
{data && (
<Stack direction="row" alignItems="center" gap="1em">
<GuacamoleReconnectButton {...session} />
<GuacamoleScreenshotButton {...session} />
<GuacamoleFullScreenButton {...session} />
<GuacamoleCtrlAltDelButton {...session} />
</Stack>
)}
</Stack>
</Stack>
{displayElement}
{data && displayElement}
</Box>
)
}

View File

@ -35,7 +35,7 @@ export const clientStateToString = (clientState) =>
* @returns {string} A string of connection parameters
*/
export const getConnectString = (options = {}) => {
const { token, display = window, dpi, width, height } = options
const { token, display = window, dpi, width, height, zone } = options
// Calculate optimal width/height for display
const pixelDensity = window.devicePixelRatio || 1
@ -47,10 +47,14 @@ export const getConnectString = (options = {}) => {
const displayHeight =
height || (isWindow(display) ? display?.innerHeight : display?.offsetHeight)
return [
const urlOptions = [
`token=${encodeURIComponent(token)}`,
`width=${Math.floor(displayWidth * pixelDensity)}`,
`height=${Math.floor(displayHeight * pixelDensity)}`,
`dpi=${Math.floor(optimalDpi)}`,
].join('&')
]
zone && urlOptions.push(`zone=${zone}`)
return urlOptions.join('&')
}

View File

@ -26,6 +26,8 @@ import {
defaultHost,
defaultPort,
defaultWebpackMode,
endpointExternalGuacamole,
endpointVmrc,
} from './utils/constants/defaults'
import { getLoggerMiddleware, initLogger } from './utils/logger'
import {
@ -43,6 +45,7 @@ import { resolve } from 'path'
import { env } from 'process'
import webpack from 'webpack'
import guacamole from './routes/websockets/guacamole'
import guacamoleProxy from './routes/websockets/guacamoleProxy'
import opennebulaWebsockets from './routes/websockets/opennebula'
import vmrc from './routes/websockets/vmrc'
import { messageTerminal } from './utils/general'
@ -137,6 +140,18 @@ let config = {
message: 'Server could not be started',
}
guacamole(appServer)
appServer.on('upgrade', (req, socket, head) => {
const url = req?.url
if (url.startsWith(endpointVmrc)) {
vmrc.upgrade(req, socket, head)
} else if (url.startsWith(endpointExternalGuacamole)) {
guacamoleProxy.upgrade(req, socket, head)
}
})
appServer.listen(port, host, (err) => {
if (!err) {
config = {
@ -147,8 +162,6 @@ appServer.listen(port, host, (err) => {
}
messageTerminal(config)
})
vmrc(appServer)
guacamole(appServer)
/**
* Handle sigterm and sigint.

View File

@ -365,12 +365,17 @@ const setZones = () => {
const parsedURL = rpc && parse(rpc)
const parsedHost = parsedURL.hostname || ''
return {
const data = {
id: oneZone.ID || '',
name: oneZone.NAME || '',
rpc: rpc,
zeromq: `tcp://${parsedHost}:2101`,
}
oneZone?.TEMPLATE?.FIREEDGE_ENDPOINT &&
(data.fireedge = oneZone?.TEMPLATE?.FIREEDGE_ENDPOINT)
return data
})
}
},

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
const { randomBytes, createCipheriv } = require('crypto')
const { createHash, createCipheriv } = require('crypto')
const { defaults, httpCodes } = require('server/utils/constants')
const {
@ -30,7 +30,12 @@ const { USER_INFO } = userActions
const { VM_INFO } = vmActions
const { ok, unauthorized, internalServerError, badRequest } = httpCodes
const { defaultEmptyFunction, defaultCommandVM, defaultTypeCrypto } = defaults
const {
defaultEmptyFunction,
defaultCommandVM,
defaultTypeCrypto,
defaultHash,
} = defaults
const appConfig = getSunstoneConfig()
const prependCommand = appConfig.sunstone_prepend || ''
@ -116,63 +121,72 @@ const generateGuacamoleSession = (
const { username } = serverAdmin
const oneClient = xmlrpc(`${username}:${username}`, authToken)
const callbackVmInfo = (vmInfoErr, VM, USER) => {
if (vmInfoErr || !VM) {
res.locals.httpCode = httpResponse(
!VM ? internalServerError : unauthorized,
vmInfoErr
)
next()
return
}
const settings = {
vnc: () => getVncSettings(VM),
ssh: () => getSshSettings(VM, USER),
rdp: () => getRdpSettings(VM),
}[ensuredType]?.() ?? { error: '' }
if (settings.error) {
res.locals.httpCode = httpResponse(badRequest, settings.error)
next()
return
}
const connection = {
// expiration,
connection: {
type: ensuredType,
settings: {
security: 'any',
'ignore-cert': 'true',
'enable-drive': 'true',
'create-drive-path': 'true',
...settings,
},
},
}
const wsToken = JSON.stringify(encryptConnection(connection))
const encodedWsToken = Buffer.from(wsToken).toString('base64')
res.locals.httpCode = httpResponse(ok, encodedWsToken)
next()
}
const callbackUserInfo = (userInfoErr, { USER } = {}) => {
if (userInfoErr || !USER) {
res.locals.httpCode = httpResponse(badRequest, userInfoErr)
next()
return
}
// get VM information by id
oneClient({
action: VM_INFO,
parameters: [parseInt(vmId, 10), true],
callback: (vmInfoErr, { VM } = {}) => callbackVmInfo(vmInfoErr, VM, USER),
})
}
// get authenticated user
oneClient({
action: USER_INFO,
parameters: [parseInt(userAuthId, 10), true],
callback: (userInfoErr, { USER } = {}) => {
if (userInfoErr || !USER) {
res.locals.httpCode = httpResponse(badRequest, userInfoErr)
next()
}
// get VM information by id
oneClient({
action: VM_INFO,
parameters: [parseInt(vmId, 10), true],
callback: (vmInfoErr, { VM } = {}) => {
if (vmInfoErr || !VM) {
res.locals.httpCode = httpResponse(unauthorized, vmInfoErr)
next()
}
const settings = {
vnc: () => getVncSettings(VM),
ssh: () => getSshSettings(VM, USER),
rdp: () => getRdpSettings(VM),
}[ensuredType]?.() ?? { error: '' }
if (settings.error) {
res.locals.httpCode = httpResponse(badRequest, settings.error)
next()
}
// const minutesToAdd = 1
// const currentDate = new Date()
// const expiration = currentDate.getTime() + minutesToAdd * 60000
const connection = {
// expiration,
connection: {
type: ensuredType,
settings: {
security: 'any',
'ignore-cert': 'true',
'enable-drive': 'true',
'create-drive-path': 'true',
...settings,
},
},
}
const wsToken = JSON.stringify(encryptConnection(connection))
const encodedWsToken = Buffer.from(wsToken).toString('base64')
res.locals.httpCode = httpResponse(ok, encodedWsToken)
next()
},
})
},
callback: callbackUserInfo,
})
}
@ -296,10 +310,11 @@ const getRdpSettings = (vmInfo) => {
}
const encryptConnection = (data) => {
const iv = randomBytes(16)
const { hash, digest } = defaultHash
const key = global.paths.FIREEDGE_KEY
const keyBuffer = Buffer.from(key, digest)
const iv = createHash(hash).update(keyBuffer).digest().slice(0, 16)
const cipher = createCipheriv(defaultTypeCrypto, key, iv)
const ensuredData = typeof data === 'string' ? data : JSON.stringify(data)
let value = cipher.update(ensuredData, 'utf-8', 'base64')
value += cipher.final('base64')

View File

@ -21,7 +21,7 @@ const {
const basepath = '/vm'
const { POST, GET } = httpMethod
const { resource, postBody } = fromData
const { resource, postBody, query } = fromData
const VM_SAVEASTEMPLATE = 'vm.saveastemplate'
const GUACAMOLE = 'vm.guacamole'
@ -61,6 +61,9 @@ module.exports = {
type: {
from: resource,
},
zone: {
from: query,
},
},
},
},

View File

@ -0,0 +1,79 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2024, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
// eslint-disable-next-line node/no-deprecated-api
const { parse } = require('url')
const { createProxyMiddleware } = require('http-proxy-middleware')
const { getFireedgeConfig } = require('server/utils/yml')
const { genPathResources } = require('server/utils/server')
const { getZone } = require('server/routes/entrypoints/Api/middlawares')
const {
endpointExternalGuacamole,
defaultPort,
defaultProtocol,
} = require('server/utils/constants/defaults')
genPathResources()
const appConfig = getFireedgeConfig()
const port = appConfig.port || defaultPort
const protocol = defaultProtocol
const url = `${protocol}://localhost:${port}`
const getZoneUrl = (req) => {
let zoneURL = ''
const externalURL = req?.url
if (externalURL) {
const parsedURL = parse(externalURL, true)
const zone = parsedURL?.query?.zone
if (zone) {
const dataZone = getZone(zone)
if (dataZone?.fireedge) {
zoneURL = dataZone?.fireedge
} else {
const URL = dataZone.rpc
const zoneDataURL = parse(URL)
zoneURL = `${protocol}://${zoneDataURL.hostname}:${defaultPort}`
}
}
}
return zoneURL
}
const setHeaders = (proxyReq, req) => {
Object.keys(req.headers).forEach((header) => {
if (header.toLowerCase().startsWith('sec-websocket-')) {
proxyReq.setHeader(header, req.headers[header])
}
})
const externalURL = getZoneUrl(req)
externalURL && proxyReq.setHeader('Origin', externalURL)
}
const guacamoleProxy = createProxyMiddleware(endpointExternalGuacamole, {
target: url,
changeOrigin: true,
ws: true,
secure: /^(https):\/\/[^ "]+$/.test(url),
pathRewrite: (path) => path.replace('/external-guacamole', '/guacamole'),
onProxyReqWs: setHeaders,
onProxyReq: setHeaders,
router: (req) => getZoneUrl(req),
})
module.exports = guacamoleProxy

View File

@ -38,7 +38,7 @@ const appConfig = getFireedgeConfig()
const port = appConfig.port || defaultPort
const protocol = defaultProtocol
const url = `${protocol}://localhost:${port}`
const vmrcProxy = createProxyMiddleware(endpointVmrc, {
const vmrc = createProxyMiddleware(endpointVmrc, {
target: url,
changeOrigin: false,
ws: true,
@ -112,15 +112,4 @@ const vmrcProxy = createProxyMiddleware(endpointVmrc, {
},
})
/**
* VMRC Proxy.
*
* @param {object} appServer - express app
*/
const vmrc = (appServer) => {
if (appServer && appServer?.on && appServer?.constructor?.name === 'Server') {
appServer.on('upgrade', vmrcProxy.upgrade)
}
}
module.exports = vmrc

View File

@ -144,6 +144,7 @@ const defaults = {
defaultBaseURL: '',
endpointVmrc: `${baseUrl}vmrc`,
endpointGuacamole: `${baseUrl}guacamole`,
endpointExternalGuacamole: `${baseUrl}external-guacamole`,
defaultNamespace: 'one',
defaultMessageInvalidZone: 'Invalid Zone',
default2FAIssuer: `${appName}-UI`,