mirror of
https://github.com/khairul169/home-lab.git
synced 2025-05-14 16:39: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=
|
||||
TERMINAL_SHELL=
|
||||
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 { verifyToken } from "./lib/jwt";
|
||||
import { createTerminalSession } from "./lib/terminal";
|
||||
import { websockify } from "./lib/websockify";
|
||||
|
||||
const VNC_PORT = parseInt(process.env.VNC_PORT || "") || 5901;
|
||||
|
||||
const createWsServer = (server: any) => {
|
||||
const wss = new WebSocketServer({ server: server as never });
|
||||
@ -12,7 +15,7 @@ const createWsServer = (server: any) => {
|
||||
try {
|
||||
await verifyToken(token || "");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
// console.log(err);
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
@ -21,6 +24,10 @@ const createWsServer = (server: any) => {
|
||||
createTerminalSession(ws);
|
||||
}
|
||||
|
||||
if (url.pathname === "/vnc") {
|
||||
websockify(ws, "localhost", VNC_PORT);
|
||||
}
|
||||
|
||||
ws.on("error", console.error);
|
||||
});
|
||||
};
|
||||
|
@ -40,6 +40,7 @@
|
||||
"react-native-toast-notifications": "^3.4.0",
|
||||
"react-native-web": "~0.19.6",
|
||||
"react-query": "^3.39.3",
|
||||
"react-vnc": "^1.0.0",
|
||||
"twrnc": "^4.1.0",
|
||||
"typescript": "^5.3.0",
|
||||
"utf8": "^3.0.0",
|
||||
|
@ -39,7 +39,11 @@ const RootLayout = () => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<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
|
||||
screenOptions={{ contentStyle: { backgroundColor: "#f2f7fb" } }}
|
||||
/>
|
||||
|
@ -4,7 +4,7 @@ import api from "@/lib/api";
|
||||
import { useAuth } from "@/stores/authStore";
|
||||
import BackButton from "@ui/BackButton";
|
||||
import Input from "@ui/Input";
|
||||
import { Stack, router, useLocalSearchParams } from "expo-router";
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import React, { useState } from "react";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import FileDrop from "@/components/pages/files/FileDrop";
|
||||
@ -13,7 +13,6 @@ import { HStack } from "@ui/Stack";
|
||||
import Button from "@ui/Button";
|
||||
import { Ionicons } from "@ui/Icons";
|
||||
import FileInlineViewer from "@/components/pages/files/FileInlineViewer";
|
||||
import { decodeUrl, encodeUrl } from "@/lib/utils";
|
||||
import { FilesContext } from "@/components/pages/files/FilesContext";
|
||||
import { FileItem } from "@/types/files";
|
||||
|
||||
@ -23,7 +22,6 @@ const FilesPage = () => {
|
||||
path: "",
|
||||
});
|
||||
const [viewFile, setViewFile] = useState<FileItem | null>(null);
|
||||
const searchParams = useLocalSearchParams();
|
||||
const parentPath =
|
||||
params.path.length > 0
|
||||
? 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 { showToast } from "@/stores/toastStore";
|
||||
import { API_BASEURL } from "@/lib/constants";
|
||||
import authStore from "@/stores/authStore";
|
||||
import { FileItem } from "@/types/files";
|
||||
|
||||
export const wakePcUp = async () => {
|
||||
try {
|
||||
@ -11,3 +14,41 @@ export const wakePcUp = async () => {
|
||||
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 React, { useEffect, useRef } from "react";
|
||||
import "xterm/css/xterm.css";
|
||||
import { BASEURL } from "@/lib/constants";
|
||||
import { BASEURL, WS_BASEURL } from "@/lib/constants";
|
||||
import { useAuth } from "@/stores/authStore";
|
||||
import { Stack } from "expo-router";
|
||||
import BackButton from "@ui/BackButton";
|
||||
@ -28,11 +28,7 @@ const TerminalPage = () => {
|
||||
const fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
|
||||
const baseUrl = BASEURL.replace("https://", "wss://").replace(
|
||||
"http://",
|
||||
"ws://"
|
||||
);
|
||||
const socket = new WebSocket(baseUrl + "/terminal?token=" + token);
|
||||
const socket = new WebSocket(WS_BASEURL + "/terminal?token=" + token);
|
||||
const attachAddon = new AttachAddon(socket);
|
||||
|
||||
// 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 Modal from "react-native-modal";
|
||||
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 AudioPlayer from "@/components/containers/AudioPlayer";
|
||||
import { FileItem } from "@/types/files";
|
||||
|
@ -4,11 +4,10 @@ import { FileItem } from "@/types/files";
|
||||
import Text from "@ui/Text";
|
||||
import List from "@ui/List";
|
||||
import { Ionicons } from "@ui/Icons";
|
||||
import { cn } from "@/lib/utils";
|
||||
import ActionSheet from "@ui/ActionSheet";
|
||||
import { HStack } from "@ui/Stack";
|
||||
import Button from "@ui/Button";
|
||||
import { openFile } from "@/app/apps/files/utils";
|
||||
import { openFile } from "@/app/apps/lib";
|
||||
|
||||
type Store = {
|
||||
isVisible: boolean;
|
||||
|
@ -17,13 +17,18 @@ const Apps = (props: Props) => {
|
||||
{
|
||||
name: "Files",
|
||||
icon: <Ionicons name="folder" />,
|
||||
path: "files/index",
|
||||
path: "files",
|
||||
},
|
||||
{
|
||||
name: "Terminal",
|
||||
icon: <Ionicons name="terminal" />,
|
||||
path: "terminal",
|
||||
},
|
||||
{
|
||||
name: "VNC",
|
||||
icon: <Ionicons name="eye" />,
|
||||
path: "vnc",
|
||||
},
|
||||
{
|
||||
name: "Turn on PC",
|
||||
icon: <Ionicons name="desktop" />,
|
||||
|
@ -3,3 +3,7 @@ export const BASEURL = __DEV__
|
||||
: location.protocol + "//" + location.host;
|
||||
|
||||
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"
|
||||
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:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
|
Loading…
x
Reference in New Issue
Block a user