diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2018-07-18 15:07:37 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2018-08-15 17:39:38 +0200 |
| commit | 71592249b2dd669b6f24f37bfb7b0f4e43b74998 (patch) | |
| tree | a6097dc7e5d94d06e99c65fdfe160e824395f50c /app/lib | |
| parent | e84e87f4ce5a8c242f756566cdc6fb59a45f7bea (diff) | |
| download | mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.tar.xz mullvadvpn-71592249b2dd669b6f24f37bfb7b0f4e43b74998.zip | |
Add workspaces
Diffstat (limited to 'app/lib')
| -rw-r--r-- | app/lib/daemon-rpc.js | 458 | ||||
| -rw-r--r-- | app/lib/formatters.js | 10 | ||||
| -rw-r--r-- | app/lib/jsonrpc-transport.js | 321 | ||||
| -rw-r--r-- | app/lib/keyframe-animation.js | 257 | ||||
| -rw-r--r-- | app/lib/platform.android.js | 33 | ||||
| -rw-r--r-- | app/lib/platform.js | 118 | ||||
| -rw-r--r-- | app/lib/problem-report.android.js | 13 | ||||
| -rw-r--r-- | app/lib/problem-report.js | 44 | ||||
| -rw-r--r-- | app/lib/proc.js | 26 | ||||
| -rw-r--r-- | app/lib/reconnection-backoff.js | 24 | ||||
| -rw-r--r-- | app/lib/relay-settings-builder.js | 194 | ||||
| -rw-r--r-- | app/lib/rpc-address-file.js | 115 | ||||
| -rw-r--r-- | app/lib/tempdir.js | 21 | ||||
| -rw-r--r-- | app/lib/transition-rule.js | 46 | ||||
| -rw-r--r-- | app/lib/window-state-observer.js | 64 |
15 files changed, 0 insertions, 1744 deletions
diff --git a/app/lib/daemon-rpc.js b/app/lib/daemon-rpc.js deleted file mode 100644 index 8e2925f83e..0000000000 --- a/app/lib/daemon-rpc.js +++ /dev/null @@ -1,458 +0,0 @@ -// @flow - -import JsonRpcTransport, { - RemoteError as JsonRpcRemoteError, - TimeOutError as JsonRpcTimeOutError, -} from './jsonrpc-transport'; -import { CommunicationError, InvalidAccountError, NoDaemonError } from '../errors'; - -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 OpenVpnConstraints = { - port: 'any' | { only: number }, - protocol: 'any' | { only: RelayProtocol }, -}; - -type TunnelConstraints<TOpenVpnConstraints> = { - openvpn: TOpenVpnConstraints, -}; - -type RelaySettingsNormal<TTunnelConstraints> = { - location: - | 'any' - | { - only: RelayLocation, - }, - tunnel: - | 'any' - | { - only: TTunnelConstraints, - }, -}; - -// types describing the structure of RelaySettings -export type RelaySettingsCustom = { - host: string, - tunnel: { - openvpn: { - port: number, - protocol: RelayProtocol, - }, - }, -}; -export type RelaySettings = - | {| - normal: RelaySettingsNormal<TunnelConstraints<OpenVpnConstraints>>, - |} - | {| - custom_tunnel_endpoint: RelaySettingsCustom, - |}; - -// types describing the partial update of RelaySettings -export type RelaySettingsNormalUpdate = $Shape< - RelaySettingsNormal<TunnelConstraints<$Shape<OpenVpnConstraints>>>, ->; -export type RelaySettingsUpdate = - | {| - normal: RelaySettingsNormalUpdate, - |} - | {| - custom_tunnel_endpoint: RelaySettingsCustom, - |}; - -const constraint = <T>(constraintValue: SchemaNode<T>) => { - 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<RelayListCountry>, -}; - -export type RelayListCountry = { - name: string, - code: string, - cities: Array<RelayListCity>, -}; - -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 type TunnelOptions = { - openvpn: { - enableIpv6: boolean, - }, -}; - -const TunnelOptionsSchema = object({ - openvpn: object({ - enable_ipv6: boolean, - mssfix: maybe(number), - }), -}); - -const AccountDataSchema = object({ - expiry: string, -}); - -const allSecurityStates: Array<SecurityState> = ['secured', 'unsecured']; -const BackendStateSchema = object({ - state: enumeration(...allSecurityStates), - target_state: enumeration(...allSecurityStates), -}); - -export interface DaemonRpcProtocol { - connect(string): void; - disconnect(): void; - getAccountData(AccountToken): Promise<AccountData>; - getRelayLocations(): Promise<RelayList>; - getAccount(): Promise<?AccountToken>; - setAccount(accountToken: ?AccountToken): Promise<void>; - updateRelaySettings(RelaySettingsUpdate): Promise<void>; - getRelaySettings(): Promise<RelaySettings>; - setAllowLan(boolean): Promise<void>; - getAllowLan(): Promise<boolean>; - setOpenVpnEnableIpv6(boolean): Promise<void>; - getTunnelOptions(): Promise<TunnelOptions>; - setAutoConnect(boolean): Promise<void>; - getAutoConnect(): Promise<boolean>; - connectTunnel(): Promise<void>; - disconnectTunnel(): Promise<void>; - getLocation(): Promise<Location>; - getState(): Promise<BackendState>; - subscribeStateListener((state: ?BackendState, error: ?Error) => void): Promise<void>; - addOpenConnectionObserver(() => void): ConnectionObserver; - addCloseConnectionObserver((error: ?Error) => void): ConnectionObserver; - authenticate(sharedSecret: string): Promise<void>; - getAccountHistory(): Promise<Array<AccountToken>>; - removeAccountFromHistory(accountToken: AccountToken): Promise<void>; -} - -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<void> { - 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<AccountData> { - // send the IPC with 30s timeout since the backend will wait - // for a HTTP request before replying - let response; - try { - response = await this._transport.send('get_account_data', accountToken, 30000); - } catch (error) { - if (error instanceof JsonRpcRemoteError) { - switch (error.code) { - case -200: // Account doesn't exist - throw new InvalidAccountError(); - case -32603: // Internal error - throw new CommunicationError(); - } - } else if (error instanceof JsonRpcTimeOutError) { - throw new NoDaemonError(); - } else { - throw error; - } - } - - try { - return validate(AccountDataSchema, response); - } catch (error) { - throw new ResponseParseError('Invalid response from get_account_data', error); - } - } - - async getRelayLocations(): Promise<RelayList> { - 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<?AccountToken> { - 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<void> { - await this._transport.send('set_account', accountToken); - } - - async updateRelaySettings(relaySettings: RelaySettingsUpdate): Promise<void> { - await this._transport.send('update_relay_settings', [relaySettings]); - } - - async getRelaySettings(): Promise<RelaySettings> { - const response = await this._transport.send('get_relay_settings'); - try { - const validatedObject = validate(RelaySettingsSchema, response); - - /* $FlowFixMe: - There is no way to express 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<void> { - await this._transport.send('set_allow_lan', [allowLan]); - } - - async getAllowLan(): Promise<boolean> { - 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 setOpenVpnEnableIpv6(enableIpv6: boolean): Promise<void> { - await this._transport.send('set_openvpn_enable_ipv6', [enableIpv6]); - } - - async getTunnelOptions(): Promise<TunnelOptions> { - const response = await this._transport.send('get_tunnel_options'); - try { - const validatedObject = validate(TunnelOptionsSchema, response); - - return { - openvpn: { - enableIpv6: validatedObject.openvpn.enable_ipv6, - }, - }; - } catch (error) { - throw new ResponseParseError('Invalid response from get_tunnel_options', error); - } - } - - async setAutoConnect(autoConnect: boolean): Promise<void> { - await this._transport.send('set_auto_connect', [autoConnect]); - } - - async getAutoConnect(): Promise<boolean> { - const response = await this._transport.send('get_auto_connect'); - if (typeof response === 'boolean') { - return response; - } else { - throw new ResponseParseError('Invalid response from get_auto_connect', null); - } - } - - async connectTunnel(): Promise<void> { - await this._transport.send('connect'); - } - - async disconnectTunnel(): Promise<void> { - await this._transport.send('disconnect'); - } - - async getLocation(): Promise<Location> { - // 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<BackendState> { - 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<void> { - 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<Array<AccountToken>> { - 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<void> { - await this._transport.send('remove_account_from_history', accountToken); - } -} diff --git a/app/lib/formatters.js b/app/lib/formatters.js deleted file mode 100644 index 37d7460768..0000000000 --- a/app/lib/formatters.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow - -export const formatAccount = (val: string): string => { - // display number altogether when longer than 12 - if (val.length > 12) { - return val; - } - // display quartets - return val.replace(/([0-9]{4})/g, '$1 ').trim(); -}; diff --git a/app/lib/jsonrpc-transport.js b/app/lib/jsonrpc-transport.js deleted file mode 100644 index cbf1e7dce8..0000000000 --- a/app/lib/jsonrpc-transport.js +++ /dev/null @@ -1,321 +0,0 @@ -// @flow - -import { EventEmitter } from 'events'; -import jsonrpc from 'jsonrpc-lite'; -import uuid from 'uuid'; -import { log } from '../lib/platform'; - -export type UnansweredRequest = { - resolve: (mixed) => void, - reject: (mixed) => void, - timerId: TimeoutID, - message: Object, -}; - -export type JsonRpcErrorResponse = { - type: 'error', - payload: { - id: string, - error: { - code: number, - message: string, - }, - }, -}; -export type JsonRpcNotification = { - type: 'notification', - payload: { - method: string, - params: { - subscription: string, - result: mixed, - }, - }, -}; -export type JsonRpcSuccess = { - type: 'success', - payload: { - id: string, - result: mixed, - }, -}; -export type JsonRpcMessage = JsonRpcErrorResponse | JsonRpcNotification | JsonRpcSuccess; - -export class RemoteError extends Error { - _code: number; - _details: string; - - constructor(code: number, details: string) { - super(`Remote JSON-RPC error ${code}: ${details}`); - this._code = code; - this._details = details; - } - - get code(): number { - return this._code; - } - - get details(): string { - return this._details; - } -} - -export class TimeOutError extends Error { - _jsonRpcMessage: Object; - - constructor(jsonRpcMessage: Object) { - super('Request timed out'); - - this._jsonRpcMessage = jsonRpcMessage; - } - - get jsonRpcMessage(): Object { - return this._jsonRpcMessage; - } -} - -export class SubscriptionError extends Error { - _reply: mixed; - - constructor(message: string, reply: mixed) { - const replyString = JSON.stringify(reply); - - super(`${message}: ${replyString}`); - - this._reply = reply; - } - - get reply(): mixed { - return this._reply; - } -} - -export class ConnectionError extends Error { - _code: number; - - constructor(code: number) { - super(ConnectionError.reason(code)); - this._code = code; - } - - get code(): number { - return this._code; - } - - static reason(code: number): string { - switch (code) { - case 1006: - return 'Abnormal closure'; - case 1011: - return 'Internal error'; - case 1012: - return 'Service restart'; - case 1014: - return 'Bad gateway'; - default: - return `Unknown (${code})`; - } - } -} - -const DEFAULT_TIMEOUT_MILLIS = 5000; - -export default class JsonRpcTransport extends EventEmitter { - _unansweredRequests: Map<string, UnansweredRequest> = new Map(); - _subscriptions: Map<string | number, (mixed) => void> = new Map(); - _webSocket: ?WebSocket; - _websocketFactory: (string) => WebSocket; - - constructor(websocketFactory: ?(string) => WebSocket) { - super(); - this._websocketFactory = - websocketFactory || ((connectionString) => new WebSocket(connectionString)); - } - - /// Connect websocket - connect(connectionString: string): Promise<void> { - return new Promise((resolve, reject) => { - this.disconnect(); - - log.info('Connecting to websocket', connectionString); - - const webSocket = this._websocketFactory(connectionString); - - // A flag used to determine if Promise was resolved. - let isPromiseResolved = false; - - webSocket.onopen = () => { - log.info('Websocket is connected'); - this.emit('open'); - - // Resolve the Promise - resolve(); - isPromiseResolved = true; - }; - - webSocket.onmessage = (event) => { - const data = event.data; - if (typeof data === 'string') { - this._onMessage(data); - } else { - log.error('Got invalid reply from the server', event); - } - }; - - webSocket.onclose = (event) => { - log.info(`The websocket connection closed with code: ${event.code}`); - - // Remove all subscriptions since they are connection based - this._subscriptions.clear(); - - // 1000 is a code used for normal connection closure. - const connectionError = event.code === 1000 ? null : new ConnectionError(event.code); - - this.emit('close', connectionError); - - // Prevent rejecting a previously resolved Promise. - if (!isPromiseResolved) { - reject(connectionError); - } - }; - - this._webSocket = webSocket; - }); - } - - disconnect() { - if (this._webSocket) { - this._webSocket.close(); - this._webSocket = null; - } - } - - async subscribe(event: string, listener: (mixed) => void): Promise<*> { - log.silly(`Adding a listener to ${event}`); - - try { - const subscriptionId = await this.send(`${event}_subscribe`); - if (typeof subscriptionId === 'string' || typeof subscriptionId === 'number') { - this._subscriptions.set(subscriptionId, listener); - } else { - throw new SubscriptionError( - 'The subscription id was not a string or a number', - subscriptionId, - ); - } - } catch (e) { - log.error(`Failed adding listener to ${event}: ${e.message}`); - throw e; - } - } - - send(action: string, data: mixed, timeout: number = DEFAULT_TIMEOUT_MILLIS): Promise<mixed> { - return new Promise((resolve, reject) => { - const webSocket = this._webSocket; - if (!webSocket) { - reject(new Error('Websocket is not connected.')); - return; - } - - const id = uuid.v4(); - const payload = this._prepareParams(data); - const timerId = setTimeout(() => this._onTimeout(id), timeout); - const message = jsonrpc.request(id, action, payload); - this._unansweredRequests.set(id, { - resolve, - reject, - timerId, - message, - }); - - try { - log.silly('Sending message', id, action); - webSocket.send(JSON.stringify(message)); - } catch (error) { - log.error(`Failed sending RPC message "${action}": ${error.message}`); - - // clean up on error - this._unansweredRequests.delete(id); - clearTimeout(timerId); - - throw error; - } - }); - } - - _prepareParams(data: mixed): Array<mixed> | Object { - // JSONRPC only accepts arrays and objects as params, but - // this isn't very nice to use, so this method wraps other - // types in an array. The choice of array is based on try-and-error - - if (data === undefined) { - return []; - } else if (data === null) { - return [null]; - } else if (Array.isArray(data) || typeof data === 'object') { - return data; - } else { - return [data]; - } - } - - _onTimeout(requestId) { - const request = this._unansweredRequests.get(requestId); - - this._unansweredRequests.delete(requestId); - - if (request) { - log.warn(`Request ${requestId} timed out: `, request.message); - request.reject(new TimeOutError(request.message)); - } else { - log.warn(`Request ${requestId} timed out but it seems to already have been answered`); - } - } - - _onMessage(message: string) { - const result = jsonrpc.parse(message); - const messages = Array.isArray(result) ? result : [result]; - - for (const message of messages) { - if (message.type === 'notification') { - this._onNotification(message); - } else { - this._onReply(message); - } - } - } - - _onNotification(message: JsonRpcNotification) { - const subscriptionId = message.payload.params.subscription; - const listener = this._subscriptions.get(subscriptionId); - - if (listener) { - log.silly('Got notification', message.payload.method, message.payload.params.result); - listener(message.payload.params.result); - } else { - log.warn('Got notification for', message.payload.method, 'but no one is listening for it'); - } - } - - _onReply(message: JsonRpcErrorResponse | JsonRpcSuccess) { - const id = message.payload.id; - const request = this._unansweredRequests.get(id); - this._unansweredRequests.delete(id); - - if (request) { - log.silly('Got answer to', id, message.type); - - clearTimeout(request.timerId); - - if (message.type === 'error') { - const error = message.payload.error; - request.reject(new RemoteError(error.code, error.message)); - } else { - const reply = message.payload.result; - request.resolve(reply); - } - } else { - log.warn(`Got reply to ${id} but no one was waiting for it`); - } - } -} diff --git a/app/lib/keyframe-animation.js b/app/lib/keyframe-animation.js deleted file mode 100644 index 028c95769a..0000000000 --- a/app/lib/keyframe-animation.js +++ /dev/null @@ -1,257 +0,0 @@ -// @flow - -import { nativeImage } from 'electron'; -import type { NativeImage } from 'electron'; - -export type OnFrameFn = (image: NativeImage) => void; -export type OnFinishFn = (void) => void; -export type KeyframeAnimationOptions = { - startFrame?: number, - endFrame?: number, - beginFromCurrentState?: boolean, - advanceTo?: 'end', -}; -export type KeyframeAnimationRange = [number, number]; - -export default class KeyframeAnimation { - _speed: number = 200; // ms - _repeat: boolean = false; - _reverse: boolean = false; - _alternate: boolean = false; - - _onFrame: ?OnFrameFn; - _onFinish: ?OnFinishFn; - - _nativeImages: Array<NativeImage>; - _frameRange: KeyframeAnimationRange; - _numFrames: number; - _currentFrame: number = 0; - - _isRunning: boolean = false; - _isFinished: boolean = false; - _isFirstRun: boolean = true; - - _timeout = null; - - set onFrame(newValue: ?OnFrameFn) { - this._onFrame = newValue; - } - get onFrame(): ?OnFrameFn { - return this._onFrame; - } - - // called when animation finished for non-repeating animations. - set onFinish(newValue: ?OnFinishFn) { - this._onFinish = newValue; - } - get onFinish(): ?OnFinishFn { - return this._onFinish; - } - - // pace per frame in ms - set speed(newValue: number) { - this._speed = parseInt(newValue); - } - get speed(): number { - return this._speed; - } - - set repeat(newValue: boolean) { - this._repeat = newValue; - } - get repeat(): boolean { - return this._repeat; - } - - set reverse(newValue: boolean) { - this._reverse = newValue; - } - get reverse(): boolean { - return this._repeat; - } - - // alternates the animation direction when it reaches the end - // only for repeating animations - set alternate(newValue: boolean) { - this._alternate = !!newValue; - } - get alternate(): boolean { - return this._alternate; - } - - get nativeImages(): Array<NativeImage> { - return this._nativeImages.slice(); - } - get isFinished(): boolean { - return this._isFinished; - } - - // create animation from files matching filename pattern. i.e (bubble-frame-{}.png) - static fromFilePattern(filePattern: string, range: KeyframeAnimationRange): KeyframeAnimation { - const images: Array<NativeImage> = []; - - if (range.length !== 2 || range[0] > range[1]) { - throw new Error('the animation range is invalid'); - } - - for (let i = range[0]; i <= range[1]; i++) { - const filePath = filePattern.replace('{}', i.toString()); - const image = nativeImage.createFromPath(filePath); - images.push(image); - } - return new KeyframeAnimation(images); - } - - static fromFileSequence(files: Array<string>): KeyframeAnimation { - const images: Array<NativeImage> = files.map((filePath) => - nativeImage.createFromPath(filePath), - ); - return new KeyframeAnimation(images); - } - - constructor(images: Array<NativeImage>) { - const len = images.length; - if (len < 1) { - throw new Error('too few images in animation'); - } - - this._nativeImages = images.slice(); - this._numFrames = len; - this._frameRange = [0, len]; - } - - get currentImage(): NativeImage { - return this._nativeImages[this._currentFrame]; - } - - play(options: KeyframeAnimationOptions = {}) { - const { startFrame, endFrame, beginFromCurrentState, advanceTo } = options; - - if (startFrame !== undefined && endFrame !== undefined) { - if (startFrame < 0 || startFrame >= this._numFrames) { - throw new Error('Invalid start frame'); - } - - if (endFrame < 0 || endFrame >= this._numFrames) { - throw new Error('Invalid end frame'); - } - - if (startFrame < endFrame) { - this._frameRange = [startFrame, endFrame]; - } else { - this._frameRange = [endFrame, startFrame]; - } - } else { - this._frameRange = [0, this._numFrames - 1]; - } - - if (!beginFromCurrentState || this._isFirstRun) { - this._currentFrame = this._frameRange[this._reverse ? 1 : 0]; - } - - if (this._isFirstRun) { - this._isFirstRun = false; - } - - if (advanceTo === 'end') { - this._currentFrame = this._frameRange[this._reverse ? 0 : 1]; - } - - this._isRunning = true; - this._isFinished = false; - - this._unscheduleUpdate(); - - this._render(); - this._scheduleUpdate(); - } - - stop() { - this._isRunning = false; - this._unscheduleUpdate(); - } - - _unscheduleUpdate() { - if (this._timeout) { - clearTimeout(this._timeout); - this._timeout = null; - } - } - - _scheduleUpdate() { - this._timeout = setTimeout(() => this._onUpdateFrame(), this._speed); - } - - _render() { - if (this._onFrame) { - this._onFrame(this._nativeImages[this._currentFrame]); - } - } - - _didFinish() { - this._isFinished = true; - - if (this._onFinish) { - this._onFinish(); - } - } - - _onUpdateFrame() { - this._advanceFrame(); - - if (this._isFinished) { - // mark animation as not running when finished - this._isRunning = false; - } else { - this._render(); - - // check once again since onFrame() may stop animation - if (this._isRunning) { - this._scheduleUpdate(); - } - } - } - - _advanceFrame() { - if (this._isFinished) { - return; - } - - const lastFrame = this._frameRange[this._reverse ? 0 : 1]; - if (this._currentFrame === lastFrame) { - // mark animation as finished if it's not repeating - if (!this._repeat) { - this._didFinish(); - return; - } - - // change animation direction if marked for alternation - if (this._alternate) { - this._reverse = !this._reverse; - - this._currentFrame = this._nextFrame(this._currentFrame, this._frameRange, this._reverse); - } else { - this._currentFrame = this._frameRange[this._reverse ? 1 : 0]; - } - } else { - this._currentFrame = this._nextFrame(this._currentFrame, this._frameRange, this._reverse); - } - } - - _nextFrame(cur: number, frameRange: KeyframeAnimationRange, isReverse: boolean): number { - if (isReverse) { - if (cur < frameRange[0]) { - return cur + 1; - } else if (cur > frameRange[0]) { - return cur - 1; - } - } else { - if (cur > frameRange[1]) { - return cur - 1; - } else if (cur < frameRange[1]) { - return cur + 1; - } - } - return cur; - } -} diff --git a/app/lib/platform.android.js b/app/lib/platform.android.js deleted file mode 100644 index e71c1facf7..0000000000 --- a/app/lib/platform.android.js +++ /dev/null @@ -1,33 +0,0 @@ -// @flow - -import { BackHandler, Linking } from 'react-native'; -import { MobileAppBridge } from 'NativeModules'; -import { version } from '../../package.json'; - -const log = console.log; - -const getAppVersion = () => { - return version; -}; - -const getOpenAtLogin = () => { - throw new Error('Not implemented'); -}; - -const setOpenAtLogin = (_autoStart: boolean) => { - throw new Error('Not implemented'); -}; - -const exit = () => { - BackHandler.exitApp(); -}; - -const openLink = (link: string) => { - Linking.openURL(link); -}; - -const openItem = (path: string) => { - MobileAppBridge.openItem(path); -}; - -export { log, exit, openLink, openItem, getAppVersion, getOpenAtLogin, setOpenAtLogin }; diff --git a/app/lib/platform.js b/app/lib/platform.js deleted file mode 100644 index cabb4951d9..0000000000 --- a/app/lib/platform.js +++ /dev/null @@ -1,118 +0,0 @@ -// @flow - -import fs from 'fs'; -import { remote, shell } from 'electron'; -import electronLog from 'electron-log'; -import { execFile } from 'child_process'; -import path from 'path'; -import { promisify } from 'util'; - -const DESKTOP_FILE_NAME = 'mullvad-vpn.desktop'; - -const execFileAsync = promisify(execFile); -const mkdirAsync = promisify(fs.mkdir); -const statAsync = promisify(fs.stat); -const symlinkAsync = promisify(fs.symlink); -const unlinkAsync = promisify(fs.unlink); - -const log = electronLog; - -const getAppVersion = () => { - return remote.app.getVersion(); -}; - -const getOpenAtLogin = (): boolean => { - if (process.platform === 'linux') { - try { - const autostartDir = path.join(remote.app.getPath('appData'), 'autostart'); - const autostartFilePath = path.join(autostartDir, DESKTOP_FILE_NAME); - - fs.accessSync(autostartFilePath); - - return true; - } catch (error) { - log.debug(`Failed to check autostart file: ${error.message}`); - return false; - } - } else { - return remote.app.getLoginItemSettings().openAtLogin; - } -}; - -const setOpenAtLogin = async (openAtLogin: boolean) => { - // setLoginItemSettings is broken on macOS and cannot delete login items. - // Issue: https://github.com/electron/electron/issues/10880 - if (process.platform === 'darwin') { - if (openAtLogin === false) { - // process.execPath in renderer process points to the sub-bundle of Electron Helper. - // This regular expression extracts the path to the app bundle, which is the first occurrence of - // file with .app extension. - const matches = process.execPath.match(/([a-z0-9 ]+)\.app/i); - if (matches && matches.length > 1) { - const bundleName = matches[1]; - const appleScript = `on run argv - set itemName to item 1 of argv - tell application "System Events" to delete login item itemName - end run`; - await execFileAsync('osascript', ['-e', appleScript, bundleName]); - } else { - log.error(`Cannot extract the app bundle name from ${process.execPath}`); - } - } else { - remote.app.setLoginItemSettings({ openAtLogin }); - } - } else if (process.platform === 'linux') { - try { - const desktopFilePath = path.join('/usr/share/applications', DESKTOP_FILE_NAME); - const autostartDir = path.join(remote.app.getPath('appData'), 'autostart'); - const autostartFilePath = path.join(autostartDir, DESKTOP_FILE_NAME); - - if (openAtLogin) { - await createDirIfNecessary(autostartDir); - await symlinkAsync(desktopFilePath, autostartFilePath); - } else { - await unlinkAsync(autostartFilePath); - } - } catch (error) { - log.error(`Failed to set auto-start: ${error.message}`); - } - } else { - remote.app.setLoginItemSettings({ openAtLogin }); - } -}; - -const createDirIfNecessary = async (directory: string) => { - let stat; - try { - stat = await statAsync(directory); - } catch (error) { - // Path doesn't exist, so it has to be created - return mkdirAsync(directory); - } - - // Is there a file instead of a directory? - if (!stat.isDirectory()) { - // Try to remove existing file and replace it with a new directory - try { - await unlinkAsync(directory); - } catch (error) { - log.debug(`Failed to remove path before creating a directory for it: ${error.message}`); - } - - return mkdirAsync(directory); - } -}; - -const exit = () => { - remote.app.quit(); -}; - -const openLink = (link: string) => { - shell.openExternal(link); -}; - -const openItem = (path: string) => { - shell.openItem(path); -}; - -export { log, exit, openLink, openItem, getAppVersion, getOpenAtLogin, setOpenAtLogin }; diff --git a/app/lib/problem-report.android.js b/app/lib/problem-report.android.js deleted file mode 100644 index 30a944e178..0000000000 --- a/app/lib/problem-report.android.js +++ /dev/null @@ -1,13 +0,0 @@ -// @flow - -import { MobileAppBridge } from 'NativeModules'; - -const collectProblemReport = (toRedact: string) => { - return MobileAppBridge.collectProblemReport(toRedact); -}; - -const sendProblemReport = (email: string, message: string, savedReport: string) => { - return MobileAppBridge.sendProblemReport(email, message, savedReport); -}; - -export { collectProblemReport, sendProblemReport }; diff --git a/app/lib/problem-report.js b/app/lib/problem-report.js deleted file mode 100644 index 19d24f3e0f..0000000000 --- a/app/lib/problem-report.js +++ /dev/null @@ -1,44 +0,0 @@ -// @flow - -import { ipcRenderer } from 'electron'; -import uuid from 'uuid'; - -const collectProblemReport = (toRedact: Array<string>): Promise<string> => { - return new Promise((resolve, reject) => { - const requestId = uuid.v4(); - const responseListener = (_event, responseId, result) => { - if (responseId === requestId) { - ipcRenderer.removeListener('collect-logs-reply', responseListener); - if (result.success) { - resolve(result.reportPath); - } else { - reject(new Error(result.error)); - } - } - }; - - ipcRenderer.on('collect-logs-reply', responseListener); - ipcRenderer.send('collect-logs', requestId, toRedact); - }); -}; - -const sendProblemReport = (email: string, message: string, savedReport: string): Promise<void> => { - return new Promise((resolve, reject) => { - const requestId = uuid.v4(); - const responseListener = (_event, responseId, result) => { - if (requestId === responseId) { - ipcRenderer.removeListener('send-problem-report-reply', responseListener); - if (result.success) { - resolve(); - } else { - reject(new Error(result.error)); - } - } - }; - - ipcRenderer.on('send-problem-report-reply', responseListener); - ipcRenderer.send('send-problem-report', requestId, email, message, savedReport); - }); -}; - -export { collectProblemReport, sendProblemReport }; diff --git a/app/lib/proc.js b/app/lib/proc.js deleted file mode 100644 index 5e7396fa9a..0000000000 --- a/app/lib/proc.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow - -import path from 'path'; - -export function resolveBin(binaryName: string) { - const basepath = getBasePath(); - return path.resolve(basepath, binaryName + getExtension()); -} - -function getBasePath() { - if (process.env.NODE_ENV === 'development') { - return process.env.MULLVAD_PATH || './target/debug'; - } else { - return process.resourcesPath; - } -} - -function getExtension() { - switch (process.platform) { - case 'win32': - return '.exe'; - - default: - return ''; - } -} diff --git a/app/lib/reconnection-backoff.js b/app/lib/reconnection-backoff.js deleted file mode 100644 index d777e86e57..0000000000 --- a/app/lib/reconnection-backoff.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow - -/* - * Used to calculate the time to wait before reconnecting to the daemon. - * It uses a linear backoff function that goes from 500ms to 3000ms. - */ -export default class ReconnectionBackoff { - _attempt = 0; - - attempt(handler: () => void) { - setTimeout(handler, this._getIncreasedBackoff()); - } - - reset() { - this._attempt = 0; - } - - _getIncreasedBackoff() { - if (this._attempt < 6) { - this._attempt++; - } - return this._attempt * 500; - } -} diff --git a/app/lib/relay-settings-builder.js b/app/lib/relay-settings-builder.js deleted file mode 100644 index 5f6c279e3f..0000000000 --- a/app/lib/relay-settings-builder.js +++ /dev/null @@ -1,194 +0,0 @@ -// @flow - -import type { - RelayLocation, - RelayProtocol, - RelaySettingsUpdate, - RelaySettingsNormalUpdate, - RelaySettingsCustom, -} from './daemon-rpc'; - -type LocationBuilder<Self> = { - country: (country: string) => Self, - city: (country: string, city: string) => Self, - any: () => Self, - fromRaw: (location: 'any' | RelayLocation) => Self, -}; - -type OpenVPNConfigurator<Self> = { - port: { - exact: (port: number) => Self, - any: () => Self, - }, - protocol: { - exact: (protocol: RelayProtocol) => Self, - any: () => Self, - }, -}; - -type TunnelBuilder<Self> = { - openvpn: (configurator: (OpenVPNConfigurator<*>) => void) => Self, -}; - -class NormalRelaySettingsBuilder { - _payload: RelaySettingsNormalUpdate = {}; - - build(): RelaySettingsUpdate { - return { - normal: this._payload, - }; - } - - get location(): LocationBuilder<NormalRelaySettingsBuilder> { - return { - country: (country: string) => { - this._payload.location = { only: { country } }; - return this; - }, - city: (country: string, city: string) => { - this._payload.location = { only: { city: [country, city] } }; - return this; - }, - any: () => { - this._payload.location = 'any'; - return this; - }, - fromRaw: function(location: 'any' | RelayLocation) { - if (location === 'any') { - return this.any(); - } - - if (location.city) { - const [country, city] = location.city; - return this.city(country, city); - } - - if (location.country) { - return this.country(location.country); - } - - throw new Error( - 'Unsupported value of RelayLocation' + (location && JSON.stringify(location)), - ); - }, - }; - } - - get tunnel(): TunnelBuilder<NormalRelaySettingsBuilder> { - const updateOpenvpn = (next) => { - const tunnel = this._payload.tunnel; - if (typeof tunnel === 'string' || typeof tunnel === 'undefined') { - this._payload.tunnel = { - only: { - openvpn: next, - }, - }; - } else if (typeof tunnel === 'object') { - const prev = (tunnel.only && tunnel.only.openvpn) || {}; - this._payload.tunnel = { - only: { - openvpn: { ...prev, ...next }, - }, - }; - } - }; - - return { - openvpn: (configurator) => { - const openvpnBuilder = { - get port() { - const apply = (port) => { - updateOpenvpn({ port }); - return this; - }; - return { - exact: (value: number) => apply({ only: value }), - any: () => apply('any'), - }; - }, - get protocol() { - const apply = (protocol) => { - updateOpenvpn({ protocol }); - return this; - }; - return { - exact: (value: RelayProtocol) => apply({ only: value }), - any: () => apply('any'), - }; - }, - }; - - configurator(openvpnBuilder); - - return this; - }, - any: () => { - this._payload.tunnel = 'any'; - return this; - }, - }; - } -} - -type CustomOpenVPNConfigurator<Self> = { - port: (port: number) => Self, - protocol: (protocol: RelayProtocol) => Self, -}; - -type CustomTunnelBuilder<Self> = { - openvpn: (configurator: (CustomOpenVPNConfigurator<*>) => void) => Self, -}; - -class CustomRelaySettingsBuilder { - _payload: RelaySettingsCustom = { - host: '', - tunnel: { - openvpn: { - port: 0, - protocol: 'udp', - }, - }, - }; - - build(): RelaySettingsUpdate { - return { - custom_tunnel_endpoint: this._payload, - }; - } - - host(value: string) { - this._payload.host = value; - return this; - } - - get tunnel(): CustomTunnelBuilder<CustomRelaySettingsBuilder> { - const updateOpenvpn = (next) => { - const tunnel = this._payload.tunnel || {}; - const prev = tunnel.openvpn || {}; - this._payload.tunnel = { - openvpn: { ...prev, ...next }, - }; - }; - - return { - openvpn: (configurator) => { - configurator({ - port: function(port: number) { - updateOpenvpn({ port }); - return this; - }, - protocol: function(protocol: RelayProtocol) { - updateOpenvpn({ protocol }); - return this; - }, - }); - return this; - }, - }; - } -} - -export default { - normal: () => new NormalRelaySettingsBuilder(), - custom: () => new CustomRelaySettingsBuilder(), -}; diff --git a/app/lib/rpc-address-file.js b/app/lib/rpc-address-file.js deleted file mode 100644 index f05cc54fdc..0000000000 --- a/app/lib/rpc-address-file.js +++ /dev/null @@ -1,115 +0,0 @@ -// @flow - -import fs from 'fs'; -import path from 'path'; -import { app } from 'electron'; -import { promisify } from 'util'; -import { getSystemTemporaryDirectory } from './tempdir'; - -const fsReadFileAsync = promisify(fs.readFile); - -const POLL_INTERVAL = 200; - -export type RpcCredentials = { - connectionString: string, - sharedSecret: string, -}; - -export class RpcAddressFile { - _filePath = getRpcAddressFilePath(); - _pollIntervalId: ?IntervalID; - _pollPromise: ?Promise<void>; - - get filePath(): string { - return this._filePath; - } - - waitUntilExists(): Promise<void> { - let promise = this._pollPromise; - - if (!promise) { - promise = new Promise((resolve) => { - const timer = setInterval(() => { - fs.exists(this._filePath, (exists) => { - if (exists) { - clearInterval(timer); - resolve(); - - this._pollPromise = null; - } - }); - }, POLL_INTERVAL); - }); - - this._pollPromise = promise; - } - - return promise; - } - - async parse(): Promise<RpcCredentials> { - const data = await fsReadFileAsync(this._filePath, 'utf8'); - const [connectionString, sharedSecret] = data.split('\n', 2); - - if (connectionString && sharedSecret !== undefined) { - return { - connectionString, - sharedSecret, - }; - } else { - throw new Error('Cannot parse the RPC address file'); - } - } - - isTrusted() { - const filePath = this._filePath; - switch (process.platform) { - case 'win32': - return isOwnedByLocalSystem(filePath); - case 'darwin': - case 'linux': - return isOwnedAndOnlyWritableByRoot(filePath); - default: - throw new Error(`Unknown platform: ${process.platform}`); - } - } -} - -function getRpcAddressFilePath() { - const rpcAddressFileName = '.mullvad_rpc_address'; - - switch (process.platform) { - case 'win32': { - // Windows: %ALLUSERSPROFILE%\{appname} - const programDataDirectory = process.env.ALLUSERSPROFILE; - if (programDataDirectory) { - const appDataDirectory = path.join(programDataDirectory, app.getName()); - return path.join(appDataDirectory, rpcAddressFileName); - } else { - throw new Error('Missing %ALLUSERSPROFILE% environment variable'); - } - } - default: - return path.join(getSystemTemporaryDirectory(), rpcAddressFileName); - } -} - -function isOwnedAndOnlyWritableByRoot(path: string): boolean { - const stat = fs.statSync(path); - const isOwnedByRoot = stat.uid === 0; - const isOnlyWritableByOwner = (stat.mode & parseInt('022', 8)) === 0; - - return isOwnedByRoot && isOnlyWritableByOwner; -} - -function isOwnedByLocalSystem(path: string): boolean { - // $FlowFixMe: this module is only available on Windows - const winsec = require('windows-security'); - const ownerSid = winsec.getFileOwnerSid(path, null); - const isWellKnownSid = winsec.isWellKnownSid( - ownerSid, - winsec.WellKnownSid.BuiltinAdministratorsSid, - ); - - return isWellKnownSid; -} diff --git a/app/lib/tempdir.js b/app/lib/tempdir.js deleted file mode 100644 index a3b4fa6d89..0000000000 --- a/app/lib/tempdir.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow - -import path from 'path'; - -export function getSystemTemporaryDirectory() { - switch (process.platform) { - case 'win32': { - const windowsPath = process.env.windir; - if (windowsPath) { - return path.join(windowsPath, 'Temp'); - } else { - throw new Error('Missing windir in environment variables.'); - } - } - case 'darwin': - case 'linux': - return '/tmp'; - default: - throw new Error(`Not implemented for ${process.platform}`); - } -} diff --git a/app/lib/transition-rule.js b/app/lib/transition-rule.js deleted file mode 100644 index dfb56b02ce..0000000000 --- a/app/lib/transition-rule.js +++ /dev/null @@ -1,46 +0,0 @@ -// @flow - -export type TransitionDescriptor = { - name: string, - duration: number, -}; - -export type TransitionFork = { - forward: TransitionDescriptor, - backward: TransitionDescriptor, -}; - -export type TransitionMatch = { - direction: 'forward' | 'backward', - descriptor: TransitionDescriptor, -}; - -export default class TransitionRule { - _from: ?string; - _to: string; - _fork: TransitionFork; - - constructor(from: ?string, to: string, fork: TransitionFork) { - this._from = from; - this._to = to; - this._fork = fork; - } - - match(fromRoute: ?string, toRoute: string): ?TransitionMatch { - if ((!this._from || this._from === fromRoute) && this._to === toRoute) { - return { - direction: 'forward', - descriptor: this._fork['forward'], - }; - } - - if ((!this._from || this._from === toRoute) && this._to === fromRoute) { - return { - direction: 'backward', - descriptor: this._fork['backward'], - }; - } - - return null; - } -} diff --git a/app/lib/window-state-observer.js b/app/lib/window-state-observer.js deleted file mode 100644 index 67252d8f6a..0000000000 --- a/app/lib/window-state-observer.js +++ /dev/null @@ -1,64 +0,0 @@ -// @flow - -import { remote } from 'electron'; - -type EventListener = () => void; - -// Tiny helper for detecting the window state. -export default class WindowStateObserver { - _onShow: ?EventListener; - _onHide: ?EventListener; - - get onShow() { - return this._onShow; - } - - set onShow(listener: ?EventListener) { - const currentWindow = remote.getCurrentWindow(); - const oldListener = this._onShow; - if (oldListener) { - currentWindow.removeListener('show', oldListener); - } - - if (listener) { - currentWindow.addListener('show', listener); - } - - this._onShow = listener; - } - - get onHide() { - return this._onHide; - } - - set onHide(listener: ?EventListener) { - const currentWindow = remote.getCurrentWindow(); - const oldListener = this._onHide; - if (oldListener) { - currentWindow.removeListener('hide', oldListener); - } - - if (listener) { - currentWindow.addListener('hide', listener); - } - - this._onHide = listener; - } - - constructor() { - // Because BrowserWindow persists between page reloads, - // it's important to release event handlers when that happens. - window.addEventListener('beforeunload', this._onBeforeUnload); - } - - _onBeforeUnload = () => { - this.dispose(); - }; - - dispose() { - this.onShow = null; - this.onHide = null; - - window.removeEventListener('beforeunload', this._onBeforeUnload); - } -} |
