diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 2fa5239cc..3caff4864 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -42,6 +42,8 @@ internalRouter.get("/idp", idp.listIdps); internalRouter.get("/idp/:idpId", idp.getIdp); +internalRouter.get("/resource/browser-target", resource.getBrowserTarget); + // Gerbil routes const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); diff --git a/server/routers/resource/getBrowserTarget.ts b/server/routers/resource/getBrowserTarget.ts new file mode 100644 index 000000000..b69de7521 --- /dev/null +++ b/server/routers/resource/getBrowserTarget.ts @@ -0,0 +1,76 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resources, targets } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; + +const getBrowserTargetSchema = z + .object({ + fullDomain: z.string().min(1, "fullDomain is required") + }) + .strict(); + +export type GetBrowserTargetResponse = { + ip: string; + port: number; +}; + +export async function getBrowserTarget( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsed = getBrowserTargetSchema.safeParse(req.query); + if (!parsed.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsed.error).toString() + ) + ); + } + + const { fullDomain } = parsed.data; + + const [row] = await db + .select({ + ip: targets.ip, + port: targets.port + }) + .from(targets) + .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .where(eq(resources.fullDomain, fullDomain)) + .limit(1); + + if (!row) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "No resource found for this domain" + ) + ); + } + + return response(res, { + data: { ip: row.ip, port: row.port }, + success: true, + error: false, + message: "Browser target retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred while retrieving the browser target" + ) + ); + } +} diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 6a259d7fe..d8ff4dba9 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -33,3 +33,4 @@ export * from "./removeUserFromResource"; export * from "./listAllResourceNames"; export * from "./removeEmailFromResourceWhitelist"; export * from "./getStatusHistory"; +export * from "./getBrowserTarget"; diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index 57be5f919..5b61e527d 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -32,10 +32,14 @@ declare module "react" { } } +type Target = { + ip: string; + port: number; +}; + type FormState = { username: string; password: string; - hostname: string; domain: string; kdcProxyUrl: string; pcb: string; @@ -53,11 +57,16 @@ const isIronError = (error: unknown): error is IronError => { ); }; -export default function RdpClient() { +export default function RdpClient({ + target, + error +}: { + target: Target | null; + error: string | null; +}) { const [form, setForm] = useState({ - username: "Administrator", - password: "Password123!", - hostname: "172.31.3.58:3389", + username: "", + password: "", domain: "", kdcProxyUrl: "", pcb: "", @@ -214,11 +223,15 @@ export default function RdpClient() { ); } + const destination = target + ? `${target.ip}:${target.port}` + : ""; + const builder = userInteraction .configBuilder() .withUsername(form.username) .withPassword(form.password) - .withDestination(form.hostname) + .withDestination(destination) .withProxyAddress( `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/rdp` ) @@ -283,6 +296,14 @@ export default function RdpClient() { setCursorOverrideActive((v) => !v); }; + if (error) { + return ( +
+

{error}

+
+ ); + } + return (
{showLogin && ( @@ -292,15 +313,6 @@ export default function RdpClient() {
- - - update("hostname", e.target.value) - } - /> - ; +export default async function RdpPage() { + const headersList = await headers(); + const host = headersList.get("host") || ""; + const hostname = host.split(":")[0]; + + let target: { ip: string; port: number } | null = null; + let error: string | null = null; + + try { + const res = await internal.get>( + `/resource/browser-target?fullDomain=${encodeURIComponent(hostname)}` + ); + target = res.data.data; + } catch { + error = "No resource found for this domain"; + } + + return ; } diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx index e5ac8227b..0c96f1355 100644 --- a/src/app/ssh/SshClient.tsx +++ b/src/app/ssh/SshClient.tsx @@ -6,24 +6,31 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +type Target = { + ip: string; + port: number; +}; + type FormState = { - hostname: string; - port: string; username: string; password: string; }; -export default function SshClient() { +export default function SshClient({ + target, + error +}: { + target: Target | null; + error: string | null; +}) { const [form, setForm] = useState({ - hostname: "", - port: "22", username: "", password: "" }); const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); - const [error, setError] = useState(null); + const [connectError, setConnectError] = useState(null); const terminalRef = useRef(null); const xtermRef = useRef(null); @@ -115,18 +122,14 @@ export default function SshClient() { }, []); function connect() { - setError(null); + setConnectError(null); setConnecting(true); const proxyAddress = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/ssh`; const url = new URL(proxyAddress); - // Pass connection parameters as query params so the proxy can route - // before any application-level framing is needed. - url.searchParams.set("host", form.hostname); - url.searchParams.set("port", form.port); + url.searchParams.set("host", target?.ip ?? ""); + url.searchParams.set("port", String(target?.port ?? 22)); url.searchParams.set("username", form.username); - // Auth token is sent as a query param; the proxy validates it before - // forwarding any data. url.searchParams.set("authToken", "test-token"); const ws = new WebSocket(url.toString(), ["ssh"]); @@ -166,7 +169,7 @@ export default function SshClient() { ws.onerror = () => { setConnecting(false); setConnected(false); - setError("WebSocket connection failed"); + setConnectError("WebSocket connection failed"); }; ws.onclose = (evt) => { @@ -185,6 +188,14 @@ export default function SshClient() { setConnected(false); } + if (error) { + return ( +
+

{error}

+
+ ); + } + return (

SSH Terminal

@@ -192,43 +203,7 @@ export default function SshClient() { {!connected && (
-
- - - setForm({ - ...form, - hostname: e.target.value - }) - } - placeholder="192.168.1.1" - className="bg-neutral-800 border-neutral-700 text-white" - /> -
- -
- - - setForm({ ...form, port: e.target.value }) - } - placeholder="22" - className="bg-neutral-800 border-neutral-700 text-white" - /> -
- -
+
-
+
- {error &&

{error}

} + {connectError && ( +

{connectError}

+ )}