basic version working
This commit is contained in:
commit
39f6e99abe
11 changed files with 782 additions and 0 deletions
64
.helix/languages.toml
Normal file
64
.helix/languages.toml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
[[language]]
|
||||
name = "javascript"
|
||||
language-id = "javascript"
|
||||
scope = "source.js"
|
||||
injection-regex = "^(js|javascript)$"
|
||||
file-types = ["js", "jsx", "mjs"]
|
||||
shebangs = ["deno", "node"]
|
||||
roots = ["deno.json", "package.json", "tsconfig.json"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
grammar = "javascript"
|
||||
language-servers = ["deno-lsp"]
|
||||
|
||||
[[language]]
|
||||
name = "jsx"
|
||||
language-id = "javascriptreact"
|
||||
scope = "source.jsx"
|
||||
injection-regex = "jsx"
|
||||
file-types = ["jsx"]
|
||||
shebangs = ["deno", "node"]
|
||||
roots = ["deno.json", "package.json", "tsconfig.json"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
grammar = "javascript"
|
||||
language-servers = ["deno-lsp"]
|
||||
|
||||
[[language]]
|
||||
name = "typescript"
|
||||
language-id = "typescript"
|
||||
scope = "source.ts"
|
||||
injection-regex = "^(ts|typescript)$"
|
||||
file-types = ["ts"]
|
||||
shebangs = ["deno", "node"]
|
||||
roots = ["deno.json", "package.json", "tsconfig.json"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
grammar = "typescript"
|
||||
language-servers = ["deno-lsp"]
|
||||
|
||||
[[language]]
|
||||
name = "tsx"
|
||||
language-id = "typescriptreact"
|
||||
scope = "source.tsx"
|
||||
injection-regex = "^(tsx)$" # |typescript
|
||||
file-types = ["tsx"]
|
||||
shebangs = ["deno", "node"]
|
||||
roots = ["deno.json", "package.json", "tsconfig.json"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
grammar = "typescript"
|
||||
language-servers = ["deno-lsp"]
|
||||
|
||||
[language-server.deno-lsp]
|
||||
command = "deno"
|
||||
args = ["lsp"]
|
||||
config = { enable = true, lint = true, unstable = true }
|
||||
|
||||
[language-server.emmet-lsp]
|
||||
command = "emmet-language-server"
|
||||
args = ["--stdio"]
|
||||
|
||||
[[language]]
|
||||
name = "html"
|
||||
language-servers = ["emmet-lsp", "vscode-html-language-server"]
|
||||
11
deno.json
Normal file
11
deno.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"tasks": {
|
||||
"dev": "deno run -A --watch src/index.ts"
|
||||
},
|
||||
"imports": {
|
||||
"@std/assert": "jsr:@std/assert@1",
|
||||
"@std/async": "jsr:@std/async@^1.0.15",
|
||||
"@std/encoding": "jsr:@std/encoding@^1.0.10",
|
||||
"@std/http": "jsr:@std/http@^1.0.21"
|
||||
}
|
||||
}
|
||||
85
deno.lock
generated
Normal file
85
deno.lock
generated
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"jsr:@std/assert@1": "1.0.15",
|
||||
"jsr:@std/async@^1.0.15": "1.0.15",
|
||||
"jsr:@std/cli@^1.0.23": "1.0.23",
|
||||
"jsr:@std/encoding@^1.0.10": "1.0.10",
|
||||
"jsr:@std/fmt@^1.0.8": "1.0.8",
|
||||
"jsr:@std/fs@^1.0.19": "1.0.19",
|
||||
"jsr:@std/html@^1.0.5": "1.0.5",
|
||||
"jsr:@std/http@^1.0.21": "1.0.21",
|
||||
"jsr:@std/internal@^1.0.10": "1.0.12",
|
||||
"jsr:@std/internal@^1.0.12": "1.0.12",
|
||||
"jsr:@std/media-types@^1.1.0": "1.1.0",
|
||||
"jsr:@std/net@^1.0.6": "1.0.6",
|
||||
"jsr:@std/path@^1.1.2": "1.1.2",
|
||||
"jsr:@std/streams@^1.0.13": "1.0.13"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/assert@1.0.15": {
|
||||
"integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal@^1.0.12"
|
||||
]
|
||||
},
|
||||
"@std/async@1.0.15": {
|
||||
"integrity": "55d1d9d04f99403fe5730ab16bdcc3c47f658a6bf054cafb38a50f046238116e"
|
||||
},
|
||||
"@std/cli@1.0.23": {
|
||||
"integrity": "bf95b7a9425ba2af1ae5a6359daf58c508f2decf711a76ed2993cd352498ccca"
|
||||
},
|
||||
"@std/encoding@1.0.10": {
|
||||
"integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1"
|
||||
},
|
||||
"@std/fmt@1.0.8": {
|
||||
"integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7"
|
||||
},
|
||||
"@std/fs@1.0.19": {
|
||||
"integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06"
|
||||
},
|
||||
"@std/html@1.0.5": {
|
||||
"integrity": "4e2d693f474cae8c16a920fa5e15a3b72267b94b84667f11a50c6dd1cb18d35e"
|
||||
},
|
||||
"@std/http@1.0.21": {
|
||||
"integrity": "abb5c747651ee6e3ea6139858fd9b1810d2c97f53a5e6722f3b6d27a6d263edc",
|
||||
"dependencies": [
|
||||
"jsr:@std/cli",
|
||||
"jsr:@std/encoding",
|
||||
"jsr:@std/fmt",
|
||||
"jsr:@std/fs",
|
||||
"jsr:@std/html",
|
||||
"jsr:@std/media-types",
|
||||
"jsr:@std/net",
|
||||
"jsr:@std/path",
|
||||
"jsr:@std/streams"
|
||||
]
|
||||
},
|
||||
"@std/internal@1.0.12": {
|
||||
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
|
||||
},
|
||||
"@std/media-types@1.1.0": {
|
||||
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
|
||||
},
|
||||
"@std/net@1.0.6": {
|
||||
"integrity": "110735f93e95bb9feb95790a8b1d1bf69ec0dc74f3f97a00a76ea5efea25500c"
|
||||
},
|
||||
"@std/path@1.1.2": {
|
||||
"integrity": "c0b13b97dfe06546d5e16bf3966b1cadf92e1cc83e56ba5476ad8b498d9e3038",
|
||||
"dependencies": [
|
||||
"jsr:@std/internal@^1.0.10"
|
||||
]
|
||||
},
|
||||
"@std/streams@1.0.13": {
|
||||
"integrity": "772d208cd0d3e5dac7c1d9e6cdb25842846d136eea4a41a62e44ed4ab0c8dd9e"
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@std/assert@1",
|
||||
"jsr:@std/async@^1.0.15",
|
||||
"jsr:@std/encoding@^1.0.10",
|
||||
"jsr:@std/http@^1.0.21"
|
||||
]
|
||||
}
|
||||
}
|
||||
2
log
Normal file
2
log
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
2025-11-05T13:45:22.658 helix_term::application [WARN] Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server
|
||||
2025-11-05T13:45:25.229 helix_lsp::transport [ERROR] Tried sending response into a closed channel (id=Num(5)), original request likely timed out
|
||||
87
src/auth.ts
Normal file
87
src/auth.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import {
|
||||
buildCommand,
|
||||
buildError,
|
||||
Command,
|
||||
parseCommand,
|
||||
} from "./commandbuilder.ts";
|
||||
import { Device } from "./device.ts";
|
||||
|
||||
function toHex(data: Uint8Array): string {
|
||||
return Array.from(data).map((b) => ("00" + b.toString(16)).slice(-2)).join(
|
||||
"",
|
||||
);
|
||||
}
|
||||
async function generateHMAC(key: string, message: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(key);
|
||||
const messageData = encoder.encode(message);
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyData,
|
||||
{ name: "HMAC", hash: { name: "SHA-256" } },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
cryptoKey,
|
||||
messageData,
|
||||
);
|
||||
|
||||
return toHex(new Uint8Array(signature));
|
||||
}
|
||||
|
||||
export class Authentication {
|
||||
_socket: WebSocket;
|
||||
_encoder: TextEncoder;
|
||||
nonce: string;
|
||||
device: Device | undefined;
|
||||
signature: string | undefined;
|
||||
|
||||
constructor(socket: WebSocket) {
|
||||
this._socket = socket;
|
||||
this._encoder = new TextEncoder();
|
||||
this.nonce = toHex(crypto.getRandomValues(new Uint8Array(32)));
|
||||
}
|
||||
|
||||
async authenticate(message: Command) { // ugh, js doesn't have async constructors, hence the seeprate init function.
|
||||
if (
|
||||
message.d == undefined || !("id" in message.d) ||
|
||||
typeof (message.d.id) !== "string" || !(message.d.id in Device.devices)
|
||||
) {
|
||||
this._socket.send(buildError(1, "Invalid packet. Missing ID"));
|
||||
this._socket.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this._socket.addEventListener("message", (msg) => this._validate(msg));
|
||||
this.device = Device.devices[message.d.id];
|
||||
this.signature = await generateHMAC(this.device.password, this.nonce);
|
||||
|
||||
this._socket.send(
|
||||
buildCommand("auth_nonce", { nonce: this.nonce }),
|
||||
);
|
||||
}
|
||||
|
||||
_validate(msg: MessageEvent<any>) {
|
||||
if (!this.device || !this.signature) return;
|
||||
|
||||
const parsed = parseCommand(msg.data);
|
||||
if (
|
||||
parsed === null || parsed.c !== "auth_validate" ||
|
||||
parsed.d === undefined || !("signature" in parsed.d)
|
||||
) return;
|
||||
|
||||
if (this.signature !== parsed.d.signature) {
|
||||
this._socket.send(buildError(2, "Invalid signature."));
|
||||
this._socket.close;
|
||||
return;
|
||||
}
|
||||
|
||||
this._socket.send(buildCommand("auth_ok"));
|
||||
this.device?.connect(this._socket);
|
||||
this._socket.removeEventListener("message", this._validate);
|
||||
}
|
||||
}
|
||||
14
src/commandbuilder.ts
Normal file
14
src/commandbuilder.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export type Command = {
|
||||
c: string,
|
||||
d: object | undefined
|
||||
}
|
||||
|
||||
export const buildCommand = (command: string, data?: any) => JSON.stringify({c: command, d: data})
|
||||
export const buildError = (error: number, info?: string) => JSON.stringify({e: error, info})
|
||||
export const parseCommand = (command: string): Command | null => {
|
||||
const parsed = JSON.parse(command);
|
||||
if (!("c" in parsed && ("d" in parsed && typeof(parsed.d) == "object") || !("d" in parsed)))
|
||||
return null
|
||||
|
||||
return parsed as Command
|
||||
}
|
||||
65
src/device.ts
Normal file
65
src/device.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { buildCommand, buildError, parseCommand } from "./commandbuilder.ts";
|
||||
|
||||
type Votes = [number, number, number, number, number]
|
||||
const EMPTY_VOTES: Votes = Array(5).fill(0) as Votes;
|
||||
|
||||
export class Device {
|
||||
static devices: {[x: string]: Device} = {}
|
||||
password: string;
|
||||
id: string;
|
||||
_socket: WebSocket | undefined;
|
||||
|
||||
latestVotes: Votes | undefined
|
||||
sessionTimeout: number | undefined;
|
||||
|
||||
|
||||
get connected() {
|
||||
return this._socket !== undefined;
|
||||
}
|
||||
|
||||
constructor(password: string, id: string) {
|
||||
this.password = password;
|
||||
this.id = id;
|
||||
|
||||
if (id in Device.devices) throw new Error("id already taken")
|
||||
Device.devices[id] = this;
|
||||
}
|
||||
connect(socket: WebSocket) {
|
||||
if (this._socket !== undefined) {
|
||||
this._socket.send(buildError(4, "Logged in at other place."))
|
||||
this._socket.close();
|
||||
}
|
||||
this._socket = socket;
|
||||
|
||||
this._socket.addEventListener("close", _ => this._close())
|
||||
this._socket.addEventListener("error", _ => this._close())
|
||||
}
|
||||
|
||||
startSession(text: string) {
|
||||
if (!this.connected) return
|
||||
this.latestVotes = structuredClone(EMPTY_VOTES);
|
||||
this._socket?.send(buildCommand("session_start", {text}))
|
||||
this._socket?.addEventListener("message", msg => this._handleVote(msg))
|
||||
|
||||
this.sessionTimeout = setTimeout(() => this.stopSession, 5 * 60 * 1000);
|
||||
}
|
||||
stopSession() {
|
||||
this._socket?.removeEventListener("message", msg => this._handleVote(msg))
|
||||
this._socket?.send(buildCommand("session_stop"))
|
||||
this.sessionTimeout = undefined;
|
||||
}
|
||||
|
||||
_close() {
|
||||
this._socket = undefined;
|
||||
this.sessionTimeout = undefined;
|
||||
clearTimeout(this.sessionTimeout);
|
||||
|
||||
}
|
||||
_handleVote(msg: MessageEvent<any>) {
|
||||
const parsed = parseCommand(msg.data);
|
||||
if (parsed == undefined || parsed.c !== "session_vote" || parsed.d == undefined || !("vote" in parsed.d) || typeof(parsed.d.vote) !== "number") return;
|
||||
if (this.latestVotes === undefined) return;
|
||||
|
||||
this.latestVotes[parsed.d.vote - 1] += 1;
|
||||
}
|
||||
}
|
||||
51
src/heartbeat.ts
Normal file
51
src/heartbeat.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { buildError, parseCommand } from "./commandbuilder.ts";
|
||||
|
||||
const HEARTBEAT_INTERVAL = 30_000;
|
||||
const HEARTBEAT_TIMEOUT = 5_000;
|
||||
const S_PING = JSON.stringify({ "c": "ping" });
|
||||
|
||||
export class WSHeartbeat {
|
||||
heartbeatTimer: number | undefined;
|
||||
pongTimer: number | undefined;
|
||||
socket: WebSocket;
|
||||
|
||||
constructor(socket: WebSocket) {
|
||||
this.socket = socket;
|
||||
socket.addEventListener("open", (_) => this._setHeartBeat());
|
||||
socket.addEventListener("close", _ => this._clearTimers());
|
||||
socket.addEventListener("error", _ => this._clearTimers());
|
||||
socket.addEventListener("message", (msg) => this._onMessage(msg));
|
||||
}
|
||||
|
||||
_setPong(state: boolean) {
|
||||
if (state) {
|
||||
clearTimeout(this.pongTimer);
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.send(S_PING);
|
||||
this.pongTimer = setTimeout(() => {
|
||||
this.socket.send(buildError(0, "Pong missed."));
|
||||
this.socket.close();
|
||||
}, HEARTBEAT_TIMEOUT);
|
||||
}
|
||||
_setHeartBeat() {
|
||||
this._setPong(false);
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
this._setPong(false);
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
}
|
||||
_clearTimers() {
|
||||
clearTimeout(this.pongTimer);
|
||||
clearTimeout(this.heartbeatTimer);
|
||||
}
|
||||
|
||||
_onMessage(msg: MessageEvent<any>) {
|
||||
const text = msg.data;
|
||||
const parsed = parseCommand(text);
|
||||
|
||||
if (parsed === null || parsed.c !== "pong") return;
|
||||
|
||||
this._setPong(true);
|
||||
}
|
||||
}
|
||||
142
src/index.ts
Normal file
142
src/index.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { serveFile } from "@std/http/file-server";
|
||||
import { WSHeartbeat } from "./heartbeat.ts";
|
||||
import { Registration } from "./registration.ts";
|
||||
import { parseCommand } from "./commandbuilder.ts";
|
||||
import { Authentication } from "./auth.ts";
|
||||
import { Device } from "./device.ts";
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
async function wsStartHandler(this: WebSocket, msg: MessageEvent<any>) {
|
||||
const data = parseCommand(msg.data);
|
||||
if (data === null) return;
|
||||
|
||||
switch (data.c as string) {
|
||||
case "reg_start":
|
||||
{
|
||||
new Registration(this);
|
||||
|
||||
this.removeEventListener("message", wsStartHandler);
|
||||
break;
|
||||
}
|
||||
case "auth_start": {
|
||||
await new Authentication(this).authenticate(data);
|
||||
this.removeEventListener("message", wsStartHandler);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrompt(prompt: string) {
|
||||
let tokens = prompt.match(/(?:[^\s"]+|"[^"]*")+/g) || [] as string[];
|
||||
tokens = tokens.map((v) => v.replaceAll(/"([^"]*)"+/g, "$1"));
|
||||
if (tokens.length === 0) return;
|
||||
|
||||
switch (tokens[0]) {
|
||||
case "pin": {
|
||||
const pin = +tokens[1];
|
||||
if (pin === 0 || isNaN(pin)) return;
|
||||
|
||||
console.log(Registration.validate(pin) ? "great success" : "invalid pin");
|
||||
break;
|
||||
}
|
||||
case "devices": {
|
||||
console.debug(Device.devices);
|
||||
break;
|
||||
}
|
||||
case "startsession": {
|
||||
if (tokens.length < 3) return;
|
||||
if (!(tokens[1] in Device.devices)) {
|
||||
console.log("invalid uuid");
|
||||
return;
|
||||
}
|
||||
|
||||
const device = Device.devices[tokens[1]];
|
||||
if (!device.connected) {
|
||||
console.log("Device not connected.");
|
||||
return;
|
||||
}
|
||||
|
||||
device.startSession(tokens[2]);
|
||||
console.log("great success");
|
||||
|
||||
break;
|
||||
}
|
||||
case "stopsession": {
|
||||
if (tokens.length < 2) return;
|
||||
if (!(tokens[1] in Device.devices)) {
|
||||
console.log("invalid uuid");
|
||||
return;
|
||||
}
|
||||
|
||||
const device = Device.devices[tokens[1]];
|
||||
if (!device.connected) {
|
||||
console.log("Device not connected.");
|
||||
return;
|
||||
}
|
||||
|
||||
device.stopSession();
|
||||
console.log("great success");
|
||||
break;
|
||||
}
|
||||
case "votes": {
|
||||
if (tokens.length < 2) return;
|
||||
if (!(tokens[1] in Device.devices)) {
|
||||
console.log("invalid uuid");
|
||||
return;
|
||||
}
|
||||
|
||||
const device = Device.devices[tokens[1]];
|
||||
if (!device.connected) {
|
||||
console.log("Device not connected.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(device.latestVotes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const WS_ROUTE = new URLPattern({ pathname: "/ws" });
|
||||
Deno.serve((req) => {
|
||||
if (WS_ROUTE.exec(req.url) === null) {
|
||||
return serveFile(req, "./static/index.html");
|
||||
}
|
||||
if (req.headers.get("upgrade") != "websocket") {
|
||||
return new Response(null, { status: 426 });
|
||||
}
|
||||
|
||||
const { socket, response } = Deno.upgradeWebSocket(req);
|
||||
new WSHeartbeat(socket);
|
||||
|
||||
socket.addEventListener("message", wsStartHandler);
|
||||
return response;
|
||||
});
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
console.info(`
|
||||
Commands:
|
||||
- pin <int>
|
||||
used to activate device.
|
||||
- devices
|
||||
list devices
|
||||
- startsession <uuid> "<text>"
|
||||
starts a voting session
|
||||
- stopsession <uuid>
|
||||
stops a voting session
|
||||
- votes <uuid>
|
||||
grabs the latest votes.
|
||||
`);
|
||||
|
||||
Deno.stdout.write(Uint8Array.from([62, 32])); // '> '
|
||||
while (true) { // ja dit is scuffed, nee het werkt niet goed, nee ik ga er niks aan doen.
|
||||
for await (const chunk of Deno.stdin.readable) {
|
||||
const prompt = decoder.decode(chunk).trim();
|
||||
if (prompt !== "") handlePrompt(prompt);
|
||||
|
||||
Deno.stdout.write(Uint8Array.from([62, 32])); // '> '
|
||||
}
|
||||
}
|
||||
53
src/registration.ts
Normal file
53
src/registration.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { buildCommand } from "./commandbuilder.ts";
|
||||
import { Device } from "./device.ts";
|
||||
import { encodeBase64 } from "@std/encoding/base64";
|
||||
|
||||
type ResolveFunc = (
|
||||
value: PromiseLike<Device | undefined> | Device | undefined,
|
||||
) => void;
|
||||
export class Registration {
|
||||
static _registrations: Registration[] = [];
|
||||
_socket: WebSocket;
|
||||
_pin: number;
|
||||
|
||||
constructor(socket: WebSocket) {
|
||||
this._socket = socket;
|
||||
this._pin = Math.floor(Math.random() * 10 ** 6); // Normally you'd want to check if this is unique or not.
|
||||
this._socket.send(
|
||||
buildCommand("reg_pin", { pin: this._pin }),
|
||||
);
|
||||
|
||||
this._socket.addEventListener("close", () => this.delete());
|
||||
this._socket.addEventListener("error", () => this.delete());
|
||||
|
||||
Registration._registrations.push(this);
|
||||
}
|
||||
|
||||
validate(pin: number): boolean {
|
||||
if (pin !== this._pin) return false;
|
||||
|
||||
const password = encodeBase64(crypto.getRandomValues(new Uint8Array(128))); // relatively secure
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
this._socket.send(buildCommand("reg_ok", { password, id }));
|
||||
this._socket.send(buildCommand("restart"));
|
||||
|
||||
new Device(password, id);
|
||||
|
||||
Registration._registrations = Registration._registrations.filter((v) =>
|
||||
v._pin !== this._pin
|
||||
);
|
||||
|
||||
this._socket.close()
|
||||
return true;
|
||||
}
|
||||
|
||||
delete() {
|
||||
Registration._registrations = Registration._registrations.filter(v => v._pin === this._pin);
|
||||
}
|
||||
|
||||
static validate(pin: number): boolean {
|
||||
return Registration._registrations.find((v) => v.validate(pin)) !==
|
||||
undefined;
|
||||
}
|
||||
}
|
||||
208
static/index.html
Normal file
208
static/index.html
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<style>
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 2em;
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto;
|
||||
gap: 2em;
|
||||
}
|
||||
|
||||
.labelgrid {
|
||||
display: grid;
|
||||
grid-template-columns: min-content auto;
|
||||
column-gap: 1em;
|
||||
}
|
||||
|
||||
.buttongrid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-basis: 0;
|
||||
width: 100%;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.buttongrid>* {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#votebuttons {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<div class="labelgrid">
|
||||
<label for="id"> ID</label>
|
||||
<input id="id" />
|
||||
|
||||
<label for="passcode"> passcode</label>
|
||||
<input id="passcode" />
|
||||
|
||||
<label for="pin"> pin</label>
|
||||
<input id="pin" />
|
||||
|
||||
<label for="auth"> authenticated</label>
|
||||
<input id="auth" type=checkbox onclick="return false" />
|
||||
</div>
|
||||
|
||||
<div class="buttongrid" id="authbuttons">
|
||||
<button onclick="startRegistration()">registrate</button>
|
||||
<button onclick="startAuthentication()">authenticate</button>
|
||||
</div>
|
||||
|
||||
<div class="buttongrid" id="votebuttons">
|
||||
<button onclick="vote(1)">1</button>
|
||||
<button onclick="vote(2)">2</button>
|
||||
<button onclick="vote(3)">3</button>
|
||||
<button onclick="vote(4)">4</button>
|
||||
<button onclick="vote(5)">5</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<h1 id="text"></h1>
|
||||
<div id="logger"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function preventDefault(e) {alert(); e.preventDefault()}
|
||||
function toHex(data) {
|
||||
return Array.from(data).map((b) => ("00" + b.toString(16)).slice(-2)).join(
|
||||
"",
|
||||
);
|
||||
}
|
||||
async function generateHMAC(key, message) {
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(key);
|
||||
const messageData = encoder.encode(message);
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyData,
|
||||
{name: "HMAC", hash: {name: "SHA-256"}},
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"HMAC",
|
||||
cryptoKey,
|
||||
messageData,
|
||||
);
|
||||
|
||||
return toHex(new Uint8Array(signature));
|
||||
}
|
||||
function log(text) {
|
||||
logger.innerHTML += text + "<br>";
|
||||
}
|
||||
function disableAuthBtns() {
|
||||
document.querySelectorAll("#authbuttons").forEach(v => v.disabled = false);
|
||||
}
|
||||
|
||||
const id = document.querySelector("#id");
|
||||
const pass = document.querySelector("#passcode");
|
||||
const pin = document.querySelector("#pin");
|
||||
const logger = document.querySelector("#logger")
|
||||
const text = document.querySelector("#text")
|
||||
const votebuttons = document.querySelector("#votebuttons")
|
||||
const auth = document.querySelector("#auth")
|
||||
|
||||
function websock() {
|
||||
let socket = new WebSocket("ws://localhost:8000/ws");
|
||||
|
||||
socket.sendln = (...e) => {
|
||||
log("device: " + e.join("\n"));
|
||||
socket.send(...e)
|
||||
}
|
||||
|
||||
socket.addEventListener("message", async (event) => {
|
||||
log("server: " + event.data);
|
||||
|
||||
let data = JSON.parse(event.data);
|
||||
switch (data.c) {
|
||||
case "ping": {
|
||||
socket.sendln(JSON.stringify({c: "pong"}));
|
||||
break
|
||||
}
|
||||
case "reg_pin": {
|
||||
pin.value = data.d.pin;
|
||||
break
|
||||
}
|
||||
case "reg_ok": {
|
||||
id.value = data.d.id;
|
||||
pass.value = data.d.password;
|
||||
socket.close();
|
||||
|
||||
pin.value = "";
|
||||
|
||||
log("### closing and reopening socket");
|
||||
websock()
|
||||
break
|
||||
}
|
||||
case "auth_nonce": {
|
||||
const signature = await generateHMAC(pass.value, data.d.nonce)
|
||||
console.log(pass.value, data.value, signature)
|
||||
socket.sendln(JSON.stringify({c: "auth_validate", d: {signature}}));
|
||||
break
|
||||
}
|
||||
case "auth_ok": {
|
||||
auth.checked = true;
|
||||
disableAuthBtns()
|
||||
break;
|
||||
}
|
||||
case "session_start": {
|
||||
text.style.display = "block";
|
||||
votebuttons.style.display = "flex";
|
||||
text.innerText = data.d.text;
|
||||
break;
|
||||
}
|
||||
case "session_stop": {
|
||||
text.style.display = "none";
|
||||
votebuttons.style.display = "none";
|
||||
text.innerText = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.addEventListener("close", () => {
|
||||
auth.checked = false;
|
||||
disableAuthBtns()
|
||||
window.startRegistration = undefined;
|
||||
window.startAuthentication = undefined;
|
||||
text.innerText = ""
|
||||
})
|
||||
|
||||
socket.addEventListener("open", () => {
|
||||
window.startRegistration = () => {
|
||||
socket.sendln(JSON.stringify({c: "reg_start"}))
|
||||
}
|
||||
window.startAuthentication = () => {
|
||||
socket.sendln(JSON.stringify({c: "auth_start", d: {id: id.value}}))
|
||||
}
|
||||
window.vote = (num) => {
|
||||
socket.sendln(JSON.stringify({c: "session_vote", d: {vote: num}}))
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
websock()
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue