diff --git a/mapsync-server/package.json b/mapsync-server/package.json index 910b623..48b0f31 100644 --- a/mapsync-server/package.json +++ b/mapsync-server/package.json @@ -19,6 +19,7 @@ "better-sqlite3": "12.8.0", "kysely": "0.28.14", "source-map-support": "0.5.21", + "uuid": "^14.0.0", "ws": "8.20.0", "zod": "3.25.76", "zod-validation-error": "1.5.0" diff --git a/mapsync-server/pnpm-lock.yaml b/mapsync-server/pnpm-lock.yaml index e6b649b..cf4f82f 100644 --- a/mapsync-server/pnpm-lock.yaml +++ b/mapsync-server/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: source-map-support: specifier: 0.5.21 version: 0.5.21 + uuid: + specifier: ^14.0.0 + version: 14.0.0 ws: specifier: 8.20.0 version: 8.20.0(bufferutil@4.1.0) @@ -229,6 +232,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -436,6 +443,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@14.0.0: {} + wrappy@1.0.2: {} ws@8.20.0(bufferutil@4.1.0): diff --git a/mapsync-server/src/auth.ts b/mapsync-server/src/auth.ts index 9ddca02..8d86df2 100644 --- a/mapsync-server/src/auth.ts +++ b/mapsync-server/src/auth.ts @@ -1,3 +1,5 @@ +import { isValidUuid } from "./deps/uuid.ts"; + export abstract class AuthState { protected constructor(public readonly logName: string | null) {} } @@ -20,6 +22,9 @@ export class Welcomed extends AuthState { public readonly uuid: string, public readonly authed: boolean, ) { + if (!isValidUuid(uuid)) { + throw new Error(`Invalid UUID: ${uuid}`); + } super(name + (authed ? "" : "?")); } } diff --git a/mapsync-server/src/deps/uuid.ts b/mapsync-server/src/deps/uuid.ts new file mode 100644 index 0000000..ff1eaa9 --- /dev/null +++ b/mapsync-server/src/deps/uuid.ts @@ -0,0 +1,24 @@ +import node_crypto from "crypto"; +import { stringify as parseUuidBytes } from "uuid"; +import { v5 as _uuidv5 } from "uuid"; +export { validate as isValidUuid } from "uuid"; + +export const uuidv5 = _uuidv5; + +// https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/UUID.html#nameUUIDFromBytes(byte[]) +export function nameUuidFromBytes(bytes: Buffer): string { + const hash = node_crypto.createHash("md5").update(bytes).digest(); + hash[6] &= 0x0f; // Clears the version + hash[6] |= 0x30; // Sets version to 3 + hash[8] &= 0x3f; // Clears the variant + hash[8] |= 0x80; // Sets variant to IETF + return parseUuidBytes(hash); +} + +export const UUID_NAMESPACE = nameUuidFromBytes( + Buffer.from("mapsync:server", "utf8"), +); + +export function createOfflineUuid(name: string) { + return uuidv5(`Offline:${name}`, UUID_NAMESPACE); +} diff --git a/mapsync-server/src/deps/zod.ts b/mapsync-server/src/deps/zod.ts new file mode 100644 index 0000000..203eb87 --- /dev/null +++ b/mapsync-server/src/deps/zod.ts @@ -0,0 +1,18 @@ +export * from "zod"; +export { fromZodError } from "zod-validation-error"; + +import * as z from "zod"; +import { createOfflineUuid } from "./uuid.ts"; + +export function offlineUuid() { + return z.preprocess((val) => { + if (typeof val !== "string") { + return val; + } + const match = /^AUTH-DISABLED-(.+)/.exec(val) ?? null; + if (match === null) { + return val; + } + return createOfflineUuid(match[1]); + }, z.string().uuid()); +} diff --git a/mapsync-server/src/main.ts b/mapsync-server/src/main.ts index 8bb7294..a567eaf 100644 --- a/mapsync-server/src/main.ts +++ b/mapsync-server/src/main.ts @@ -24,6 +24,7 @@ import { Welcomed, } from "./auth.ts"; import { SUPPORTED_VERSIONS } from "./constants.ts"; +import { createOfflineUuid } from "./deps/uuid.ts"; let config: metadata.Config = null!; let main: ProtocolHandler = null!; @@ -150,7 +151,7 @@ export class ProtocolHandler { } client.auth = new Welcomed( packet.claimedUsername, - `AUTH-DISABLED-${packet.claimedUsername}`, + createOfflineUuid(packet.claimedUsername), false, ); } diff --git a/mapsync-server/src/metadata.ts b/mapsync-server/src/metadata.ts index 5dc5ea4..6a7331e 100644 --- a/mapsync-server/src/metadata.ts +++ b/mapsync-server/src/metadata.ts @@ -3,8 +3,7 @@ import node_path from "node:path"; import { Mutex } from "async-mutex"; import * as errors from "./deps/errors.ts"; import * as json from "./deps/json.ts"; -import * as z from "zod"; -import { fromZodError } from "zod-validation-error"; +import * as z from "./deps/zod.ts"; export const DATA_FOLDER = process.env["MAPSYNC_DATA_DIR"] ?? "./mapsync"; try { @@ -49,7 +48,7 @@ function parseConfigFile( return parser(json.parse(fileContents)); } catch (e) { if (e instanceof z.ZodError) { - throw "Could not parse " + file + ": " + fromZodError(e); + throw "Could not parse " + file + ": " + z.fromZodError(e); } throw e; } @@ -96,9 +95,7 @@ export function getConfig(): Config { const WHITELIST_FILE = "whitelist.json"; const WHITELIST_MUTEX = new Mutex(); -const WHITELIST_SCHEMA = z.array( - z.union([z.string().uuid(), z.string().regex(/^AUTH-DISABLED-.+/)]), -); +const WHITELIST_SCHEMA = z.array(z.offlineUuid()); export const whitelist = new Set(); export async function loadWhitelist() { @@ -130,9 +127,7 @@ export async function saveWhitelist() { const UUID_CACHE_FILE = "uuid_cache.json"; const UUID_CACHE_MUTEX = new Mutex(); -const UUID_CACHE_SCHEMA = z.record( - z.union([z.string().uuid(), z.string().regex(/^AUTH-DISABLED-.+/)]), -); +const UUID_CACHE_SCHEMA = z.record(z.offlineUuid()); // IGN UUID const uuid_cache = new Map();