diff --git a/src/usimp.ts b/src/usimp.ts index 6c3eaec..576bcd6 100644 --- a/src/usimp.ts +++ b/src/usimp.ts @@ -1,46 +1,135 @@ "use strict"; -interface DomainJson { +const DNS_RE = /^([a-z0-9_-]+\.)+[a-z]{2,}$/i; +const UUID_RE = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i; + +type DomainJson = { name: string, id: string, } -interface DomainServerJson { +function isDomainJson(json: any): json is DomainJson { + return ( + json !== null && typeof json === 'object' && json.constructor === Object && + typeof json.name === 'string' && typeof json.id === 'string' && + DNS_RE.test(json.name) && UUID_RE.test(json.id) + ); +} + +type DomainServerJson = { host: string, id: string, priority: number, weight: number, protocols: { - https: number | undefined, - wss: number | undefined, - http: number | undefined, - ws: number | undefined, - udp: number | undefined + https?: number | null, + wss?: number | null, + http?: number | null, + ws?: number | null, + udp?: number | null } } -interface OutputEnvelopeJson { +function isDomainServerJson(json: any): json is DomainServerJson { + if ( + json === null || typeof json !== 'object' || json.constructor !== Object || + typeof json.host !== 'string' || typeof json.id !== 'string' || typeof json.priority !== 'number' || + typeof json.weight !== 'number' || + json.protocols === null || typeof json.protocols !== 'object' || json.protocols.constructor !== Object || + !DNS_RE.test(json.host) || !UUID_RE.test(json.id) || json.priority < 0 || json.weight < 0 + ) return false; + + + return ( + (json.protocols.https === undefined || json.protocols.https === null || ( + typeof json.protocols.https === 'number' && json.protocols.https > 0 && json.protocols.https <= 65535 + )) && (json.protocols.wss === undefined || json.protocols.wss === null || ( + typeof json.protocols.wss === 'number' && json.protocols.wss > 0 && json.protocols.wss <= 65535 + )) && (json.protocols.http === undefined || json.protocols.http === null || ( + typeof json.protocols.http === 'number' && json.protocols.http > 0 && json.protocols.http <= 65535 + )) && (json.protocols.ws === undefined || json.protocols.ws === null || ( + typeof json.protocols.ws === 'number' && json.protocols.ws > 0 && json.protocols.ws <= 65535 + )) && (json.protocols.udp === undefined || json.protocols.udp === null || ( + typeof json.protocols.udp === 'number' && json.protocols.udp > 0 && json.protocols.udp <= 65535 + )) + ); +} + +type WellKnownJson = { + domain: DomainJson, + domain_servers: DomainServerJson[], +} + +function isWellKnownJson(json: any): json is WellKnownJson { + if ( + json === null || typeof json !== 'object' || json.constructor !== Object || + !json.hasOwnProperty('domain') || !json.hasOwnProperty('domain_servers') || + json.domain_servers === null || typeof json.domain_servers !== 'object' || + json.domain_servers.constructor !== Array + ) return false; + + return isDomainJson(json.domain) && json.domain_servers.every((item: unknown) => isDomainServerJson(item)); +} + +type OutputEnvelopeJson = { status: string, request_nr: number, - data: any, - action: string | null | undefined, - error: { + data: T, + action?: string | null, + error?: { code: string, message: string | null, description: string | null, } | null, } -interface EventJson { - data: { - message: string, +function isOutputEnvelopeJson(json: any): json is OutputEnvelopeJson { + if ( + json === null || typeof json !== 'object' || json.constructor !== Object || + typeof json.status !== 'string' || typeof json.request_nr !== 'number' || json.request_nr < 0 || + (typeof json.action !== 'string' && json.action !== null && json.action === undefined) || + json.data === null || typeof json.data !== 'object' || json.data.constructor !== Object + ) return false; + + if (json.error !== null && json.error !== undefined) { + if ( + typeof json.error !== 'object' || json.error.constructor !== Object || + typeof json.error.code !== 'string' || + (typeof json.error.message !== 'string' && json.error.message !== null) || + (typeof json.error.description !== 'string' && json.error.description !== null) + ) return false; } - id: string, + + return true; } -interface WellKnownJson { - domain: DomainJson, - domain_servers: DomainServerJson[] +type EventPushEnvelopeJson = { + events: EventJson[], +} + +function isEventPushEnvelopeJson(json: any): json is EventPushEnvelopeJson { + if ( + json === null || typeof json !== 'object' || json.constructor !== Object || + json.events === null || typeof json.events !== 'object' || json.events.constructor !== Array + ) return false; + + return json.events.every((item: unknown) => isEventJson(item)); +} + +type EventJson = { + id: string, + data: { + message: string, + }, +} + +function isEventJson(json: any): json is EventJson { + return !( + json === null || typeof json !== 'object' || json.constructor !== Object || + typeof json.id !== 'string' || !UUID_RE.test(json.id) || + json.data === null || typeof json.data !== 'object' || json.data.constructor !== Object || + typeof json.data.message !== 'string' + ); } export class Domain { @@ -53,10 +142,9 @@ export class Domain { return `[${this.name}]`; } - constructor(jsonData: DomainJson, domainServers: DomainServerJson[]) { - // FIXME check values - this.name = jsonData.name; - this.id = jsonData.id; + constructor(json: DomainJson, domainServers: DomainServerJson[]) { + this.name = json.name; + this.id = json.id; this.servers = []; for (const domainServer of domainServers) { this.servers.push(new DomainServer(domainServer)); @@ -78,8 +166,12 @@ export class Domain { throw new Error("Invalid response"); } - const data: WellKnownJson = await response.json(); - return new Domain(data.domain, data.domain_servers); + const data = await response.json(); + if (isWellKnownJson(data)) { + return new Domain(data.domain, data.domain_servers); + } else { + throw Error("Invalid contents in usimp.json"); + } } chooseDomainServer(): DomainServer { @@ -112,23 +204,29 @@ export class DomainServer { priority: number; weight: number; protocols: { - https: number | undefined, - wss: number | undefined, - http: number | undefined, - ws: number | undefined, - udp: number | undefined + https: number | null, + wss: number | null, + http: number | null, + ws: number | null, + udp: number | null }; toString() { return `[${this.host}/${this.priority}/${this.weight}]`; } - constructor(jsonData: DomainServerJson) { - this.host = jsonData.host; - this.id = jsonData.id; - this.priority = jsonData.priority; - this.weight = jsonData.weight; - this.protocols = jsonData.protocols; + constructor(json: DomainServerJson) { + this.host = json.host; + this.id = json.id; + this.priority = json.priority; + this.weight = json.weight; + this.protocols = { + https: json.protocols.https || null, + wss: json.protocols.wss || null, + http: json.protocols.http || null, + ws: json.protocols.ws || null, + udp: json.protocols.udp || null, + }; } } @@ -145,9 +243,51 @@ export class Room { } export class Event { + id: string; + data: { + message: string; + }; + constructor(json: EventJson) { + this.data = { + message: json.data.message, + }; + this.id = json.id; + } } +type EventHandler = (event: Event) => void; + +export class OutputEnvelope { + status: string; + requestNr: number; + data: T; + action: string | null; + error: { + code: string, + message: string | null, + description: string | null, + } | null; + + constructor(json: OutputEnvelopeJson) { + this.status = json.status; + this.requestNr = json.request_nr; + this.data = json.data; + this.action = json.action || null; + if (json.error) { + this.error = { + code: json.error.code, + message: json.error.message, + description: json.error.description, + }; + } else { + this.error = null; + } + } +} + +type OutputEnvelopeHandler = (envelope: OutputEnvelope) => void; + export class Session { domain: Domain; server: DomainServer | null; @@ -158,12 +298,12 @@ export class Session { numRequests: number; requestNumDiscriminator: number; subscriptions: { - callback: (a: EventJson) => void, + callback: EventHandler, requestNr: number | undefined, abortController: AbortController | undefined, }[]; subscriptionEndpoints: { - callback: (a: EventJson) => void, + callback: EventHandler, }[]; constructor(domain: Domain) { @@ -303,11 +443,11 @@ export class Session { async send( endpoint: string, - data: any, + data: Object, timeout: number = 2000, forceHttp: boolean = false, abortController: AbortController | undefined = undefined, - cb: ((a: OutputEnvelopeJson) => void) | undefined = undefined, + cb: OutputEnvelopeHandler | undefined = undefined, ) { this.numRequests++; while (true) { @@ -324,8 +464,12 @@ export class Session { } else { this.websocket.addEventListener("message", (msg) => { const data = JSON.parse(msg.data); - if (data['request_nr'] === req_nr) { - cb(data); + if (data !== null && data.constructor === Object && data['request_nr'] === req_nr) { + if (isOutputEnvelopeJson(data)) { + cb(new OutputEnvelope(data)); + } else { + throw new Error("Invalid server response"); + } } }); } @@ -415,7 +559,7 @@ export class Session { return response; } - async newEvent(roomId: string, data: any, id: string | undefined = undefined) { + async newEvent(roomId: string, data: Object, id: string | undefined = undefined) { return this.send("new_event", { room_id: roomId, events: [{ @@ -425,17 +569,21 @@ export class Session { }); } - async subscribe(cb: (a: EventJson) => void) { + async subscribe(cb: EventHandler) { this.subscriptionEndpoints.push({callback: cb}); await this._subscribe(cb); } - private async _subscribe(cb: (a: EventJson) => void) { + private async _subscribe(cb: EventHandler) { if (this.websocket !== null) { const subscription = await this.send('subscribe', {}, 60_000, false, undefined, (res) => { if (res.action === 'push') { - for (const event of res.data.events) { - cb(event); + if (isEventPushEnvelopeJson(res.data)) { + for (const event of res.data.events) { + cb(new Event(event)); + } + } else { + throw new Error("Invalid server response"); } } });