// @flow import JsonRpcWs, { InvalidReply } from './jsonrpc-ws-ipc'; import { object, maybe, string, number, boolean, enumeration, arrayOf, oneOf } from 'validated/schema'; import { validate } from 'validated/object'; import type { Node as SchemaNode } from 'validated/schema'; export type AccountData = { expiry: string }; export type AccountToken = string; export type Ip = string; export type Location = { ip: Ip, country: string, city: ?string, latitude: number, longitude: number, mullvad_exit_ip: boolean, }; const LocationSchema = object({ ip: string, country: string, city: maybe(string), latitude: number, longitude: number, mullvad_exit_ip: boolean, }); export type SecurityState = 'secured' | 'unsecured'; export type BackendState = { state: SecurityState, target_state: SecurityState, }; export type RelayProtocol = 'tcp' | 'udp'; export type RelayLocation = {| city: [string, string] |} | {| country: string |}; type OpenVpnParameters = { port: 'any' | { only: number }, protocol: 'any' | { only: RelayProtocol }, }; type TunnelOptions = { openvpn: TOpenVpnParameters, }; type RelaySettingsNormal = { location: 'any' | { only: RelayLocation, }, tunnel: 'any' | { only: TTunnelOptions }, }; // types describing the structure of RelaySettings export type RelaySettingsCustom = { host: string, tunnel: { openvpn: { port: number, protocol: RelayProtocol } } }; export type RelaySettings = {| normal: RelaySettingsNormal> |} | {| custom_tunnel_endpoint: RelaySettingsCustom |}; // types describing the partial update of RelaySettings export type RelaySettingsNormalUpdate = $Shape< RelaySettingsNormal< TunnelOptions<$Shape > > >; export type RelaySettingsUpdate = {| normal: RelaySettingsNormalUpdate |} | {| custom_tunnel_endpoint: RelaySettingsCustom |}; const constraint = (constraintValue: SchemaNode) => oneOf(string, object({ only: constraintValue, })); const RelaySettingsSchema = oneOf( object({ normal: object({ location: constraint(oneOf( object({ city: arrayOf(string), }), object({ country: string }), )), tunnel: constraint(object({ openvpn: object({ port: constraint(number), protocol: constraint(enumeration('udp', 'tcp')), }), })), }) }), object({ custom_tunnel_endpoint: object({ host: string, tunnel: object({ openvpn: object({ port: number, protocol: enumeration('udp', 'tcp'), }) }) }) }) ); export type RelayList = { countries: Array, }; export type RelayListCountry = { name: string, code: string, cities: Array, }; export type RelayListCity = { name: string, code: string, latitude: number, longitude: number, has_active_relays: boolean, }; const RelayListSchema = object({ countries: arrayOf(object({ name: string, code: string, cities: arrayOf(object({ name: string, code: string, latitude: number, longitude: number, has_active_relays: boolean, })), })), }); export interface IpcFacade { setConnectionString(string): void, getAccountData(AccountToken): Promise, getRelayLocations(): Promise, getAccount(): Promise, setAccount(accountToken: ?AccountToken): Promise, updateRelaySettings(RelaySettingsUpdate): Promise, getRelaySettings(): Promise, setAllowLan(boolean): Promise, getAllowLan(): Promise, connect(): Promise, disconnect(): Promise, shutdown(): Promise, getLocation(): Promise, getState(): Promise, registerStateListener((BackendState) => void): void, setCloseConnectionHandler(() => void): void, authenticate(sharedSecret: string): Promise, getAccountHistory(): Promise>, removeAccountFromHistory(accountToken: AccountToken): 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'); } }); } async getRelayLocations(): Promise { const raw = await this._ipc.send('get_relay_locations'); try { const validated: any = validate(RelayListSchema, raw); return (validated: RelayList); } catch (e) { throw new InvalidReply(raw, e); } } 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; } updateRelaySettings(relaySettings: RelaySettingsUpdate): Promise { return this._ipc.send('update_relay_settings', [relaySettings]) .then(this._ignoreResponse); } getRelaySettings(): Promise { return this._ipc.send('get_relay_settings') .then( raw => { try { const validated: any = validate(RelaySettingsSchema, raw); return (validated: RelaySettings); } catch (e) { throw new InvalidReply(raw, e); } }); } setAllowLan(allowLan: boolean): Promise { return this._ipc.send('set_allow_lan', [allowLan]) .then(this._ignoreResponse); } async getAllowLan(): Promise { const raw = await this._ipc.send('get_allow_lan'); if(typeof(raw) === 'boolean') { return raw; } else { throw new InvalidReply(raw, 'Expected a boolean'); } } 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); } getLocation(): Promise { // send the IPC with 30s timeout since the backend will wait // for a HTTP request before replying return this._ipc.send('get_current_location', [], 30000) .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); } getAccountHistory(): Promise> { return this._ipc.send('get_account_history') .then(raw => { if(Array.isArray(raw) && raw.every(i => typeof i === 'string')) { const checked: any = raw; return (checked: Array); } else { throw new InvalidReply(raw, 'Expected an array of strings'); } }); } removeAccountFromHistory(accountToken: AccountToken): Promise { return this._ipc.send('remove_account_from_history', accountToken) .then(this._ignoreResponse); } }