From d3b89113aef422deb017dd50212a8f9a973835d4 Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Mon, 18 Mar 2024 02:33:04 +0700 Subject: [PATCH] feat: add vnc app --- backend/.env.example | 1 + backend/lib/websockify.ts | 50 +++++++++++++++ backend/websocket.ts | 9 ++- package.json | 1 + src/app/_layout.tsx | 6 +- src/app/apps/{files/index.tsx => files.tsx} | 4 +- src/app/apps/files/utils.ts | 16 ----- src/app/apps/lib.ts | 41 +++++++++++++ src/app/apps/terminal.tsx | 8 +-- src/app/apps/vnc.tsx | 61 +++++++++++++++++++ .../pages/files/FileInlineViewer.tsx | 2 +- src/components/pages/files/FileMenu.tsx | 3 +- src/components/pages/home/Apps.tsx | 7 ++- src/lib/constants.ts | 4 ++ yarn.lock | 5 ++ 15 files changed, 187 insertions(+), 31 deletions(-) create mode 100644 backend/lib/websockify.ts rename src/app/apps/{files/index.tsx => files.tsx} (95%) delete mode 100644 src/app/apps/files/utils.ts create mode 100644 src/app/apps/vnc.tsx diff --git a/backend/.env.example b/backend/.env.example index eb93faa..3792cb7 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -8,3 +8,4 @@ AUTH_PASSWORD= PC_MAC_ADDR= TERMINAL_SHELL= FILE_DIRS="/some/path;/another/path" +VNC_PORT=5901 diff --git a/backend/lib/websockify.ts b/backend/lib/websockify.ts new file mode 100644 index 0000000..6c34cba --- /dev/null +++ b/backend/lib/websockify.ts @@ -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(); + }); +}; diff --git a/backend/websocket.ts b/backend/websocket.ts index 41935ea..04d8d5c 100644 --- a/backend/websocket.ts +++ b/backend/websocket.ts @@ -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); }); }; diff --git a/package.json b/package.json index f712080..8f744b9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index 44111bb..54fa87a 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -39,7 +39,11 @@ const RootLayout = () => { return ( - + diff --git a/src/app/apps/files/index.tsx b/src/app/apps/files.tsx similarity index 95% rename from src/app/apps/files/index.tsx rename to src/app/apps/files.tsx index ffc56a7..5fe45c3 100644 --- a/src/app/apps/files/index.tsx +++ b/src/app/apps/files.tsx @@ -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(null); - const searchParams = useLocalSearchParams(); const parentPath = params.path.length > 0 ? params.path.split("/").slice(0, -1).join("/") diff --git a/src/app/apps/files/utils.ts b/src/app/apps/files/utils.ts deleted file mode 100644 index 8867b7d..0000000 --- a/src/app/apps/files/utils.ts +++ /dev/null @@ -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(); -} diff --git a/src/app/apps/lib.ts b/src/app/apps/lib.ts index 9b652e2..b59fb16 100644 --- a/src/app/apps/lib.ts +++ b/src/app/apps/lib.ts @@ -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(); + } +} diff --git a/src/app/apps/terminal.tsx b/src/app/apps/terminal.tsx index c1f6cdc..43ff78a 100644 --- a/src/app/apps/terminal.tsx +++ b/src/app/apps/terminal.tsx @@ -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 diff --git a/src/app/apps/vnc.tsx b/src/app/apps/vnc.tsx new file mode 100644 index 0000000..9273378 --- /dev/null +++ b/src/app/apps/vnc.tsx @@ -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(null!); + const vncRef = useRef(null!); + const { token } = useAuth(); + + const onFullscreen = () => { + openFullscreen(containerRef.current); + }; + + if (Platform.OS !== "web") { + return null; + } + + return ( + <> + , + headerRight: () => ( +