// @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>>, >; 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; 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); } 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); } }