feat: add pve vnc session

This commit is contained in:
Khairul Hidayat 2024-11-06 17:32:39 +00:00
parent bde42ca729
commit 2adde048b0
11 changed files with 380 additions and 90 deletions

View File

@ -1,13 +1,33 @@
import { View, Text, ScrollView, Button } from "react-native"; import { View, ScrollView, Button } from "react-native";
import React, { useState } from "react"; import React, { useState } from "react";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import InteractiveSession from "@/components/containers/interactive-session"; import InteractiveSession, {
InteractiveSessionProps,
} from "@/components/containers/interactive-session";
import PagerView from "@/components/ui/pager-view"; import PagerView from "@/components/ui/pager-view";
let nextSession = 1; let nextSession = 1;
type Session = InteractiveSessionProps & { id: string };
const HomePage = () => { const HomePage = () => {
const [sessions, setSessions] = useState<string[]>(["1"]); const [sessions, setSessions] = useState<Session[]>([
{
id: "1",
type: "ssh",
params: { serverId: "1" },
},
{
id: "2",
type: "pve",
params: { client: "vnc", serverId: "2" },
},
{
id: "3",
type: "pve",
params: { client: "xtermjs", serverId: "3" },
},
]);
const [curSession, setSession] = useState(0); const [curSession, setSession] = useState(0);
return ( return (
@ -21,18 +41,18 @@ const HomePage = () => {
> >
{sessions.map((session, idx) => ( {sessions.map((session, idx) => (
<View <View
key={session} key={session.id}
style={{ flexDirection: "row", alignItems: "center" }} style={{ flexDirection: "row", alignItems: "center" }}
> >
<Button <Button
title={"Session " + session} title={"Session " + session.id}
color="#222" color="#222"
onPress={() => setSession(idx)} onPress={() => setSession(idx)}
/> />
<Button <Button
title="X" title="X"
onPress={() => { onPress={() => {
const newSessions = sessions.filter((s) => s !== session); const newSessions = sessions.filter((s) => s.id !== session.id);
setSessions(newSessions); setSessions(newSessions);
setSession( setSession(
Math.min(Math.max(curSession, 0), newSessions.length - 1) Math.min(Math.max(curSession, 0), newSessions.length - 1)
@ -42,23 +62,19 @@ const HomePage = () => {
</View> </View>
))} ))}
<Button {/* <Button
title="[ + ]" title="[ + ]"
onPress={() => { onPress={() => {
nextSession += 1; nextSession += 1;
setSessions([...sessions, nextSession.toString()]); setSessions([...sessions, nextSession.toString()]);
setSession(sessions.length); setSession(sessions.length);
}} }}
/> /> */}
</ScrollView> </ScrollView>
<PagerView style={{ flex: 1 }} page={curSession}> <PagerView style={{ flex: 1 }} page={curSession}>
{sessions.map((session) => ( {sessions.map((session) => (
<InteractiveSession <InteractiveSession key={session.id} {...session} />
key={session}
type="ssh"
options={{ serverId: session }}
/>
))} ))}
</PagerView> </PagerView>
</View> </View>

View File

@ -1,32 +1,45 @@
import React from "react"; import React from "react";
import Terminal from "./terminal"; import Terminal from "./terminal";
import { BASE_WS_URL } from "@/lib/api"; import { BASE_WS_URL } from "@/lib/api";
import VNCViewer from "./vncviewer";
type SSHSessionProps = { type SSHSessionProps = {
type: "ssh"; type: "ssh";
options: { params: {
serverId: string; serverId: string;
}; };
}; };
type Props = SSHSessionProps; type PVESessionProps = {
type: "pve";
params: {
client: "vnc" | "xtermjs";
serverId: string;
};
};
export type InteractiveSessionProps = SSHSessionProps | PVESessionProps;
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
const query = new URLSearchParams({
...params,
});
const InteractiveSession = ({ type, options }: Props) => {
switch (type) { switch (type) {
case "ssh": case "ssh":
const params = new URLSearchParams({ return <Terminal wsUrl={`${BASE_WS_URL}/ws/ssh?${query}`} />;
serverId: options.serverId,
token: "token", case "pve":
}); const url = `${BASE_WS_URL}/ws/pve?${query}`;
return ( return params.client === "vnc" ? (
<Terminal client="xtermjs" wsUrl={`${BASE_WS_URL}/ws/ssh?${params}`} /> <VNCViewer url={url} />
) : (
<Terminal wsUrl={url} />
); );
default: default:
throw new Error("Unknown interactive session type"); throw new Error("Unknown interactive session type");
} }
return null;
}; };
export default InteractiveSession; export default InteractiveSession;

View File

@ -27,13 +27,13 @@ const Keys = {
}; };
type XTermJsProps = { type XTermJsProps = {
client: "xtermjs"; client?: "xtermjs";
wsUrl: string; wsUrl: string;
}; };
type TerminalProps = ComponentPropsWithoutRef<typeof View> & XTermJsProps; type TerminalProps = ComponentPropsWithoutRef<typeof View> & XTermJsProps;
const Terminal = ({ client, style, ...props }: TerminalProps) => { const Terminal = ({ client = "xtermjs", style, ...props }: TerminalProps) => {
const xtermRef = React.useRef<XTermRef>(null); const xtermRef = React.useRef<XTermRef>(null);
const send = (data: string) => { const send = (data: string) => {

View File

@ -0,0 +1,90 @@
"use dom";
import React, { useEffect, useRef } from "react";
type VNCViewerProps = {
url: string;
};
const VNCViewer = ({ ...props }: VNCViewerProps) => {
const screenRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
let clean = () => {};
(async function () {
// Dynamically load noVNC library
const { default: RFB } = await import("@novnc/novnc/lib/rfb");
const rfb = new RFB(screenRef.current!, props.url);
rfb.scaleViewport = true;
// @ts-ignore
const ws: WebSocket = rfb._sock._websocket;
let password: string | null = null;
const onConnect = () => {
// console.log("Connected");
};
const onDisconnect = () => {
// console.log("Disconnected");
};
const onMessage = (e: MessageEvent) => {
const msg = String(e.data);
// Capture password from server
if (msg.startsWith("\x01")) {
password = msg.substring(1);
ws.removeEventListener("message", onMessage);
}
};
const onCredentialsRequired = () => {
rfb.sendCredentials({
password: password || "",
username: "",
target: "",
});
password = null;
};
// const onDesktopName = (e: CustomEvent<{ name: string }>) => {
// console.log("Desktop name:", e.detail.name);
// };
ws.addEventListener("message", onMessage);
rfb.addEventListener("connect", onConnect);
rfb.addEventListener("disconnect", onDisconnect);
rfb.addEventListener("credentialsrequired", onCredentialsRequired);
// rfb.addEventListener("desktopname", onDesktopName);
// Hack: trigger scale update on visibility change
const observer = new ResizeObserver(([entry]) => {
if (entry.contentRect.width > 0 && rfb.scaleViewport) {
(rfb as any)._updateScale();
}
});
observer.observe(screenRef.current!);
clean = () => {
ws.removeEventListener("message", onMessage);
rfb.disconnect();
rfb.removeEventListener("connect", onConnect);
rfb.removeEventListener("disconnect", onDisconnect);
rfb.removeEventListener("credentialsrequired", onCredentialsRequired);
// rfb.removeEventListener("desktopname", onDesktopName);
observer.disconnect();
};
})();
return () => {
clean();
};
}, []);
return <div ref={screenRef} style={{ width: "100%", height: "100vh" }}></div>;
};
export default VNCViewer;

View File

@ -1,10 +1,11 @@
"use dom"; "use dom";
import React, { CSSProperties, FC, forwardRef, useEffect, useRef } from "react"; import React, { CSSProperties, forwardRef, useEffect, useRef } from "react";
import { import {
DOMImperativeFactory, DOMImperativeFactory,
DOMProps, DOMProps,
useDOMImperativeHandle, useDOMImperativeHandle,
IS_DOM,
} from "expo/dom"; } from "expo/dom";
import { Terminal as XTerm } from "@xterm/xterm"; import { Terminal as XTerm } from "@xterm/xterm";
import "@xterm/xterm/css/xterm.css"; import "@xterm/xterm/css/xterm.css";
@ -19,9 +20,6 @@ type XTermJsProps = {
wsUrl: string; wsUrl: string;
}; };
// @ts-ignore
const IS_DOM = typeof ReactNativeWebView !== "undefined";
export interface XTermRef extends DOMImperativeFactory { export interface XTermRef extends DOMImperativeFactory {
send: (...args: JSONValue[]) => void; send: (...args: JSONValue[]) => void;
} }
@ -89,6 +87,15 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
ws.addEventListener("close", onClose); ws.addEventListener("close", onClose);
xterm.onResize(resizeTerminal); xterm.onResize(resizeTerminal);
window.addEventListener("resize", onResize); window.addEventListener("resize", onResize);
// Hack: trigger scale update on visibility change
const observer = new ResizeObserver(([entry]) => {
if (entry.contentRect.width > 0) {
fitAddon.fit();
}
});
observer.observe(containerRef.current!);
onLoad?.(); onLoad?.();
return () => { return () => {
@ -100,6 +107,8 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
ws.removeEventListener("open", onOpen); ws.removeEventListener("open", onOpen);
ws.removeEventListener("close", onClose); ws.removeEventListener("close", onClose);
window.removeEventListener("resize", onResize); window.removeEventListener("resize", onResize);
observer.disconnect();
}; };
}, [wsUrl]); }, [wsUrl]);

View File

@ -17,6 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.0.2", "@expo/vector-icons": "^14.0.2",
"@novnc/novnc": "^1.5.0",
"@react-navigation/bottom-tabs": "7.0.0-rc.36", "@react-navigation/bottom-tabs": "7.0.0-rc.36",
"@react-navigation/native": "7.0.0-rc.21", "@react-navigation/native": "7.0.0-rc.21",
"@xterm/addon-attach": "^0.11.0", "@xterm/addon-attach": "^0.11.0",
@ -48,6 +49,7 @@
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/novnc__novnc": "^1.5.0",
"@types/react": "~18.3.12", "@types/react": "~18.3.12",
"@types/react-test-renderer": "^18.3.0", "@types/react-test-renderer": "^18.3.0",
"jest": "^29.2.1", "jest": "^29.2.1",

125
frontend/pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ importers:
'@expo/vector-icons': '@expo/vector-icons':
specifier: ^14.0.2 specifier: ^14.0.2
version: 14.0.4 version: 14.0.4
'@novnc/novnc':
specifier: ^1.5.0
version: 1.5.0
'@react-navigation/bottom-tabs': '@react-navigation/bottom-tabs':
specifier: 7.0.0-rc.36 specifier: 7.0.0-rc.36
version: 7.0.0-rc.36(@react-navigation/native@7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-screens@4.0.0-beta.16(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1) version: 7.0.0-rc.36(@react-navigation/native@7.0.0-rc.21(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@4.12.0(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native-screens@4.0.0-beta.16(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)
@ -99,6 +102,9 @@ importers:
'@types/jest': '@types/jest':
specifier: ^29.5.12 specifier: ^29.5.12
version: 29.5.14 version: 29.5.14
'@types/novnc__novnc':
specifier: ^1.5.0
version: 1.5.0
'@types/react': '@types/react':
specifier: ~18.3.12 specifier: ~18.3.12
version: 18.3.12 version: 18.3.12
@ -107,10 +113,10 @@ importers:
version: 18.3.0 version: 18.3.0
jest: jest:
specifier: ^29.2.1 specifier: ^29.2.1
version: 29.7.0(@types/node@22.9.0) version: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)
jest-expo: jest-expo:
specifier: ~52.0.0-preview.3 specifier: ~52.0.0-preview.3
version: 52.0.0-preview.3(@babel/core@7.26.0)(expo@52.0.0-preview.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-native-webview@13.12.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(jest@29.7.0(@types/node@22.9.0))(react-dom@18.3.1(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)(webpack@5.96.1) version: 52.0.0-preview.3(@babel/core@7.26.0)(expo@52.0.0-preview.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-native-webview@13.12.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(jest@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0))(react-dom@18.3.1(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)(webpack@5.96.1)
react-test-renderer: react-test-renderer:
specifier: 18.3.1 specifier: 18.3.1
version: 18.3.1(react@18.3.1) version: 18.3.1(react@18.3.1)
@ -1060,6 +1066,9 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
'@novnc/novnc@1.5.0':
resolution: {integrity: sha512-4yGHOtUCnEJUCsgEt/L78eeJu00kthurLBWXFiaXfonNx0pzbs6R/3gJb1byZe6iAE8V9MF0syQb0xIL8MSOtQ==}
'@npmcli/fs@3.1.1': '@npmcli/fs@3.1.1':
resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@ -1298,6 +1307,12 @@ packages:
'@types/node@22.9.0': '@types/node@22.9.0':
resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==}
'@types/novnc__novnc@1.5.0':
resolution: {integrity: sha512-9DrDJK1hUT6Cbp4t03IsU/DsR6ndnIrDgZVrzITvspldHQ7n81F3wUDfq89zmPM3wg4GErH11IQa0QuTgLMf+w==}
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
'@types/prop-types@15.7.13': '@types/prop-types@15.7.13':
resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==}
@ -1568,6 +1583,10 @@ packages:
resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
babel-plugin-macros@3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'}
babel-plugin-polyfill-corejs2@0.4.11: babel-plugin-polyfill-corejs2@0.4.11:
resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==}
peerDependencies: peerDependencies:
@ -1884,6 +1903,10 @@ packages:
resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==}
engines: {node: '>=4'} engines: {node: '>=4'}
cosmiconfig@7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
engines: {node: '>=10'}
create-jest@29.7.0: create-jest@29.7.0:
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -2598,6 +2621,10 @@ packages:
resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==}
engines: {node: '>=4'} engines: {node: '>=4'}
import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'}
import-local@3.2.0: import-local@3.2.0:
resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -3458,6 +3485,10 @@ packages:
package-json-from-dist@1.0.1: package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
parse-json@4.0.0: parse-json@4.0.0:
resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -3807,6 +3838,10 @@ packages:
resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==}
engines: {node: '>=4'} engines: {node: '>=4'}
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
resolve-from@5.0.0: resolve-from@5.0.0:
resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -4575,6 +4610,10 @@ packages:
yallist@4.0.0: yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
yargs-parser@21.1.1: yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -5849,7 +5888,7 @@ snapshots:
jest-util: 29.7.0 jest-util: 29.7.0
slash: 3.0.0 slash: 3.0.0
'@jest/core@29.7.0': '@jest/core@29.7.0(babel-plugin-macros@3.1.0)':
dependencies: dependencies:
'@jest/console': 29.7.0 '@jest/console': 29.7.0
'@jest/reporters': 29.7.0 '@jest/reporters': 29.7.0
@ -5863,7 +5902,7 @@ snapshots:
exit: 0.1.2 exit: 0.1.2
graceful-fs: 4.2.11 graceful-fs: 4.2.11
jest-changed-files: 29.7.0 jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@22.9.0) jest-config: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)
jest-haste-map: 29.7.0 jest-haste-map: 29.7.0
jest-message-util: 29.7.0 jest-message-util: 29.7.0
jest-regex-util: 29.6.3 jest-regex-util: 29.6.3
@ -6040,6 +6079,8 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5 '@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1 fastq: 1.17.1
'@novnc/novnc@1.5.0': {}
'@npmcli/fs@3.1.1': '@npmcli/fs@3.1.1':
dependencies: dependencies:
semver: 7.6.3 semver: 7.6.3
@ -6399,6 +6440,11 @@ snapshots:
dependencies: dependencies:
undici-types: 6.19.8 undici-types: 6.19.8
'@types/novnc__novnc@1.5.0': {}
'@types/parse-json@4.0.2':
optional: true
'@types/prop-types@15.7.13': {} '@types/prop-types@15.7.13': {}
'@types/react-test-renderer@18.3.0': '@types/react-test-renderer@18.3.0':
@ -6690,6 +6736,13 @@ snapshots:
'@types/babel__core': 7.20.5 '@types/babel__core': 7.20.5
'@types/babel__traverse': 7.20.6 '@types/babel__traverse': 7.20.6
babel-plugin-macros@3.1.0:
dependencies:
'@babel/runtime': 7.26.0
cosmiconfig: 7.1.0
resolve: 1.22.8
optional: true
babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.26.0): babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.26.0):
dependencies: dependencies:
'@babel/compat-data': 7.26.2 '@babel/compat-data': 7.26.2
@ -7044,13 +7097,22 @@ snapshots:
js-yaml: 3.14.1 js-yaml: 3.14.1
parse-json: 4.0.0 parse-json: 4.0.0
create-jest@29.7.0(@types/node@22.9.0): cosmiconfig@7.1.0:
dependencies:
'@types/parse-json': 4.0.2
import-fresh: 3.3.0
parse-json: 5.2.0
path-type: 4.0.0
yaml: 1.10.2
optional: true
create-jest@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0):
dependencies: dependencies:
'@jest/types': 29.6.3 '@jest/types': 29.6.3
chalk: 4.1.2 chalk: 4.1.2
exit: 0.1.2 exit: 0.1.2
graceful-fs: 4.2.11 graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@22.9.0) jest-config: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)
jest-util: 29.7.0 jest-util: 29.7.0
prompts: 2.4.2 prompts: 2.4.2
transitivePeerDependencies: transitivePeerDependencies:
@ -7121,7 +7183,9 @@ snapshots:
decode-uri-component@0.2.2: {} decode-uri-component@0.2.2: {}
dedent@1.5.3: {} dedent@1.5.3(babel-plugin-macros@3.1.0):
optionalDependencies:
babel-plugin-macros: 3.1.0
deep-extend@0.6.0: {} deep-extend@0.6.0: {}
@ -7790,6 +7854,12 @@ snapshots:
caller-path: 2.0.0 caller-path: 2.0.0
resolve-from: 3.0.0 resolve-from: 3.0.0
import-fresh@3.3.0:
dependencies:
parent-module: 1.0.1
resolve-from: 4.0.0
optional: true
import-local@3.2.0: import-local@3.2.0:
dependencies: dependencies:
pkg-dir: 4.2.0 pkg-dir: 4.2.0
@ -7944,7 +8014,7 @@ snapshots:
jest-util: 29.7.0 jest-util: 29.7.0
p-limit: 3.1.0 p-limit: 3.1.0
jest-circus@29.7.0: jest-circus@29.7.0(babel-plugin-macros@3.1.0):
dependencies: dependencies:
'@jest/environment': 29.7.0 '@jest/environment': 29.7.0
'@jest/expect': 29.7.0 '@jest/expect': 29.7.0
@ -7953,7 +8023,7 @@ snapshots:
'@types/node': 22.9.0 '@types/node': 22.9.0
chalk: 4.1.2 chalk: 4.1.2
co: 4.6.0 co: 4.6.0
dedent: 1.5.3 dedent: 1.5.3(babel-plugin-macros@3.1.0)
is-generator-fn: 2.1.0 is-generator-fn: 2.1.0
jest-each: 29.7.0 jest-each: 29.7.0
jest-matcher-utils: 29.7.0 jest-matcher-utils: 29.7.0
@ -7970,16 +8040,16 @@ snapshots:
- babel-plugin-macros - babel-plugin-macros
- supports-color - supports-color
jest-cli@29.7.0(@types/node@22.9.0): jest-cli@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0):
dependencies: dependencies:
'@jest/core': 29.7.0 '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)
'@jest/test-result': 29.7.0 '@jest/test-result': 29.7.0
'@jest/types': 29.6.3 '@jest/types': 29.6.3
chalk: 4.1.2 chalk: 4.1.2
create-jest: 29.7.0(@types/node@22.9.0) create-jest: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)
exit: 0.1.2 exit: 0.1.2
import-local: 3.2.0 import-local: 3.2.0
jest-config: 29.7.0(@types/node@22.9.0) jest-config: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)
jest-util: 29.7.0 jest-util: 29.7.0
jest-validate: 29.7.0 jest-validate: 29.7.0
yargs: 17.7.2 yargs: 17.7.2
@ -7989,7 +8059,7 @@ snapshots:
- supports-color - supports-color
- ts-node - ts-node
jest-config@29.7.0(@types/node@22.9.0): jest-config@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0):
dependencies: dependencies:
'@babel/core': 7.26.0 '@babel/core': 7.26.0
'@jest/test-sequencer': 29.7.0 '@jest/test-sequencer': 29.7.0
@ -8000,7 +8070,7 @@ snapshots:
deepmerge: 4.3.1 deepmerge: 4.3.1
glob: 7.2.3 glob: 7.2.3
graceful-fs: 4.2.11 graceful-fs: 4.2.11
jest-circus: 29.7.0 jest-circus: 29.7.0(babel-plugin-macros@3.1.0)
jest-environment-node: 29.7.0 jest-environment-node: 29.7.0
jest-get-type: 29.6.3 jest-get-type: 29.6.3
jest-regex-util: 29.6.3 jest-regex-util: 29.6.3
@ -8062,7 +8132,7 @@ snapshots:
jest-mock: 29.7.0 jest-mock: 29.7.0
jest-util: 29.7.0 jest-util: 29.7.0
jest-expo@52.0.0-preview.3(@babel/core@7.26.0)(expo@52.0.0-preview.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-native-webview@13.12.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(jest@29.7.0(@types/node@22.9.0))(react-dom@18.3.1(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)(webpack@5.96.1): jest-expo@52.0.0-preview.3(@babel/core@7.26.0)(expo@52.0.0-preview.19(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-native-webview@13.12.2(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1))(jest@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0))(react-dom@18.3.1(react@18.3.1))(react-native@0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1))(react@18.3.1)(webpack@5.96.1):
dependencies: dependencies:
'@expo/config': 10.0.2 '@expo/config': 10.0.2
'@expo/json-file': 9.0.0 '@expo/json-file': 9.0.0
@ -8075,7 +8145,7 @@ snapshots:
jest-environment-jsdom: 29.7.0 jest-environment-jsdom: 29.7.0
jest-snapshot: 29.7.0 jest-snapshot: 29.7.0
jest-watch-select-projects: 2.0.0 jest-watch-select-projects: 2.0.0
jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@22.9.0)) jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0))
json5: 2.2.3 json5: 2.2.3
lodash: 4.17.21 lodash: 4.17.21
react-native: 0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1) react-native: 0.76.1(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(react@18.3.1)
@ -8270,11 +8340,11 @@ snapshots:
chalk: 3.0.0 chalk: 3.0.0
prompts: 2.4.2 prompts: 2.4.2
jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@22.9.0)): jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)):
dependencies: dependencies:
ansi-escapes: 6.2.1 ansi-escapes: 6.2.1
chalk: 4.1.2 chalk: 4.1.2
jest: 29.7.0(@types/node@22.9.0) jest: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)
jest-regex-util: 29.6.3 jest-regex-util: 29.6.3
jest-watcher: 29.7.0 jest-watcher: 29.7.0
slash: 5.1.0 slash: 5.1.0
@ -8305,12 +8375,12 @@ snapshots:
merge-stream: 2.0.0 merge-stream: 2.0.0
supports-color: 8.1.1 supports-color: 8.1.1
jest@29.7.0(@types/node@22.9.0): jest@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0):
dependencies: dependencies:
'@jest/core': 29.7.0 '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)
'@jest/types': 29.6.3 '@jest/types': 29.6.3
import-local: 3.2.0 import-local: 3.2.0
jest-cli: 29.7.0(@types/node@22.9.0) jest-cli: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- babel-plugin-macros - babel-plugin-macros
@ -8944,6 +9014,11 @@ snapshots:
package-json-from-dist@1.0.1: {} package-json-from-dist@1.0.1: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
optional: true
parse-json@4.0.0: parse-json@4.0.0:
dependencies: dependencies:
error-ex: 1.3.2 error-ex: 1.3.2
@ -9355,6 +9430,9 @@ snapshots:
resolve-from@3.0.0: {} resolve-from@3.0.0: {}
resolve-from@4.0.0:
optional: true
resolve-from@5.0.0: {} resolve-from@5.0.0: {}
resolve-workspace-root@2.0.0: {} resolve-workspace-root@2.0.0: {}
@ -10072,6 +10150,9 @@ snapshots:
yallist@4.0.0: {} yallist@4.0.0: {}
yaml@1.10.2:
optional: true
yargs-parser@21.1.1: {} yargs-parser@21.1.1: {}
yargs@17.7.2: yargs@17.7.2:

