diff --git a/flottform/forms/src/default-component.ts b/flottform/forms/src/default-component.ts index c772b8c..c03719f 100644 --- a/flottform/forms/src/default-component.ts +++ b/flottform/forms/src/default-component.ts @@ -92,7 +92,11 @@ export const createDefaultFlottformComponent = ({ onSuccessText }: { flottformApi: string; - createClientUrl: (params: { endpointId: string }) => Promise; + createClientUrl: (params: { + endpointId: string; + encryptionKey: string; + optionalData?: object; + }) => Promise; inputField: HTMLInputElement; id?: string; additionalItemClasses?: string; @@ -147,7 +151,11 @@ export const createDefaultFlottformComponent = ({ onSuccessText }: { flottformApi: string; - createClientUrl: (params: { endpointId: string }) => Promise; + createClientUrl: (params: { + endpointId: string; + encryptionKey: string; + optionalData?: object; + }) => Promise; inputField?: HTMLInputElement | HTMLTextAreaElement; id?: string; additionalItemClasses?: string; diff --git a/flottform/forms/src/encryption.ts b/flottform/forms/src/encryption.ts new file mode 100644 index 0000000..e7e2916 --- /dev/null +++ b/flottform/forms/src/encryption.ts @@ -0,0 +1,106 @@ +export async function generateKey(): Promise { + return await crypto.subtle.generateKey( + { + name: 'AES-GCM', + length: 256 + }, + true, // extractable + ['encrypt', 'decrypt'] + ); +} + +export async function cryptoKeyToEncryptionKey(key: CryptoKey) { + // CryptoKey --> Exported bytes of the cryptoKey (i.e. the encryption key) + return (await crypto.subtle.exportKey('jwk', key)).k; +} + +export async function encryptionKeyToCryptoKey(encryptionKey: string) { + // Create a complete JWK object structure + const jwk = { + kty: 'oct', + k: encryptionKey, + alg: 'A256GCM', + ext: true, + key_ops: ['encrypt', 'decrypt'] + }; + + // Import the complete JWK + return await crypto.subtle.importKey( + 'jwk', + jwk, + { + name: 'AES-GCM', + length: 256 + }, + true, // extractable + ['encrypt', 'decrypt'] + ); +} + +export async function encrypt(plaintext: string, cryptoKey: CryptoKey): Promise { + const data = plaintextToTypedArray(plaintext); + const iv = getInitializationVector(); + + const encryptedData = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv + }, + cryptoKey, + data + ); + + // Prepend the cyphertext with the initialization vector. + const combinedData = new Uint8Array(iv.length + encryptedData.byteLength); + combinedData.set(iv, 0); + combinedData.set(new Uint8Array(encryptedData), iv.length); + + return typedArrayToBase64(combinedData); +} + +export async function decrypt(ciphertext: string, cryptoKey: CryptoKey) { + const combinedData = base64ToTypedArray(ciphertext); + + // Step 2: Extract IV and ciphertext + const iv = combinedData.slice(0, 12); + const data = combinedData.slice(12); + + const decryptedData = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv + }, + cryptoKey, + data + ); + + return typedArrayToPlaintext(new Uint8Array(decryptedData)); +} + +function plaintextToTypedArray(plainText: string): Uint8Array { + // Then encode to Uint8Array + const encoder = new TextEncoder(); + return encoder.encode(plainText); +} + +function getInitializationVector(): Uint8Array { + return crypto.getRandomValues(new Uint8Array(12)); +} + +function typedArrayToBase64(typedArray: Uint8Array): string { + // Uint8Array --> Base64 + return btoa(String.fromCharCode(...new Uint8Array(typedArray))); +} + +function base64ToTypedArray(messageAsBase64: string): Uint8Array { + // Base64 --> Uint8Array + const binaryString = atob(messageAsBase64); + return Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); +} + +function typedArrayToPlaintext(typedArray: Uint8Array, isOriginalDataJson = true) { + // Uint8Array --> data (string, number, object, array..) + const decoder = new TextDecoder(); + const plaintext = decoder.decode(typedArray); + return isOriginalDataJson ? JSON.parse(plaintext) : plaintext; +} diff --git a/flottform/forms/src/flottform-channel-client.ts b/flottform/forms/src/flottform-channel-client.ts index f1e1db2..88bfc92 100644 --- a/flottform/forms/src/flottform-channel-client.ts +++ b/flottform/forms/src/flottform-channel-client.ts @@ -1,3 +1,4 @@ +import { decrypt, encrypt, encryptionKeyToCryptoKey } from './encryption'; import { ClientState, EventEmitter, @@ -28,6 +29,8 @@ export class FlottformChannelClient extends EventEmitter { private pollTimeForIceInMs: number; private logger: Logger; + private cryptoKey: CryptoKey | null = null; + private encryptionKey: string; private state: ClientState = 'init'; private openPeerConnection: RTCPeerConnection | null = null; private dataChannel: RTCDataChannel | null = null; @@ -38,12 +41,14 @@ export class FlottformChannelClient extends EventEmitter { endpointId, flottformApi, rtcConfiguration, + encryptionKey, pollTimeForIceInMs = POLL_TIME_IN_MS, logger = console }: { endpointId: string; flottformApi: string | URL; rtcConfiguration: RTCConfiguration; + encryptionKey: string; pollTimeForIceInMs?: number; logger?: Logger; }) { @@ -51,6 +56,7 @@ export class FlottformChannelClient extends EventEmitter { this.endpointId = endpointId; this.flottformApi = flottformApi; this.rtcConfiguration = rtcConfiguration; + this.encryptionKey = encryptionKey; this.pollTimeForIceInMs = pollTimeForIceInMs; this.logger = logger; } @@ -66,6 +72,9 @@ export class FlottformChannelClient extends EventEmitter { if (this.openPeerConnection) { this.close(); } + // Import cryptoKey from encryptionKey + this.cryptoKey = await encryptionKeyToCryptoKey(this.encryptionKey); + const baseApi = ( this.flottformApi instanceof URL ? this.flottformApi : new URL(this.flottformApi) ) @@ -73,7 +82,7 @@ export class FlottformChannelClient extends EventEmitter { .replace(/\/$/, ''); // For now the fetching can be done outside of these classes and should be passed as an argument. - + /* try { this.rtcConfiguration.iceServers = await this.fetchIceServers(baseApi); } catch (error) { @@ -88,8 +97,16 @@ export class FlottformChannelClient extends EventEmitter { const putClientInfoUrl = `${this.flottformApi}/${this.endpointId}/client`; this.changeState('retrieving-info-from-endpoint'); - const { hostInfo } = await retrieveEndpointInfo(getEndpointInfoUrl); - await this.openPeerConnection.setRemoteDescription(hostInfo.session); + const hostInfoCipherText = await retrieveEndpointInfo(getEndpointInfoUrl); + + if (!this.cryptoKey) { + throw new Error('CryptoKey is null! Decryption is not possible!!'); + } + const hostInfo = await decrypt(hostInfoCipherText.hostInfo, this.cryptoKey); + + const hostSession: RTCSessionDescriptionInit = JSON.parse(hostInfo.session); + + await this.openPeerConnection.setRemoteDescription(hostSession); const session = await this.openPeerConnection.createAnswer(); await this.openPeerConnection.setLocalDescription(session); @@ -179,6 +196,7 @@ export class FlottformChannelClient extends EventEmitter { this.logger.error(`onicecandidateerror - ${this.openPeerConnection!.connectionState}`, e); }; }; + private setUpConnectionStateGathering = (getEndpointInfoUrl: string) => { if (this.openPeerConnection === null) { this.changeState( @@ -216,12 +234,14 @@ export class FlottformChannelClient extends EventEmitter { } }; }; + private stopPollingForIceCandidates = async () => { if (this.pollForIceTimer) { clearTimeout(this.pollForIceTimer); } this.pollForIceTimer = null; }; + private startPollingForIceCandidates = async (getEndpointInfoUrl: string) => { if (this.pollForIceTimer) { clearTimeout(this.pollForIceTimer); @@ -231,6 +251,7 @@ export class FlottformChannelClient extends EventEmitter { this.pollForIceTimer = setTimeout(this.startPollingForIceCandidates, this.pollTimeForIceInMs); }; + private pollForConnection = async (getEndpointInfoUrl: string) => { if (this.openPeerConnection === null) { this.changeState('error', "openPeerConnection is null. Unable to retrieve Host's details"); @@ -238,11 +259,19 @@ export class FlottformChannelClient extends EventEmitter { } this.logger.log('polling for host ice candidates', this.openPeerConnection.iceGatheringState); - const { hostInfo } = await retrieveEndpointInfo(getEndpointInfoUrl); - for (const iceCandidate of hostInfo.iceCandidates) { + const hostInfoCipherText = await retrieveEndpointInfo(getEndpointInfoUrl); + if (!this.cryptoKey) { + throw new Error('CryptoKey is null! Decryption is not possible!!'); + } + const hostInfo = await decrypt(hostInfoCipherText.hostInfo, this.cryptoKey); + + const hostIceCandidates: RTCIceCandidateInit[] = JSON.parse(hostInfo.iceCandidates); + + for (const iceCandidate of hostIceCandidates) { await this.openPeerConnection.addIceCandidate(iceCandidate); } }; + private putClientInfo = async ( putClientInfoUrl: string, clientKey: string, @@ -250,13 +279,23 @@ export class FlottformChannelClient extends EventEmitter { session: RTCSessionDescriptionInit ) => { this.logger.log('Updating client info with new list of ice candidates'); + if (!this.cryptoKey) { + throw new Error('CryptoKey is null! Encryption is not possible!!'); + } + const encryptedClientInfo = await encrypt( + JSON.stringify({ + session: JSON.stringify(session), + iceCandidates: JSON.stringify([...clientIceCandidates]) + }), + this.cryptoKey + ); + const response = await fetch(putClientInfoUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientKey, - iceCandidates: [...clientIceCandidates], - session + clientInfo: encryptedClientInfo }) }); if (!response.ok) { diff --git a/flottform/forms/src/flottform-channel-host.ts b/flottform/forms/src/flottform-channel-host.ts index 997edae..e4dbb82 100644 --- a/flottform/forms/src/flottform-channel-host.ts +++ b/flottform/forms/src/flottform-channel-host.ts @@ -7,14 +7,19 @@ import { retrieveEndpointInfo, setIncludes } from './internal'; +import { cryptoKeyToEncryptionKey, decrypt, encrypt, generateKey } from './encryption'; export class FlottformChannelHost extends EventEmitter { private flottformApi: string | URL; - private createClientUrl: (params: { endpointId: string }) => Promise; + private createClientUrl: (params: { + endpointId: string; + encryptionKey: string; + }) => Promise; private rtcConfiguration: RTCConfiguration; private pollTimeForIceInMs: number; private logger: Logger; + private cryptoKey: CryptoKey | null = null; private state: FlottformState | 'disconnected' = 'new'; private channelNumber: number = 0; private openPeerConnection: RTCPeerConnection | null = null; @@ -29,7 +34,7 @@ export class FlottformChannelHost extends EventEmitter { logger }: { flottformApi: string | URL; - createClientUrl: (params: { endpointId: string }) => Promise; + createClientUrl: (params: { endpointId: string; encryptionKey: string }) => Promise; rtcConfiguration: RTCConfiguration; pollTimeForIceInMs: number; logger: Logger; @@ -56,6 +61,9 @@ export class FlottformChannelHost extends EventEmitter { if (this.openPeerConnection) { this.close(); } + // Generate Encryption/Decryption Key. + this.cryptoKey = await generateKey(); + const baseApi = ( this.flottformApi instanceof URL ? this.flottformApi : new URL(this.flottformApi) ) @@ -89,7 +97,11 @@ export class FlottformChannelHost extends EventEmitter { this.setupHostIceGathering(putHostInfoUrl, hostKey, hostIceCandidates, session); this.setupDataChannelForTransfer(); - const connectLink = await this.createClientUrl({ endpointId }); + const encryptionKey = await cryptoKeyToEncryptionKey(this.cryptoKey); + if (!encryptionKey) { + throw new Error('Encryption Key is undefined!'); + } + const connectLink = await this.createClientUrl({ endpointId, encryptionKey }); this.changeState('waiting-for-client', { qrCode: await toDataURL(connectLink), link: connectLink, @@ -214,13 +226,18 @@ export class FlottformChannelHost extends EventEmitter { }; private createEndpoint = async (baseApi: string, session: RTCSessionDescriptionInit) => { + if (!this.cryptoKey) { + throw new Error('CryptoKey is null! Encryption is not possible!!'); + } + const encryptedSession = await encrypt(JSON.stringify({ session }), this.cryptoKey); + const response = await fetch(`${baseApi}/create`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify({ session }) + body: JSON.stringify({ hostInfo: encryptedSession }) }); return response.json(); @@ -252,15 +269,30 @@ export class FlottformChannelHost extends EventEmitter { } this.logger.log('polling for client ice candidates', this.openPeerConnection.iceGatheringState); - const { clientInfo } = await retrieveEndpointInfo(getEndpointInfoUrl); + const clientInfoCipherText = await retrieveEndpointInfo(getEndpointInfoUrl); + + if (!this.cryptoKey) { + throw new Error('CryptoKey is null! Decryption is not possible!!'); + } + + let clientInfo; + let decryptedSession; + let decryptedIceCandidates: RTCIceCandidateInit[] = []; + + if (clientInfoCipherText.clientInfo) { + clientInfo = await decrypt(clientInfoCipherText.clientInfo, this.cryptoKey); + + decryptedSession = JSON.parse(clientInfo.session); + decryptedIceCandidates = JSON.parse(clientInfo.iceCandidates); + } if (clientInfo && this.state === 'waiting-for-client') { this.logger.log('Found a client that wants to connect!'); this.changeState('waiting-for-ice'); - await this.openPeerConnection.setRemoteDescription(clientInfo.session); + await this.openPeerConnection.setRemoteDescription(decryptedSession); } - for (const iceCandidate of clientInfo?.iceCandidates ?? []) { + for (const iceCandidate of decryptedIceCandidates ?? []) { await this.openPeerConnection.addIceCandidate(iceCandidate); } }; @@ -305,13 +337,23 @@ export class FlottformChannelHost extends EventEmitter { ) => { try { this.logger.log('Updating host info with new list of ice candidates'); + if (!this.cryptoKey) { + throw new Error('CryptoKey is null! Encryption is not possible!!'); + } + + const encryptedHostInfo = await encrypt( + JSON.stringify({ + session: JSON.stringify(session), + iceCandidates: JSON.stringify([...hostIceCandidates]) + }), + this.cryptoKey + ); const response = await fetch(putHostInfoUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hostKey, - iceCandidates: [...hostIceCandidates], - session + hostInfo: encryptedHostInfo }) }); if (!response.ok) { diff --git a/flottform/forms/src/flottform-file-input-client.ts b/flottform/forms/src/flottform-file-input-client.ts index 7151e81..a99059f 100644 --- a/flottform/forms/src/flottform-file-input-client.ts +++ b/flottform/forms/src/flottform-file-input-client.ts @@ -32,6 +32,7 @@ export class FlottformFileInputClient extends EventEmitter { endpointId, fileInput, flottformApi, + encryptionKey, rtcConfiguration = DEFAULT_WEBRTC_CONFIG, pollTimeForIceInMs = POLL_TIME_IN_MS, logger = console @@ -39,6 +40,7 @@ export class FlottformFileInputClient extends EventEmitter { endpointId: string; fileInput: HTMLInputElement; flottformApi: string; + encryptionKey: string; rtcConfiguration?: RTCConfiguration; pollTimeForIceInMs?: number; logger?: Logger; @@ -48,6 +50,7 @@ export class FlottformFileInputClient extends EventEmitter { endpointId, flottformApi, rtcConfiguration, + encryptionKey, pollTimeForIceInMs, logger }); diff --git a/flottform/forms/src/flottform-file-input-host.ts b/flottform/forms/src/flottform-file-input-host.ts index 42f838b..57d132d 100644 --- a/flottform/forms/src/flottform-file-input-host.ts +++ b/flottform/forms/src/flottform-file-input-host.ts @@ -45,7 +45,7 @@ export class FlottformFileInputHost extends BaseInputHost { logger = console }: { flottformApi: string | URL; - createClientUrl: (params: { endpointId: string }) => Promise; + createClientUrl: (params: { endpointId: string; encryptionKey: string }) => Promise; inputField?: HTMLInputElement; rtcConfiguration?: RTCConfiguration; pollTimeForIceInMs?: number; diff --git a/flottform/forms/src/flottform-text-input-client.ts b/flottform/forms/src/flottform-text-input-client.ts index c0e11ce..4a64b84 100644 --- a/flottform/forms/src/flottform-text-input-client.ts +++ b/flottform/forms/src/flottform-text-input-client.ts @@ -17,6 +17,7 @@ export class FlottformTextInputClient extends EventEmitter { constructor({ endpointId, flottformApi, + encryptionKey, rtcConfiguration = DEFAULT_WEBRTC_CONFIG, pollTimeForIceInMs = POLL_TIME_IN_MS, logger = console @@ -24,6 +25,7 @@ export class FlottformTextInputClient extends EventEmitter { endpointId: string; flottformApi: string; rtcConfiguration?: RTCConfiguration; + encryptionKey: string; pollTimeForIceInMs?: number; logger?: Logger; }) { @@ -32,6 +34,7 @@ export class FlottformTextInputClient extends EventEmitter { endpointId, flottformApi, rtcConfiguration, + encryptionKey, pollTimeForIceInMs, logger }); diff --git a/flottform/forms/src/flottform-text-input-host.ts b/flottform/forms/src/flottform-text-input-host.ts index e2d342d..e14be86 100644 --- a/flottform/forms/src/flottform-text-input-host.ts +++ b/flottform/forms/src/flottform-text-input-host.ts @@ -30,7 +30,7 @@ export class FlottformTextInputHost extends BaseInputHost { logger = console }: { flottformApi: string | URL; - createClientUrl: (params: { endpointId: string }) => Promise; + createClientUrl: (params: { endpointId: string; encryptionKey: string }) => Promise; inputField?: HTMLInputElement | HTMLTextAreaElement; rtcConfiguration?: RTCConfiguration; pollTimeForIceInMs?: number; diff --git a/flottform/forms/src/internal.ts b/flottform/forms/src/internal.ts index 2658b9c..6e54716 100644 --- a/flottform/forms/src/internal.ts +++ b/flottform/forms/src/internal.ts @@ -6,15 +6,9 @@ type EndpointId = string; type EndpointInfo = { hostKey: HostKey; endpointId: EndpointId; - hostInfo: { - session: RTCSessionDescriptionInit; - iceCandidates: RTCIceCandidateInit[]; - }; + hostInfo: string; clientKey?: ClientKey; - clientInfo?: { - session: RTCSessionDescriptionInit; - iceCandidates: RTCIceCandidateInit[]; - }; + clientInfo?: string; }; export type BaseListeners = { diff --git a/flottform/forms/src/types.ts b/flottform/forms/src/types.ts index 95bb10c..6057abc 100644 --- a/flottform/forms/src/types.ts +++ b/flottform/forms/src/types.ts @@ -1,6 +1,10 @@ export interface FlottformCreateItemParams { flottformApi: string; - createClientUrl: (params: { endpointId: string }) => Promise; + createClientUrl: (params: { + endpointId: string; + encryptionKey: string; + optionalData?: object; + }) => Promise; id?: string; additionalItemClasses?: string; label?: string; diff --git a/flottform/server/src/database.ts b/flottform/server/src/database.ts index 271a410..2631fcb 100644 --- a/flottform/server/src/database.ts +++ b/flottform/server/src/database.ts @@ -4,15 +4,9 @@ type EndpointId = string; type EndpointInfo = { hostKey: HostKey; endpointId: EndpointId; - hostInfo: { - session: RTCSessionDescriptionInit; - iceCandidates: RTCIceCandidateInit[]; - }; + hostInfo: string; clientKey?: ClientKey; - clientInfo?: { - session: RTCSessionDescriptionInit; - iceCandidates: RTCIceCandidateInit[]; - }; + clientInfo?: string; }; type SafeEndpointInfo = Omit; @@ -28,14 +22,11 @@ class FlottformDatabase { constructor() {} - async createEndpoint({ session }: { session: RTCSessionDescriptionInit }): Promise { + async createEndpoint({ hostInfo }: { hostInfo: string }): Promise { const entry = { hostKey: createRandomHostKey(), endpointId: createRandomEndpointId(), - hostInfo: { - session, - iceCandidates: [] - } + hostInfo }; this.map.set(entry.endpointId, entry); return entry; @@ -54,13 +45,11 @@ class FlottformDatabase { async putHostInfo({ endpointId, hostKey, - session, - iceCandidates + hostInfo }: { endpointId: EndpointId; hostKey: HostKey; - session: RTCSessionDescriptionInit; - iceCandidates: RTCIceCandidateInit[]; + hostInfo: string; }): Promise { const existingSession = this.map.get(endpointId); if (!existingSession) { @@ -72,7 +61,7 @@ class FlottformDatabase { const newInfo = { ...existingSession, - hostInfo: { ...existingSession.hostInfo, session, iceCandidates } + hostInfo }; this.map.set(endpointId, newInfo); @@ -84,13 +73,11 @@ class FlottformDatabase { async putClientInfo({ endpointId, clientKey, - session, - iceCandidates + clientInfo }: { endpointId: EndpointId; clientKey: ClientKey; - session: RTCSessionDescriptionInit; - iceCandidates: RTCIceCandidateInit[]; + clientInfo: string; }): Promise> { const existingSession = this.map.get(endpointId); if (!existingSession) { @@ -105,7 +92,7 @@ class FlottformDatabase { const newInfo = { ...existingSession, clientKey, - clientInfo: { session, iceCandidates } + clientInfo }; this.map.set(endpointId, newInfo); diff --git a/servers/chrome-extension/src/lib/isOfTypeRTCIceServer.test.ts b/servers/chrome-extension/src/lib/isOfTypeRTCIceServer.test.ts new file mode 100644 index 0000000..ee15335 --- /dev/null +++ b/servers/chrome-extension/src/lib/isOfTypeRTCIceServer.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from 'vitest'; +import { isOfTypeRTCIceServer } from './isOfTypeRTCIceServer'; + +describe('isOfTypeRTCIceServer()', () => { + it('returns false for undefined', () => { + const result = isOfTypeRTCIceServer(undefined); + expect(result).toBe(false); + }); + it('returns false for null', () => { + const result = isOfTypeRTCIceServer(null); + expect(result).toBe(false); + }); + it('returns false for empty object', () => { + const result = isOfTypeRTCIceServer({}); + expect(result).toBe(false); + }); + it('returns false for object with a urls property set to undefined', () => { + const result = isOfTypeRTCIceServer({ urls: undefined }); + expect(result).toBe(false); + }); + it('returns false for object with a urls property set to null', () => { + const result = isOfTypeRTCIceServer({ urls: null }); + expect(result).toBe(false); + }); + it('returns false for object with a urls property set to an empty string', () => { + const result = isOfTypeRTCIceServer({ urls: '' }); + expect(result).toBe(false); + }); + it('returns true for object with a urls property set to a string', () => { + const result = isOfTypeRTCIceServer({ urls: 'stun:somewhere' }); + expect(result).toBe(true); + }); + it('returns false for object with a urls property set to an empty array', () => { + const result = isOfTypeRTCIceServer({ urls: [] }); + expect(result).toBe(false); + }); + it('returns false for object with a urls property set to an array with an undefined value', () => { + const result = isOfTypeRTCIceServer({ urls: [undefined] }); + expect(result).toBe(false); + }); + it('returns false for object with a urls property set to an array with an null value', () => { + const result = isOfTypeRTCIceServer({ urls: [null] }); + expect(result).toBe(false); + }); + it('returns false for object with a urls property set to an array with an empty string value', () => { + const result = isOfTypeRTCIceServer({ urls: [''] }); + expect(result).toBe(false); + }); + it('returns true for object with a urls property set to an array of non empty strings', () => { + const result = isOfTypeRTCIceServer({ urls: ['stun:somewhere'] }); + expect(result).toBe(true); + }); + it('returns false for object with a username property being undefined', () => { + const result = isOfTypeRTCIceServer({ urls: 'stun:somewhere', username: undefined }); + expect(result).toBe(false); + }); + it('returns false for object with a username property being null', () => { + const result = isOfTypeRTCIceServer({ urls: 'stun:somewhere', username: null }); + expect(result).toBe(false); + }); + it('returns false for object with a username property being an integer', () => { + const result = isOfTypeRTCIceServer({ urls: 'stun:somewhere', username: 123 }); + expect(result).toBe(false); + }); + it('returns true for object with a username property being an empty string', () => { + const result = isOfTypeRTCIceServer({ urls: 'stun:somewhere', username: '' }); + expect(result).toBe(true); + }); + it('returns false for object with a credentials property being undefined', () => { + const result = isOfTypeRTCIceServer({ urls: 'stun:somewhere', credentials: undefined }); + expect(result).toBe(false); + }); + it('returns false for object with a credentials property being null', () => { + const result = isOfTypeRTCIceServer({ urls: 'stun:somewhere', credentials: null }); + expect(result).toBe(false); + }); + it('returns false for object with a credentials property being an integer', () => { + const result = isOfTypeRTCIceServer({ urls: 'stun:somewhere', credentials: 123 }); + expect(result).toBe(false); + }); + it('returns true for object with a credential property being an empty string', () => { + const result = isOfTypeRTCIceServer({ urls: 'stun:somewhere', credentials: '' }); + expect(result).toBe(true); + }); +}); diff --git a/servers/chrome-extension/src/lib/isOfTypeRTCIceServer.ts b/servers/chrome-extension/src/lib/isOfTypeRTCIceServer.ts new file mode 100644 index 0000000..4bb6cf4 --- /dev/null +++ b/servers/chrome-extension/src/lib/isOfTypeRTCIceServer.ts @@ -0,0 +1,31 @@ +function isNonEmptyString(something: unknown): something is string { + return typeof something === 'string' && something !== ''; +} + +export function isOfTypeRTCIceServer(obj: unknown): obj is RTCIceServer { + if (!obj) { + return false; + } + + if (!(typeof obj === 'object')) { + return false; + } + + if (!('urls' in obj)) { + return false; + } + + if (!obj.urls) { + return false; + } + + const urlsIsNonEmptyString = isNonEmptyString(obj.urls); + + const urlsIsNonEmptyArrayOfNonEmptyStrings = + Array.isArray(obj.urls) && obj.urls.length > 0 && obj.urls.every(isNonEmptyString); + const urlsIsCorrect = urlsIsNonEmptyString || urlsIsNonEmptyArrayOfNonEmptyStrings; + const optionalUsernameWouldBeString = !('username' in obj) || typeof obj.username === 'string'; + const optionalCredentialsWouldBeString = + !('credentials' in obj) || typeof obj.credentials === 'string'; + return urlsIsCorrect && optionalUsernameWouldBeString && optionalCredentialsWouldBeString; +} diff --git a/servers/chrome-extension/src/lib/options.ts b/servers/chrome-extension/src/lib/options.ts index 4b48a5d..f4b89bb 100644 --- a/servers/chrome-extension/src/lib/options.ts +++ b/servers/chrome-extension/src/lib/options.ts @@ -1,3 +1,3 @@ -export const defaultTurnServerMeteredEndpointValue = ''; -export const defaultSignalingServerUrlBase = 'https://192.168.0.169:5177/flottform'; //'https://demo.flottform.io/flottform'; -export const defaultExtensionClientUrlBase = 'https://192.168.0.169:5175/browser-extension'; //'https://demo.flottform.io/browser-extension'; +export const defaultGetIceServersEndpoint = ''; +export const defaultSignalingServerUrlBase = 'https://192.168.0.169:5177/flottform'; //'https://api.flottform.io/v1'; +export const defaultExtensionClientUrlBase = 'https://192.168.0.169:5175/browser-extension'; //'https://api.flottform.io/client'; diff --git a/servers/chrome-extension/src/routes/+layout.svelte b/servers/chrome-extension/src/routes/+layout.svelte index 0a59f92..7fe5b48 100644 --- a/servers/chrome-extension/src/routes/+layout.svelte +++ b/servers/chrome-extension/src/routes/+layout.svelte @@ -7,6 +7,3 @@
- - diff --git a/servers/chrome-extension/src/routes/+page.svelte b/servers/chrome-extension/src/routes/+page.svelte index ab85462..9bf6a52 100644 --- a/servers/chrome-extension/src/routes/+page.svelte +++ b/servers/chrome-extension/src/routes/+page.svelte @@ -1,11 +1,12 @@