diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2023-10-04 11:35:59 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2023-10-09 10:16:53 +0200 |
| commit | 469c501f736e98ea6a20f1e76b40550d6ad995cd (patch) | |
| tree | 88c0fbab003216eff6bcaf2fb87c02d634025558 /gui/src | |
| parent | 4e26e4c36345afbca25a1a1e760927cd74d2c1a5 (diff) | |
| download | mullvadvpn-469c501f736e98ea6a20f1e76b40550d6ad995cd.tar.xz mullvadvpn-469c501f736e98ea6a20f1e76b40550d6ad995cd.zip | |
Add custom lists to settings, ipc and rpc calls
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/main/daemon-rpc.ts | 169 | ||||
| -rw-r--r-- | gui/src/main/default-settings.ts | 1 | ||||
| -rw-r--r-- | gui/src/main/index.ts | 10 | ||||
| -rw-r--r-- | gui/src/main/settings.ts | 3 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 36 | ||||
| -rw-r--r-- | gui/src/renderer/components/Filter.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/select-location/select-location-helpers.ts | 42 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/actions.ts | 25 | ||||
| -rw-r--r-- | gui/src/renderer/redux/settings/reducers.ts | 13 | ||||
| -rw-r--r-- | gui/src/shared/daemon-rpc-types.ts | 87 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 7 | ||||
| -rw-r--r-- | gui/src/shared/relay-location-builder.ts | 4 |
12 files changed, 298 insertions, 103 deletions
diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index fbaa2fe9d9..608bf82241 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -17,6 +17,8 @@ import { BridgeState, ConnectionConfig, Constraint, + CustomListError, + CustomLists, DaemonEvent, DeviceEvent, DeviceState, @@ -27,6 +29,7 @@ import { FirewallPolicyErrorType, IAppVersionInfo, IBridgeConstraints, + ICustomList, IDevice, IDeviceRemoval, IDnsOptions, @@ -52,6 +55,7 @@ import { ProxyType, RelayEndpointType, RelayLocation, + RelayLocationGeographical, RelayProtocol, RelaySettings, RelaySettingsUpdate, @@ -611,6 +615,39 @@ export class DaemonRpc { await this.call<grpcTypes.DeviceRemoval, Empty>(this.client.removeDevice, grpcDeviceRemoval); } + public async createCustomList(name: string): Promise<void | CustomListError> { + try { + await this.callString<Empty>(this.client.createCustomList, name); + } catch (e) { + const error = e as grpc.ServiceError; + if (error.code === 6) { + return { type: 'name already exists' }; + } else { + throw error; + } + } + } + + public async deleteCustomList(id: string): Promise<void> { + await this.callString<Empty>(this.client.deleteCustomList, id); + } + + public async updateCustomList(customList: ICustomList): Promise<void | CustomListError> { + try { + await this.call<grpcTypes.CustomList, Empty>( + this.client.updateCustomList, + convertToCustomList(customList), + ); + } catch (e) { + const error = e as grpc.ServiceError; + if (error.code === 6) { + return { type: 'name already exists' }; + } else { + throw error; + } + } + } + private subscriptionId(): number { const current = this.nextSubscriptionId; this.nextSubscriptionId += 1; @@ -1062,10 +1099,11 @@ function convertFromSettings(settings: grpcTypes.Settings): ISettings | undefine const settingsObject = settings.toObject(); const bridgeState = convertFromBridgeState(settingsObject.bridgeState!.state!); const relaySettings = convertFromRelaySettings(settings.getRelaySettings())!; - const bridgeSettings = convertFromBridgeSettings(settingsObject.bridgeSettings!); + 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()); return { ...settings.toObject(), bridgeState, @@ -1074,6 +1112,7 @@ function convertFromSettings(settings: grpcTypes.Settings): ISettings | undefine tunnelOptions, splitTunnel, obfuscationSettings, + customLists, }; } @@ -1110,10 +1149,8 @@ function convertFromRelaySettings( } case grpcTypes.RelaySettings.EndpointCase.NORMAL: { const normal = relaySettings.getNormal()!; - const grpcLocation = normal.getLocation(); - const location = grpcLocation - ? { only: convertFromLocation(grpcLocation.toObject()) } - : 'any'; + const locationConstraint = convertFromLocationConstraint(normal.getLocation()); + const location = locationConstraint ? { only: locationConstraint } : 'any'; const tunnelProtocol = convertFromTunnelTypeConstraint(normal.getTunnelType()!); const providers = normal.getProvidersList(); const ownership = convertFromOwnership(normal.getOwnership()); @@ -1139,13 +1176,14 @@ function convertFromRelaySettings( } } -function convertFromBridgeSettings( - bridgeSettings: grpcTypes.BridgeSettings.AsObject, -): BridgeSettings { - const normalSettings = bridgeSettings.normal; +function convertFromBridgeSettings(bridgeSettings: grpcTypes.BridgeSettings): BridgeSettings { + const bridgeSettingsObject = bridgeSettings.toObject(); + const normalSettings = bridgeSettingsObject.normal; if (normalSettings) { - const grpcLocation = normalSettings.location; - const location = grpcLocation ? { only: convertFromLocation(grpcLocation) } : 'any'; + const locationConstraint = convertFromLocationConstraint( + bridgeSettings.getNormal()?.getLocation(), + ); + const location = locationConstraint ? { only: locationConstraint } : 'any'; const providers = normalSettings.providersList; const ownership = convertFromOwnership(normalSettings.ownership); return { @@ -1161,7 +1199,7 @@ function convertFromBridgeSettings( return { custom: settings }; }; - const localSettings = bridgeSettings.local; + const localSettings = bridgeSettingsObject.local; if (localSettings) { return customSettings({ port: localSettings.port, @@ -1169,7 +1207,7 @@ function convertFromBridgeSettings( }); } - const remoteSettings = bridgeSettings.remote; + const remoteSettings = bridgeSettingsObject.remote; if (remoteSettings) { return customSettings({ address: remoteSettings.address, @@ -1177,7 +1215,7 @@ function convertFromBridgeSettings( }); } - const shadowsocksSettings = bridgeSettings.shadowsocks!; + const shadowsocksSettings = bridgeSettingsObject.shadowsocks!; return customSettings({ peer: shadowsocksSettings.peer!, password: shadowsocksSettings.password!, @@ -1229,23 +1267,32 @@ function convertFromConnectionConfig( } } -function convertFromLocation(location: grpcTypes.LocationConstraint.AsObject): RelayLocation { - // FIXME: This is a hack that assumes that the LocationConstraint is not a custom list. - // If it is we just set the country to "any" even if that isn't correct. - if (location.location == undefined) { - return { country: 'any' }; - } - const loc = location.location; - - if (loc.hostname) { - return { hostname: [loc.country, loc.city, loc.hostname] }; +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 && convertFromRelayLocation(innerLocation); } +} - if (loc.city) { - return { city: [loc.country, loc.city] }; +function convertFromRelayLocation(location: grpcTypes.RelayLocation.AsObject): RelayLocation { + if (location.hostname) { + return location; + } else if (location.city) { + return { + country: location.country, + city: location.city, + }; + } else { + return { + country: location.country, + }; } - - return { country: loc.country }; } function convertFromTunnelOptions(tunnelOptions: grpcTypes.TunnelOptions.AsObject): ITunnelOptions { @@ -1423,7 +1470,8 @@ function convertFromWireguardConstraints( const entryLocation = constraints.getEntryLocation(); if (entryLocation) { - result.entryLocation = { only: convertFromLocation(entryLocation.toObject()) }; + const location = convertFromLocationConstraint(entryLocation); + result.entryLocation = location ? { only: location } : 'any'; } return result; @@ -1467,24 +1515,32 @@ function convertToLocation( constraint: RelayLocation | undefined, ): grpcTypes.LocationConstraint | undefined { const locationConstraint = new grpcTypes.LocationConstraint(); - const location = new grpcTypes.RelayLocation(); - if (constraint && 'hostname' in constraint) { - const [countryCode, cityCode, hostname] = constraint.hostname; - location.setCountry(countryCode); - location.setCity(cityCode); - location.setHostname(hostname); - } else if (constraint && 'city' in constraint) { - location.setCountry(constraint.city[0]); - location.setCity(constraint.city[1]); - } else if (constraint && 'country' in constraint) { - location.setCountry(constraint.country); + if (constraint && 'customList' in constraint && constraint.customList) { + locationConstraint.setCustomList(constraint.customList); } else { - return undefined; + const location = constraint && convertToRelayLocation(constraint); + locationConstraint.setLocation(location); } - locationConstraint.setLocation(location); + return locationConstraint; } +function convertToRelayLocation(location: RelayLocation): grpcTypes.RelayLocation { + const relayLocation = new grpcTypes.RelayLocation(); + 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 convertToTunnelTypeConstraint( constraint: Constraint<TunnelType>, ): grpcTypes.TunnelTypeConstraint | undefined { @@ -1618,6 +1674,35 @@ function convertFromDevice(device: grpcTypes.Device): IDevice { }; } +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) => + convertFromRelayLocation(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(convertToRelayLocation); + grpcCustomList.setLocationsList(locations); + + return grpcCustomList; +} + function ensureExists<T>(value: T | undefined, errorMessage: string): T { if (value) { return value; diff --git a/gui/src/main/default-settings.ts b/gui/src/main/default-settings.ts index 40c75042f8..55f420b659 100644 --- a/gui/src/main/default-settings.ts +++ b/gui/src/main/default-settings.ts @@ -68,5 +68,6 @@ export function getDefaultSettings(): ISettings { port: 'any', }, }, + customLists: [], }; } diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 484f4db4b3..c674ca22d8 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -787,6 +787,16 @@ class ApplicationMain this.navigationHistory = history; }); + IpcMainEventChannel.customLists.handleCreateCustomList((name) => { + return this.daemonRpc.createCustomList(name); + }); + IpcMainEventChannel.customLists.handleDeleteCustomList((id) => { + return this.daemonRpc.deleteCustomList(id); + }); + IpcMainEventChannel.customLists.handleUpdateCustomList((customList) => { + return this.daemonRpc.updateCustomList(customList); + }); + problemReport.registerIpcListeners(); this.userInterface!.registerIpcListeners(); this.settings.registerIpcListeners(); diff --git a/gui/src/main/settings.ts b/gui/src/main/settings.ts index 08871f42ea..3016e75289 100644 --- a/gui/src/main/settings.ts +++ b/gui/src/main/settings.ts @@ -132,6 +132,9 @@ export default class Settings implements Readonly<ISettings> { public get obfuscationSettings() { return this.settingsValue.obfuscationSettings; } + public get customLists() { + return this.settingsValue.customLists; + } public get gui() { return this.guiSettings; diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 155922bc4d..7c7df73796 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -12,6 +12,7 @@ import { DeviceState, IAccountData, IAppVersionInfo, + ICustomList, IDevice, IDeviceRemoval, IDnsOptions, @@ -336,6 +337,12 @@ export default class AppRenderer { public openUrl = (url: string) => IpcRendererEventChannel.app.openUrl(url); public showOpenDialog = (options: Electron.OpenDialogOptions) => IpcRendererEventChannel.app.showOpenDialog(options); + public createCustomList = (name: string) => + IpcRendererEventChannel.customLists.createCustomList(name); + public deleteCustomList = (id: string) => + IpcRendererEventChannel.customLists.deleteCustomList(id); + public updateCustomList = (customList: ICustomList) => + IpcRendererEventChannel.customLists.updateCustomList(customList); public login = async (accountToken: AccountToken) => { const actions = this.reduxActions; @@ -778,6 +785,7 @@ export default class AppRenderer { reduxSettings.updateDnsOptions(newSettings.tunnelOptions.dns); reduxSettings.updateSplitTunnelingState(newSettings.splitTunnel.enableExclusions); reduxSettings.updateObfuscationSettings(newSettings.obfuscationSettings); + reduxSettings.updateCustomLists(newSettings.customLists); this.setRelaySettings(newSettings.relaySettings); this.setBridgeSettings(newSettings.bridgeSettings); @@ -1002,20 +1010,11 @@ export default class AppRenderer { const location = relaySettings.normal.location; if (location !== 'any' && 'only' in location) { const constraint = location.only; - const relayLocations = state.settings.relayLocations; - if ('country' in constraint) { - const country = relayLocations.find(({ code }) => constraint.country === code); - return { country: country?.name, ...coordinates }; - } else if ('city' in constraint) { - const country = relayLocations.find(({ code }) => constraint.city[0] === code); - const city = country?.cities.find(({ code }) => constraint.city[1] === code); - - return { country: country?.name, city: city?.name, ...coordinates }; - } else if ('hostname' in constraint) { - const country = relayLocations.find(({ code }) => constraint.hostname[0] === code); - const city = country?.cities.find((location) => location.code === constraint.hostname[1]); + if ('hostname' in constraint) { + const country = relayLocations.find(({ code }) => constraint.country === code); + const city = country?.cities.find(({ code }) => constraint.city === code); let entryHostname: string | undefined; const multihopConstraint = relaySettings.normal.wireguardConstraints.useMultihop; @@ -1026,16 +1025,25 @@ export default class AppRenderer { 'hostname' in entryLocationConstraint.only && entryLocationConstraint.only.hostname.length === 3 ) { - entryHostname = entryLocationConstraint.only.hostname[2]; + entryHostname = entryLocationConstraint.only.hostname; } return { country: country?.name, city: city?.name, - hostname: constraint.hostname[2], + hostname: constraint.hostname, entryHostname, ...coordinates, }; + } else if ('city' in constraint) { + const country = relayLocations.find(({ code }) => constraint.country === code); + const city = country?.cities.find(({ code }) => constraint.city === code); + + return { country: country?.name, city: city?.name, ...coordinates }; + } else if ('country' in constraint) { + const country = relayLocations.find(({ code }) => constraint.country === code); + + return { country: country?.name, ...coordinates }; } } } diff --git a/gui/src/renderer/components/Filter.tsx b/gui/src/renderer/components/Filter.tsx index 26ed781396..6b369aa39b 100644 --- a/gui/src/renderer/components/Filter.tsx +++ b/gui/src/renderer/components/Filter.tsx @@ -12,7 +12,7 @@ import { } from '../lib/filter-locations'; import { useHistory } from '../lib/history'; import { useBoolean, useNormalRelaySettings } from '../lib/utilityHooks'; -import { IRelayLocationRedux } from '../redux/settings/reducers'; +import { IRelayLocationCountryRedux } from '../redux/settings/reducers'; import { IReduxState, useSelector } from '../redux/store'; import Accordion from './Accordion'; import * as AppButton from './AppButton'; @@ -158,7 +158,7 @@ function useFilteredFilters(providers: string[], ownership: Ownership) { } // Returns all available providers in the provided relay list. -function providersFromRelays(relays: IRelayLocationRedux[]) { +function providersFromRelays(relays: IRelayLocationCountryRedux[]) { const providers = relays.flatMap((country) => country.cities.flatMap((city) => city.relays.map((relay) => relay.provider)), ); diff --git a/gui/src/renderer/components/select-location/select-location-helpers.ts b/gui/src/renderer/components/select-location/select-location-helpers.ts index 46225645cc..23d059ac0a 100644 --- a/gui/src/renderer/components/select-location/select-location-helpers.ts +++ b/gui/src/renderer/components/select-location/select-location-helpers.ts @@ -5,16 +5,20 @@ import { compareRelayLocationLoose, LiftedConstraint, RelayLocation, + RelayLocationCity, + RelayLocationCountry, + RelayLocationCustomList, + RelayLocationRelay, } from '../../../shared/daemon-rpc-types'; import { messages, relayLocations } from '../../../shared/gettext'; import { IRelayLocationCityRedux, - IRelayLocationRedux, + IRelayLocationCountryRedux, IRelayLocationRelayRedux, NormalBridgeSettingsRedux, NormalRelaySettingsRedux, } from '../../redux/settings/reducers'; -import { DisabledReason, LocationType } from './select-location-types'; +import { DisabledReason, LocationSpecification, LocationType } from './select-location-types'; export function isSelected( relayLocation: RelayLocation, @@ -58,13 +62,10 @@ export function defaultExpandedLocations( // Expands a relay location and its parents function expandRelayLocation(location: RelayLocation): RelayLocation[] { - if ('city' in location) { - return [{ country: location.city[0] }]; - } else if ('hostname' in location) { - return [ - { country: location.hostname[0] }, - { city: [location.hostname[0], location.hostname[1]] }, - ]; + if ('hostname' in location) { + return [{ country: location.country }, { country: location.country, city: location.city }]; + } else if ('city' in location) { + return [{ country: location.country }]; } else { return []; } @@ -104,15 +105,12 @@ export function formatRowName( export function isRelayDisabled( relay: IRelayLocationRelayRedux, - location: [string, string, string], + location: RelayLocationRelay, disabledLocation?: { location: RelayLocation; reason: DisabledReason }, ): DisabledReason | undefined { if (!relay.active) { return DisabledReason.inactive; - } else if ( - disabledLocation && - compareRelayLocation({ hostname: location }, disabledLocation.location) - ) { + } else if (disabledLocation && compareRelayLocation(location, disabledLocation.location)) { return disabledLocation.reason; } else { return undefined; @@ -121,11 +119,11 @@ export function isRelayDisabled( export function isCityDisabled( city: IRelayLocationCityRedux, - location: [string, string], + location: RelayLocationCity, disabledLocation?: { location: RelayLocation; reason: DisabledReason }, ): DisabledReason | undefined { const relaysDisabled = city.relays.map((relay) => - isRelayDisabled(relay, [...location, relay.hostname]), + isRelayDisabled(relay, { ...location, hostname: relay.hostname }), ); if (relaysDisabled.every((status) => status === DisabledReason.inactive)) { return DisabledReason.inactive; @@ -144,7 +142,7 @@ export function isCityDisabled( if ( disabledLocation && - compareRelayLocation({ city: location }, disabledLocation.location) && + compareRelayLocation(location, disabledLocation.location) && city.relays.filter((relay) => relay.active).length <= 1 ) { return disabledLocation.reason; @@ -154,11 +152,13 @@ export function isCityDisabled( } export function isCountryDisabled( - country: IRelayLocationRedux, - location: string, + country: IRelayLocationCountryRedux, + location: RelayLocationCountry, disabledLocation?: { location: RelayLocation; reason: DisabledReason }, ): DisabledReason | undefined { - const citiesDisabled = country.cities.map((city) => isCityDisabled(city, [location, city.code])); + const citiesDisabled = country.cities.map((city) => + isCityDisabled(city, { ...location, city: city.code }), + ); if (citiesDisabled.every((status) => status === DisabledReason.inactive)) { return DisabledReason.inactive; } @@ -175,7 +175,7 @@ export function isCountryDisabled( if ( disabledLocation && - compareRelayLocation({ country: location }, disabledLocation.location) && + compareRelayLocation(location, disabledLocation.location) && country.cities.flatMap((city) => city.relays).filter((relay) => relay.active).length <= 1 ) { return disabledLocation.reason; diff --git a/gui/src/renderer/redux/settings/actions.ts b/gui/src/renderer/redux/settings/actions.ts index dad71de024..585aad5732 100644 --- a/gui/src/renderer/redux/settings/actions.ts +++ b/gui/src/renderer/redux/settings/actions.ts @@ -1,12 +1,13 @@ import { IWindowsApplication } from '../../../shared/application-types'; import { BridgeState, + CustomLists, IDnsOptions, IWireguardEndpointData, ObfuscationSettings, } from '../../../shared/daemon-rpc-types'; import { IGuiSettingsState } from '../../../shared/gui-settings-state'; -import { BridgeSettingsRedux, IRelayLocationRedux, RelaySettingsRedux } from './reducers'; +import { BridgeSettingsRedux, IRelayLocationCountryRedux, RelaySettingsRedux } from './reducers'; export interface IUpdateGuiSettingsAction { type: 'UPDATE_GUI_SETTINGS'; @@ -20,7 +21,7 @@ export interface IUpdateRelayAction { export interface IUpdateRelayLocationsAction { type: 'UPDATE_RELAY_LOCATIONS'; - relayLocations: IRelayLocationRedux[]; + relayLocations: IRelayLocationCountryRedux[]; } export interface IUpdateWireguardEndpointData { @@ -98,6 +99,11 @@ export interface ISetObfuscationSettings { obfuscationSettings: ObfuscationSettings; } +export interface ISetCustomLists { + type: 'SET_CUSTOM_LISTS'; + customLists: CustomLists; +} + export type SettingsAction = | IUpdateGuiSettingsAction | IUpdateRelayAction @@ -116,7 +122,8 @@ export type SettingsAction = | IUpdateDnsOptionsAction | IUpdateSplitTunnelingStateAction | ISetSplitTunnelingApplicationsAction - | ISetObfuscationSettings; + | ISetObfuscationSettings + | ISetCustomLists; function updateGuiSettings(guiSettings: IGuiSettingsState): IUpdateGuiSettingsAction { return { @@ -132,7 +139,9 @@ function updateRelay(relay: RelaySettingsRedux): IUpdateRelayAction { }; } -function updateRelayLocations(relayLocations: IRelayLocationRedux[]): IUpdateRelayLocationsAction { +function updateRelayLocations( + relayLocations: IRelayLocationCountryRedux[], +): IUpdateRelayLocationsAction { return { type: 'UPDATE_RELAY_LOCATIONS', relayLocations, @@ -254,6 +263,13 @@ function updateObfuscationSettings( }; } +function updateCustomLists(customLists: CustomLists): ISetCustomLists { + return { + type: 'SET_CUSTOM_LISTS', + customLists, + }; +} + export default { updateGuiSettings, updateRelay, @@ -273,4 +289,5 @@ export default { updateSplitTunnelingState, setSplitTunnelingApplications, updateObfuscationSettings, + updateCustomLists, }; diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts index 2030d70844..b400799095 100644 --- a/gui/src/renderer/redux/settings/reducers.ts +++ b/gui/src/renderer/redux/settings/reducers.ts @@ -1,6 +1,7 @@ import { IWindowsApplication } from '../../../shared/application-types'; import { BridgeState, + CustomLists, IDnsOptions, IpVersion, IWireguardEndpointData, @@ -77,7 +78,7 @@ export interface IRelayLocationCityRedux { relays: IRelayLocationRelayRedux[]; } -export interface IRelayLocationRedux { +export interface IRelayLocationCountryRedux { name: string; code: string; cities: IRelayLocationCityRedux[]; @@ -87,7 +88,7 @@ export interface ISettingsReduxState { autoStart: boolean; guiSettings: IGuiSettingsState; relaySettings: RelaySettingsRedux; - relayLocations: IRelayLocationRedux[]; + relayLocations: IRelayLocationCountryRedux[]; wireguardEndpointData: IWireguardEndpointData; allowLan: boolean; enableIpv6: boolean; @@ -106,6 +107,7 @@ export interface ISettingsReduxState { splitTunneling: boolean; splitTunnelingApplications: IWindowsApplication[]; obfuscationSettings: ObfuscationSettings; + customLists: CustomLists; } const initialState: ISettingsReduxState = { @@ -169,6 +171,7 @@ const initialState: ISettingsReduxState = { port: 'any', }, }, + customLists: [], }; export default function ( @@ -293,6 +296,12 @@ export default function ( obfuscationSettings: action.obfuscationSettings, }; + case 'SET_CUSTOM_LISTS': + return { + ...state, + customLists: action.customLists, + }; + default: return state; } diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 9af927070a..51c1d67c0d 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -179,10 +179,28 @@ export type TunnelState = | { state: 'disconnecting'; details: AfterDisconnect } | { state: 'error'; details: ErrorState }; -export type RelayLocation = - | { hostname: [string, string, string] } - | { city: [string, string] } - | { country: string }; +export interface RelayLocationCountry extends Partial<RelayLocationCustomList> { + country: string; +} + +export interface RelayLocationCity extends RelayLocationCountry { + city: string; +} + +export interface RelayLocationRelay extends RelayLocationCity { + hostname: string; +} + +export interface RelayLocationCustomList { + customList: string; +} + +export type RelayLocationGeographical = + | RelayLocationRelay + | RelayLocationCountry + | RelayLocationCity; + +export type RelayLocation = RelayLocationGeographical | RelayLocationCustomList; export interface IOpenVpnConstraints { port: Constraint<number>; @@ -386,6 +404,16 @@ export interface IDeviceRemoval { deviceId: string; } +export type CustomLists = Array<ICustomList>; + +export interface ICustomList { + id: string; + name: string; + locations: Array<RelayLocationGeographical>; +} + +export type CustomListError = { type: 'name already exists' }; + export interface ISettings { allowLan: boolean; autoConnect: boolean; @@ -397,6 +425,7 @@ export interface ISettings { bridgeState: BridgeState; splitTunnel: SplitTunnelSettings; obfuscationSettings: ObfuscationSettings; + customLists: CustomLists; } export type BridgeState = 'auto' | 'on' | 'off'; @@ -452,25 +481,51 @@ export function parseSocketAddress(socketAddrStr: string): ISocketAddress { return socketAddress; } -export function relayLocationComponents(location: RelayLocation): string[] { - if ('country' in location) { - return [location.country]; - } else if ('city' in location) { - return location.city; - } else { - return location.hostname; +export function compareRelayLocationCount(lhs: RelayLocation, rhs: RelayLocation): boolean { + if ( + ('count' in lhs || 'count' in rhs) && + !('count' in lhs && 'count' in rhs && lhs.count === rhs.count) + ) { + return false; } + + return compareRelayLocation(lhs, rhs); } export function compareRelayLocation(lhs: RelayLocation, rhs: RelayLocation): boolean { - const lhsComponents = relayLocationComponents(lhs); - const rhsComponents = relayLocationComponents(rhs); + if ( + ('customList' in lhs || 'customList' in rhs) && + !('customList' in lhs && 'customList' in rhs && lhs.customList === rhs.customList) + ) { + return false; + } - if (lhsComponents.length === rhsComponents.length) { - return lhsComponents.every((value, index) => value === rhsComponents[index]); - } else { + return compareRelayLocationGeographical(lhs, rhs); +} + +export function compareRelayLocationGeographical(lhs: RelayLocation, rhs: RelayLocation): boolean { + if ( + ('country' in lhs || 'country' in rhs) && + !('country' in lhs && 'country' in rhs && lhs.country === rhs.country) + ) { + return false; + } + + if ( + ('city' in lhs || 'city' in rhs) && + !('city' in lhs && 'city' in rhs && lhs.city === rhs.city) + ) { return false; } + + if ( + ('hostname' in lhs || 'hostname' in rhs) && + !('hostname' in lhs && 'hostname' in rhs && lhs.hostname === rhs.hostname) + ) { + return false; + } + + return true; } export function compareRelayLocationLoose(lhs?: RelayLocation, rhs?: RelayLocation) { diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts index 265d546b5d..946b4fa99a 100644 --- a/gui/src/shared/ipc-schema.ts +++ b/gui/src/shared/ipc-schema.ts @@ -6,10 +6,12 @@ import { AccountToken, BridgeSettings, BridgeState, + CustomListError, DeviceEvent, DeviceState, IAccountData, IAppVersionInfo, + ICustomList, IDevice, IDeviceRemoval, IDnsOptions, @@ -133,6 +135,11 @@ export const ipcSchema = { relays: { '': notifyRenderer<IRelayListWithEndpointData>(), }, + customLists: { + createCustomList: invoke<string, void | CustomListError>(), + deleteCustomList: invoke<string, void>(), + updateCustomList: invoke<ICustomList, void | CustomListError>(), + }, currentVersion: { '': notifyRenderer<ICurrentAppVersionInfo>(), displayedChangelog: send<void>(), diff --git a/gui/src/shared/relay-location-builder.ts b/gui/src/shared/relay-location-builder.ts index 14bf72b65f..7e585f9eaf 100644 --- a/gui/src/shared/relay-location-builder.ts +++ b/gui/src/shared/relay-location-builder.ts @@ -18,11 +18,11 @@ export default function makeLocationBuilder<T>( return context; }, city: (country: string, city: string) => { - receiver({ only: { city: [country, city] } }); + receiver({ only: { country, city } }); return context; }, hostname: (country: string, city: string, hostname: string) => { - receiver({ only: { hostname: [country, city, hostname] } }); + receiver({ only: { country, city, hostname } }); return context; }, any: () => { |
