// @flow import JsonRpcWs, { InvalidReply } from './jsonrpc-ws-ipc'; import { object, string, arrayOf, number, enumeration, oneOf } from 'validated/schema'; import { validate } from 'validated/object'; import type { Coordinate2d } from '../types'; export type AccountData = {expiry: string}; export type AccountToken = string; export type Ip = string; export type Location = { country: string, city: string, position: Coordinate2d, }; const LocationSchema = object({ country: string, city: string, position: arrayOf(number), }); export type SecurityState = 'secured' | 'unsecured'; export type BackendState = { state: SecurityState, target_state: SecurityState, }; type RelayConstraints = { host: 'any' | { only: string }, tunnel: { openvpn: { port: 'any' | { only: number }, protocol: 'any' | { only: 'tcp' | 'udp' }, }, }, }; export type RelayConstraintsUpdate = { host?: 'any' | { only: string }, tunnel: { openvpn: { port?: 'any' | { only: number }, protocol?: 'any' | { only: 'tcp' | 'udp' }, }, }, }; const Constraint = (v) => oneOf(string, object({ only: v, })); const RelayConstraintsSchema = object({ host: Constraint(string), tunnel: object({ openvpn: object({ port: Constraint(number), protocol: Constraint(enumeration('udp', 'tcp')), }), }), }); export interface IpcFacade { setConnectionString(string): void, getAccountData(AccountToken): Promise, getAccount(): Promise, setAccount(accountToken: ?AccountToken): Promise, updateRelayConstraints(RelayConstraintsUpdate): Promise, getRelayContraints(): Promise, connect(): Promise, disconnect(): Promise, shutdown(): Promise, getIp(): Promise, getLocation(): Promise, getState(): Promise, registerStateListener((BackendState) => void): void, setCloseConnectionHandler(() => void): void, authenticate(sharedSecret: string): Promise, } export class RealIpc implements IpcFacade { _ipc: JsonRpcWs; constructor(connectionString: string) { this._ipc = new JsonRpcWs(connectionString); } setConnectionString(str: string) { this._ipc.setConnectionString(str); } getAccountData(accountToken: AccountToken): Promise { // send the IPC with 30s timeout since the backend will wait // for a HTTP request before replying return this._ipc.send('get_account_data', accountToken, 30000) .then(raw => { if (typeof raw === 'object' && raw && raw.expiry) { return raw; } else { throw new InvalidReply(raw, 'Expected an object with expiry'); } }); } getAccount(): Promise { return this._ipc.send('get_account') .then( raw => { if (raw === undefined || raw === null || typeof raw === 'string') { return raw; } else { throw new InvalidReply(raw); } }); } setAccount(accountToken: ?AccountToken): Promise { return this._ipc.send('set_account', accountToken) .then(this._ignoreResponse); } _ignoreResponse(_response: mixed): void { return; } updateRelayConstraints(relayConstraints: RelayConstraintsUpdate): Promise { return this._ipc.send('update_relay_constraints', [relayConstraints]) .then(this._ignoreResponse); } getRelayContraints(): Promise { return this._ipc.send('get_relay_constraints') .then( raw => { try { const validated: any = validate(RelayConstraintsSchema, raw); return (validated: RelayConstraints); } catch (e) { throw new InvalidReply(raw, e); } }); } connect(): Promise { return this._ipc.send('connect') .then(this._ignoreResponse); } disconnect(): Promise { return this._ipc.send('disconnect') .then(this._ignoreResponse); } shutdown(): Promise { return this._ipc.send('shutdown') .then(this._ignoreResponse); } getIp(): Promise { return this._ipc.send('get_ip') .then(raw => { if (typeof raw === 'string' && raw) { return raw; } else { throw new InvalidReply(raw, 'Expected a string'); } }); } getLocation(): Promise { return this._ipc.send('get_location') .then(raw => { try { const validated: any = validate(LocationSchema, raw); return (validated: Location); } catch (e) { throw new InvalidReply(raw, e); } }); } getState(): Promise { return this._ipc.send('get_state') .then(raw => { return this._parseBackendState(raw); }); } _parseBackendState(raw: mixed): BackendState { if (raw && raw.state && raw.target_state) { const uncheckedRaw: any = raw; const states: Array = ['secured', 'unsecured']; const correctState = states.includes(uncheckedRaw.state); const correctTargetState = states.includes(uncheckedRaw.target_state); if (!correctState || !correctTargetState) { throw new InvalidReply(raw); } return (uncheckedRaw: BackendState); } else { throw new InvalidReply(raw); } } registerStateListener(listener: (BackendState) => void) { this._ipc.on('new_state', (rawEvent) => { const parsedEvent : BackendState = this._parseBackendState(rawEvent); listener(parsedEvent); }); } setCloseConnectionHandler(handler: () => void) { this._ipc.setCloseConnectionHandler(handler); } authenticate(sharedSecret: string): Promise { return this._ipc.send('auth', sharedSecret) .then(this._ignoreResponse); } }