feat: add vnc app

This commit is contained in:
Khairul Hidayat 2024-03-18 02:33:04 +07:00
parent d1ecd6d00b
commit d3b89113ae
15 changed files with 187 additions and 31 deletions

View File

@ -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
View 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();
});
};

View File

@ -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);
});
};

View File

@ -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",

View File

@ -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" } }}
/>

View File

@ -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("/")

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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
View 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;

View File

@ -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";

View File

@ -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;

View File

@ -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" />,

View File

@ -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://"
);

View File

@ -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"