diff options
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 1554 | ||||
| -rw-r--r-- | gui/src/main/grpc-client.ts | 246 | ||||
| -rw-r--r-- | gui/src/main/grpc-type-convertions.ts | 1264 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 3 |
4 files changed, 1562 insertions, 1505 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index c09ee1b40a..4bd5d58116 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -1,113 +1,54 @@ import * as grpc from '@grpc/grpc-js'; import { Empty } from 'google-protobuf/google/protobuf/empty_pb.js'; -import { - BoolValue, - StringValue, - UInt32Value, -} from 'google-protobuf/google/protobuf/wrappers_pb.js'; -import { promisify } from 'util'; +import { BoolValue, StringValue } from 'google-protobuf/google/protobuf/wrappers_pb.js'; import { - AccessMethod, AccessMethodSetting, AccountDataError, AccountDataResponse, AccountToken, - AfterDisconnect, - ApiAccessMethodSettings, - AuthFailedError, BridgeSettings, - BridgesMethod, BridgeState, - BridgeType, - ConnectionConfig, - Constraint, CustomListError, - CustomLists, CustomProxy, DaemonEvent, - DeviceEvent, DeviceState, - DirectMethod, - EndpointObfuscationType, - ErrorStateCause, - ErrorStateDetails, - FeatureIndicator, - FirewallPolicyError, - FirewallPolicyErrorType, IAppVersionInfo, - IBridgeConstraints, ICustomList, IDevice, IDeviceRemoval, IDnsOptions, - IObfuscationEndpoint, - IOpenVpnConstraints, - IProxyEndpoint, - IRelayListCity, - IRelayListCountry, - IRelayListHostname, IRelayListWithEndpointData, - IRelaySettingsNormal, ISettings, - ITunnelOptions, - ITunnelStateRelayInfo, - IWireguardConstraints, - IWireguardEndpointData, - LoggedInDeviceState, - LoggedOutDeviceState, NewAccessMethodSetting, ObfuscationSettings, ObfuscationType, - Ownership, - ProxyType, - RelayEndpointType, - RelayLocation, - RelayLocationGeographical, - RelayProtocol, RelaySettings, - SocksAuth, - TunnelParameterError, - TunnelProtocol, TunnelState, - TunnelType, VoucherResponse, - wrapConstraint, } from '../shared/daemon-rpc-types'; -import log from '../shared/logging'; -import { ManagementServiceClient } from './management_interface/management_interface_grpc_pb'; +import { ConnectionObserver, GrpcClient, noConnectionError } from './grpc-client'; +import { + convertFromApiAccessMethodSetting, + convertFromDaemonEvent, + convertFromDevice, + convertFromDeviceState, + convertFromRelayList, + convertFromSettings, + convertFromTunnelState, + convertToApiAccessMethodSetting, + convertToCustomList, + convertToCustomProxy, + convertToNewApiAccessMethodSetting, + convertToNormalBridgeSettings, + convertToRelayConstraints, + ensureExists, +} from './grpc-type-convertions'; import * as grpcTypes from './management_interface/management_interface_pb'; const DAEMON_RPC_PATH = process.platform === 'win32' ? 'unix:////./pipe/Mullvad VPN' : 'unix:///var/run/mullvad-vpn'; -const NETWORK_CALL_TIMEOUT = 10000; -const CHANNEL_STATE_TIMEOUT = 1000 * 60 * 60; - -const noConnectionError = new Error('No connection established to daemon'); -const invalidErrorStateCause = new Error( - 'VPN_PERMISSION_DENIED is not a valid error state cause on desktop', -); - -export class ConnectionObserver { - constructor( - private openHandler: () => void, - private closeHandler: (wasConnected: boolean, error?: Error) => void, - ) {} - - // Only meant to be called by DaemonRpc - // @internal - public onOpen = () => { - this.openHandler(); - }; - - // Only meant to be called by DaemonRpc - // @internal - public onClose = (wasConnected: boolean, error?: Error) => { - this.closeHandler(wasConnected, error); - }; -} - export class SubscriptionListener<T> { // Only meant to be used by DaemonRpc // @internal @@ -131,85 +72,51 @@ export class SubscriptionListener<T> { } } -export class ResponseParseError extends Error { - constructor(message: string) { - super(message); - } -} - -type CallFunctionArgument<T, R> = - | ((arg: T, callback: (error: Error | null, result: R) => void) => void) - | undefined; - -export class DaemonRpc { - private client: ManagementServiceClient; - private isConnectedValue = false; - private isClosed = false; +export class DaemonRpc extends GrpcClient { private nextSubscriptionId = 0; private subscriptions: Map<number, grpc.ClientReadableStream<grpcTypes.DaemonEvent>> = new Map(); - private reconnectionTimeout?: NodeJS.Timeout; - - constructor(private connectionObserver?: ConnectionObserver) { - this.client = new ManagementServiceClient( - DAEMON_RPC_PATH, - grpc.credentials.createInsecure(), - this.channelOptions(), - ); - } - public get isConnected() { - return this.isConnectedValue; + public constructor(connectionObserver?: ConnectionObserver) { + super(DAEMON_RPC_PATH, connectionObserver); } - public reopen(connectionObserver?: ConnectionObserver) { - if (this.isClosed) { - this.isClosed = false; - this.client = new ManagementServiceClient( - DAEMON_RPC_PATH, - grpc.credentials.createInsecure(), - this.channelOptions(), - ); - - this.connectionObserver = connectionObserver; + public disconnect() { + for (const subscriptionId of this.subscriptions.keys()) { + this.removeSubscription(subscriptionId); } + + super.disconnect(); } - public connect(): Promise<void> { - return new Promise((resolve, reject) => { - const usedClient = this.client; - this.client.waitForReady(this.deadlineFromNow(), (error) => { - if (this.client !== usedClient) { - reject(new Error('Stale connection attempt')); - return; - } + public subscribeDaemonEventListener(listener: SubscriptionListener<DaemonEvent>) { + const call = this.isConnected && this.client.eventsListen(new Empty()); + if (!call) { + throw noConnectionError; + } + const subscriptionId = this.subscriptionId(); + listener.subscriptionId = subscriptionId; + this.subscriptions.set(subscriptionId, call); - if (error) { - this.onClose(error); - this.ensureConnectivity(); - reject(error); - } else { - this.reconnectionTimeout = undefined; - this.isConnectedValue = true; - this.connectionObserver?.onOpen(); - this.setChannelCallback(); - resolve(); - } - }); + call.on('data', (data: grpcTypes.DaemonEvent) => { + try { + const daemonEvent = convertFromDaemonEvent(data); + listener.onEvent(daemonEvent); + } catch (e) { + const error = e as Error; + listener.onError(error); + } }); - } - - public disconnect() { - this.isConnectedValue = false; - for (const subscriptionId of this.subscriptions.keys()) { + call.on('error', (error) => { + listener.onError(error); this.removeSubscription(subscriptionId); - } + }); + } - this.isClosed = true; - this.client.close(); - this.connectionObserver = undefined; - if (this.reconnectionTimeout) { - clearTimeout(this.reconnectionTimeout); + public unsubscribeDaemonEventListener(listener: SubscriptionListener<DaemonEvent>) { + const id = listener.subscriptionId; + if (id !== undefined) { + this.removeSubscription(id); } } @@ -475,38 +382,6 @@ export class DaemonRpc { return convertFromSettings(response)!; } - public subscribeDaemonEventListener(listener: SubscriptionListener<DaemonEvent>) { - const call = this.isConnected && this.client.eventsListen(new Empty()); - if (!call) { - throw noConnectionError; - } - const subscriptionId = this.subscriptionId(); - listener.subscriptionId = subscriptionId; - this.subscriptions.set(subscriptionId, call); - - call.on('data', (data: grpcTypes.DaemonEvent) => { - try { - const daemonEvent = convertFromDaemonEvent(data); - listener.onEvent(daemonEvent); - } catch (e) { - const error = e as Error; - listener.onError(error); - } - }); - - call.on('error', (error) => { - listener.onError(error); - this.removeSubscription(subscriptionId); - }); - } - - public unsubscribeDaemonEventListener(listener: SubscriptionListener<DaemonEvent>) { - const id = listener.subscriptionId; - if (id !== undefined) { - this.removeSubscription(id); - } - } - public async getAccountHistory(): Promise<AccountToken | undefined> { const response = await this.callEmpty<grpcTypes.AccountHistory>(this.client.getAccountHistory); return response.getToken()?.getValue(); @@ -714,66 +589,6 @@ export class DaemonRpc { return current; } - private deadlineFromNow() { - return Date.now() + NETWORK_CALL_TIMEOUT; - } - - private channelStateTimeout(): number { - return Date.now() + CHANNEL_STATE_TIMEOUT; - } - - private callEmpty<R = Empty>(fn: CallFunctionArgument<Empty, R>): Promise<R> { - return this.call<Empty, R>(fn, new Empty()); - } - - private callString<R = Empty>( - fn: CallFunctionArgument<StringValue, R>, - value?: string, - ): Promise<R> { - const googleString = new StringValue(); - - if (value !== undefined) { - googleString.setValue(value); - } - - return this.call<StringValue, R>(fn, googleString); - } - - private callBool<R>(fn: CallFunctionArgument<BoolValue, R>, value?: boolean): Promise<R> { - const googleBool = new BoolValue(); - - if (value !== undefined) { - googleBool.setValue(value); - } - - return this.call<BoolValue, R>(fn, googleBool); - } - - private callNumber<R>(fn: CallFunctionArgument<UInt32Value, R>, value?: number): Promise<R> { - const googleNumber = new UInt32Value(); - - if (value !== undefined) { - googleNumber.setValue(value); - } - - return this.call<UInt32Value, R>(fn, googleNumber); - } - - private call<T, R>(fn: CallFunctionArgument<T, R>, arg: T): Promise<R> { - if (fn && this.isConnected) { - return promisify<T, R>(fn.bind(this.client))(arg); - } else { - throw noConnectionError; - } - } - - private onClose(error?: Error) { - const wasConnected = this.isConnectedValue; - this.isConnectedValue = false; - - this.connectionObserver?.onClose(wasConnected, error); - } - private removeSubscription(id: number) { const subscription = this.subscriptions.get(id); if (subscription !== undefined) { @@ -793,1273 +608,4 @@ export class DaemonRpc { setImmediate(() => subscription.cancel()); } } - - private channelOptions(): grpc.ClientOptions { - return { - 'grpc.max_reconnect_backoff_ms': 3000, - 'grpc.initial_reconnect_backoff_ms': 3000, - 'grpc.keepalive_time_ms': Math.pow(2, 30), - 'grpc.keepalive_timeout_ms': Math.pow(2, 30), - 'grpc.client_idle_timeout_ms': Math.pow(2, 30), - }; - } - - private connectivityChangeCallback(timeoutErr?: Error) { - const channel = this.client.getChannel(); - const currentState = channel?.getConnectivityState(true); - log.verbose(`GRPC Channel connectivity state changed to ${currentState}`); - if (channel) { - if (timeoutErr) { - this.setChannelCallback(currentState); - return; - } - const wasConnected = this.isConnected; - if (this.channelDisconnected(currentState)) { - this.onClose(); - // Try and reconnect in case - void this.connect().catch((error) => { - log.error(`Failed to reconnect - ${error}`); - }); - this.setChannelCallback(currentState); - } else if (!wasConnected && currentState === grpc.connectivityState.READY) { - this.isConnectedValue = true; - this.connectionObserver?.onOpen(); - this.setChannelCallback(currentState); - } - } - } - - private channelDisconnected(state: grpc.connectivityState): boolean { - return ( - (state === grpc.connectivityState.SHUTDOWN || - state === grpc.connectivityState.TRANSIENT_FAILURE || - state === grpc.connectivityState.IDLE) && - this.isConnected - ); - } - - private setChannelCallback(currentState?: grpc.connectivityState) { - const channel = this.client.getChannel(); - if (currentState === undefined && channel) { - currentState = channel?.getConnectivityState(false); - } - if (currentState) { - channel.watchConnectivityState(currentState, this.channelStateTimeout(), (error) => - this.connectivityChangeCallback(error), - ); - } - } - - // Since grpc.Channel.watchConnectivityState() isn't always running as intended, whenever the - // client fails to connect at first, `ensureConnectivity()` should be called so that it tries to - // check the connectivity state and nudge the client into connecting. - // `grpc.Channel.getConnectivityState(true)` should make it attempt to connect. - private ensureConnectivity() { - if (this.reconnectionTimeout) { - clearTimeout(this.reconnectionTimeout); - } - this.reconnectionTimeout = setTimeout(() => { - const lastState = this.client.getChannel().getConnectivityState(true); - if (this.channelDisconnected(lastState)) { - this.onClose(); - } - if (!this.isConnected) { - void this.connect().catch((error) => { - log.error(`Failed to reconnect - ${error}`); - }); - } - }, 3000); - } -} - -function liftConstraint<T>(constraint: Constraint<T> | undefined): T | undefined { - if (constraint !== undefined && constraint !== 'any') { - return constraint.only; - } - return undefined; -} - -function convertFromRelayList(relayList: grpcTypes.RelayList): IRelayListWithEndpointData { - return { - relayList: { - countries: relayList - .getCountriesList() - .map((country: grpcTypes.RelayListCountry) => convertFromRelayListCountry(country)), - }, - wireguardEndpointData: convertWireguardEndpointData(relayList.getWireguard()!), - }; -} - -function convertWireguardEndpointData( - data: grpcTypes.WireguardEndpointData, -): IWireguardEndpointData { - return { - portRanges: data.getPortRangesList().map((range) => [range.getFirst(), range.getLast()]), - udp2tcpPorts: data.getUdp2tcpPortsList(), - }; -} - -function convertFromRelayListCountry(country: grpcTypes.RelayListCountry): IRelayListCountry { - const countryObject = country.toObject(); - return { - ...countryObject, - cities: country.getCitiesList().map(convertFromRelayListCity), - }; -} - -function convertFromRelayListCity(city: grpcTypes.RelayListCity): IRelayListCity { - const cityObject = city.toObject(); - return { - ...cityObject, - relays: city.getRelaysList().map(convertFromRelayListRelay), - }; -} - -function convertFromRelayListRelay(relay: grpcTypes.Relay): IRelayListHostname { - const relayObject = relay.toObject(); - - let daita = false; - if (relayObject.endpointType === grpcTypes.Relay.RelayType.WIREGUARD) { - const endpointDataU8 = relay.getEndpointData()?.getValue_asU8(); - if (endpointDataU8) { - daita = grpcTypes.WireguardRelayEndpointData.deserializeBinary(endpointDataU8).getDaita(); - } - } - - return { - ...relayObject, - endpointType: convertFromRelayType(relayObject.endpointType), - daita, - }; -} - -function convertFromRelayType(relayType: grpcTypes.Relay.RelayType): RelayEndpointType { - const protocolMap: Record<grpcTypes.Relay.RelayType, RelayEndpointType> = { - [grpcTypes.Relay.RelayType.OPENVPN]: 'openvpn', - [grpcTypes.Relay.RelayType.BRIDGE]: 'bridge', - [grpcTypes.Relay.RelayType.WIREGUARD]: 'wireguard', - }; - return protocolMap[relayType]; -} - -function convertFromWireguardKey(publicKey: Uint8Array | string): string { - if (typeof publicKey === 'string') { - return publicKey; - } - return Buffer.from(publicKey).toString('base64'); -} - -function convertFromTransportProtocol(protocol: grpcTypes.TransportProtocol): RelayProtocol { - const protocolMap: Record<grpcTypes.TransportProtocol, RelayProtocol> = { - [grpcTypes.TransportProtocol.TCP]: 'tcp', - [grpcTypes.TransportProtocol.UDP]: 'udp', - }; - return protocolMap[protocol]; -} - -function convertFromTunnelState(tunnelState: grpcTypes.TunnelState): TunnelState | undefined { - const tunnelStateObject = tunnelState.toObject(); - switch (tunnelState.getStateCase()) { - case grpcTypes.TunnelState.StateCase.STATE_NOT_SET: - return undefined; - case grpcTypes.TunnelState.StateCase.DISCONNECTED: - return { - state: 'disconnected', - location: tunnelStateObject.disconnected!.disconnectedLocation, - }; - case grpcTypes.TunnelState.StateCase.DISCONNECTING: { - const detailsMap: Record<grpcTypes.AfterDisconnect, AfterDisconnect> = { - [grpcTypes.AfterDisconnect.NOTHING]: 'nothing', - [grpcTypes.AfterDisconnect.BLOCK]: 'block', - [grpcTypes.AfterDisconnect.RECONNECT]: 'reconnect', - }; - return ( - tunnelStateObject.disconnecting && { - state: 'disconnecting', - details: detailsMap[tunnelStateObject.disconnecting.afterDisconnect], - } - ); - } - case grpcTypes.TunnelState.StateCase.ERROR: - return ( - tunnelStateObject.error?.errorState && { - state: 'error', - details: convertFromTunnelStateError(tunnelStateObject.error.errorState), - } - ); - case grpcTypes.TunnelState.StateCase.CONNECTING: - return { - state: 'connecting', - details: - tunnelStateObject.connecting?.relayInfo && - convertFromTunnelStateRelayInfo(tunnelStateObject.connecting.relayInfo), - featureIndicators: convertFromFeatureIndicators( - tunnelStateObject.connecting?.featureIndicators?.activeFeaturesList, - ), - }; - case grpcTypes.TunnelState.StateCase.CONNECTED: { - const relayInfo = - tunnelStateObject.connected?.relayInfo && - convertFromTunnelStateRelayInfo(tunnelStateObject.connected.relayInfo); - return ( - relayInfo && { - state: 'connected', - details: relayInfo, - featureIndicators: convertFromFeatureIndicators( - tunnelStateObject.connected?.featureIndicators?.activeFeaturesList, - ), - } - ); - } - } -} - -function convertFromTunnelStateError(state: grpcTypes.ErrorState.AsObject): ErrorStateDetails { - const baseError = { - blockingError: state.blockingError && convertFromBlockingError(state.blockingError), - }; - - switch (state.cause) { - case grpcTypes.ErrorState.Cause.AUTH_FAILED: - return { - ...baseError, - cause: ErrorStateCause.authFailed, - authFailedError: convertFromAuthFailedError(state.authFailedError), - }; - case grpcTypes.ErrorState.Cause.TUNNEL_PARAMETER_ERROR: - return { - ...baseError, - cause: ErrorStateCause.tunnelParameterError, - parameterError: convertFromParameterError(state.parameterError), - }; - case grpcTypes.ErrorState.Cause.SET_FIREWALL_POLICY_ERROR: - return { - ...baseError, - cause: ErrorStateCause.setFirewallPolicyError, - policyError: convertFromBlockingError(state.policyError!), - }; - - case grpcTypes.ErrorState.Cause.IS_OFFLINE: - return { - ...baseError, - cause: ErrorStateCause.isOffline, - }; - case grpcTypes.ErrorState.Cause.SET_DNS_ERROR: - return { - ...baseError, - cause: ErrorStateCause.setDnsError, - }; - case grpcTypes.ErrorState.Cause.IPV6_UNAVAILABLE: - return { - ...baseError, - cause: ErrorStateCause.ipv6Unavailable, - }; - case grpcTypes.ErrorState.Cause.START_TUNNEL_ERROR: - return { - ...baseError, - cause: ErrorStateCause.startTunnelError, - }; - case grpcTypes.ErrorState.Cause.CREATE_TUNNEL_DEVICE: - return { - ...baseError, - cause: ErrorStateCause.createTunnelDeviceError, - osError: state.createTunnelError, - }; - case grpcTypes.ErrorState.Cause.SPLIT_TUNNEL_ERROR: - return { - ...baseError, - cause: ErrorStateCause.splitTunnelError, - }; - case grpcTypes.ErrorState.Cause.NEED_FULL_DISK_PERMISSIONS: - return { - ...baseError, - cause: ErrorStateCause.needFullDiskPermissions, - }; - case grpcTypes.ErrorState.Cause.VPN_PERMISSION_DENIED: - // VPN_PERMISSION_DENIED is only ever created on Android - throw invalidErrorStateCause; - } -} - -function convertFromBlockingError( - error: grpcTypes.ErrorState.FirewallPolicyError.AsObject, -): FirewallPolicyError { - switch (error.type) { - case grpcTypes.ErrorState.FirewallPolicyError.ErrorType.GENERIC: - return { type: FirewallPolicyErrorType.generic }; - case grpcTypes.ErrorState.FirewallPolicyError.ErrorType.LOCKED: { - const pid = error.lockPid; - const name = error.lockName!; - return { type: FirewallPolicyErrorType.locked, pid, name }; - } - } -} - -function convertFromAuthFailedError(error: grpcTypes.ErrorState.AuthFailedError): AuthFailedError { - switch (error) { - case grpcTypes.ErrorState.AuthFailedError.UNKNOWN: - return AuthFailedError.unknown; - case grpcTypes.ErrorState.AuthFailedError.INVALID_ACCOUNT: - return AuthFailedError.invalidAccount; - case grpcTypes.ErrorState.AuthFailedError.EXPIRED_ACCOUNT: - return AuthFailedError.expiredAccount; - case grpcTypes.ErrorState.AuthFailedError.TOO_MANY_CONNECTIONS: - return AuthFailedError.tooManyConnections; - } -} - -function convertFromParameterError( - error: grpcTypes.ErrorState.GenerationError, -): TunnelParameterError { - switch (error) { - case grpcTypes.ErrorState.GenerationError.NO_MATCHING_RELAY: - return TunnelParameterError.noMatchingRelay; - case grpcTypes.ErrorState.GenerationError.NO_MATCHING_BRIDGE_RELAY: - return TunnelParameterError.noMatchingBridgeRelay; - case grpcTypes.ErrorState.GenerationError.NO_WIREGUARD_KEY: - return TunnelParameterError.noWireguardKey; - case grpcTypes.ErrorState.GenerationError.CUSTOM_TUNNEL_HOST_RESOLUTION_ERROR: - return TunnelParameterError.customTunnelHostResolutionError; - } -} - -function convertFromTunnelStateRelayInfo( - state: grpcTypes.TunnelStateRelayInfo.AsObject, -): ITunnelStateRelayInfo | undefined { - if (state.tunnelEndpoint) { - return { - ...state, - endpoint: { - ...state.tunnelEndpoint, - tunnelType: convertFromTunnelType(state.tunnelEndpoint.tunnelType), - protocol: convertFromTransportProtocol(state.tunnelEndpoint.protocol), - proxy: state.tunnelEndpoint.proxy && convertFromProxyEndpoint(state.tunnelEndpoint.proxy), - obfuscationEndpoint: - state.tunnelEndpoint.obfuscation && - convertFromObfuscationEndpoint(state.tunnelEndpoint.obfuscation), - entryEndpoint: - state.tunnelEndpoint.entryEndpoint && - convertFromEntryEndpoint(state.tunnelEndpoint.entryEndpoint), - }, - }; - } - return undefined; -} - -function convertFromFeatureIndicators( - featureIndicators?: Array<grpcTypes.FeatureIndicator>, -): Array<FeatureIndicator> | undefined { - return featureIndicators?.map(convertFromFeatureIndicator); -} - -function convertFromFeatureIndicator( - featureIndicator: grpcTypes.FeatureIndicator, -): FeatureIndicator { - switch (featureIndicator) { - case grpcTypes.FeatureIndicator.QUANTUM_RESISTANCE: - return FeatureIndicator.quantumResistance; - case grpcTypes.FeatureIndicator.MULTIHOP: - return FeatureIndicator.multihop; - case grpcTypes.FeatureIndicator.BRIDGE_MODE: - return FeatureIndicator.bridgeMode; - case grpcTypes.FeatureIndicator.SPLIT_TUNNELING: - return FeatureIndicator.splitTunneling; - case grpcTypes.FeatureIndicator.LOCKDOWN_MODE: - return FeatureIndicator.lockdownMode; - case grpcTypes.FeatureIndicator.UDP_2_TCP: - return FeatureIndicator.udp2tcp; - case grpcTypes.FeatureIndicator.LAN_SHARING: - return FeatureIndicator.lanSharing; - case grpcTypes.FeatureIndicator.DNS_CONTENT_BLOCKERS: - return FeatureIndicator.dnsContentBlockers; - case grpcTypes.FeatureIndicator.CUSTOM_DNS: - return FeatureIndicator.customDns; - case grpcTypes.FeatureIndicator.SERVER_IP_OVERRIDE: - return FeatureIndicator.serverIpOverride; - case grpcTypes.FeatureIndicator.CUSTOM_MTU: - return FeatureIndicator.customMtu; - case grpcTypes.FeatureIndicator.CUSTOM_MSS_FIX: - return FeatureIndicator.customMssFix; - case grpcTypes.FeatureIndicator.DAITA: - return FeatureIndicator.daita; - case grpcTypes.FeatureIndicator.DAITA_SMART_ROUTING: - return FeatureIndicator.daitaSmartRouting; - case grpcTypes.FeatureIndicator.SHADOWSOCKS: - return FeatureIndicator.shadowsocks; - } -} - -function convertFromTunnelType(tunnelType: grpcTypes.TunnelType): TunnelType { - const tunnelTypeMap: Record<grpcTypes.TunnelType, TunnelType> = { - [grpcTypes.TunnelType.WIREGUARD]: 'wireguard', - [grpcTypes.TunnelType.OPENVPN]: 'openvpn', - }; - - return tunnelTypeMap[tunnelType]; -} - -function convertFromProxyEndpoint(proxyEndpoint: grpcTypes.ProxyEndpoint.AsObject): IProxyEndpoint { - const proxyTypeMap: Record<grpcTypes.ProxyEndpoint.ProxyType, ProxyType> = { - [grpcTypes.ProxyEndpoint.ProxyType.CUSTOM]: 'custom', - [grpcTypes.ProxyEndpoint.ProxyType.SHADOWSOCKS]: 'shadowsocks', - }; - - return { - ...proxyEndpoint, - protocol: convertFromTransportProtocol(proxyEndpoint.protocol), - proxyType: proxyTypeMap[proxyEndpoint.proxyType], - }; -} - -function convertFromObfuscationEndpoint( - obfuscationEndpoint: grpcTypes.ObfuscationEndpoint.AsObject, -): IObfuscationEndpoint { - let obfuscationType: EndpointObfuscationType; - switch (obfuscationEndpoint.obfuscationType) { - case grpcTypes.ObfuscationEndpoint.ObfuscationType.UDP2TCP: - obfuscationType = 'udp2tcp'; - break; - case grpcTypes.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS: - obfuscationType = 'shadowsocks'; - break; - default: - throw new Error('unsupported obfuscation protocol'); - } - - return { - ...obfuscationEndpoint, - protocol: convertFromTransportProtocol(obfuscationEndpoint.protocol), - obfuscationType: obfuscationType, - }; -} - -function convertFromEntryEndpoint(entryEndpoint: grpcTypes.Endpoint.AsObject) { - return { - address: entryEndpoint.address, - transportProtocol: convertFromTransportProtocol(entryEndpoint.protocol), - }; -} - -function convertFromSettings(settings: grpcTypes.Settings): ISettings | undefined { - const settingsObject = settings.toObject(); - const bridgeState = convertFromBridgeState(settingsObject.bridgeState!.state!); - const relaySettings = convertFromRelaySettings(settings.getRelaySettings())!; - const bridgeSettings = convertFromBridgeSettings(settings.getBridgeSettings()!); - const tunnelOptions = convertFromTunnelOptions(settingsObject.tunnelOptions!); - const splitTunnel = settingsObject.splitTunnel ?? { enableExclusions: false, appsList: [] }; - const obfuscationSettings = convertFromObfuscationSettings(settingsObject.obfuscationSettings); - const customLists = convertFromCustomListSettings(settings.getCustomLists()); - const apiAccessMethods = convertFromApiAccessMethodSettings(settings.getApiAccessMethods()!); - const relayOverrides = settingsObject.relayOverridesList; - return { - ...settings.toObject(), - bridgeState, - relaySettings, - bridgeSettings, - tunnelOptions, - splitTunnel, - obfuscationSettings, - customLists, - apiAccessMethods, - relayOverrides, - }; -} - -function convertFromBridgeState(bridgeState: grpcTypes.BridgeState.State): BridgeState { - const bridgeStateMap: Record<grpcTypes.BridgeState.State, BridgeState> = { - [grpcTypes.BridgeState.State.AUTO]: 'auto', - [grpcTypes.BridgeState.State.ON]: 'on', - [grpcTypes.BridgeState.State.OFF]: 'off', - }; - - return bridgeStateMap[bridgeState]; -} - -function convertFromRelaySettings( - relaySettings?: grpcTypes.RelaySettings, -): RelaySettings | undefined { - if (relaySettings) { - switch (relaySettings.getEndpointCase()) { - case grpcTypes.RelaySettings.EndpointCase.ENDPOINT_NOT_SET: - return undefined; - case grpcTypes.RelaySettings.EndpointCase.CUSTOM: { - const custom = relaySettings.getCustom()?.toObject(); - const config = relaySettings.getCustom()?.getConfig(); - const connectionConfig = config && convertFromConnectionConfig(config); - return ( - custom && - connectionConfig && { - customTunnelEndpoint: { - ...custom, - config: connectionConfig, - }, - } - ); - } - case grpcTypes.RelaySettings.EndpointCase.NORMAL: { - const normal = relaySettings.getNormal()!; - const locationConstraint = convertFromLocationConstraint(normal.getLocation()); - const location = wrapConstraint(locationConstraint); - // `getTunnelType()` is not falsy if type is 'any' - const tunnelProtocol = convertFromTunnelTypeConstraint( - normal.hasTunnelType() ? normal.getTunnelType() : undefined, - ); - const providers = normal.getProvidersList(); - const ownership = convertFromOwnership(normal.getOwnership()); - const openvpnConstraints = convertFromOpenVpnConstraints(normal.getOpenvpnConstraints()!); - const wireguardConstraints = convertFromWireguardConstraints( - normal.getWireguardConstraints()!, - ); - - return { - normal: { - location, - tunnelProtocol, - providers, - ownership, - wireguardConstraints, - openvpnConstraints, - }, - }; - } - } - } else { - return undefined; - } -} - -function convertFromBridgeSettings(bridgeSettings: grpcTypes.BridgeSettings): BridgeSettings { - const bridgeSettingsObject = bridgeSettings.toObject(); - - const detailsMap: Record<grpcTypes.BridgeSettings.BridgeType, BridgeType> = { - [grpcTypes.BridgeSettings.BridgeType.NORMAL]: 'normal', - [grpcTypes.BridgeSettings.BridgeType.CUSTOM]: 'custom', - }; - const type = detailsMap[bridgeSettingsObject.bridgeType]; - - const normalSettings = bridgeSettingsObject.normal; - const locationConstraint = convertFromLocationConstraint( - bridgeSettings.getNormal()?.getLocation(), - ); - const location = wrapConstraint(locationConstraint); - const providers = normalSettings!.providersList; - const ownership = convertFromOwnership(normalSettings!.ownership); - - const normal = { - location, - providers, - ownership, - }; - - const grpcCustom = bridgeSettings.getCustom(); - const custom = grpcCustom ? convertFromCustomProxy(grpcCustom) : undefined; - - return { type, normal, custom }; -} - -function convertFromConnectionConfig( - connectionConfig: grpcTypes.ConnectionConfig, -): ConnectionConfig | undefined { - const connectionConfigObject = connectionConfig.toObject(); - switch (connectionConfig.getConfigCase()) { - case grpcTypes.ConnectionConfig.ConfigCase.CONFIG_NOT_SET: - return undefined; - case grpcTypes.ConnectionConfig.ConfigCase.WIREGUARD: - return ( - connectionConfigObject.wireguard && - connectionConfigObject.wireguard.tunnel && - connectionConfigObject.wireguard.peer && { - wireguard: { - ...connectionConfigObject.wireguard, - tunnel: { - privateKey: convertFromWireguardKey( - connectionConfigObject.wireguard.tunnel.privateKey, - ), - addresses: connectionConfigObject.wireguard.tunnel.addressesList, - }, - peer: { - ...connectionConfigObject.wireguard.peer, - addresses: connectionConfigObject.wireguard.peer.allowedIpsList, - publicKey: convertFromWireguardKey(connectionConfigObject.wireguard.peer.publicKey), - }, - }, - } - ); - case grpcTypes.ConnectionConfig.ConfigCase.OPENVPN: { - const [ip, port] = connectionConfigObject.openvpn!.address.split(':'); - return { - openvpn: { - ...connectionConfigObject.openvpn!, - endpoint: { - ip, - port: parseInt(port, 10), - protocol: convertFromTransportProtocol(connectionConfigObject.openvpn!.protocol), - }, - }, - }; - } - } -} - -function convertFromLocationConstraint( - location?: grpcTypes.LocationConstraint, -): RelayLocation | undefined { - if (location === undefined) { - return undefined; - } else if (location.getTypeCase() === grpcTypes.LocationConstraint.TypeCase.CUSTOM_LIST) { - return { customList: location.getCustomList() }; - } else { - const innerLocation = location.getLocation()?.toObject(); - return innerLocation && convertFromGeographicConstraint(innerLocation); - } -} - -function convertFromGeographicConstraint( - location: grpcTypes.GeographicLocationConstraint.AsObject, -): RelayLocation { - if (location.hostname) { - return location; - } else if (location.city) { - return { - country: location.country, - city: location.city, - }; - } else { - return { - country: location.country, - }; - } -} - -function convertFromTunnelOptions(tunnelOptions: grpcTypes.TunnelOptions.AsObject): ITunnelOptions { - return { - openvpn: { - mssfix: tunnelOptions.openvpn!.mssfix, - }, - wireguard: { - mtu: tunnelOptions.wireguard!.mtu, - quantumResistant: convertFromQuantumResistantState( - tunnelOptions.wireguard?.quantumResistant?.state, - ), - daita: tunnelOptions.wireguard!.daita, - }, - generic: { - enableIpv6: tunnelOptions.generic!.enableIpv6, - }, - dns: { - state: - tunnelOptions.dnsOptions?.state === grpcTypes.DnsOptions.DnsState.CUSTOM - ? 'custom' - : 'default', - defaultOptions: { - blockAds: tunnelOptions.dnsOptions?.defaultOptions?.blockAds ?? false, - blockTrackers: tunnelOptions.dnsOptions?.defaultOptions?.blockTrackers ?? false, - blockMalware: tunnelOptions.dnsOptions?.defaultOptions?.blockMalware ?? false, - blockAdultContent: tunnelOptions.dnsOptions?.defaultOptions?.blockAdultContent ?? false, - blockGambling: tunnelOptions.dnsOptions?.defaultOptions?.blockGambling ?? false, - blockSocialMedia: tunnelOptions.dnsOptions?.defaultOptions?.blockSocialMedia ?? false, - }, - customOptions: { - addresses: tunnelOptions.dnsOptions?.customOptions?.addressesList ?? [], - }, - }, - }; -} - -function convertFromQuantumResistantState( - state?: grpcTypes.QuantumResistantState.State, -): boolean | undefined { - return state === undefined - ? undefined - : { - [grpcTypes.QuantumResistantState.State.ON]: true, - [grpcTypes.QuantumResistantState.State.OFF]: false, - [grpcTypes.QuantumResistantState.State.AUTO]: undefined, - }[state]; -} - -function convertFromObfuscationSettings( - obfuscationSettings?: grpcTypes.ObfuscationSettings.AsObject, -): ObfuscationSettings { - let selectedObfuscationType = ObfuscationType.auto; - switch (obfuscationSettings?.selectedObfuscation) { - case grpcTypes.ObfuscationSettings.SelectedObfuscation.OFF: - selectedObfuscationType = ObfuscationType.off; - break; - case grpcTypes.ObfuscationSettings.SelectedObfuscation.UDP2TCP: - selectedObfuscationType = ObfuscationType.udp2tcp; - break; - case grpcTypes.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS: - selectedObfuscationType = ObfuscationType.shadowsocks; - break; - } - - return { - selectedObfuscation: selectedObfuscationType, - udp2tcpSettings: obfuscationSettings?.udp2tcp - ? { port: convertFromConstraint(obfuscationSettings.udp2tcp.port) } - : { port: 'any' }, - shadowsocksSettings: obfuscationSettings?.shadowsocks - ? { port: convertFromConstraint(obfuscationSettings.shadowsocks.port) } - : { port: 'any' }, - }; -} - -function convertFromDaemonEvent(data: grpcTypes.DaemonEvent): DaemonEvent { - const tunnelState = data.getTunnelState(); - if (tunnelState !== undefined) { - return { tunnelState: convertFromTunnelState(tunnelState)! }; - } - - const settings = data.getSettings(); - if (settings !== undefined) { - return { settings: convertFromSettings(settings)! }; - } - - const relayList = data.getRelayList(); - if (relayList !== undefined) { - return { relayList: convertFromRelayList(relayList) }; - } - - const deviceConfig = data.getDevice(); - if (deviceConfig !== undefined) { - return { device: convertFromDeviceEvent(deviceConfig) }; - } - - const deviceRemoval = data.getRemoveDevice(); - if (deviceRemoval !== undefined) { - return { deviceRemoval: convertFromDeviceRemoval(deviceRemoval) }; - } - - const versionInfo = data.getVersionInfo(); - if (versionInfo !== undefined) { - return { appVersionInfo: versionInfo.toObject() }; - } - - const newAccessMethod = data.getNewAccessMethod(); - if (newAccessMethod !== undefined) { - return { accessMethodSetting: convertFromApiAccessMethodSetting(newAccessMethod) }; - } - - // Handle unknown daemon events - const keys = Object.entries(data.toObject()) - .filter(([, value]) => value !== undefined) - .map(([key]) => key); - throw new Error(`Unknown daemon event received containing ${keys}`); -} - -function convertFromOwnership(ownership: grpcTypes.Ownership): Ownership { - switch (ownership) { - case grpcTypes.Ownership.ANY: - return Ownership.any; - case grpcTypes.Ownership.MULLVAD_OWNED: - return Ownership.mullvadOwned; - case grpcTypes.Ownership.RENTED: - return Ownership.rented; - } -} - -function convertToOwnership(ownership: Ownership): grpcTypes.Ownership { - switch (ownership) { - case Ownership.any: - return grpcTypes.Ownership.ANY; - case Ownership.mullvadOwned: - return grpcTypes.Ownership.MULLVAD_OWNED; - case Ownership.rented: - return grpcTypes.Ownership.RENTED; - } -} - -function convertFromOpenVpnConstraints( - constraints: grpcTypes.OpenvpnConstraints, -): IOpenVpnConstraints { - const transportPort = convertFromConstraint(constraints.getPort()); - if (transportPort !== 'any' && 'only' in transportPort) { - const port = convertFromConstraint(transportPort.only.getPort()); - let protocol: Constraint<RelayProtocol> = 'any'; - switch (transportPort.only.getProtocol()) { - case grpcTypes.TransportProtocol.TCP: - protocol = { only: 'tcp' }; - break; - case grpcTypes.TransportProtocol.UDP: - protocol = { only: 'udp' }; - break; - } - return { port, protocol }; - } - return { port: 'any', protocol: 'any' }; -} - -function convertFromWireguardConstraints( - constraints: grpcTypes.WireguardConstraints, -): IWireguardConstraints { - const result: IWireguardConstraints = { - port: 'any', - ipVersion: 'any', - useMultihop: constraints.getUseMultihop(), - entryLocation: 'any', - }; - - const port = constraints.getPort(); - if (port) { - result.port = { only: port }; - } - - // `getIpVersion()` is not falsy if type is 'any' - if (constraints.hasIpVersion()) { - switch (constraints.getIpVersion()) { - case grpcTypes.IpVersion.V4: - result.ipVersion = { only: 'ipv4' }; - break; - case grpcTypes.IpVersion.V6: - result.ipVersion = { only: 'ipv6' }; - break; - } - } - - const entryLocation = constraints.getEntryLocation(); - if (entryLocation) { - const location = convertFromLocationConstraint(entryLocation); - result.entryLocation = wrapConstraint(location); - } - - return result; -} - -function convertFromTunnelTypeConstraint( - constraint: grpcTypes.TunnelType | undefined, -): Constraint<TunnelProtocol> { - switch (constraint) { - case grpcTypes.TunnelType.WIREGUARD: { - return { only: 'wireguard' }; - } - case grpcTypes.TunnelType.OPENVPN: { - return { only: 'openvpn' }; - } - default: { - return 'any'; - } - } -} - -function convertFromConstraint<T>(value: T | undefined): Constraint<T> { - if (value) { - return { only: value }; - } else { - return 'any'; - } -} - -function convertToRelayConstraints( - constraints: IRelaySettingsNormal<IOpenVpnConstraints, IWireguardConstraints>, -): grpcTypes.NormalRelaySettings { - const relayConstraints = new grpcTypes.NormalRelaySettings(); - - if (constraints.tunnelProtocol !== 'any') { - relayConstraints.setTunnelType(convertToTunnelType(constraints.tunnelProtocol.only)); - } - relayConstraints.setLocation(convertToLocation(liftConstraint(constraints.location))); - relayConstraints.setWireguardConstraints( - convertToWireguardConstraints(constraints.wireguardConstraints), - ); - relayConstraints.setOpenvpnConstraints( - convertToOpenVpnConstraints(constraints.openvpnConstraints), - ); - relayConstraints.setProvidersList(constraints.providers); - relayConstraints.setOwnership(convertToOwnership(constraints.ownership)); - - return relayConstraints; -} - -function convertToNormalBridgeSettings( - constraints: IBridgeConstraints, -): grpcTypes.BridgeSettings.BridgeConstraints { - const normalBridgeSettings = new grpcTypes.BridgeSettings.BridgeConstraints(); - normalBridgeSettings.setLocation(convertToLocation(liftConstraint(constraints.location))); - normalBridgeSettings.setProvidersList(constraints.providers); - - return normalBridgeSettings; -} - -function convertToLocation( - constraint: RelayLocation | undefined, -): grpcTypes.LocationConstraint | undefined { - const locationConstraint = new grpcTypes.LocationConstraint(); - if (constraint && 'customList' in constraint && constraint.customList) { - locationConstraint.setCustomList(constraint.customList); - } else { - const location = constraint && convertToGeographicConstraint(constraint); - locationConstraint.setLocation(location); - } - - return locationConstraint; -} - -function convertToGeographicConstraint( - location: RelayLocation, -): grpcTypes.GeographicLocationConstraint { - const relayLocation = new grpcTypes.GeographicLocationConstraint(); - if ('hostname' in location) { - relayLocation.setCountry(location.country); - relayLocation.setCity(location.city); - relayLocation.setHostname(location.hostname); - } else if ('city' in location) { - relayLocation.setCountry(location.country); - relayLocation.setCity(location.city); - } else if ('country' in location) { - relayLocation.setCountry(location.country); - } - - return relayLocation; -} - -function convertToTunnelType(tunnelProtocol: TunnelProtocol): grpcTypes.TunnelType { - switch (tunnelProtocol) { - case 'wireguard': - return grpcTypes.TunnelType.WIREGUARD; - case 'openvpn': - return grpcTypes.TunnelType.OPENVPN; - } -} - -function convertToOpenVpnConstraints( - constraints: Partial<IOpenVpnConstraints> | undefined, -): grpcTypes.OpenvpnConstraints | undefined { - const openvpnConstraints = new grpcTypes.OpenvpnConstraints(); - if (constraints) { - const protocol = liftConstraint(constraints.protocol); - if (protocol) { - const portConstraints = new grpcTypes.TransportPort(); - const port = liftConstraint(constraints.port); - if (port) { - portConstraints.setPort(port); - } - portConstraints.setProtocol(convertToTransportProtocol(protocol)); - openvpnConstraints.setPort(portConstraints); - } - return openvpnConstraints; - } - - return undefined; -} - -function convertToWireguardConstraints( - constraint: Partial<IWireguardConstraints> | undefined, -): grpcTypes.WireguardConstraints | undefined { - if (constraint) { - const wireguardConstraints = new grpcTypes.WireguardConstraints(); - - const port = liftConstraint(constraint.port); - if (port) { - wireguardConstraints.setPort(port); - } - - const ipVersion = liftConstraint(constraint.ipVersion); - if (ipVersion) { - const ipVersionProtocol = - ipVersion === 'ipv4' ? grpcTypes.IpVersion.V4 : grpcTypes.IpVersion.V6; - wireguardConstraints.setIpVersion(ipVersionProtocol); - } - - if (constraint.useMultihop) { - wireguardConstraints.setUseMultihop(constraint.useMultihop); - } - - const entryLocation = liftConstraint(constraint.entryLocation); - if (entryLocation) { - const entryLocationConstraint = convertToLocation(entryLocation); - wireguardConstraints.setEntryLocation(entryLocationConstraint); - } - - return wireguardConstraints; - } - return undefined; -} - -function convertToTransportProtocol(protocol: RelayProtocol): grpcTypes.TransportProtocol { - switch (protocol) { - case 'udp': - return grpcTypes.TransportProtocol.UDP; - case 'tcp': - return grpcTypes.TransportProtocol.TCP; - } -} - -function convertFromDeviceEvent(deviceEvent: grpcTypes.DeviceEvent): DeviceEvent { - const deviceState = convertFromDeviceState(deviceEvent.getNewState()!); - switch (deviceEvent.getCause()) { - case grpcTypes.DeviceEvent.Cause.LOGGED_IN: - return { type: 'logged in', deviceState: deviceState as LoggedInDeviceState }; - case grpcTypes.DeviceEvent.Cause.LOGGED_OUT: - return { type: 'logged out', deviceState: deviceState as LoggedOutDeviceState }; - case grpcTypes.DeviceEvent.Cause.REVOKED: - return { type: 'revoked', deviceState: deviceState as LoggedOutDeviceState }; - case grpcTypes.DeviceEvent.Cause.UPDATED: - return { type: 'updated', deviceState: deviceState as LoggedInDeviceState }; - case grpcTypes.DeviceEvent.Cause.ROTATED_KEY: - return { type: 'rotated_key', deviceState: deviceState as LoggedInDeviceState }; - } -} - -function convertFromDeviceState(deviceState: grpcTypes.DeviceState): DeviceState { - switch (deviceState.getState()) { - case grpcTypes.DeviceState.State.LOGGED_IN: { - const accountAndDevice = deviceState.getDevice()!; - const device = accountAndDevice.getDevice(); - return { - type: 'logged in', - accountAndDevice: { - accountToken: accountAndDevice.getAccountToken(), - device: device && convertFromDevice(device), - }, - }; - } - case grpcTypes.DeviceState.State.LOGGED_OUT: - return { type: 'logged out' }; - case grpcTypes.DeviceState.State.REVOKED: - return { type: 'revoked' }; - } -} - -function convertFromDeviceRemoval(deviceRemoval: grpcTypes.RemoveDeviceEvent): Array<IDevice> { - return deviceRemoval.getNewDeviceListList().map(convertFromDevice); -} - -function convertFromDevice(device: grpcTypes.Device): IDevice { - const created = ensureExists(device.getCreated(), "no 'created' field for device").toDate(); - const asObject = device.toObject(); - - return { - ...asObject, - created: created, - }; -} - -function convertFromCustomListSettings( - customListSettings?: grpcTypes.CustomListSettings, -): CustomLists { - return customListSettings ? convertFromCustomLists(customListSettings.getCustomListsList()) : []; -} - -function convertFromCustomLists(customLists: Array<grpcTypes.CustomList>): CustomLists { - return customLists.map((list) => ({ - id: list.getId(), - name: list.getName(), - locations: list - .getLocationsList() - .map((location) => - convertFromGeographicConstraint(location.toObject()), - ) as Array<RelayLocationGeographical>, - })); -} - -function convertToCustomList(customList: ICustomList): grpcTypes.CustomList { - const grpcCustomList = new grpcTypes.CustomList(); - grpcCustomList.setId(customList.id); - grpcCustomList.setName(customList.name); - - const locations = customList.locations.map(convertToGeographicConstraint); - grpcCustomList.setLocationsList(locations); - - return grpcCustomList; -} - -function convertToApiAccessMethodSetting( - method: AccessMethodSetting, -): grpcTypes.AccessMethodSetting { - const updatedMethod = new grpcTypes.AccessMethodSetting(); - const uuid = new grpcTypes.UUID(); - uuid.setValue(method.id); - updatedMethod.setId(uuid); - return fillApiAccessMethodSetting(updatedMethod, method); -} - -function convertToNewApiAccessMethodSetting( - method: NewAccessMethodSetting, -): grpcTypes.NewAccessMethodSetting { - const newMethod = new grpcTypes.NewAccessMethodSetting(); - return fillApiAccessMethodSetting(newMethod, method); -} - -function fillApiAccessMethodSetting<T extends grpcTypes.NewAccessMethodSetting>( - newMethod: T, - method: NewAccessMethodSetting, -): T { - newMethod.setName(method.name); - newMethod.setEnabled(method.enabled); - - const accessMethod = new grpcTypes.AccessMethod(); - switch (method.type) { - case 'direct': { - const direct = new grpcTypes.AccessMethod.Direct(); - accessMethod.setDirect(direct); - break; - } - case 'bridges': { - const bridges = new grpcTypes.AccessMethod.Bridges(); - accessMethod.setBridges(bridges); - break; - } - default: - accessMethod.setCustom(convertToCustomProxy(method)); - } - - newMethod.setAccessMethod(accessMethod); - return newMethod; -} - -function convertToCustomProxy(proxy: CustomProxy): grpcTypes.CustomProxy { - const customProxy = new grpcTypes.CustomProxy(); - - switch (proxy.type) { - case 'socks5-local': { - const socks5Local = new grpcTypes.Socks5Local(); - socks5Local.setRemoteIp(proxy.remoteIp); - socks5Local.setRemotePort(proxy.remotePort); - socks5Local.setRemoteTransportProtocol( - convertToTransportProtocol(proxy.remoteTransportProtocol), - ); - socks5Local.setLocalPort(proxy.localPort); - customProxy.setSocks5local(socks5Local); - break; - } - case 'socks5-remote': { - const socks5Remote = new grpcTypes.Socks5Remote(); - socks5Remote.setIp(proxy.ip); - socks5Remote.setPort(proxy.port); - if (proxy.authentication !== undefined) { - socks5Remote.setAuth(convertToSocksAuth(proxy.authentication)); - } - customProxy.setSocks5remote(socks5Remote); - break; - } - case 'shadowsocks': { - const shadowsocks = new grpcTypes.Shadowsocks(); - shadowsocks.setIp(proxy.ip); - shadowsocks.setPort(proxy.port); - shadowsocks.setPassword(proxy.password); - shadowsocks.setCipher(proxy.cipher); - customProxy.setShadowsocks(shadowsocks); - break; - } - } - - return customProxy; -} - -function convertToSocksAuth(authentication: SocksAuth): grpcTypes.SocksAuth { - const auth = new grpcTypes.SocksAuth(); - auth.setUsername(authentication.username); - auth.setPassword(authentication.password); - return auth; -} - -function convertFromApiAccessMethodSettings( - accessMethods: grpcTypes.ApiAccessMethodSettings, -): ApiAccessMethodSettings { - const direct = convertFromApiAccessMethodSetting( - ensureExists(accessMethods.getDirect(), "no 'Direct' access method was found"), - ) as AccessMethodSetting<DirectMethod>; - const bridges = convertFromApiAccessMethodSetting( - ensureExists(accessMethods.getMullvadBridges(), "no 'Mullvad Bridges' access method was found"), - ) as AccessMethodSetting<BridgesMethod>; - const custom = accessMethods - .getCustomList() - .filter((setting) => setting.hasId() && setting.hasAccessMethod()) - .map(convertFromApiAccessMethodSetting) - // The last filter helps TypeScript infer the custom proxy type. - .filter(isCustomProxy); - - return { - direct, - mullvadBridges: bridges, - custom, - }; -} - -function isCustomProxy( - accessMethod: AccessMethodSetting, -): accessMethod is AccessMethodSetting<CustomProxy> { - return accessMethod.type !== 'direct' && accessMethod.type !== 'bridges'; -} - -function convertFromApiAccessMethodSetting( - setting: grpcTypes.AccessMethodSetting, -): AccessMethodSetting { - const id = setting.getId()!; - const accessMethod = setting.getAccessMethod()!; - - return { - id: id.getValue(), - name: setting.getName(), - enabled: setting.getEnabled(), - ...convertFromAccessMethod(accessMethod), - }; -} - -function convertFromAccessMethod(method: grpcTypes.AccessMethod): AccessMethod { - switch (method.getAccessMethodCase()) { - case grpcTypes.AccessMethod.AccessMethodCase.DIRECT: - return { type: 'direct' }; - case grpcTypes.AccessMethod.AccessMethodCase.BRIDGES: - return { type: 'bridges' }; - case grpcTypes.AccessMethod.AccessMethodCase.CUSTOM: { - return convertFromCustomProxy(method.getCustom()!); - } - case grpcTypes.AccessMethod.AccessMethodCase.ACCESS_METHOD_NOT_SET: - throw new Error('Access method not set, which should always be set'); - } -} - -function convertFromCustomProxy(proxy: grpcTypes.CustomProxy): CustomProxy { - switch (proxy.getProxyMethodCase()) { - case grpcTypes.CustomProxy.ProxyMethodCase.SOCKS5LOCAL: { - const socks5Local = proxy.getSocks5local()!; - return { - type: 'socks5-local', - remoteIp: socks5Local.getRemoteIp(), - remotePort: socks5Local.getRemotePort(), - remoteTransportProtocol: convertFromTransportProtocol( - socks5Local.getRemoteTransportProtocol(), - ), - localPort: socks5Local.getLocalPort(), - }; - } - case grpcTypes.CustomProxy.ProxyMethodCase.SOCKS5REMOTE: { - const socks5Remote = proxy.getSocks5remote()!; - const auth = socks5Remote.getAuth(); - return { - type: 'socks5-remote', - ip: socks5Remote.getIp(), - port: socks5Remote.getPort(), - authentication: auth === undefined ? undefined : convertFromSocksAuth(auth), - }; - } - case grpcTypes.CustomProxy.ProxyMethodCase.SHADOWSOCKS: { - const shadowsocks = proxy.getShadowsocks()!; - return { - type: 'shadowsocks', - ip: shadowsocks.getIp(), - port: shadowsocks.getPort(), - password: shadowsocks.getPassword(), - cipher: shadowsocks.getCipher(), - }; - } - case grpcTypes.CustomProxy.ProxyMethodCase.PROXY_METHOD_NOT_SET: - throw new Error('Custom method not set, which should always be set'); - } -} - -function convertFromSocksAuth(auth: grpcTypes.SocksAuth): SocksAuth { - return { - username: auth.getUsername(), - password: auth.getPassword(), - }; -} - -function ensureExists<T>(value: T | undefined, errorMessage: string): T { - if (value) { - return value; - } - throw new ResponseParseError(errorMessage); } diff --git a/gui/src/main/grpc-client.ts b/gui/src/main/grpc-client.ts new file mode 100644 index 0000000000..fa04cdf855 --- /dev/null +++ b/gui/src/main/grpc-client.ts @@ -0,0 +1,246 @@ +import * as grpc from '@grpc/grpc-js'; +import { Empty } from 'google-protobuf/google/protobuf/empty_pb.js'; +import { + BoolValue, + StringValue, + UInt32Value, +} from 'google-protobuf/google/protobuf/wrappers_pb.js'; +import { promisify } from 'util'; + +import log from '../shared/logging'; +import { ManagementServiceClient } from './management_interface/management_interface_grpc_pb'; + +const NETWORK_CALL_TIMEOUT = 10000; +const CHANNEL_STATE_TIMEOUT = 1000 * 60 * 60; + +type CallFunctionArgument<T, R> = + | ((arg: T, callback: (error: Error | null, result: R) => void) => void) + | undefined; + +export const noConnectionError = new Error('No connection established to daemon'); + +export class ConnectionObserver { + constructor( + private openHandler: () => void, + private closeHandler: (wasConnected: boolean, error?: Error) => void, + ) {} + + // Only meant to be called by DaemonRpc + // @internal + public onOpen = () => { + this.openHandler(); + }; + + // Only meant to be called by DaemonRpc + // @internal + public onClose = (wasConnected: boolean, error?: Error) => { + this.closeHandler(wasConnected, error); + }; +} + +export class GrpcClient { + protected client: ManagementServiceClient; + private isConnectedValue = false; + private isClosed = false; + private reconnectionTimeout?: NodeJS.Timeout; + + constructor( + private rpcPath: string, + private connectionObserver?: ConnectionObserver, + ) { + this.client = new ManagementServiceClient( + rpcPath, + grpc.credentials.createInsecure(), + this.channelOptions(), + ); + } + + public get isConnected() { + return this.isConnectedValue; + } + + public reopen(connectionObserver?: ConnectionObserver) { + if (this.isClosed) { + this.isClosed = false; + this.client = new ManagementServiceClient( + this.rpcPath, + grpc.credentials.createInsecure(), + this.channelOptions(), + ); + + this.connectionObserver = connectionObserver; + } + } + + public connect(): Promise<void> { + return new Promise((resolve, reject) => { + const usedClient = this.client; + this.client.waitForReady(this.deadlineFromNow(), (error) => { + if (this.client !== usedClient) { + reject(new Error('Stale connection attempt')); + return; + } + + if (error) { + this.onClose(error); + this.ensureConnectivity(); + reject(error); + } else { + this.reconnectionTimeout = undefined; + this.isConnectedValue = true; + this.connectionObserver?.onOpen(); + this.setChannelCallback(); + resolve(); + } + }); + }); + } + + public disconnect() { + this.isConnectedValue = false; + + this.isClosed = true; + this.client.close(); + this.connectionObserver = undefined; + if (this.reconnectionTimeout) { + clearTimeout(this.reconnectionTimeout); + } + } + + protected callEmpty<R = Empty>(fn: CallFunctionArgument<Empty, R>): Promise<R> { + return this.call<Empty, R>(fn, new Empty()); + } + + protected callString<R = Empty>( + fn: CallFunctionArgument<StringValue, R>, + value?: string, + ): Promise<R> { + const googleString = new StringValue(); + + if (value !== undefined) { + googleString.setValue(value); + } + + return this.call<StringValue, R>(fn, googleString); + } + + protected callBool<R>(fn: CallFunctionArgument<BoolValue, R>, value?: boolean): Promise<R> { + const googleBool = new BoolValue(); + + if (value !== undefined) { + googleBool.setValue(value); + } + + return this.call<BoolValue, R>(fn, googleBool); + } + + protected callNumber<R>(fn: CallFunctionArgument<UInt32Value, R>, value?: number): Promise<R> { + const googleNumber = new UInt32Value(); + + if (value !== undefined) { + googleNumber.setValue(value); + } + + return this.call<UInt32Value, R>(fn, googleNumber); + } + + protected call<T, R>(fn: CallFunctionArgument<T, R>, arg: T): Promise<R> { + if (fn && this.isConnected) { + return promisify<T, R>(fn.bind(this.client))(arg); + } else { + throw noConnectionError; + } + } + + private deadlineFromNow() { + return Date.now() + NETWORK_CALL_TIMEOUT; + } + + private channelStateTimeout(): number { + return Date.now() + CHANNEL_STATE_TIMEOUT; + } + + private onClose(error?: Error) { + const wasConnected = this.isConnectedValue; + this.isConnectedValue = false; + + this.connectionObserver?.onClose(wasConnected, error); + } + + private channelOptions(): grpc.ClientOptions { + return { + 'grpc.max_reconnect_backoff_ms': 3000, + 'grpc.initial_reconnect_backoff_ms': 3000, + 'grpc.keepalive_time_ms': Math.pow(2, 30), + 'grpc.keepalive_timeout_ms': Math.pow(2, 30), + 'grpc.client_idle_timeout_ms': Math.pow(2, 30), + }; + } + + private connectivityChangeCallback(timeoutErr?: Error) { + const channel = this.client.getChannel(); + const currentState = channel?.getConnectivityState(true); + log.verbose(`GRPC Channel connectivity state changed to ${currentState}`); + if (channel) { + if (timeoutErr) { + this.setChannelCallback(currentState); + return; + } + const wasConnected = this.isConnected; + if (this.channelDisconnected(currentState)) { + this.onClose(); + // Try and reconnect in case + void this.connect().catch((error) => { + log.error(`Failed to reconnect - ${error}`); + }); + this.setChannelCallback(currentState); + } else if (!wasConnected && currentState === grpc.connectivityState.READY) { + this.isConnectedValue = true; + this.connectionObserver?.onOpen(); + this.setChannelCallback(currentState); + } + } + } + + private channelDisconnected(state: grpc.connectivityState): boolean { + return ( + (state === grpc.connectivityState.SHUTDOWN || + state === grpc.connectivityState.TRANSIENT_FAILURE || + state === grpc.connectivityState.IDLE) && + this.isConnected + ); + } + + private setChannelCallback(currentState?: grpc.connectivityState) { + const channel = this.client.getChannel(); + if (currentState === undefined && channel) { + currentState = channel?.getConnectivityState(false); + } + if (currentState) { + channel.watchConnectivityState(currentState, this.channelStateTimeout(), (error) => + this.connectivityChangeCallback(error), + ); + } + } + + // Since grpc.Channel.watchConnectivityState() isn't always running as intended, whenever the + // client fails to connect at first, `ensureConnectivity()` should be called so that it tries to + // check the connectivity state and nudge the client into connecting. + // `grpc.Channel.getConnectivityState(true)` should make it attempt to connect. + private ensureConnectivity() { + if (this.reconnectionTimeout) { + clearTimeout(this.reconnectionTimeout); + } + this.reconnectionTimeout = setTimeout(() => { + const lastState = this.client.getChannel().getConnectivityState(true); + if (this.channelDisconnected(lastState)) { + this.onClose(); + } + if (!this.isConnected) { + void this.connect().catch((error) => { + log.error(`Failed to reconnect - ${error}`); + }); + } + }, 3000); + } +} diff --git a/gui/src/main/grpc-type-convertions.ts b/gui/src/main/grpc-type-convertions.ts new file mode 100644 index 0000000000..caa4e8f160 --- /dev/null +++ b/gui/src/main/grpc-type-convertions.ts @@ -0,0 +1,1264 @@ +import { + AccessMethod, + AccessMethodSetting, + AfterDisconnect, + ApiAccessMethodSettings, + AuthFailedError, + BridgeSettings, + BridgesMethod, + BridgeState, + BridgeType, + ConnectionConfig, + Constraint, + CustomLists, + CustomProxy, + DaemonEvent, + DeviceEvent, + DeviceState, + DirectMethod, + EndpointObfuscationType, + ErrorStateCause, + ErrorStateDetails, + FeatureIndicator, + FirewallPolicyError, + FirewallPolicyErrorType, + IBridgeConstraints, + ICustomList, + IDevice, + IObfuscationEndpoint, + IOpenVpnConstraints, + IProxyEndpoint, + IRelayListCity, + IRelayListCountry, + IRelayListHostname, + IRelayListWithEndpointData, + IRelaySettingsNormal, + ISettings, + ITunnelOptions, + ITunnelStateRelayInfo, + IWireguardConstraints, + IWireguardEndpointData, + LoggedInDeviceState, + LoggedOutDeviceState, + NewAccessMethodSetting, + ObfuscationSettings, + ObfuscationType, + Ownership, + ProxyType, + RelayEndpointType, + RelayLocation, + RelayLocationGeographical, + RelayProtocol, + RelaySettings, + SocksAuth, + TunnelParameterError, + TunnelProtocol, + TunnelState, + TunnelType, + wrapConstraint, +} from '../shared/daemon-rpc-types'; +import * as grpcTypes from './management_interface/management_interface_pb'; + +const invalidErrorStateCause = new Error( + 'VPN_PERMISSION_DENIED is not a valid error state cause on desktop', +); + +export class ResponseParseError extends Error { + constructor(message: string) { + super(message); + } +} + +function unwrapConstraint<T>(constraint: Constraint<T> | undefined): T | undefined { + if (constraint !== undefined && constraint !== 'any') { + return constraint.only; + } + return undefined; +} + +export function convertFromRelayList(relayList: grpcTypes.RelayList): IRelayListWithEndpointData { + return { + relayList: { + countries: relayList + .getCountriesList() + .map((country: grpcTypes.RelayListCountry) => convertFromRelayListCountry(country)), + }, + wireguardEndpointData: convertWireguardEndpointData(relayList.getWireguard()!), + }; +} + +function convertWireguardEndpointData( + data: grpcTypes.WireguardEndpointData, +): IWireguardEndpointData { + return { + portRanges: data.getPortRangesList().map((range) => [range.getFirst(), range.getLast()]), + udp2tcpPorts: data.getUdp2tcpPortsList(), + }; +} + +function convertFromRelayListCountry(country: grpcTypes.RelayListCountry): IRelayListCountry { + const countryObject = country.toObject(); + return { + ...countryObject, + cities: country.getCitiesList().map(convertFromRelayListCity), + }; +} + +function convertFromRelayListCity(city: grpcTypes.RelayListCity): IRelayListCity { + const cityObject = city.toObject(); + return { + ...cityObject, + relays: city.getRelaysList().map(convertFromRelayListRelay), + }; +} + +function convertFromRelayListRelay(relay: grpcTypes.Relay): IRelayListHostname { + const relayObject = relay.toObject(); + + let daita = false; + if (relayObject.endpointType === grpcTypes.Relay.RelayType.WIREGUARD) { + const endpointDataU8 = relay.getEndpointData()?.getValue_asU8(); + if (endpointDataU8) { + daita = grpcTypes.WireguardRelayEndpointData.deserializeBinary(endpointDataU8).getDaita(); + } + } + + return { + ...relayObject, + endpointType: convertFromRelayType(relayObject.endpointType), + daita, + }; +} + +function convertFromRelayType(relayType: grpcTypes.Relay.RelayType): RelayEndpointType { + const protocolMap: Record<grpcTypes.Relay.RelayType, RelayEndpointType> = { + [grpcTypes.Relay.RelayType.OPENVPN]: 'openvpn', + [grpcTypes.Relay.RelayType.BRIDGE]: 'bridge', + [grpcTypes.Relay.RelayType.WIREGUARD]: 'wireguard', + }; + return protocolMap[relayType]; +} + +function convertFromWireguardKey(publicKey: Uint8Array | string): string { + if (typeof publicKey === 'string') { + return publicKey; + } + return Buffer.from(publicKey).toString('base64'); +} + +function convertFromTransportProtocol(protocol: grpcTypes.TransportProtocol): RelayProtocol { + const protocolMap: Record<grpcTypes.TransportProtocol, RelayProtocol> = { + [grpcTypes.TransportProtocol.TCP]: 'tcp', + [grpcTypes.TransportProtocol.UDP]: 'udp', + }; + return protocolMap[protocol]; +} + +export function convertFromTunnelState( + tunnelState: grpcTypes.TunnelState, +): TunnelState | undefined { + const tunnelStateObject = tunnelState.toObject(); + switch (tunnelState.getStateCase()) { + case grpcTypes.TunnelState.StateCase.STATE_NOT_SET: + return undefined; + case grpcTypes.TunnelState.StateCase.DISCONNECTED: + return { + state: 'disconnected', + location: tunnelStateObject.disconnected!.disconnectedLocation, + }; + case grpcTypes.TunnelState.StateCase.DISCONNECTING: { + const detailsMap: Record<grpcTypes.AfterDisconnect, AfterDisconnect> = { + [grpcTypes.AfterDisconnect.NOTHING]: 'nothing', + [grpcTypes.AfterDisconnect.BLOCK]: 'block', + [grpcTypes.AfterDisconnect.RECONNECT]: 'reconnect', + }; + return ( + tunnelStateObject.disconnecting && { + state: 'disconnecting', + details: detailsMap[tunnelStateObject.disconnecting.afterDisconnect], + } + ); + } + case grpcTypes.TunnelState.StateCase.ERROR: + return ( + tunnelStateObject.error?.errorState && { + state: 'error', + details: convertFromTunnelStateError(tunnelStateObject.error.errorState), + } + ); + case grpcTypes.TunnelState.StateCase.CONNECTING: + return { + state: 'connecting', + details: + tunnelStateObject.connecting?.relayInfo && + convertFromTunnelStateRelayInfo(tunnelStateObject.connecting.relayInfo), + featureIndicators: convertFromFeatureIndicators( + tunnelStateObject.connecting?.featureIndicators?.activeFeaturesList, + ), + }; + case grpcTypes.TunnelState.StateCase.CONNECTED: { + const relayInfo = + tunnelStateObject.connected?.relayInfo && + convertFromTunnelStateRelayInfo(tunnelStateObject.connected.relayInfo); + return ( + relayInfo && { + state: 'connected', + details: relayInfo, + featureIndicators: convertFromFeatureIndicators( + tunnelStateObject.connected?.featureIndicators?.activeFeaturesList, + ), + } + ); + } + } +} + +function convertFromTunnelStateError(state: grpcTypes.ErrorState.AsObject): ErrorStateDetails { + const baseError = { + blockingError: state.blockingError && convertFromBlockingError(state.blockingError), + }; + + switch (state.cause) { + case grpcTypes.ErrorState.Cause.AUTH_FAILED: + return { + ...baseError, + cause: ErrorStateCause.authFailed, + authFailedError: convertFromAuthFailedError(state.authFailedError), + }; + case grpcTypes.ErrorState.Cause.TUNNEL_PARAMETER_ERROR: + return { + ...baseError, + cause: ErrorStateCause.tunnelParameterError, + parameterError: convertFromParameterError(state.parameterError), + }; + case grpcTypes.ErrorState.Cause.SET_FIREWALL_POLICY_ERROR: + return { + ...baseError, + cause: ErrorStateCause.setFirewallPolicyError, + policyError: convertFromBlockingError(state.policyError!), + }; + + case grpcTypes.ErrorState.Cause.IS_OFFLINE: + return { + ...baseError, + cause: ErrorStateCause.isOffline, + }; + case grpcTypes.ErrorState.Cause.SET_DNS_ERROR: + return { + ...baseError, + cause: ErrorStateCause.setDnsError, + }; + case grpcTypes.ErrorState.Cause.IPV6_UNAVAILABLE: + return { + ...baseError, + cause: ErrorStateCause.ipv6Unavailable, + }; + case grpcTypes.ErrorState.Cause.START_TUNNEL_ERROR: + return { + ...baseError, + cause: ErrorStateCause.startTunnelError, + }; + case grpcTypes.ErrorState.Cause.CREATE_TUNNEL_DEVICE: + return { + ...baseError, + cause: ErrorStateCause.createTunnelDeviceError, + osError: state.createTunnelError, + }; + case grpcTypes.ErrorState.Cause.SPLIT_TUNNEL_ERROR: + return { + ...baseError, + cause: ErrorStateCause.splitTunnelError, + }; + case grpcTypes.ErrorState.Cause.NEED_FULL_DISK_PERMISSIONS: + return { + ...baseError, + cause: ErrorStateCause.needFullDiskPermissions, + }; + case grpcTypes.ErrorState.Cause.VPN_PERMISSION_DENIED: + // VPN_PERMISSION_DENIED is only ever created on Android + throw invalidErrorStateCause; + } +} + +function convertFromBlockingError( + error: grpcTypes.ErrorState.FirewallPolicyError.AsObject, +): FirewallPolicyError { + switch (error.type) { + case grpcTypes.ErrorState.FirewallPolicyError.ErrorType.GENERIC: + return { type: FirewallPolicyErrorType.generic }; + case grpcTypes.ErrorState.FirewallPolicyError.ErrorType.LOCKED: { + const pid = error.lockPid; + const name = error.lockName!; + return { type: FirewallPolicyErrorType.locked, pid, name }; + } + } +} + +function convertFromAuthFailedError(error: grpcTypes.ErrorState.AuthFailedError): AuthFailedError { + switch (error) { + case grpcTypes.ErrorState.AuthFailedError.UNKNOWN: + return AuthFailedError.unknown; + case grpcTypes.ErrorState.AuthFailedError.INVALID_ACCOUNT: + return AuthFailedError.invalidAccount; + case grpcTypes.ErrorState.AuthFailedError.EXPIRED_ACCOUNT: + return AuthFailedError.expiredAccount; + case grpcTypes.ErrorState.AuthFailedError.TOO_MANY_CONNECTIONS: + return AuthFailedError.tooManyConnections; + } +} + +function convertFromParameterError( + error: grpcTypes.ErrorState.GenerationError, +): TunnelParameterError { + switch (error) { + case grpcTypes.ErrorState.GenerationError.NO_MATCHING_RELAY: + return TunnelParameterError.noMatchingRelay; + case grpcTypes.ErrorState.GenerationError.NO_MATCHING_BRIDGE_RELAY: + return TunnelParameterError.noMatchingBridgeRelay; + case grpcTypes.ErrorState.GenerationError.NO_WIREGUARD_KEY: + return TunnelParameterError.noWireguardKey; + case grpcTypes.ErrorState.GenerationError.CUSTOM_TUNNEL_HOST_RESOLUTION_ERROR: + return TunnelParameterError.customTunnelHostResolutionError; + } +} + +function convertFromTunnelStateRelayInfo( + state: grpcTypes.TunnelStateRelayInfo.AsObject, +): ITunnelStateRelayInfo | undefined { + if (state.tunnelEndpoint) { + return { + ...state, + endpoint: { + ...state.tunnelEndpoint, + tunnelType: convertFromTunnelType(state.tunnelEndpoint.tunnelType), + protocol: convertFromTransportProtocol(state.tunnelEndpoint.protocol), + proxy: state.tunnelEndpoint.proxy && convertFromProxyEndpoint(state.tunnelEndpoint.proxy), + obfuscationEndpoint: + state.tunnelEndpoint.obfuscation && + convertFromObfuscationEndpoint(state.tunnelEndpoint.obfuscation), + entryEndpoint: + state.tunnelEndpoint.entryEndpoint && + convertFromEntryEndpoint(state.tunnelEndpoint.entryEndpoint), + }, + }; + } + return undefined; +} + +function convertFromFeatureIndicators( + featureIndicators?: Array<grpcTypes.FeatureIndicator>, +): Array<FeatureIndicator> | undefined { + return featureIndicators?.map(convertFromFeatureIndicator); +} + +function convertFromFeatureIndicator( + featureIndicator: grpcTypes.FeatureIndicator, +): FeatureIndicator { + switch (featureIndicator) { + case grpcTypes.FeatureIndicator.QUANTUM_RESISTANCE: + return FeatureIndicator.quantumResistance; + case grpcTypes.FeatureIndicator.MULTIHOP: + return FeatureIndicator.multihop; + case grpcTypes.FeatureIndicator.BRIDGE_MODE: + return FeatureIndicator.bridgeMode; + case grpcTypes.FeatureIndicator.SPLIT_TUNNELING: + return FeatureIndicator.splitTunneling; + case grpcTypes.FeatureIndicator.LOCKDOWN_MODE: + return FeatureIndicator.lockdownMode; + case grpcTypes.FeatureIndicator.UDP_2_TCP: + return FeatureIndicator.udp2tcp; + case grpcTypes.FeatureIndicator.LAN_SHARING: + return FeatureIndicator.lanSharing; + case grpcTypes.FeatureIndicator.DNS_CONTENT_BLOCKERS: + return FeatureIndicator.dnsContentBlockers; + case grpcTypes.FeatureIndicator.CUSTOM_DNS: + return FeatureIndicator.customDns; + case grpcTypes.FeatureIndicator.SERVER_IP_OVERRIDE: + return FeatureIndicator.serverIpOverride; + case grpcTypes.FeatureIndicator.CUSTOM_MTU: + return FeatureIndicator.customMtu; + case grpcTypes.FeatureIndicator.CUSTOM_MSS_FIX: + return FeatureIndicator.customMssFix; + case grpcTypes.FeatureIndicator.DAITA: + return FeatureIndicator.daita; + case grpcTypes.FeatureIndicator.DAITA_SMART_ROUTING: + return FeatureIndicator.daitaSmartRouting; + case grpcTypes.FeatureIndicator.SHADOWSOCKS: + return FeatureIndicator.shadowsocks; + } +} + +function convertFromTunnelType(tunnelType: grpcTypes.TunnelType): TunnelType { + const tunnelTypeMap: Record<grpcTypes.TunnelType, TunnelType> = { + [grpcTypes.TunnelType.WIREGUARD]: 'wireguard', + [grpcTypes.TunnelType.OPENVPN]: 'openvpn', + }; + + return tunnelTypeMap[tunnelType]; +} + +function convertFromProxyEndpoint(proxyEndpoint: grpcTypes.ProxyEndpoint.AsObject): IProxyEndpoint { + const proxyTypeMap: Record<grpcTypes.ProxyEndpoint.ProxyType, ProxyType> = { + [grpcTypes.ProxyEndpoint.ProxyType.CUSTOM]: 'custom', + [grpcTypes.ProxyEndpoint.ProxyType.SHADOWSOCKS]: 'shadowsocks', + }; + + return { + ...proxyEndpoint, + protocol: convertFromTransportProtocol(proxyEndpoint.protocol), + proxyType: proxyTypeMap[proxyEndpoint.proxyType], + }; +} + +function convertFromObfuscationEndpoint( + obfuscationEndpoint: grpcTypes.ObfuscationEndpoint.AsObject, +): IObfuscationEndpoint { + let obfuscationType: EndpointObfuscationType; + switch (obfuscationEndpoint.obfuscationType) { + case grpcTypes.ObfuscationEndpoint.ObfuscationType.UDP2TCP: + obfuscationType = 'udp2tcp'; + break; + case grpcTypes.ObfuscationEndpoint.ObfuscationType.SHADOWSOCKS: + obfuscationType = 'shadowsocks'; + break; + default: + throw new Error('unsupported obfuscation protocol'); + } + + return { + ...obfuscationEndpoint, + protocol: convertFromTransportProtocol(obfuscationEndpoint.protocol), + obfuscationType: obfuscationType, + }; +} + +function convertFromEntryEndpoint(entryEndpoint: grpcTypes.Endpoint.AsObject) { + return { + address: entryEndpoint.address, + transportProtocol: convertFromTransportProtocol(entryEndpoint.protocol), + }; +} + +export function convertFromSettings(settings: grpcTypes.Settings): ISettings | undefined { + const settingsObject = settings.toObject(); + const bridgeState = convertFromBridgeState(settingsObject.bridgeState!.state!); + const relaySettings = convertFromRelaySettings(settings.getRelaySettings())!; + const bridgeSettings = convertFromBridgeSettings(settings.getBridgeSettings()!); + const tunnelOptions = convertFromTunnelOptions(settingsObject.tunnelOptions!); + const splitTunnel = settingsObject.splitTunnel ?? { enableExclusions: false, appsList: [] }; + const obfuscationSettings = convertFromObfuscationSettings(settingsObject.obfuscationSettings); + const customLists = convertFromCustomListSettings(settings.getCustomLists()); + const apiAccessMethods = convertFromApiAccessMethodSettings(settings.getApiAccessMethods()!); + const relayOverrides = settingsObject.relayOverridesList; + return { + ...settings.toObject(), + bridgeState, + relaySettings, + bridgeSettings, + tunnelOptions, + splitTunnel, + obfuscationSettings, + customLists, + apiAccessMethods, + relayOverrides, + }; +} + +function convertFromBridgeState(bridgeState: grpcTypes.BridgeState.State): BridgeState { + const bridgeStateMap: Record<grpcTypes.BridgeState.State, BridgeState> = { + [grpcTypes.BridgeState.State.AUTO]: 'auto', + [grpcTypes.BridgeState.State.ON]: 'on', + [grpcTypes.BridgeState.State.OFF]: 'off', + }; + + return bridgeStateMap[bridgeState]; +} + +function convertFromRelaySettings( + relaySettings?: grpcTypes.RelaySettings, +): RelaySettings | undefined { + if (relaySettings) { + switch (relaySettings.getEndpointCase()) { + case grpcTypes.RelaySettings.EndpointCase.ENDPOINT_NOT_SET: + return undefined; + case grpcTypes.RelaySettings.EndpointCase.CUSTOM: { + const custom = relaySettings.getCustom()?.toObject(); + const config = relaySettings.getCustom()?.getConfig(); + const connectionConfig = config && convertFromConnectionConfig(config); + return ( + custom && + connectionConfig && { + customTunnelEndpoint: { + ...custom, + config: connectionConfig, + }, + } + ); + } + case grpcTypes.RelaySettings.EndpointCase.NORMAL: { + const normal = relaySettings.getNormal()!; + const locationConstraint = convertFromLocationConstraint(normal.getLocation()); + const location = wrapConstraint(locationConstraint); + // `getTunnelType()` is not falsy if type is 'any' + const tunnelProtocol = convertFromTunnelTypeConstraint( + normal.hasTunnelType() ? normal.getTunnelType() : undefined, + ); + const providers = normal.getProvidersList(); + const ownership = convertFromOwnership(normal.getOwnership()); + const openvpnConstraints = convertFromOpenVpnConstraints(normal.getOpenvpnConstraints()!); + const wireguardConstraints = convertFromWireguardConstraints( + normal.getWireguardConstraints()!, + ); + + return { + normal: { + location, + tunnelProtocol, + providers, + ownership, + wireguardConstraints, + openvpnConstraints, + }, + }; + } + } + } else { + return undefined; + } +} + +function convertFromBridgeSettings(bridgeSettings: grpcTypes.BridgeSettings): BridgeSettings { + const bridgeSettingsObject = bridgeSettings.toObject(); + + const detailsMap: Record<grpcTypes.BridgeSettings.BridgeType, BridgeType> = { + [grpcTypes.BridgeSettings.BridgeType.NORMAL]: 'normal', + [grpcTypes.BridgeSettings.BridgeType.CUSTOM]: 'custom', + }; + const type = detailsMap[bridgeSettingsObject.bridgeType]; + + const normalSettings = bridgeSettingsObject.normal; + const locationConstraint = convertFromLocationConstraint( + bridgeSettings.getNormal()?.getLocation(), + ); + const location = wrapConstraint(locationConstraint); + const providers = normalSettings!.providersList; + const ownership = convertFromOwnership(normalSettings!.ownership); + + const normal = { + location, + providers, + ownership, + }; + + const grpcCustom = bridgeSettings.getCustom(); + const custom = grpcCustom ? convertFromCustomProxy(grpcCustom) : undefined; + + return { type, normal, custom }; +} + +function convertFromConnectionConfig( + connectionConfig: grpcTypes.ConnectionConfig, +): ConnectionConfig | undefined { + const connectionConfigObject = connectionConfig.toObject(); + switch (connectionConfig.getConfigCase()) { + case grpcTypes.ConnectionConfig.ConfigCase.CONFIG_NOT_SET: + return undefined; + case grpcTypes.ConnectionConfig.ConfigCase.WIREGUARD: + return ( + connectionConfigObject.wireguard && + connectionConfigObject.wireguard.tunnel && + connectionConfigObject.wireguard.peer && { + wireguard: { + ...connectionConfigObject.wireguard, + tunnel: { + privateKey: convertFromWireguardKey( + connectionConfigObject.wireguard.tunnel.privateKey, + ), + addresses: connectionConfigObject.wireguard.tunnel.addressesList, + }, + peer: { + ...connectionConfigObject.wireguard.peer, + addresses: connectionConfigObject.wireguard.peer.allowedIpsList, + publicKey: convertFromWireguardKey(connectionConfigObject.wireguard.peer.publicKey), + }, + }, + } + ); + case grpcTypes.ConnectionConfig.ConfigCase.OPENVPN: { + const [ip, port] = connectionConfigObject.openvpn!.address.split(':'); + return { + openvpn: { + ...connectionConfigObject.openvpn!, + endpoint: { + ip, + port: parseInt(port, 10), + protocol: convertFromTransportProtocol(connectionConfigObject.openvpn!.protocol), + }, + }, + }; + } + } +} + +function convertFromLocationConstraint( + location?: grpcTypes.LocationConstraint, +): RelayLocation | undefined { + if (location === undefined) { + return undefined; + } else if (location.getTypeCase() === grpcTypes.LocationConstraint.TypeCase.CUSTOM_LIST) { + return { customList: location.getCustomList() }; + } else { + const innerLocation = location.getLocation()?.toObject(); + return innerLocation && convertFromGeographicConstraint(innerLocation); + } +} + +function convertFromGeographicConstraint( + location: grpcTypes.GeographicLocationConstraint.AsObject, +): RelayLocation { + if (location.hostname) { + return location; + } else if (location.city) { + return { + country: location.country, + city: location.city, + }; + } else { + return { + country: location.country, + }; + } +} + +function convertFromTunnelOptions(tunnelOptions: grpcTypes.TunnelOptions.AsObject): ITunnelOptions { + return { + openvpn: { + mssfix: tunnelOptions.openvpn!.mssfix, + }, + wireguard: { + mtu: tunnelOptions.wireguard!.mtu, + quantumResistant: convertFromQuantumResistantState( + tunnelOptions.wireguard?.quantumResistant?.state, + ), + daita: tunnelOptions.wireguard!.daita, + }, + generic: { + enableIpv6: tunnelOptions.generic!.enableIpv6, + }, + dns: { + state: + tunnelOptions.dnsOptions?.state === grpcTypes.DnsOptions.DnsState.CUSTOM + ? 'custom' + : 'default', + defaultOptions: { + blockAds: tunnelOptions.dnsOptions?.defaultOptions?.blockAds ?? false, + blockTrackers: tunnelOptions.dnsOptions?.defaultOptions?.blockTrackers ?? false, + blockMalware: tunnelOptions.dnsOptions?.defaultOptions?.blockMalware ?? false, + blockAdultContent: tunnelOptions.dnsOptions?.defaultOptions?.blockAdultContent ?? false, + blockGambling: tunnelOptions.dnsOptions?.defaultOptions?.blockGambling ?? false, + blockSocialMedia: tunnelOptions.dnsOptions?.defaultOptions?.blockSocialMedia ?? false, + }, + customOptions: { + addresses: tunnelOptions.dnsOptions?.customOptions?.addressesList ?? [], + }, + }, + }; +} + +function convertFromQuantumResistantState( + state?: grpcTypes.QuantumResistantState.State, +): boolean | undefined { + return state === undefined + ? undefined + : { + [grpcTypes.QuantumResistantState.State.ON]: true, + [grpcTypes.QuantumResistantState.State.OFF]: false, + [grpcTypes.QuantumResistantState.State.AUTO]: undefined, + }[state]; +} + +function convertFromObfuscationSettings( + obfuscationSettings?: grpcTypes.ObfuscationSettings.AsObject, +): ObfuscationSettings { + let selectedObfuscationType = ObfuscationType.auto; + switch (obfuscationSettings?.selectedObfuscation) { + case grpcTypes.ObfuscationSettings.SelectedObfuscation.OFF: + selectedObfuscationType = ObfuscationType.off; + break; + case grpcTypes.ObfuscationSettings.SelectedObfuscation.UDP2TCP: + selectedObfuscationType = ObfuscationType.udp2tcp; + break; + case grpcTypes.ObfuscationSettings.SelectedObfuscation.SHADOWSOCKS: + selectedObfuscationType = ObfuscationType.shadowsocks; + break; + } + + return { + selectedObfuscation: selectedObfuscationType, + udp2tcpSettings: obfuscationSettings?.udp2tcp + ? { port: convertFromConstraint(obfuscationSettings.udp2tcp.port) } + : { port: 'any' }, + shadowsocksSettings: obfuscationSettings?.shadowsocks + ? { port: convertFromConstraint(obfuscationSettings.shadowsocks.port) } + : { port: 'any' }, + }; +} + +export function convertFromDaemonEvent(data: grpcTypes.DaemonEvent): DaemonEvent { + const tunnelState = data.getTunnelState(); + if (tunnelState !== undefined) { + return { tunnelState: convertFromTunnelState(tunnelState)! }; + } + + const settings = data.getSettings(); + if (settings !== undefined) { + return { settings: convertFromSettings(settings)! }; + } + + const relayList = data.getRelayList(); + if (relayList !== undefined) { + return { relayList: convertFromRelayList(relayList) }; + } + + const deviceConfig = data.getDevice(); + if (deviceConfig !== undefined) { + return { device: convertFromDeviceEvent(deviceConfig) }; + } + + const deviceRemoval = data.getRemoveDevice(); + if (deviceRemoval !== undefined) { + return { deviceRemoval: convertFromDeviceRemoval(deviceRemoval) }; + } + + const versionInfo = data.getVersionInfo(); + if (versionInfo !== undefined) { + return { appVersionInfo: versionInfo.toObject() }; + } + + const newAccessMethod = data.getNewAccessMethod(); + if (newAccessMethod !== undefined) { + return { accessMethodSetting: convertFromApiAccessMethodSetting(newAccessMethod) }; + } + + // Handle unknown daemon events + const keys = Object.entries(data.toObject()) + .filter(([, value]) => value !== undefined) + .map(([key]) => key); + throw new Error(`Unknown daemon event received containing ${keys}`); +} + +function convertFromOwnership(ownership: grpcTypes.Ownership): Ownership { + switch (ownership) { + case grpcTypes.Ownership.ANY: + return Ownership.any; + case grpcTypes.Ownership.MULLVAD_OWNED: + return Ownership.mullvadOwned; + case grpcTypes.Ownership.RENTED: + return Ownership.rented; + } +} + +function convertToOwnership(ownership: Ownership): grpcTypes.Ownership { + switch (ownership) { + case Ownership.any: + return grpcTypes.Ownership.ANY; + case Ownership.mullvadOwned: + return grpcTypes.Ownership.MULLVAD_OWNED; + case Ownership.rented: + return grpcTypes.Ownership.RENTED; + } +} + +function convertFromOpenVpnConstraints( + constraints: grpcTypes.OpenvpnConstraints, +): IOpenVpnConstraints { + const transportPort = convertFromConstraint(constraints.getPort()); + if (transportPort !== 'any' && 'only' in transportPort) { + const port = convertFromConstraint(transportPort.only.getPort()); + let protocol: Constraint<RelayProtocol> = 'any'; + switch (transportPort.only.getProtocol()) { + case grpcTypes.TransportProtocol.TCP: + protocol = { only: 'tcp' }; + break; + case grpcTypes.TransportProtocol.UDP: + protocol = { only: 'udp' }; + break; + } + return { port, protocol }; + } + return { port: 'any', protocol: 'any' }; +} + +function convertFromWireguardConstraints( + constraints: grpcTypes.WireguardConstraints, +): IWireguardConstraints { + const result: IWireguardConstraints = { + port: 'any', + ipVersion: 'any', + useMultihop: constraints.getUseMultihop(), + entryLocation: 'any', + }; + + const port = constraints.getPort(); + if (port) { + result.port = { only: port }; + } + + // `getIpVersion()` is not falsy if type is 'any' + if (constraints.hasIpVersion()) { + switch (constraints.getIpVersion()) { + case grpcTypes.IpVersion.V4: + result.ipVersion = { only: 'ipv4' }; + break; + case grpcTypes.IpVersion.V6: + result.ipVersion = { only: 'ipv6' }; + break; + } + } + + const entryLocation = constraints.getEntryLocation(); + if (entryLocation) { + const location = convertFromLocationConstraint(entryLocation); + result.entryLocation = wrapConstraint(location); + } + + return result; +} + +function convertFromTunnelTypeConstraint( + constraint: grpcTypes.TunnelType | undefined, +): Constraint<TunnelProtocol> { + switch (constraint) { + case grpcTypes.TunnelType.WIREGUARD: { + return { only: 'wireguard' }; + } + case grpcTypes.TunnelType.OPENVPN: { + return { only: 'openvpn' }; + } + default: { + return 'any'; + } + } +} + +function convertFromConstraint<T>(value: T | undefined): Constraint<T> { + if (value) { + return { only: value }; + } else { + return 'any'; + } +} + +export function convertToRelayConstraints( + constraints: IRelaySettingsNormal<IOpenVpnConstraints, IWireguardConstraints>, +): grpcTypes.NormalRelaySettings { + const relayConstraints = new grpcTypes.NormalRelaySettings(); + + if (constraints.tunnelProtocol !== 'any') { + relayConstraints.setTunnelType(convertToTunnelType(constraints.tunnelProtocol.only)); + } + relayConstraints.setLocation(convertToLocation(unwrapConstraint(constraints.location))); + relayConstraints.setWireguardConstraints( + convertToWireguardConstraints(constraints.wireguardConstraints), + ); + relayConstraints.setOpenvpnConstraints( + convertToOpenVpnConstraints(constraints.openvpnConstraints), + ); + relayConstraints.setProvidersList(constraints.providers); + relayConstraints.setOwnership(convertToOwnership(constraints.ownership)); + + return relayConstraints; +} + +export function convertToNormalBridgeSettings( + constraints: IBridgeConstraints, +): grpcTypes.BridgeSettings.BridgeConstraints { + const normalBridgeSettings = new grpcTypes.BridgeSettings.BridgeConstraints(); + normalBridgeSettings.setLocation(convertToLocation(unwrapConstraint(constraints.location))); + normalBridgeSettings.setProvidersList(constraints.providers); + + return normalBridgeSettings; +} + +function convertToLocation( + constraint: RelayLocation | undefined, +): grpcTypes.LocationConstraint | undefined { + const locationConstraint = new grpcTypes.LocationConstraint(); + if (constraint && 'customList' in constraint && constraint.customList) { + locationConstraint.setCustomList(constraint.customList); + } else { + const location = constraint && convertToGeographicConstraint(constraint); + locationConstraint.setLocation(location); + } + + return locationConstraint; +} + +function convertToGeographicConstraint( + location: RelayLocation, +): grpcTypes.GeographicLocationConstraint { + const relayLocation = new grpcTypes.GeographicLocationConstraint(); + if ('hostname' in location) { + relayLocation.setCountry(location.country); + relayLocation.setCity(location.city); + relayLocation.setHostname(location.hostname); + } else if ('city' in location) { + relayLocation.setCountry(location.country); + relayLocation.setCity(location.city); + } else if ('country' in location) { + relayLocation.setCountry(location.country); + } + + return relayLocation; +} + +function convertToTunnelType(tunnelProtocol: TunnelProtocol): grpcTypes.TunnelType { + switch (tunnelProtocol) { + case 'wireguard': + return grpcTypes.TunnelType.WIREGUARD; + case 'openvpn': + return grpcTypes.TunnelType.OPENVPN; + } +} + +function convertToOpenVpnConstraints( + constraints: Partial<IOpenVpnConstraints> | undefined, +): grpcTypes.OpenvpnConstraints | undefined { + const openvpnConstraints = new grpcTypes.OpenvpnConstraints(); + if (constraints) { + const protocol = unwrapConstraint(constraints.protocol); + if (protocol) { + const portConstraints = new grpcTypes.TransportPort(); + const port = unwrapConstraint(constraints.port); + if (port) { + portConstraints.setPort(port); + } + portConstraints.setProtocol(convertToTransportProtocol(protocol)); + openvpnConstraints.setPort(portConstraints); + } + return openvpnConstraints; + } + + return undefined; +} + +function convertToWireguardConstraints( + constraint: Partial<IWireguardConstraints> | undefined, +): grpcTypes.WireguardConstraints | undefined { + if (constraint) { + const wireguardConstraints = new grpcTypes.WireguardConstraints(); + + const port = unwrapConstraint(constraint.port); + if (port) { + wireguardConstraints.setPort(port); + } + + const ipVersion = unwrapConstraint(constraint.ipVersion); + if (ipVersion) { + const ipVersionProtocol = + ipVersion === 'ipv4' ? grpcTypes.IpVersion.V4 : grpcTypes.IpVersion.V6; + wireguardConstraints.setIpVersion(ipVersionProtocol); + } + + if (constraint.useMultihop) { + wireguardConstraints.setUseMultihop(constraint.useMultihop); + } + + const entryLocation = unwrapConstraint(constraint.entryLocation); + if (entryLocation) { + const entryLocationConstraint = convertToLocation(entryLocation); + wireguardConstraints.setEntryLocation(entryLocationConstraint); + } + + return wireguardConstraints; + } + return undefined; +} + +function convertToTransportProtocol(protocol: RelayProtocol): grpcTypes.TransportProtocol { + switch (protocol) { + case 'udp': + return grpcTypes.TransportProtocol.UDP; + case 'tcp': + return grpcTypes.TransportProtocol.TCP; + } +} + +function convertFromDeviceEvent(deviceEvent: grpcTypes.DeviceEvent): DeviceEvent { + const deviceState = convertFromDeviceState(deviceEvent.getNewState()!); + switch (deviceEvent.getCause()) { + case grpcTypes.DeviceEvent.Cause.LOGGED_IN: + return { type: 'logged in', deviceState: deviceState as LoggedInDeviceState }; + case grpcTypes.DeviceEvent.Cause.LOGGED_OUT: + return { type: 'logged out', deviceState: deviceState as LoggedOutDeviceState }; + case grpcTypes.DeviceEvent.Cause.REVOKED: + return { type: 'revoked', deviceState: deviceState as LoggedOutDeviceState }; + case grpcTypes.DeviceEvent.Cause.UPDATED: + return { type: 'updated', deviceState: deviceState as LoggedInDeviceState }; + case grpcTypes.DeviceEvent.Cause.ROTATED_KEY: + return { type: 'rotated_key', deviceState: deviceState as LoggedInDeviceState }; + } +} + +export function convertFromDeviceState(deviceState: grpcTypes.DeviceState): DeviceState { + switch (deviceState.getState()) { + case grpcTypes.DeviceState.State.LOGGED_IN: { + const accountAndDevice = deviceState.getDevice()!; + const device = accountAndDevice.getDevice(); + return { + type: 'logged in', + accountAndDevice: { + accountToken: accountAndDevice.getAccountToken(), + device: device && convertFromDevice(device), + }, + }; + } + case grpcTypes.DeviceState.State.LOGGED_OUT: + return { type: 'logged out' }; + case grpcTypes.DeviceState.State.REVOKED: + return { type: 'revoked' }; + } +} + +function convertFromDeviceRemoval(deviceRemoval: grpcTypes.RemoveDeviceEvent): Array<IDevice> { + return deviceRemoval.getNewDeviceListList().map(convertFromDevice); +} + +export function convertFromDevice(device: grpcTypes.Device): IDevice { + const created = ensureExists(device.getCreated(), "no 'created' field for device").toDate(); + const asObject = device.toObject(); + + return { + ...asObject, + created: created, + }; +} + +function convertFromCustomListSettings( + customListSettings?: grpcTypes.CustomListSettings, +): CustomLists { + return customListSettings ? convertFromCustomLists(customListSettings.getCustomListsList()) : []; +} + +function convertFromCustomLists(customLists: Array<grpcTypes.CustomList>): CustomLists { + return customLists.map((list) => ({ + id: list.getId(), + name: list.getName(), + locations: list + .getLocationsList() + .map((location) => + convertFromGeographicConstraint(location.toObject()), + ) as Array<RelayLocationGeographical>, + })); +} + +export function convertToCustomList(customList: ICustomList): grpcTypes.CustomList { + const grpcCustomList = new grpcTypes.CustomList(); + grpcCustomList.setId(customList.id); + grpcCustomList.setName(customList.name); + + const locations = customList.locations.map(convertToGeographicConstraint); + grpcCustomList.setLocationsList(locations); + + return grpcCustomList; +} + +export function convertToApiAccessMethodSetting( + method: AccessMethodSetting, +): grpcTypes.AccessMethodSetting { + const updatedMethod = new grpcTypes.AccessMethodSetting(); + const uuid = new grpcTypes.UUID(); + uuid.setValue(method.id); + updatedMethod.setId(uuid); + return fillApiAccessMethodSetting(updatedMethod, method); +} + +export function convertToNewApiAccessMethodSetting( + method: NewAccessMethodSetting, +): grpcTypes.NewAccessMethodSetting { + const newMethod = new grpcTypes.NewAccessMethodSetting(); + return fillApiAccessMethodSetting(newMethod, method); +} + +function fillApiAccessMethodSetting<T extends grpcTypes.NewAccessMethodSetting>( + newMethod: T, + method: NewAccessMethodSetting, +): T { + newMethod.setName(method.name); + newMethod.setEnabled(method.enabled); + + const accessMethod = new grpcTypes.AccessMethod(); + switch (method.type) { + case 'direct': { + const direct = new grpcTypes.AccessMethod.Direct(); + accessMethod.setDirect(direct); + break; + } + case 'bridges': { + const bridges = new grpcTypes.AccessMethod.Bridges(); + accessMethod.setBridges(bridges); + break; + } + default: + accessMethod.setCustom(convertToCustomProxy(method)); + } + + newMethod.setAccessMethod(accessMethod); + return newMethod; +} + +export function convertToCustomProxy(proxy: CustomProxy): grpcTypes.CustomProxy { + const customProxy = new grpcTypes.CustomProxy(); + + switch (proxy.type) { + case 'socks5-local': { + const socks5Local = new grpcTypes.Socks5Local(); + socks5Local.setRemoteIp(proxy.remoteIp); + socks5Local.setRemotePort(proxy.remotePort); + socks5Local.setRemoteTransportProtocol( + convertToTransportProtocol(proxy.remoteTransportProtocol), + ); + socks5Local.setLocalPort(proxy.localPort); + customProxy.setSocks5local(socks5Local); + break; + } + case 'socks5-remote': { + const socks5Remote = new grpcTypes.Socks5Remote(); + socks5Remote.setIp(proxy.ip); + socks5Remote.setPort(proxy.port); + if (proxy.authentication !== undefined) { + socks5Remote.setAuth(convertToSocksAuth(proxy.authentication)); + } + customProxy.setSocks5remote(socks5Remote); + break; + } + case 'shadowsocks': { + const shadowsocks = new grpcTypes.Shadowsocks(); + shadowsocks.setIp(proxy.ip); + shadowsocks.setPort(proxy.port); + shadowsocks.setPassword(proxy.password); + shadowsocks.setCipher(proxy.cipher); + customProxy.setShadowsocks(shadowsocks); + break; + } + } + + return customProxy; +} + +function convertToSocksAuth(authentication: SocksAuth): grpcTypes.SocksAuth { + const auth = new grpcTypes.SocksAuth(); + auth.setUsername(authentication.username); + auth.setPassword(authentication.password); + return auth; +} + +function convertFromApiAccessMethodSettings( + accessMethods: grpcTypes.ApiAccessMethodSettings, +): ApiAccessMethodSettings { + const direct = convertFromApiAccessMethodSetting( + ensureExists(accessMethods.getDirect(), "no 'Direct' access method was found"), + ) as AccessMethodSetting<DirectMethod>; + const bridges = convertFromApiAccessMethodSetting( + ensureExists(accessMethods.getMullvadBridges(), "no 'Mullvad Bridges' access method was found"), + ) as AccessMethodSetting<BridgesMethod>; + const custom = accessMethods + .getCustomList() + .filter((setting) => setting.hasId() && setting.hasAccessMethod()) + .map(convertFromApiAccessMethodSetting) + // The last filter helps TypeScript infer the custom proxy type. + .filter(isCustomProxy); + + return { + direct, + mullvadBridges: bridges, + custom, + }; +} + +function isCustomProxy( + accessMethod: AccessMethodSetting, +): accessMethod is AccessMethodSetting<CustomProxy> { + return accessMethod.type !== 'direct' && accessMethod.type !== 'bridges'; +} + +export function convertFromApiAccessMethodSetting( + setting: grpcTypes.AccessMethodSetting, +): AccessMethodSetting { + const id = setting.getId()!; + const accessMethod = setting.getAccessMethod()!; + + return { + id: id.getValue(), + name: setting.getName(), + enabled: setting.getEnabled(), + ...convertFromAccessMethod(accessMethod), + }; +} + +function convertFromAccessMethod(method: grpcTypes.AccessMethod): AccessMethod { + switch (method.getAccessMethodCase()) { + case grpcTypes.AccessMethod.AccessMethodCase.DIRECT: + return { type: 'direct' }; + case grpcTypes.AccessMethod.AccessMethodCase.BRIDGES: + return { type: 'bridges' }; + case grpcTypes.AccessMethod.AccessMethodCase.CUSTOM: { + return convertFromCustomProxy(method.getCustom()!); + } + case grpcTypes.AccessMethod.AccessMethodCase.ACCESS_METHOD_NOT_SET: + throw new Error('Access method not set, which should always be set'); + } +} + +function convertFromCustomProxy(proxy: grpcTypes.CustomProxy): CustomProxy { + switch (proxy.getProxyMethodCase()) { + case grpcTypes.CustomProxy.ProxyMethodCase.SOCKS5LOCAL: { + const socks5Local = proxy.getSocks5local()!; + return { + type: 'socks5-local', + remoteIp: socks5Local.getRemoteIp(), + remotePort: socks5Local.getRemotePort(), + remoteTransportProtocol: convertFromTransportProtocol( + socks5Local.getRemoteTransportProtocol(), + ), + localPort: socks5Local.getLocalPort(), + }; + } + case grpcTypes.CustomProxy.ProxyMethodCase.SOCKS5REMOTE: { + const socks5Remote = proxy.getSocks5remote()!; + const auth = socks5Remote.getAuth(); + return { + type: 'socks5-remote', + ip: socks5Remote.getIp(), + port: socks5Remote.getPort(), + authentication: auth === undefined ? undefined : convertFromSocksAuth(auth), + }; + } + case grpcTypes.CustomProxy.ProxyMethodCase.SHADOWSOCKS: { + const shadowsocks = proxy.getShadowsocks()!; + return { + type: 'shadowsocks', + ip: shadowsocks.getIp(), + port: shadowsocks.getPort(), + password: shadowsocks.getPassword(), + cipher: shadowsocks.getCipher(), + }; + } + case grpcTypes.CustomProxy.ProxyMethodCase.PROXY_METHOD_NOT_SET: + throw new Error('Custom method not set, which should always be set'); + } +} + +function convertFromSocksAuth(auth: grpcTypes.SocksAuth): SocksAuth { + return { + username: auth.getUsername(), + password: auth.getPassword(), + }; +} + +export function ensureExists<T>(value: T | undefined, errorMessage: string): T { + if (value) { + return value; + } + throw new ResponseParseError(errorMessage); +} diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 2401e70b69..3eccc403b1 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -36,8 +36,9 @@ import { printCommandLineOptions, printElectronOptions, } from './command-line-options'; -import { ConnectionObserver, DaemonRpc, SubscriptionListener } from './daemon-rpc'; +import { DaemonRpc, SubscriptionListener } from './daemon-rpc'; import Expectation from './expectation'; +import { ConnectionObserver } from './grpc-client'; import { IpcMainEventChannel } from './ipc-event-channel'; import { findIconPath } from './linux-desktop-entry'; import { loadTranslations } from './load-translations'; |
