// @flow import JsonRpcTransport from './jsonrpc-transport'; 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) => { return oneOf( string, // any 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, }), ), }), ), }); const AccountDataSchema = object({ expiry: string, }); const allSecurityStates: Array = ['secured', 'unsecured']; const BackendStateSchema = object({ state: enumeration(...allSecurityStates), target_state: enumeration(...allSecurityStates), }); export interface DaemonRpcProtocol { connect(string): void; disconnect(): void; getAccountData(AccountToken): Promise; getRelayLocations(): Promise; getAccount(): Promise; setAccount(accountToken: ?AccountToken): Promise; updateRelaySettings(RelaySettingsUpdate): Promise; getRelaySettings(): Promise; setAllowLan(boolean): Promise; getAllowLan(): Promise; connectTunnel(): Promise; disconnectTunnel(): Promise; getLocation(): Promise; getState(): Promise; subscribeStateListener((state: ?BackendState, error: ?Error) => void): Promise; addOpenConnectionObserver(() => void): ConnectionObserver; addCloseConnectionObserver((error: ?Error) => void): ConnectionObserver; authenticate(sharedSecret: string): Promise; getAccountHistory(): Promise>; removeAccountFromHistory(accountToken: AccountToken): Promise; } export class ResponseParseError extends Error { _validationError: ?Error; constructor(message: string, validationError: ?Error) { super(message); this._validationError = validationError; } get validationError(): ?Error { return this._validationError; } } export type ConnectionObserver = { unsubscribe: () => void, }; export class DaemonRpc implements DaemonRpcProtocol { _transport = new JsonRpcTransport(); async authenticate(sharedSecret: string): Promise { await this._transport.send('auth', sharedSecret); } connect(connectionString: string) { this._transport.connect(connectionString); } disconnect() { this._transport.disconnect(); } addOpenConnectionObserver(handler: () => void): ConnectionObserver { this._transport.on('open', handler); return { unsubscribe: () => { this._transport.off('open', handler); }, }; } addCloseConnectionObserver(handler: (error: ?Error) => void): ConnectionObserver { this._transport.on('close', handler); return { unsubscribe: () => { this._transport.off('close', handler); }, }; } async getAccountData(accountToken: AccountToken): Promise { // send the IPC with 30s timeout since the backend will wait // for a HTTP request before replying const response = await this._transport.send('get_account_data', accountToken, 30000); try { return validate(AccountDataSchema, response); } catch (error) { throw new ResponseParseError('Invalid response from get_account_data', error); } } async getRelayLocations(): Promise { const response = await this._transport.send('get_relay_locations'); try { return validate(RelayListSchema, response); } catch (error) { throw new ResponseParseError('Invalid response from get_relay_locations', error); } } async getAccount(): Promise { const response = await this._transport.send('get_account'); if (response === null || typeof response === 'string') { return response; } else { throw new ResponseParseError('Invalid response from get_account', null); } } async setAccount(accountToken: ?AccountToken): Promise { await this._transport.send('set_account', accountToken); } async updateRelaySettings(relaySettings: RelaySettingsUpdate): Promise { await this._transport.send('update_relay_settings', [relaySettings]); } async getRelaySettings(): Promise { const response = await this._transport.send('get_relay_settings'); try { const validatedObject = validate(RelaySettingsSchema, response); /* $FlowFixMe: There is no way to express the constraints with string literals, i.e: RelaySettingsSchema constraint: oneOf(string, object) RelaySettings constraint: 'any' | object These two are incompatible so we simply enforce the type for now. */ return ((validatedObject: any): RelaySettings); } catch (e) { throw new ResponseParseError('Invalid response from get_relay_settings', e); } } async setAllowLan(allowLan: boolean): Promise { await this._transport.send('set_allow_lan', [allowLan]); } async getAllowLan(): Promise { const response = await this._transport.send('get_allow_lan'); if (typeof response === 'boolean') { return response; } else { throw new ResponseParseError('Invalid response from get_allow_lan', null); } } async connectTunnel(): Promise { await this._transport.send('connect'); } async disconnectTunnel(): Promise { await this._transport.send('disconnect'); } async getLocation(): Promise { // send the IPC with 30s timeout since the backend will wait // for a HTTP request before replying const response = await this._transport.send('get_current_location', [], 30000); try { return validate(LocationSchema, response); } catch (error) { throw new ResponseParseError('Invalid response from get_current_location', error); } } async getState(): Promise { const response = await this._transport.send('get_state'); try { return validate(BackendStateSchema, response); } catch (error) { throw new ResponseParseError('Invalid response from get_state', error); } } subscribeStateListener(listener: (state: ?BackendState, error: ?Error) => void): Promise { return this._transport.subscribe('new_state', (payload) => { try { const newState = validate(BackendStateSchema, payload); listener(newState, null); } catch (error) { listener(null, new ResponseParseError('Invalid payload from new_state', error)); } }); } async getAccountHistory(): Promise> { const response = await this._transport.send('get_account_history'); try { return validate(arrayOf(string), response); } catch (error) { throw new ResponseParseError('Invalid response from get_account_history', null); } } async removeAccountFromHistory(accountToken: AccountToken): Promise { await this._transport.send('remove_account_from_history', accountToken); } }