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,
|
RequestValidationError,
|
||||||
} from "@src/lib/errors.ts";
|
} from "@src/lib/errors.ts";
|
||||||
import devices from "@src/lib/devices.ts";
|
import devices from "@src/lib/devices.ts";
|
||||||
|
import { WebSocketClientsGroup } from "@src/lib/websocket.ts";
|
||||||
|
|
||||||
const AUTH_COOKIE_NAME = "token";
|
const AUTH_COOKIE_NAME = "token";
|
||||||
const VERSION = "0.1.0-a.1";
|
const VERSION = "0.1.0-a.1";
|
||||||
@ -75,8 +76,6 @@ router
|
|||||||
)
|
)
|
||||||
.toBoolean();
|
.toBoolean();
|
||||||
|
|
||||||
console.log(alreadyLoggedIn);
|
|
||||||
|
|
||||||
return c.html(eta.render("./login.html", { alreadyLoggedIn }));
|
return c.html(eta.render("./login.html", { alreadyLoggedIn }));
|
||||||
})
|
})
|
||||||
.get("/setup", (c) => {
|
.get("/setup", (c) => {
|
||||||
@ -93,6 +92,8 @@ router
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const group = new WebSocketClientsGroup();
|
||||||
|
|
||||||
router.get("/api/admin/ws", (c) => {
|
router.get("/api/admin/ws", (c) => {
|
||||||
if (c.req.headers.get("upgrade") != "websocket") {
|
if (c.req.headers.get("upgrade") != "websocket") {
|
||||||
return new Response(null, { status: 501 });
|
return new Response(null, { status: 501 });
|
||||||
@ -100,6 +101,8 @@ router.get("/api/admin/ws", (c) => {
|
|||||||
|
|
||||||
const { socket, response } = Deno.upgradeWebSocket(c.req);
|
const { socket, response } = Deno.upgradeWebSocket(c.req);
|
||||||
|
|
||||||
|
group.addClient(socket);
|
||||||
|
|
||||||
socket.addEventListener("open", () => {
|
socket.addEventListener("open", () => {
|
||||||
console.log("a client connected!");
|
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 { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
|
||||||
import { type Cookie } from "@std/http/cookie";
|
import { type Cookie } from "@std/http/cookie";
|
||||||
import { getMessageFromError, ok } from "@shared/utils/result.ts";
|
import { getMessageFromError, ok } from "@shared/utils/result.ts";
|
||||||
import {
|
import { InferSchemaType, Schema } from "@shared/utils/validator.ts";
|
||||||
InferSchemaType,
|
import { ResultAsync } from "@shared/utils/resultasync.ts";
|
||||||
Schema,
|
|
||||||
SchemaValidationError,
|
|
||||||
} from "@shared/utils/validator.ts";
|
|
||||||
import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
|
|
||||||
import log from "@shared/utils/logger.ts";
|
|
||||||
import {
|
import {
|
||||||
FailedToParseRequestAsJSONError,
|
FailedToParseRequestAsJSONError,
|
||||||
failedToParseRequestAsJSONError,
|
failedToParseRequestAsJSONError,
|
||||||
@ -55,6 +50,7 @@ export class Context<
|
|||||||
S extends string = string,
|
S extends string = string,
|
||||||
ReqSchema extends Schema<any> = Schema<unknown>,
|
ReqSchema extends Schema<any> = Schema<unknown>,
|
||||||
ResSchema 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 _url?: URL;
|
||||||
private _hostname?: string;
|
private _hostname?: string;
|
||||||
@ -263,6 +259,19 @@ export class Context<
|
|||||||
delete: (name: string) => deleteCookie(this.res.headers, name),
|
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
|
type ExtractPath<S extends string> = S extends
|
||||||
|
|||||||
@ -174,3 +174,11 @@ export const tooManyConnectionError = createErrorFactory(
|
|||||||
export type TooManyConnectionError = InferSchemaType<
|
export type TooManyConnectionError = InferSchemaType<
|
||||||
typeof tooManyConnectionErrorSchema
|
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,
|
status: 405,
|
||||||
})) as RequestHandler;
|
})) as RequestHandler;
|
||||||
|
|
||||||
class HttpRouter {
|
class HttpRouter<
|
||||||
|
Variables extends Record<string | number, any> = Record<
|
||||||
|
string | number,
|
||||||
|
any
|
||||||
|
>,
|
||||||
|
> {
|
||||||
public readonly routerTree = new RouterTree<MethodHandlers>();
|
public readonly routerTree = new RouterTree<MethodHandlers>();
|
||||||
public pathTransformer?: (path: string) => string;
|
public pathTransformer?: (path: string) => string;
|
||||||
private middlewares: Middleware[] = [];
|
private middlewares: Middleware[] = [];
|
||||||
@ -152,8 +157,8 @@ class HttpRouter {
|
|||||||
? this.pathTransformer(ctx.path)
|
? this.pathTransformer(ctx.path)
|
||||||
: ctx.path;
|
: ctx.path;
|
||||||
|
|
||||||
const { handler, params } = this.resolveRoute(ctx, path);
|
const { handler, params, ctx: routeCtx } = this.resolveRoute(ctx, path);
|
||||||
ctx = ctx.setParams(params);
|
ctx = routeCtx.setParams(params);
|
||||||
|
|
||||||
const res =
|
const res =
|
||||||
(await this.runMiddlewares(this.middlewares, handler, ctx)).res;
|
(await this.runMiddlewares(this.middlewares, handler, ctx)).res;
|
||||||
@ -163,7 +168,11 @@ class HttpRouter {
|
|||||||
private resolveRoute(
|
private resolveRoute(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
path: string,
|
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);
|
const routeOption = this.routerTree.find(path);
|
||||||
|
|
||||||
if (routeOption.isSome()) {
|
if (routeOption.isSome()) {
|
||||||
@ -174,17 +183,21 @@ class HttpRouter {
|
|||||||
route = methodHandlers["GET"];
|
route = methodHandlers["GET"];
|
||||||
} else if (!route && ctx.req.method !== "GET") {
|
} else if (!route && ctx.req.method !== "GET") {
|
||||||
if (ctx.preferredType.map((v) => v === "json").toBoolean()) {
|
if (ctx.preferredType.map((v) => v === "json").toBoolean()) {
|
||||||
return { handler: this.methodNotAllowedHandler, params };
|
return {
|
||||||
|
handler: this.methodNotAllowedHandler,
|
||||||
|
params,
|
||||||
|
ctx,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (route) {
|
if (route) {
|
||||||
if (route.schema) {
|
if (route.schema) {
|
||||||
ctx = ctx.setSchema(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 {
|
private removeBodyFromResponse(res: Response): Response {
|
||||||
|
|||||||
@ -1,18 +1,43 @@
|
|||||||
import {
|
import {
|
||||||
TooManyConnectionError,
|
TooManyConnectionError,
|
||||||
tooManyConnectionError,
|
tooManyConnectionError,
|
||||||
|
WebSocketMsgSendError,
|
||||||
|
webSocketMsgSendError,
|
||||||
} from "@src/lib/errors.ts";
|
} 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_PER_TOKEN = 2;
|
||||||
const MAX_CONNECTIONS = 500;
|
const MAX_CONNECTIONS = 500;
|
||||||
|
|
||||||
class WebSocketManager {
|
export class WebSocketClientsGroup<
|
||||||
private adminSockets: Map<string, WebSocket[]> = new Map();
|
ReceiveSchema extends Schema<unknown> = Schema<unknown>,
|
||||||
private userSockets: Map<string, WebSocket[]> = new Map();
|
SendSchema extends Schema<unknown> = Schema<unknown>,
|
||||||
|
> {
|
||||||
|
private clients: Map<string, Map<string, WebSocket>> = new Map();
|
||||||
private connectionsCounter: number = 0;
|
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,
|
token: string,
|
||||||
socket: WebSocket,
|
socket: WebSocket,
|
||||||
): Result<void, TooManyConnectionError> {
|
): Result<void, TooManyConnectionError> {
|
||||||
@ -20,19 +45,56 @@ class WebSocketManager {
|
|||||||
return err(tooManyConnectionError("Too many connections"));
|
return err(tooManyConnectionError("Too many connections"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const sockets = this.adminSockets.get(token);
|
let clientConnections = this.clients.get(token);
|
||||||
|
|
||||||
if (!sockets) {
|
if (!clientConnections) {
|
||||||
const sockets = [socket];
|
this.clients.set(token, new Map());
|
||||||
this.adminSockets.set(token, sockets);
|
clientConnections = this.clients.get(token) as Map<
|
||||||
this.connectionsCounter++;
|
string,
|
||||||
return ok();
|
WebSocket
|
||||||
} else if (sockets.length < MAX_CONNECTIONS_PER_TOKEN) {
|
>;
|
||||||
sockets.push(socket);
|
} else if (clientConnections.size >= MAX_CONNECTIONS_PER_TOKEN) {
|
||||||
this.connectionsCounter++;
|
|
||||||
return ok();
|
|
||||||
} else {
|
|
||||||
return err(tooManyConnectionError("Too many connections"));
|
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