mirror of
https://github.com/khairul169/home-lab.git
synced 2025-05-15 00:49:34 +07:00
feat: add vnc app
This commit is contained in:
parent
d1ecd6d00b
commit
d3b89113ae
@ -8,3 +8,4 @@ AUTH_PASSWORD=
|
|||||||
PC_MAC_ADDR=
|
PC_MAC_ADDR=
|
||||||
TERMINAL_SHELL=
|
TERMINAL_SHELL=
|
||||||
FILE_DIRS="/some/path;/another/path"
|
FILE_DIRS="/some/path;/another/path"
|
||||||
|
VNC_PORT=5901
|
||||||
|
50
backend/lib/websockify.ts
Normal file
50
backend/lib/websockify.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import type { WebSocket } from "ws";
|
||||||
|
import net from "node:net";
|
||||||
|
|
||||||
|
// const log = console.log;
|
||||||
|
const log = (..._args: any[]) => null;
|
||||||
|
|
||||||
|
export const websockify = (
|
||||||
|
client: WebSocket,
|
||||||
|
targetHost: string,
|
||||||
|
targetPort: number
|
||||||
|
) => {
|
||||||
|
const target = net.createConnection(targetPort, targetHost, function () {
|
||||||
|
log("connected to target");
|
||||||
|
});
|
||||||
|
|
||||||
|
target.on("data", function (data) {
|
||||||
|
try {
|
||||||
|
client.send(data);
|
||||||
|
} catch (e) {
|
||||||
|
log("Client closed, cleaning up target");
|
||||||
|
target.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
target.on("end", function () {
|
||||||
|
log("target disconnected");
|
||||||
|
client.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
target.on("error", function () {
|
||||||
|
log("target connection error");
|
||||||
|
target.end();
|
||||||
|
client.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("message", function (msg) {
|
||||||
|
// log("CLIENT message: " + msg);
|
||||||
|
target.write(msg as never);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("close", function (code, reason) {
|
||||||
|
log("WebSocket client disconnected: " + code + " [" + reason + "]");
|
||||||
|
target.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on("error", function (a) {
|
||||||
|
log("WebSocket client error: " + a);
|
||||||
|
target.end();
|
||||||
|
});
|
||||||
|
};
|
@ -1,6 +1,9 @@
|
|||||||
import { WebSocketServer } from "ws";
|
import { WebSocketServer } from "ws";
|
||||||
import { verifyToken } from "./lib/jwt";
|
import { verifyToken } from "./lib/jwt";
|
||||||
import { createTerminalSession } from "./lib/terminal";
|
import { createTerminalSession } from "./lib/terminal";
|
||||||
|
import { websockify } from "./lib/websockify";
|
||||||
|
|
||||||
|
const VNC_PORT = parseInt(process.env.VNC_PORT || "") || 5901;
|
||||||
|
|
||||||
const createWsServer = (server: any) => {
|
const createWsServer = (server: any) => {
|
||||||
const wss = new WebSocketServer({ server: server as never });
|
const wss = new WebSocketServer({ server: server as never });
|
||||||
@ -12,7 +15,7 @@ const createWsServer = (server: any) => {
|
|||||||
try {
|
try {
|
||||||
await verifyToken(token || "");
|
await verifyToken(token || "");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
// console.log(err);
|
||||||
ws.close();
|
ws.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -21,6 +24,10 @@ const createWsServer = (server: any) => {
|
|||||||
createTerminalSession(ws);
|
createTerminalSession(ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.pathname === "/vnc") {
|
||||||
|
websockify(ws, "localhost", VNC_PORT);
|
||||||
|
}
|
||||||
|
|
||||||
ws.on("error", console.error);
|
ws.on("error", console.error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"react-native-toast-notifications": "^3.4.0",
|
"react-native-toast-notifications": "^3.4.0",
|
||||||
"react-native-web": "~0.19.6",
|
"react-native-web": "~0.19.6",
|
||||||
"react-query": "^3.39.3",
|
"react-query": "^3.39.3",
|
||||||
|
"react-vnc": "^1.0.0",
|
||||||
"twrnc": "^4.1.0",
|
"twrnc": "^4.1.0",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"utf8": "^3.0.0",
|
"utf8": "^3.0.0",
|
||||||
|
@ -39,7 +39,11 @@ const RootLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<StatusBar style="auto" />
|
<StatusBar style="auto" />
|
||||||
<View style={cn("flex-1 bg-[#f2f7fb]", { paddingTop: insets.top })}>
|
<View
|
||||||
|
style={cn("flex-1 bg-[#f2f7fb] overflow-hidden", {
|
||||||
|
paddingTop: insets.top,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{ contentStyle: { backgroundColor: "#f2f7fb" } }}
|
screenOptions={{ contentStyle: { backgroundColor: "#f2f7fb" } }}
|
||||||
/>
|
/>
|
||||||
|
@ -4,7 +4,7 @@ import api from "@/lib/api";
|
|||||||
import { useAuth } from "@/stores/authStore";
|
import { useAuth } from "@/stores/authStore";
|
||||||
import BackButton from "@ui/BackButton";
|
import BackButton from "@ui/BackButton";
|
||||||
import Input from "@ui/Input";
|
import Input from "@ui/Input";
|
||||||
import { Stack, router, useLocalSearchParams } from "expo-router";
|
import { Stack, useLocalSearchParams } from "expo-router";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useMutation, useQuery } from "react-query";
|
import { useMutation, useQuery } from "react-query";
|
||||||
import FileDrop from "@/components/pages/files/FileDrop";
|
import FileDrop from "@/components/pages/files/FileDrop";
|
||||||
@ -13,7 +13,6 @@ import { HStack } from "@ui/Stack";
|
|||||||
import Button from "@ui/Button";
|
import Button from "@ui/Button";
|
||||||
import { Ionicons } from "@ui/Icons";
|
import { Ionicons } from "@ui/Icons";
|
||||||
import FileInlineViewer from "@/components/pages/files/FileInlineViewer";
|
import FileInlineViewer from "@/components/pages/files/FileInlineViewer";
|
||||||
import { decodeUrl, encodeUrl } from "@/lib/utils";
|
|
||||||
import { FilesContext } from "@/components/pages/files/FilesContext";
|
import { FilesContext } from "@/components/pages/files/FilesContext";
|
||||||
import { FileItem } from "@/types/files";
|
import { FileItem } from "@/types/files";
|
||||||
|
|
||||||
@ -23,7 +22,6 @@ const FilesPage = () => {
|
|||||||
path: "",
|
path: "",
|
||||||
});
|
});
|
||||||
const [viewFile, setViewFile] = useState<FileItem | null>(null);
|
const [viewFile, setViewFile] = useState<FileItem | null>(null);
|
||||||
const searchParams = useLocalSearchParams();
|
|
||||||
const parentPath =
|
const parentPath =
|
||||||
params.path.length > 0
|
params.path.length > 0
|
||||||
? params.path.split("/").slice(0, -1).join("/")
|
? params.path.split("/").slice(0, -1).join("/")
|
@ -1,16 +0,0 @@
|
|||||||
import { API_BASEURL } from "@/lib/constants";
|
|
||||||
import authStore from "@/stores/authStore";
|
|
||||||
import { FileItem } from "@/types/files";
|
|
||||||
|
|
||||||
export function openFile(file: FileItem | string, dl = false) {
|
|
||||||
const url = getFileUrl(file, dl);
|
|
||||||
window.open(url, "_blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFileUrl(file: FileItem | string, dl = false) {
|
|
||||||
const filepath = typeof file === "string" ? file : file.path;
|
|
||||||
const url = new URL(API_BASEURL + "/files/download" + filepath);
|
|
||||||
url.searchParams.set("token", authStore.getState().token);
|
|
||||||
dl && url.searchParams.set("dl", "true");
|
|
||||||
return url.toString();
|
|
||||||
}
|
|
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
import api from "@/lib/api";
|
import api from "@/lib/api";
|
||||||
import { showToast } from "@/stores/toastStore";
|
import { showToast } from "@/stores/toastStore";
|
||||||
|
import { API_BASEURL } from "@/lib/constants";
|
||||||
|
import authStore from "@/stores/authStore";
|
||||||
|
import { FileItem } from "@/types/files";
|
||||||
|
|
||||||
export const wakePcUp = async () => {
|
export const wakePcUp = async () => {
|
||||||
try {
|
try {
|
||||||
@ -11,3 +14,41 @@ export const wakePcUp = async () => {
|
|||||||
showToast("Cannot wake up the PC!", { type: "danger" });
|
showToast("Cannot wake up the PC!", { type: "danger" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function openFile(file: FileItem | string, dl = false) {
|
||||||
|
const url = getFileUrl(file, dl);
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileUrl(file: FileItem | string, dl = false) {
|
||||||
|
const filepath = typeof file === "string" ? file : file.path;
|
||||||
|
const url = new URL(API_BASEURL + "/files/download" + filepath);
|
||||||
|
url.searchParams.set("token", authStore.getState().token);
|
||||||
|
dl && url.searchParams.set("dl", "true");
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openFullscreen(elem: any) {
|
||||||
|
if (elem.requestFullscreen) {
|
||||||
|
elem.requestFullscreen();
|
||||||
|
} else if (elem.webkitRequestFullscreen) {
|
||||||
|
/* Safari */
|
||||||
|
elem.webkitRequestFullscreen();
|
||||||
|
} else if (elem.msRequestFullscreen) {
|
||||||
|
/* IE11 */
|
||||||
|
elem.msRequestFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close fullscreen */
|
||||||
|
export function closeFullscreen() {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else if ((document as any).webkitExitFullscreen) {
|
||||||
|
/* Safari */
|
||||||
|
(document as any).webkitExitFullscreen();
|
||||||
|
} else if ((document as any).msExitFullscreen) {
|
||||||
|
/* IE11 */
|
||||||
|
(document as any).msExitFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,7 +7,7 @@ import Box from "@ui/Box";
|
|||||||
import Text from "@ui/Text";
|
import Text from "@ui/Text";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import "xterm/css/xterm.css";
|
import "xterm/css/xterm.css";
|
||||||
import { BASEURL } from "@/lib/constants";
|
import { BASEURL, WS_BASEURL } from "@/lib/constants";
|
||||||
import { useAuth } from "@/stores/authStore";
|
import { useAuth } from "@/stores/authStore";
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import BackButton from "@ui/BackButton";
|
import BackButton from "@ui/BackButton";
|
||||||
@ -28,11 +28,7 @@ const TerminalPage = () => {
|
|||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
term.loadAddon(fitAddon);
|
term.loadAddon(fitAddon);
|
||||||
|
|
||||||
const baseUrl = BASEURL.replace("https://", "wss://").replace(
|
const socket = new WebSocket(WS_BASEURL + "/terminal?token=" + token);
|
||||||
"http://",
|
|
||||||
"ws://"
|
|
||||||
);
|
|
||||||
const socket = new WebSocket(baseUrl + "/terminal?token=" + token);
|
|
||||||
const attachAddon = new AttachAddon(socket);
|
const attachAddon = new AttachAddon(socket);
|
||||||
|
|
||||||
// Attach the socket to term
|
// Attach the socket to term
|
||||||
|
61
src/app/apps/vnc.tsx
Normal file
61
src/app/apps/vnc.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { WS_BASEURL } from "@/lib/constants";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useAuth } from "@/stores/authStore";
|
||||||
|
import BackButton from "@ui/BackButton";
|
||||||
|
import Box from "@ui/Box";
|
||||||
|
import Button from "@ui/Button";
|
||||||
|
import { Ionicons } from "@ui/Icons";
|
||||||
|
import { Stack } from "expo-router";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import { VncScreen, VncScreenHandle } from "react-vnc";
|
||||||
|
import { openFullscreen } from "./lib";
|
||||||
|
|
||||||
|
const VncPage = () => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null!);
|
||||||
|
const vncRef = useRef<VncScreenHandle>(null!);
|
||||||
|
const { token } = useAuth();
|
||||||
|
|
||||||
|
const onFullscreen = () => {
|
||||||
|
openFullscreen(containerRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Platform.OS !== "web") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack.Screen
|
||||||
|
options={{
|
||||||
|
title: "VNC",
|
||||||
|
headerLeft: () => <BackButton />,
|
||||||
|
headerRight: () => (
|
||||||
|
<Button
|
||||||
|
icon={<Ionicons name="expand-outline" />}
|
||||||
|
variant="ghost"
|
||||||
|
className="h-14 w-14"
|
||||||
|
iconClassName="text-black text-2xl"
|
||||||
|
onPress={onFullscreen}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{token ? (
|
||||||
|
<div ref={containerRef} style={cn("w-full h-full")}>
|
||||||
|
<VncScreen
|
||||||
|
url={WS_BASEURL + "/vnc?token=" + token}
|
||||||
|
resizeSession
|
||||||
|
scaleViewport
|
||||||
|
background="#000000"
|
||||||
|
style={cn("w-full h-full")}
|
||||||
|
ref={vncRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VncPage;
|
@ -7,7 +7,7 @@ import Text from "@ui/Text";
|
|||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import Modal from "react-native-modal";
|
import Modal from "react-native-modal";
|
||||||
import { Video, ResizeMode } from "expo-av";
|
import { Video, ResizeMode } from "expo-av";
|
||||||
import { getFileUrl, openFile } from "@/app/apps/files/utils";
|
import { getFileUrl, openFile } from "@/app/apps/lib";
|
||||||
import { Image } from "react-native";
|
import { Image } from "react-native";
|
||||||
import AudioPlayer from "@/components/containers/AudioPlayer";
|
import AudioPlayer from "@/components/containers/AudioPlayer";
|
||||||
import { FileItem } from "@/types/files";
|
import { FileItem } from "@/types/files";
|
||||||
|
@ -4,11 +4,10 @@ import { FileItem } from "@/types/files";
|
|||||||
import Text from "@ui/Text";
|
import Text from "@ui/Text";
|
||||||
import List from "@ui/List";
|
import List from "@ui/List";
|
||||||
import { Ionicons } from "@ui/Icons";
|
import { Ionicons } from "@ui/Icons";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import ActionSheet from "@ui/ActionSheet";
|
import ActionSheet from "@ui/ActionSheet";
|
||||||
import { HStack } from "@ui/Stack";
|
import { HStack } from "@ui/Stack";
|
||||||
import Button from "@ui/Button";
|
import Button from "@ui/Button";
|
||||||
import { openFile } from "@/app/apps/files/utils";
|
import { openFile } from "@/app/apps/lib";
|
||||||
|
|
||||||
type Store = {
|
type Store = {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
@ -17,13 +17,18 @@ const Apps = (props: Props) => {
|
|||||||
{
|
{
|
||||||
name: "Files",
|
name: "Files",
|
||||||
icon: <Ionicons name="folder" />,
|
icon: <Ionicons name="folder" />,
|
||||||
path: "files/index",
|
path: "files",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Terminal",
|
name: "Terminal",
|
||||||
icon: <Ionicons name="terminal" />,
|
icon: <Ionicons name="terminal" />,
|
||||||
path: "terminal",
|
path: "terminal",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "VNC",
|
||||||
|
icon: <Ionicons name="eye" />,
|
||||||
|
path: "vnc",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Turn on PC",
|
name: "Turn on PC",
|
||||||
icon: <Ionicons name="desktop" />,
|
icon: <Ionicons name="desktop" />,
|
||||||
|
@ -3,3 +3,7 @@ export const BASEURL = __DEV__
|
|||||||
: location.protocol + "//" + location.host;
|
: location.protocol + "//" + location.host;
|
||||||
|
|
||||||
export const API_BASEURL = BASEURL + "/api";
|
export const API_BASEURL = BASEURL + "/api";
|
||||||
|
export const WS_BASEURL = BASEURL.replace("https://", "wss://").replace(
|
||||||
|
"http://",
|
||||||
|
"ws://"
|
||||||
|
);
|
||||||
|
@ -6541,6 +6541,11 @@ react-shallow-renderer@^16.15.0:
|
|||||||
object-assign "^4.1.1"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.12.0 || ^17.0.0 || ^18.0.0"
|
react-is "^16.12.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
|
||||||
|
react-vnc@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-vnc/-/react-vnc-1.0.0.tgz#6ddaa877265a034fe33934f2ca09ceecbfda1789"
|
||||||
|
integrity sha512-LE9i2H6X/njuzbJTRIsopadgyka18k5avUMWDj6kuRsis5O192qm2EIqJ4l8xRRpwrKVFrOkAC5wIgw+PBNAyw==
|
||||||
|
|
||||||
react@18.2.0:
|
react@18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user