chore: reorganize go code

This commit is contained in:
Aarnav Tale
2025-06-08 08:59:33 -04:00
parent 7a6ad8d2d5
commit 0f9bf73b82
28 changed files with 796 additions and 220 deletions
+1 -1
View File
@@ -10,6 +10,7 @@ export default [
route('/logout', 'routes/auth/logout.ts'),
route('/oidc/callback', 'routes/auth/oidc-callback.ts'),
route('/oidc/start', 'routes/auth/oidc-start.ts'),
route('/ssh', 'routes/ssh/console.tsx'),
// All the main logged-in dashboard routes
// Double nested to separate error propagations
@@ -25,7 +26,6 @@ export default [
route('/users', 'routes/users/overview.tsx'),
route('/acls', 'routes/acls/overview.tsx'),
route('/dns', 'routes/dns/overview.tsx'),
route('/ssh', 'routes/ssh/overview.tsx'),
...prefix('/settings', [
index('routes/settings/overview.tsx'),
+166
View File
@@ -0,0 +1,166 @@
import { faker } from '@faker-js/faker';
import { useState } from 'react';
import { LoaderFunctionArgs } from 'react-router';
import { LoadContext } from '~/server';
import { Loader2 } from 'lucide-react';
import '~/wasm_exec';
export async function loader({
request,
context,
}: LoaderFunctionArgs<LoadContext>) {
// const session = await context.sessions.auth(request);
// const user = session.get('user');
// if (!user) {
// throw data('Unauthorized', 401);
// }
// if (user.subject === 'unknown-non-oauth') {
// throw data('Only OAuth users are allowed to use WebSSH', 403);
// }
// const { users } = await context.client.get<{ users: User[] }>(
// 'v1/user',
// session.get('api_key')!,
// );
// // MARK: This assumes that a user has authenticated with Headscale first
// // Since the only way to enforce permissions via ACLs is to generate a
// // pre-authkey which REQUIRES a user ID, meaning the user has to have
// // authenticated with Headscale first.
// const lookup = users.find((u) => {
// const subject = u.providerId?.split('/').pop();
// if (!subject) {
// return false;
// }
// return subject === user.subject;
// });
// if (!lookup) {
// throw data(
// `User with subject ${user.subject} not found within Headscale`,
// 404,
// );
// }
// const { preAuthKey } = await context.client.post<{ preAuthKey: PreAuthKey }>(
// 'v1/preauthkey',
// session.get('api_key')!,
// {
// user: lookup.id,
// reusable: false,
// ephemeral: true,
// expiration: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour
// },
// );
// // TODO: Enable config to enforce generate_authkeys capability
// // For now, any user is capable of WebSSH connections
// // const check = await context.sessions.check(
// // request,
// // Capabilities.generate_authkeys,
// // );
// const qp = new URL(request.url).searchParams;
// const username = qp.get('username') || undefined;
// const hostname = qp.get('hostname') || undefined;
// const port = qp.get('port')
// ? Number.parseInt(qp.get('port')!, 10)
// : undefined;
// if (!username || !hostname || !port) {
// throw data('Missing required parameters: username, hostname, port', 400);
// }
// // TODO: Verify the Host headers to ensure CORS friendly
// // TODO: Check if the URL actually resolves correctly
// // TODO: Keep track of hostname since ephemeral nodes are broken atm
// return {
// PreAuthKey: preAuthKey.key,
// ControlURL:
// context.config.headscale.public_url ?? context.config.headscale.url,
// Hostname: generateHostname(username),
// ssh: {
// username,
// hostname,
// },
// };
}
function generateHostname(username: string) {
const adjective = faker.word.adjective({
length: {
min: 3,
max: 6,
},
});
const noun = faker.word.noun({
length: {
min: 3,
max: 6,
},
});
return `ssh-${adjective}-${noun}-${username}`;
}
export default function Page() {
// const { pause } = useLiveData();
const [ipn, setIpn] = useState<TsWasmNet | null>(null);
// const { PreAuthKey, ControlURL, Hostname, ssh } =
// useLoaderData<typeof loader>();
// useEffect(() => {
// pause();
// const go = new Go(); // Go is defined by wasm_exec.js
// WebAssembly.instantiateStreaming(fetch(wasm), go.importObject).then(
// (value) => {
// go.run(value.instance);
// const handle = TsWasmNet(
// {
// PreAuthKey,
// ControlURL,
// Hostname,
// },
// {
// NotifyState: (state) => {
// console.log('State changed:', state);
// if (state === 'Running') {
// setIpn(handle);
// }
// },
// NotifyNetMap: (netmap) => {
// console.log('NetMap updated:', netmap);
// },
// NotifyBrowseToURL: (url) => {
// console.log('Browse to URL:', url);
// },
// NotifyPanicRecover: (message) => {
// console.error('Panic recover:', message);
// },
// },
// );
// handle.Start();
// },
// );
// }, []);
return (
<div className="w-screen h-screen bg-headplane-900">
{ipn === null ? (
<div className="mx-auto h-screen flex items-center justify-center">
<Loader2 className="animate-spin size-10" />
</div>
) : (
<div className="flex flex-col h-screen">
{/* <h1>Session ID: {sessionId}</h1>
{queue.current.length > 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400">
{queue.current.length} frames queued
</p>
)} */}
{/* <XTerm ipn={ipn} /> */}
<div className="flex-1 overflow-auto">
{/* Render your terminal component here */}
{/* <Terminal ipn={ipn} /> */}
</div>
</div>
)}
</div>
);
}
+48
View File
@@ -0,0 +1,48 @@
declare function newIPN(config: NewIPNConfig): IPNHandle;
declare function TsWasmNet(
options: TsWasmNetOptions,
callbacks: TsWasmNetCallbacks,
): TsWasmNet;
interface TsWasmNetOptions {
ControlURL: string;
PreAuthKey: string;
Hostname: string;
}
interface TsWasmNetCallbacks {
NotifyState: (state: IPNState) => void;
NotifyNetMap: (netMapJson: string) => void;
NotifyBrowseToURL: (url: string) => void;
NotifyPanicRecover: (err: string) => void;
}
interface TsWasmNet {
Start: () => void;
}
type IPNState =
| 'NoState'
| 'InUseOtherUser'
| 'NeedsLogin'
| 'NeedsMachineAuth'
| 'Stopped'
| 'Starting'
| 'Running';
interface SSHTerminalConfig {
writeFn: (data: string) => void;
writeErrorFn: (error: string) => void;
setReadFn: (cb: (input: string) => void) => void;
rows: number;
cols: number;
timeoutSeconds?: number;
onConnectionProgress: (message: string) => void;
onConnected: () => void;
onDone: () => void;
}
interface SSHSession {
close(): boolean;
resize(rows: number, cols: number): boolean;
}
-145
View File
@@ -1,145 +0,0 @@
import { decode } from 'cborg';
import { Loader2 } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { LoaderFunctionArgs, data, useLoaderData } from 'react-router';
import { ClientOnly } from 'remix-utils/client-only';
import {
Command,
SSHConnectData,
SSHConnectFailedData,
} from '~/server/agent/dispatcher';
import { useLiveData } from '~/utils/live-data';
import toast from '~/utils/toast';
import XTerm from './xterm.client';
export async function loader({ request }: LoaderFunctionArgs) {
const qp = new URL(request.url).searchParams;
const username = qp.get('username') || undefined;
const hostname = qp.get('hostname') || undefined;
const port = qp.get('port')
? Number.parseInt(qp.get('port')!, 10)
: undefined;
if (!username || !hostname || !port) {
throw data('Missing required parameters: username, hostname, port', 400);
}
const baseUrl = new URL(request.url).origin;
const wsUrl = new URL('/_ssh_plexer', baseUrl);
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
wsUrl.searchParams.set('username', username);
wsUrl.searchParams.set('hostname', hostname);
wsUrl.searchParams.set('port', port.toString());
return {
socketUrl: wsUrl.toString(),
};
}
type SessionStatus = 'loading' | 'connected' | 'error';
export default function Page() {
const { pause } = useLiveData();
const { socketUrl } = useLoaderData<typeof loader>();
const [socket, setSocket] = useState<WebSocket | null>(null);
const [status, setStatus] = useState<SessionStatus>('loading');
const [sessionId, setSessionId] = useState<string | null>(null);
const queue = useRef<Array<Uint8Array>>([]);
const validated = useRef<boolean>(false);
useEffect(() => {
// SSH connections should not use stale while revalidate logic.
pause();
const ws = new WebSocket(socketUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
setSocket(ws);
setStatus('loading');
};
// We need to wait for the WebSocket to open and respond with the
// connection ID. Without a session ID, we do not have a mux.
const messageHandler = (event: MessageEvent) => {
if (!(event.data instanceof ArrayBuffer)) {
toast('Invalid message received from server');
return;
}
const data = new Uint8Array(event.data);
const obj = decode(data) as Command;
if (obj.op === 'ssh_conn_successful') {
const data = obj as SSHConnectData;
if (!validated.current) {
validated.current = true;
toast(
`SSH connection established with session ID: ${data.payload.sessionId}`,
);
setStatus('connected');
setSessionId(data.payload.sessionId);
}
return;
}
if (obj.op === 'ssh_conn_failed') {
const data = obj as SSHConnectFailedData;
if (!validated.current) {
validated.current = true;
toast(`SSH connection failed: ${data.payload.reason}`);
setStatus('error');
}
return;
}
if (obj.op === 'ssh_frame') {
queue.current.push(new Uint8Array(event.data));
return;
}
};
ws.addEventListener('message', messageHandler);
ws.onerror = (error) => {
setStatus('error');
toast(`WebSocket error: ${error}`);
};
ws.onclose = () => {
if (status !== 'error') {
toast('SSH connection closed');
}
setSocket(null);
setStatus('error');
};
return () => {
ws.removeEventListener('message', messageHandler);
ws.close();
};
}, [socketUrl]);
if (socket === null || !sessionId || status === 'loading') {
return (
<Loader2 className="animate-spin text-gray-500 dark:text-gray-400 w-6 h-6 mx-auto mt-4" />
);
}
return (
<div className="flex flex-col h-full">
<h1>Session ID: {sessionId}</h1>
{queue.current.length > 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400">
{queue.current.length} frames queued
</p>
)}
<XTerm ws={socket} sessionId={sessionId} queue={queue.current} />
</div>
);
}
+7
View File
@@ -0,0 +1,7 @@
declare class Go {
importObject: WebAssembly.Imports;
run(instance: WebAssembly.Instance): Promise<void>;
argv?: string[];
env?: Record<string, string>;
exit?: (code: number) => void;
}
+3
View File
@@ -114,6 +114,9 @@ export default function XTerm({ ws, sessionId, queue }: XTermProps) {
return;
}
const size = event.data.byteLength;
console.log(`[WS] Received ${size} bytes at ${Date.now()}`);
const data = new Uint8Array(event.data);
handleFrame(data);
};