View File

@ -2,6 +2,7 @@
"extends": "expo/tsconfig.base", "extends": "expo/tsconfig.base",
"compilerOptions": { "compilerOptions": {
"strict": true, "strict": true,
"module": "esnext",
"paths": { "paths": {
"@/*": [ "@/*": [
"./*" "./*"

View File

@ -89,7 +89,7 @@ func (pve *PVEServer) GetAccessTicket() (*PVEAccessTicket, error) {
} }
type PVEInstance struct { type PVEInstance struct {
Type string Type string // "qemu" | "lxc"
Node string Node string
VMID string VMID string
} }

View File

@ -22,7 +22,19 @@ type PVEConfig struct {
PrivateKeyPassphrase string PrivateKeyPassphrase string
} }
func (pve *PVEServer) NewTerminalSession(c *websocket.Conn, access *PVEAccessTicket, instance *PVEInstance, ticket *PVEVNCTicketData) error { // https://github.com/proxmox/pve-xtermjs/blob/master/README
func (pve *PVEServer) NewTerminalSession(c *websocket.Conn, instance *PVEInstance) error {
access, err := pve.GetAccessTicket()
if err != nil {
return err
}
ticket, err := pve.GetVNCTicket(access, instance, false)
if err != nil {
return err
}
url := fmt.Sprintf("wss://%s:%d/api2/json/nodes/%s/%s/%s/vncwebsocket?port=%s&vncticket=%s", url := fmt.Sprintf("wss://%s:%d/api2/json/nodes/%s/%s/%s/vncwebsocket?port=%s&vncticket=%s",
pve.HostName, pve.Port, instance.Node, instance.Type, instance.VMID, ticket.Port, url.QueryEscape(ticket.Ticket)) pve.HostName, pve.Port, instance.Node, instance.Type, instance.VMID, ticket.Port, url.QueryEscape(ticket.Ticket))
@ -44,8 +56,6 @@ func (pve *PVEServer) NewTerminalSession(c *websocket.Conn, access *PVEAccessTic
// Send first ticket line // Send first ticket line
ws.WriteMessage(fastWs.TextMessage, []byte(fmt.Sprintf("%s:%s\n", access.Username, access.Ticket))) ws.WriteMessage(fastWs.TextMessage, []byte(fmt.Sprintf("%s:%s\n", access.Username, access.Ticket)))
// https://github.com/proxmox/pve-xtermjs/blob/master/README
go func() { go func() {
for { for {
t, msg, err := c.ReadMessage() t, msg, err := c.ReadMessage()
@ -65,9 +75,8 @@ func (pve *PVEServer) NewTerminalSession(c *websocket.Conn, access *PVEAccessTic
} }
msg = []byte(fmt.Sprintf("0:%d:%s\n", len(msg), string(msg))) msg = []byte(fmt.Sprintf("0:%d:%s\n", len(msg), string(msg)))
err = ws.WriteMessage(t, msg)
if err != nil { if err = ws.WriteMessage(t, msg); err != nil {
log.Println("Error writing to Proxmox:", err) log.Println("Error writing to Proxmox:", err)
break break
} }
@ -85,8 +94,70 @@ func (pve *PVEServer) NewTerminalSession(c *websocket.Conn, access *PVEAccessTic
continue continue
} }
err = c.WriteMessage(t, msg) if err = c.WriteMessage(t, msg); err != nil {
if err != nil { log.Println("Error writing to client:", err)
break
}
}
return nil
}
func (pve *PVEServer) NewVNCSession(c *websocket.Conn, instance *PVEInstance) error {
access, err := pve.GetAccessTicket()
if err != nil {
return err
}
ticket, err := pve.GetVNCTicket(access, instance, true)
if err != nil {
return err
}
url := fmt.Sprintf("wss://%s:%d/api2/json/nodes/%s/%s/%s/vncwebsocket?port=%s&vncticket=%s",
pve.HostName, pve.Port, instance.Node, instance.Type, instance.VMID, ticket.Port, url.QueryEscape(ticket.Ticket))
headers := http.Header{}
headers.Add("Authorization", "PVEAPIToken="+access.Username)
headers.Add("Cookie", "PVEAuthCookie="+access.Ticket)
dialer := fastWs.Dialer{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
ws, _, err := dialer.Dial(url, headers)
if err != nil {
log.Println("Error connecting to Proxmox WebSocket:", err)
return err
}
defer ws.Close()
// Send vnc password
c.WriteMessage(fastWs.TextMessage, []byte(fmt.Sprintf("\x01%s", ticket.Ticket)))
go func() {
for {
t, msg, err := c.ReadMessage()
if err != nil {
log.Println("Error reading from client:", err)
break
}
if err = ws.WriteMessage(t, msg); err != nil {
log.Println("Error writing to Proxmox:", err)
break
}
}
}()
for {
t, msg, err := ws.ReadMessage()
if err != nil {
log.Println("Error reading from Proxmox:", err)
break
}
if err = c.WriteMessage(t, msg); err != nil {
log.Println("Error writing to client:", err) log.Println("Error writing to client:", err)
break break
} }

View File

@ -31,42 +31,49 @@ func main() {
return fiber.ErrUpgradeRequired return fiber.ErrUpgradeRequired
}) })
// app.Get("/ws/ssh", websocket.New(func(c *websocket.Conn) {
// err := lib.NewSSHWebsocketSession(c, &lib.SSHConfig{
// HostName: "10.0.0.102",
// User: "root",
// Password: "ausya2",
// })
// if err != nil {
// msg := fmt.Sprintf("\r\n%s\r\n", err.Error())
// c.WriteMessage(websocket.TextMessage, []byte(msg))
// }
// }))
app.Get("/ws/ssh", websocket.New(func(c *websocket.Conn) { app.Get("/ws/ssh", websocket.New(func(c *websocket.Conn) {
node := &lib.PVEInstance{ cfg := &lib.SSHConfig{
HostName: "10.0.0.102",
User: "root",
Password: "ausya2",
}
if err := lib.NewSSHWebsocketSession(c, cfg); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
}))
app.Get("/ws/pve", websocket.New(func(c *websocket.Conn) {
client := c.Query("client")
serverId := c.Query("serverId")
var node *lib.PVEInstance
switch serverId {
case "2":
node = &lib.PVEInstance{
Type: "qemu",
Node: "pve",
VMID: "105",
}
case "3":
node = &lib.PVEInstance{
Type: "lxc", Type: "lxc",
Node: "pve", Node: "pve",
VMID: "102", VMID: "102",
} }
}
var err error
if client == "vnc" {
err = pve.NewVNCSession(c, node)
} else {
err = pve.NewTerminalSession(c, node)
}
access, err := pve.GetAccessTicket()
if err != nil { if err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error())) c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
} }
ticket, err := pve.GetVNCTicket(access, node, false)
if err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
return
}
if err := pve.NewTerminalSession(c, access, node, ticket); err != nil {
c.WriteMessage(websocket.TextMessage, []byte(err.Error()))
}
})) }))
app.Listen(":3000") app.Listen(":3000")
} }