Skip to content

Commit

Permalink
init remote sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
jsbroks committed Nov 11, 2024
1 parent 26dae51 commit 7dae040
Show file tree
Hide file tree
Showing 51 changed files with 1,779 additions and 311 deletions.
3 changes: 0 additions & 3 deletions apps/ctrlshell/README.md

This file was deleted.

16 changes: 0 additions & 16 deletions apps/ctrlshell/src/payloads/session-create.ts

This file was deleted.

19 changes: 0 additions & 19 deletions apps/ctrlshell/src/payloads/session-input.ts

This file was deleted.

11 changes: 0 additions & 11 deletions apps/ctrlshell/src/payloads/session-output.ts

This file was deleted.

55 changes: 0 additions & 55 deletions apps/ctrlshell/src/routing.ts

This file was deleted.

17 changes: 0 additions & 17 deletions apps/ctrlshell/src/session-auditor.ts

This file was deleted.

27 changes: 0 additions & 27 deletions apps/ctrlshell/src/user-socket.ts

This file was deleted.

22 changes: 0 additions & 22 deletions apps/ctrlshell/src/utils.ts

This file was deleted.

2 changes: 1 addition & 1 deletion apps/ctrlshell/Dockerfile → apps/target-proxy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ RUN adduser --system --uid 1001 expressjs
USER expressjs

COPY --from=installer /app .
CMD ["node", "apps/webshell-router/dist/index.js"]
CMD ["node", "apps/target-proxy/dist/index.js"]
30 changes: 30 additions & 0 deletions apps/target-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Target Proxy Router

Simple router that redirects web terminal requests to instances and vis-versa.

### Sequence Diagram

```mermaid
sequenceDiagram
autonumber
participant AG as Agebt
participant PR as Proxy
participant CP as Ctrlplane
participant DE as Developer
opt Init Agent
AG->>PR: Connects to Proxy
PR->>CP: Register as target
AG->>PR: Sends heartbeat
end
opt Session
DE->>CP: Opens session
CP->>PR: Forwards commands
PR->>AG: Receives commands
AG->>PR: Sends output
PR->>CP: Sends output
CP->>DE: Displays output
end
```
File renamed without changes.
10 changes: 7 additions & 3 deletions apps/ctrlshell/package.json → apps/target-proxy/package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
{
"name": "@ctrlplane/ctrlshell",
"name": "@ctrlplane/target-proxy",
"private": true,
"type": "module",
"scripts": {
"clean": "rm -rf .turbo node_modules",
"dev": "tsx watch --clear-screen=false src/index.ts",
"dev:t": "pnpm with-env tsx watch --clear-screen=false src/index.ts",
"lint": "eslint",
"format": "prettier --check . --ignore-path ../../.gitignore"
"format": "prettier --check . --ignore-path ../../.gitignore",
"with-env": "dotenv -e ../../.env --"
},
"dependencies": {
"@ctrlplane/db": "workspace:*",
"@ctrlplane/job-dispatch": "workspace:*",
"@ctrlplane/logger": "workspace:*",
"@ctrlplane/validators": "workspace:*",
"@t3-oss/env-core": "catalog:",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,35 +1,90 @@
import type { IncomingMessage } from "http";
import type WebSocket from "ws";
import type { MessageEvent } from "ws";
import { v4 as uuidv4 } from "uuid";

import type { InsertTarget, Target } from "@ctrlplane/db/schema";
import type {
AgentHeartbeat,
SessionCreate,
SessionDelete,
SessionInput,
SessionOutput,
} from "./payloads";
import { agentHeartbeat, sessionOutput } from "./payloads";
SessionResize,
} from "@ctrlplane/validators/session";
import type { IncomingMessage } from "http";
import type WebSocket from "ws";
import type { MessageEvent } from "ws";

import { eq } from "@ctrlplane/db";
import { db } from "@ctrlplane/db/client";
import * as schema from "@ctrlplane/db/schema";
import { upsertTargets } from "@ctrlplane/job-dispatch";
import { logger } from "@ctrlplane/logger";
import {
agentConnect,
agentHeartbeat,
sessionOutput,
} from "@ctrlplane/validators/session";

import { ifMessage } from "./utils";

export class AgentSocket {
static from(socket: WebSocket, request: IncomingMessage) {
if (request.headers["x-api-key"] == null) return null;
return new AgentSocket(socket, request);
static async from(socket: WebSocket, request: IncomingMessage) {
const agentName = request.headers["x-agent-name"]?.toString();
if (agentName == null) {
logger.warn("Agent connection rejected - missing agent name");
return null;
}

const apiKey = request.headers["x-api-key"]?.toString();
if (apiKey == null) {
logger.error("Agent connection rejected - missing API key");
throw new Error("API key is required.");
}

const workspaceSlug = request.headers["x-workspace"]?.toString();
if (workspaceSlug == null) {
logger.error("Agent connection rejected - missing workspace slug");
throw new Error("Workspace slug is required.");
}

const workspace = await db.query.workspace.findFirst({
where: eq(schema.workspace.slug, workspaceSlug),
});
if (workspace == null) {
logger.error("Agent connection rejected - workspace not found");
return null;
}

const targetInfo: InsertTarget = {
name: agentName,
version: "ctrlplane/v1",
kind: "TargetSession",
identifier: `ctrlplane/target-agent/${agentName}`,
workspaceId: workspace.id,
};
const [target] = await upsertTargets(db, [targetInfo]);
if (target == null) return null;
return new AgentSocket(socket, request, target);
}

private stdoutListeners = new Set<(data: SessionOutput) => void>();
readonly id: string;

private constructor(
private readonly socket: WebSocket,
private readonly request: IncomingMessage,
private readonly _: IncomingMessage,
public readonly target: Target,
) {
this.id = uuidv4();
this.socket.addEventListener(
this.target = target;
this.socket.on(
"message",
ifMessage()
.is(agentConnect, async (data) => {
await upsertTargets(db, [
{
...target,
config: data.config,
metadata: data.metadata,
version: "ctrlplane/v1",
},
]);
})
.is(sessionOutput, (data) => this.notifySubscribers(data))
.is(agentHeartbeat, (data) => this.updateStatus(data))
.handle(),
Expand All @@ -50,15 +105,8 @@ export class AgentSocket {
console.log("status", data.timestamp);
}

createSession(username = "", shell = "") {
const createSession: SessionCreate = {
type: "session.create",
username,
shell,
};

this.send(createSession);

createSession(session: SessionCreate) {
this.send(session);
return this.waitForResponse(
(response) => response.type === "session.created",
);
Expand All @@ -80,7 +128,7 @@ export class AgentSocket {
return waitForResponse<T>(this.socket, predicate, timeoutMs);
}

send(data: SessionCreate | SessionDelete | SessionInput) {
send(data: SessionCreate | SessionDelete | SessionInput | SessionResize) {

Check failure on line 131 in apps/target-proxy/src/controller/agent-socket.ts

View workflow job for this annotation

GitHub Actions / Lint

'any' overrides all other types in this union type
return this.socket.send(JSON.stringify(data));
}
}
Expand Down
Loading

0 comments on commit 7dae040

Please sign in to comment.