adding variables to router
This commit is contained in:
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Keyborg
|
||||
|
||||
A Dockerized USB-over-IP server + Tauri-powered client for seamless USB device export and control.
|
||||
@ -22,6 +22,7 @@ import {
|
||||
RequestValidationError,
|
||||
} from "@src/lib/errors.ts";
|
||||
import devices from "@src/lib/devices.ts";
|
||||
import { WebSocketClientsGroup } from "@src/lib/websocket.ts";
|
||||
|
||||
const AUTH_COOKIE_NAME = "token";
|
||||
const VERSION = "0.1.0-a.1";
|
||||
@ -75,8 +76,6 @@ router
|
||||
)
|
||||
.toBoolean();
|
||||
|
||||
console.log(alreadyLoggedIn);
|
||||
|
||||
return c.html(eta.render("./login.html", { alreadyLoggedIn }));
|
||||
})
|
||||
.get("/setup", (c) => {
|
||||
@ -93,6 +92,8 @@ router
|
||||
);
|
||||
});
|
||||
|
||||
const group = new WebSocketClientsGroup();
|
||||
|
||||
router.get("/api/admin/ws", (c) => {
|
||||
if (c.req.headers.get("upgrade") != "websocket") {
|
||||
return new Response(null, { status: 501 });
|
||||
@ -100,6 +101,8 @@ router.get("/api/admin/ws", (c) => {
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(c.req);
|
||||
|
||||
group.addClient(socket);
|
||||
|
||||
socket.addEventListener("open", () => {
|
||||
console.log("a client connected!");
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -4,13 +4,8 @@ import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
|
||||
import { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
|
||||
import { type Cookie } from "@std/http/cookie";
|
||||
import { getMessageFromError, ok } from "@shared/utils/result.ts";
|
||||
import {
|
||||
InferSchemaType,
|
||||
Schema,
|
||||
SchemaValidationError,
|
||||
} from "@shared/utils/validator.ts";
|
||||
import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
|
||||
import log from "@shared/utils/logger.ts";
|
||||
import { InferSchemaType, Schema } from "@shared/utils/validator.ts";
|
||||
import { ResultAsync } from "@shared/utils/resultasync.ts";
|
||||
import {
|
||||
FailedToParseRequestAsJSONError,
|
||||
failedToParseRequestAsJSONError,
|
||||
@ -55,6 +50,7 @@ export class Context<
|
||||
S extends string = string,
|
||||
ReqSchema extends Schema<any> = Schema<unknown>,
|
||||
ResSchema extends Schema<any> = Schema<unknown>,
|
||||
Vars extends Record<string | number, any> = Record<string | number, any>,
|
||||
> {
|
||||
private _url?: URL;
|
||||
private _hostname?: string;
|
||||
@ -263,6 +259,19 @@ export class Context<
|
||||
delete: (name: string) => deleteCookie(this.res.headers, name),
|
||||
};
|
||||
}
|
||||
|
||||
private _var: Vars = {} as Vars;
|
||||
|
||||
public get var() {
|
||||
return {
|
||||
set: (key: keyof Vars, value: Vars[number]) => {
|
||||
this._var[key] = value;
|
||||
},
|
||||
get: <K extends keyof Vars>(key: K): Vars[K] => {
|
||||
return this._var[key];
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type ExtractPath<S extends string> = S extends
|
||||
|
||||
@ -174,3 +174,11 @@ export const tooManyConnectionError = createErrorFactory(
|
||||
export type TooManyConnectionError = InferSchemaType<
|
||||
typeof tooManyConnectionErrorSchema
|
||||
>;
|
||||
|
||||
export const webSocketMsgSendErrorSchema = defineError("WebSocketMsgSendError");
|
||||
export const webSocketMsgSendError = createErrorFactory(
|
||||
webSocketMsgSendErrorSchema,
|
||||
);
|
||||
export type WebSocketMsgSendError = InferSchemaType<
|
||||
typeof webSocketMsgSendErrorSchema
|
||||
>;
|
||||
|
||||
0
server/src/lib/events.ts
Normal file
0
server/src/lib/events.ts
Normal file
@ -34,7 +34,12 @@ const DEFAULT_NOT_ALLOWED_HANDLER =
|
||||
status: 405,
|
||||
})) as RequestHandler;
|
||||
|
||||
class HttpRouter {
|
||||
class HttpRouter<
|
||||
Variables extends Record<string | number, any> = Record<
|
||||
string | number,
|
||||
any
|
||||
>,
|
||||
> {
|
||||
public readonly routerTree = new RouterTree<MethodHandlers>();
|
||||
public pathTransformer?: (path: string) => string;
|
||||
private middlewares: Middleware[] = [];
|
||||
@ -152,8 +157,8 @@ class HttpRouter {
|
||||
? this.pathTransformer(ctx.path)
|
||||
: ctx.path;
|
||||
|
||||
const { handler, params } = this.resolveRoute(ctx, path);
|
||||
ctx = ctx.setParams(params);
|
||||
const { handler, params, ctx: routeCtx } = this.resolveRoute(ctx, path);
|
||||
ctx = routeCtx.setParams(params);
|
||||
|
||||
const res =
|
||||
(await this.runMiddlewares(this.middlewares, handler, ctx)).res;
|
||||
@ -163,7 +168,11 @@ class HttpRouter {
|
||||
private resolveRoute(
|
||||
ctx: Context,
|
||||
path: string,
|
||||
): { handler: RequestHandler; params: Record<string, string> } {
|
||||
): {
|
||||
handler: RequestHandler;
|
||||
params: Record<string, string>;
|
||||
ctx: Context<any, any, any, Variables>;
|
||||
} {
|
||||
const routeOption = this.routerTree.find(path);
|
||||
|
||||
if (routeOption.isSome()) {
|
||||
@ -174,17 +183,21 @@ class HttpRouter {
|
||||
route = methodHandlers["GET"];
|
||||
} else if (!route && ctx.req.method !== "GET") {
|
||||
if (ctx.preferredType.map((v) => v === "json").toBoolean()) {
|
||||
return { handler: this.methodNotAllowedHandler, params };
|
||||
return {
|
||||
handler: this.methodNotAllowedHandler,
|
||||
params,
|
||||
ctx,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (route) {
|
||||
if (route.schema) {
|
||||
ctx = ctx.setSchema(route.schema);
|
||||
}
|
||||
return { handler: route.handler, params };
|
||||
return { handler: route.handler, params, ctx };
|
||||
}
|
||||
}
|
||||
return { handler: this.notFoundHandler, params: {} };
|
||||
return { handler: this.notFoundHandler, params: {}, ctx };
|
||||
}
|
||||
|
||||
private removeBodyFromResponse(res: Response): Response {
|
||||
|
||||
@ -1,18 +1,43 @@
|
||||
import {
|
||||
TooManyConnectionError,
|
||||
tooManyConnectionError,
|
||||
WebSocketMsgSendError,
|
||||
webSocketMsgSendError,
|
||||
} from "@src/lib/errors.ts";
|
||||
import { err, ok, Result } from "@shared/utils/result.ts";
|
||||
import { err, getMessageFromError, ok, Result } from "@shared/utils/result.ts";
|
||||
import {
|
||||
InferSchemaType,
|
||||
Schema,
|
||||
SchemaValidationError,
|
||||
z,
|
||||
} from "@shared/utils/validator.ts";
|
||||
import log from "@shared/utils/logger.ts";
|
||||
|
||||
const MAX_CONNECTIONS_PER_TOKEN = 2;
|
||||
const MAX_CONNECTIONS = 500;
|
||||
|
||||
class WebSocketManager {
|
||||
private adminSockets: Map<string, WebSocket[]> = new Map();
|
||||
private userSockets: Map<string, WebSocket[]> = new Map();
|
||||
export class WebSocketClientsGroup<
|
||||
ReceiveSchema extends Schema<unknown> = Schema<unknown>,
|
||||
SendSchema extends Schema<unknown> = Schema<unknown>,
|
||||
> {
|
||||
private clients: Map<string, Map<string, WebSocket>> = new Map();
|
||||
private connectionsCounter: number = 0;
|
||||
|
||||
public addAdminClient(
|
||||
constructor(
|
||||
public schemas: {
|
||||
onReceive: ReceiveSchema;
|
||||
onSend: SendSchema;
|
||||
} = {
|
||||
onReceive: z.unknown() as Schema<unknown> as ReceiveSchema,
|
||||
onSend: z.unknown() as Schema<unknown> as SendSchema,
|
||||
},
|
||||
public onopen?: EventListenerOrEventListenerObject,
|
||||
public onclose?: EventListenerOrEventListenerObject,
|
||||
public onerror?: EventListenerOrEventListenerObject,
|
||||
public onmessage?: EventListenerOrEventListenerObject,
|
||||
) {}
|
||||
|
||||
public addClient(
|
||||
token: string,
|
||||
socket: WebSocket,
|
||||
): Result<void, TooManyConnectionError> {
|
||||
@ -20,19 +45,56 @@ class WebSocketManager {
|
||||
return err(tooManyConnectionError("Too many connections"));
|
||||
}
|
||||
|
||||
const sockets = this.adminSockets.get(token);
|
||||
let clientConnections = this.clients.get(token);
|
||||
|
||||
if (!sockets) {
|
||||
const sockets = [socket];
|
||||
this.adminSockets.set(token, sockets);
|
||||
this.connectionsCounter++;
|
||||
return ok();
|
||||
} else if (sockets.length < MAX_CONNECTIONS_PER_TOKEN) {
|
||||
sockets.push(socket);
|
||||
this.connectionsCounter++;
|
||||
return ok();
|
||||
} else {
|
||||
if (!clientConnections) {
|
||||
this.clients.set(token, new Map());
|
||||
clientConnections = this.clients.get(token) as Map<
|
||||
string,
|
||||
WebSocket
|
||||
>;
|
||||
} else if (clientConnections.size >= MAX_CONNECTIONS_PER_TOKEN) {
|
||||
return err(tooManyConnectionError("Too many connections"));
|
||||
}
|
||||
|
||||
const uuid = crypto.randomUUID();
|
||||
clientConnections.set(uuid, socket);
|
||||
socket.addEventListener("close", () => {
|
||||
clientConnections.delete(uuid);
|
||||
});
|
||||
socket.addEventListener("error", () => {
|
||||
clientConnections.delete(uuid);
|
||||
});
|
||||
this.connectionsCounter++;
|
||||
|
||||
socket.addEventListener("open", this.onopen!);
|
||||
socket.addEventListener("open", this.onclose!);
|
||||
socket.addEventListener("open", this.onerror!);
|
||||
socket.addEventListener("open", this.onmessage!);
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
sendToAll(
|
||||
msg: InferSchemaType<SendSchema>,
|
||||
): Result<void, SchemaValidationError | WebSocketMsgSendError[]> {
|
||||
return this.schemas.onSend.parse(msg)
|
||||
.andThen((msg) => {
|
||||
const errors = [];
|
||||
for (const client of this.clients.values()) {
|
||||
for (const connection of client) {
|
||||
try {
|
||||
connection.send(JSON.stringify(msg));
|
||||
} catch (e) {
|
||||
log.error("Failed to send messages to all clients");
|
||||
errors.push(
|
||||
webSocketMsgSendError(getMessageFromError(e)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.length === 0 ? ok() : err(errors);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
BIN
server/test.db
BIN
server/test.db
Binary file not shown.
11
shared/utils/test.ts
Normal file
11
shared/utils/test.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { z } from "@shared/utils/validator.ts";
|
||||
|
||||
const schema = z.obj({
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
console.log(
|
||||
schema.parse({
|
||||
passwor: "string",
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user