Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom lists to desktop GUI #5234

Merged
merged 9 commits into from
Oct 9, 2023
4 changes: 4 additions & 0 deletions gui/assets/images/icon-edit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions gui/locales/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ msgstr ""
msgid "Reconnect"
msgstr ""

msgid "Save"
msgstr ""

msgid "Search for..."
msgstr ""

Expand Down Expand Up @@ -1005,6 +1008,33 @@ msgctxt "select-location-view"
msgid "%(location)s (%(info)s)"
msgstr ""

#. This is a label shown above a list of options.
#. Available placeholder:
#. %(locationType) - Could be either "Country", "City" and "Relay"
msgctxt "select-location-view"
msgid "Add <b>%(locationType)s</b> to list"
msgstr ""

msgctxt "select-location-view"
msgid "All locations"
msgstr ""

msgctxt "select-location-view"
msgid "City"
msgstr ""

msgctxt "select-location-view"
msgid "Country"
msgstr ""

msgctxt "select-location-view"
msgid "Custom lists"
msgstr ""

msgctxt "select-location-view"
msgid "Edit list name"
msgstr ""

msgctxt "select-location-view"
msgid "Entry"
msgstr ""
Expand All @@ -1017,10 +1047,22 @@ msgctxt "select-location-view"
msgid "Filtered:"
msgstr ""

msgctxt "select-location-view"
msgid "List names must be unique."
msgstr ""

msgctxt "select-location-view"
msgid "Name is already taken."
msgstr ""

msgctxt "select-location-view"
msgid "Providers: %(numberOfProviders)d"
msgstr ""

msgctxt "select-location-view"
msgid "Relay"
msgstr ""

msgctxt "select-location-view"
msgid "The app selects a random bridge server, but servers have a higher probability the closer they are to you."
msgstr ""
Expand Down
1 change: 1 addition & 0 deletions gui/src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"white40": "rgba(255, 255, 255, 0.4)",
"white20": "rgba(255, 255, 255, 0.2)",
"white10": "rgba(255, 255, 255, 0.1)",
"blue10": "rgba(41, 77, 115, 0.1)",
"blue20": "rgba(41, 77, 115, 0.2)",
"blue40": "rgba(41, 77, 115, 0.4)",
"blue60": "rgba(41, 77, 115, 0.6)",
Expand Down
169 changes: 127 additions & 42 deletions gui/src/main/daemon-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
BridgeState,
ConnectionConfig,
Constraint,
CustomListError,
CustomLists,
DaemonEvent,
DeviceEvent,
DeviceState,
Expand All @@ -27,6 +29,7 @@ import {
FirewallPolicyErrorType,
IAppVersionInfo,
IBridgeConstraints,
ICustomList,
IDevice,
IDeviceRemoval,
IDnsOptions,
Expand All @@ -52,6 +55,7 @@ import {
ProxyType,
RelayEndpointType,
RelayLocation,
RelayLocationGeographical,
RelayProtocol,
RelaySettings,
RelaySettingsUpdate,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -1074,6 +1112,7 @@ function convertFromSettings(settings: grpcTypes.Settings): ISettings | undefine
tunnelOptions,
splitTunnel,
obfuscationSettings,
customLists,
};
}

Expand Down Expand Up @@ -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());
Expand All @@ -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 {
Expand All @@ -1161,23 +1199,23 @@ function convertFromBridgeSettings(
return { custom: settings };
};

const localSettings = bridgeSettings.local;
const localSettings = bridgeSettingsObject.local;
if (localSettings) {
return customSettings({
port: localSettings.port,
peer: localSettings.peer,
});
}

const remoteSettings = bridgeSettings.remote;
const remoteSettings = bridgeSettingsObject.remote;
if (remoteSettings) {
return customSettings({
address: remoteSettings.address,
auth: remoteSettings.auth && { ...remoteSettings.auth },
});
}

const shadowsocksSettings = bridgeSettings.shadowsocks!;
const shadowsocksSettings = bridgeSettingsObject.shadowsocks!;
return customSettings({
peer: shadowsocksSettings.peer!,
password: shadowsocksSettings.password!,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions gui/src/main/default-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@ export function getDefaultSettings(): ISettings {
port: 'any',
},
},
customLists: [],
};
}
10 changes: 10 additions & 0 deletions gui/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions gui/src/main/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading