summaryrefslogtreecommitdiffhomepage
path: root/gui/src/renderer/components/EditApiAccessMethod.tsx
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2024-04-08 07:57:05 +0200
committerOskar Nyberg <oskar@mullvad.net>2024-04-11 17:21:23 +0200
commit41a01db5963e0f499c0dc23419af8ed6bd8bf772 (patch)
tree4824fd3a7818cdb9cbfb1241b29334ca0336ce7b /gui/src/renderer/components/EditApiAccessMethod.tsx
parent9e7b4bf6638c1b57d5291305a838367865ce8f76 (diff)
downloadmullvadvpn-41a01db5963e0f499c0dc23419af8ed6bd8bf772.tar.xz
mullvadvpn-41a01db5963e0f499c0dc23419af8ed6bd8bf772.zip
Refactor custom proxy form out of Api access method component
Diffstat (limited to 'gui/src/renderer/components/EditApiAccessMethod.tsx')
-rw-r--r--gui/src/renderer/components/EditApiAccessMethod.tsx453
1 files changed, 23 insertions, 430 deletions
diff --git a/gui/src/renderer/components/EditApiAccessMethod.tsx b/gui/src/renderer/components/EditApiAccessMethod.tsx
index b5b3bd6d48..59a5f2ff37 100644
--- a/gui/src/renderer/components/EditApiAccessMethod.tsx
+++ b/gui/src/renderer/components/EditApiAccessMethod.tsx
@@ -1,38 +1,27 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useCallback, useEffect, useRef } from 'react';
import { useParams } from 'react-router';
import { sprintf } from 'sprintf-js';
import {
- AccessMethod,
- AccessMethodSetting,
CustomProxy,
+ NamedCustomProxy,
NewAccessMethodSetting,
- RelayProtocol,
- ShadowsocksAccessMethod,
- Socks5LocalAccessMethod,
- Socks5RemoteAccessMethod,
} from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { useScheduler } from '../../shared/scheduler';
import { useAppContext } from '../context';
import { useApiAccessMethodTest } from '../lib/api-access-methods';
import { useHistory } from '../lib/history';
-import { IpAddress } from '../lib/ip';
import { useSelector } from '../redux/store';
-import * as Cell from './cell';
-import { SettingsForm, useSettingsFormSubmittable } from './cell/SettingsForm';
-import { SettingsGroup } from './cell/SettingsGroup';
-import { SettingsRadioGroup } from './cell/SettingsRadioGroup';
-import { SettingsRow } from './cell/SettingsRow';
-import { SettingsSelect, SettingsSelectItem } from './cell/SettingsSelect';
-import { SettingsNumberInput, SettingsTextInput } from './cell/SettingsTextInput';
+import { SettingsForm } from './cell/SettingsForm';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
import { NavigationBar, NavigationContainer, NavigationItems, TitleBarItem } from './NavigationBar';
+import { NamedProxyForm } from './ProxyForm';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import { StyledContent, StyledNavigationScrollbars, StyledSettingsContent } from './SettingsStyles';
-import { SmallButton, SmallButtonGroup } from './SmallButton';
+import { SmallButton } from './SmallButton';
export function EditApiAccessMethod() {
return (
@@ -45,7 +34,7 @@ export function EditApiAccessMethod() {
function AccessMethodForm() {
const history = useHistory();
const { addApiAccessMethod, updateApiAccessMethod } = useAppContext();
- const methods = useSelector((state) => state.settings.apiAccessMethods);
+ const methods = useSelector((state) => state.settings.apiAccessMethods.custom);
const [testing, testResult, testApiAccessMethod, resetTestResult] = useApiAccessMethodTest(
false,
@@ -55,19 +44,9 @@ function AccessMethodForm() {
// Use id in url to figure out which method is to be edited. undefined means this is a new method.
const { id } = useParams<{ id: string | undefined }>();
- // Ugly way of iterating over all access methods, but it works.
- const method = [methods.direct, methods.mullvadBridges, ...methods.custom].find(
- (method) => method.id === id,
- );
-
- const updatedMethod = useRef<NewAccessMethodSetting | undefined>(method);
- const updateMethod = useCallback(
- (method: NewAccessMethodSetting) => (updatedMethod.current = method),
- [],
- );
+ const method = methods.find((method) => method.id === id);
- // Contains form submittability to know whether or not to enable the Add/Save button.
- const formSubmittable = useSettingsFormSubmittable();
+ const updatedMethod = useRef<NewAccessMethodSetting<CustomProxy> | undefined>(method);
const save = useCallback(() => {
if (updatedMethod.current !== undefined) {
@@ -81,15 +60,20 @@ function AccessMethodForm() {
}
}, [updatedMethod.current, id]);
- const onSave = useCallback(async () => {
- if (
- updatedMethod.current !== undefined &&
- (await testApiAccessMethod(updatedMethod.current as CustomProxy))
- ) {
- // Hide the save dialog after 1.5 seconds.
- saveScheduler.schedule(save, 1500);
- }
- }, [updatedMethod, save, history.pop]);
+ const onSave = useCallback(
+ async (newMethod: NamedCustomProxy) => {
+ const enabled = id === undefined ? true : method?.enabled ?? true;
+ updatedMethod.current = { ...newMethod, enabled };
+ if (
+ updatedMethod.current !== undefined &&
+ (await testApiAccessMethod(updatedMethod.current as CustomProxy))
+ ) {
+ // Hide the save dialog after 1.5 seconds.
+ saveScheduler.schedule(save, 1500);
+ }
+ },
+ [updatedMethod, save, history.pop],
+ );
const title = getTitle(id === undefined);
const subtitle = getSubtitle(id === undefined);
@@ -116,15 +100,8 @@ function AccessMethodForm() {
{id !== undefined && method === undefined ? (
<span>Failed to open method</span>
) : (
- <AccessMethodFormImpl method={method} updateMethod={updateMethod} />
+ <NamedProxyForm proxy={method} onSave={onSave} onCancel={history.pop} />
)}
-
- <SmallButtonGroup>
- <SmallButton onClick={history.pop}>{messages.gettext('Cancel')}</SmallButton>
- <SmallButton onClick={onSave} disabled={!formSubmittable}>
- {id === undefined ? messages.gettext('Add') : messages.gettext('Save')}
- </SmallButton>
- </SmallButtonGroup>
</StyledSettingsContent>
<TestingDialog
@@ -257,387 +234,3 @@ function getTestingDialogButtons(type: ModalAlertType, save: () => void, cancel:
return [cancelButton];
}
}
-
-interface EditApiAccessMethodImplProps {
- method?: AccessMethodSetting;
- updateMethod: (method: NewAccessMethodSetting) => void;
-}
-
-function AccessMethodFormImpl(props: EditApiAccessMethodImplProps) {
- // Available method types.
- const types = useMemo<Array<SettingsSelectItem<AccessMethod['type']>>>(
- () => [
- { value: 'shadowsocks', label: 'Shadowsocks' },
- {
- value: 'socks5-remote',
- label: messages.pgettext('api-access-methods-view', 'SOCKS5 remote'),
- },
- {
- value: 'socks5-local',
- label: messages.pgettext('api-access-methods-view', 'SOCKS5 local'),
- },
- ],
- [],
- );
- const [type, setType] = useState(props.method?.type ?? 'shadowsocks');
-
- // State for the name input.
- const name = useRef(props.method?.name ?? '');
- const method = useRef<AccessMethod | undefined>(props.method);
-
- // When the form makes up a valid method the parent is updated.
- const onUpdate = useCallback(() => {
- if (method.current !== undefined && name.current !== '') {
- props.updateMethod({ ...method.current, name: name.current, enabled: true });
- }
- }, []);
-
- const updateName = useCallback(
- (value: string) => {
- name.current = value;
- onUpdate();
- },
- [onUpdate],
- );
-
- const updateMethod = useCallback(
- (value: AccessMethod) => {
- method.current = value;
- onUpdate();
- },
- [onUpdate],
- );
-
- return (
- <>
- <SettingsRow label={messages.gettext('Name')}>
- <SettingsTextInput
- defaultValue={name.current}
- placeholder={messages.pgettext('api-access-methods-view', 'Enter name')}
- onUpdate={updateName}
- />
- </SettingsRow>
-
- <SettingsRow label={messages.gettext('Type')}>
- <SettingsSelect defaultValue={type} onUpdate={setType} items={types} />
- </SettingsRow>
-
- {type === 'shadowsocks' && (
- <EditShadowsocks
- onUpdate={updateMethod}
- method={props.method?.type === 'shadowsocks' ? props.method : undefined}
- />
- )}
- {type === 'socks5-remote' && (
- <EditSocks5Remote
- onUpdate={updateMethod}
- method={props.method?.type === 'socks5-remote' ? props.method : undefined}
- />
- )}
- {type === 'socks5-local' && (
- <EditSocks5Local
- onUpdate={updateMethod}
- method={props.method?.type === 'socks5-local' ? props.method : undefined}
- />
- )}
- </>
- );
-}
-
-interface EditMethodProps<T> {
- method?: T;
- onUpdate: (method: AccessMethod) => void;
-}
-
-function EditShadowsocks(props: EditMethodProps<ShadowsocksAccessMethod>) {
- const [ip, setIp] = useState(props.method?.ip ?? '');
- const [port, setPort] = useState(props.method?.port);
- const [password, setPassword] = useState(props.method?.password ?? '');
- const [cipher, setCipher] = useState(props.method?.cipher);
-
- const ciphers = useMemo(
- () =>
- [
- { value: 'aes-128-cfb', label: 'aes-128-cfb' },
- { value: 'aes-128-cfb1', label: 'aes-128-cfb1' },
- { value: 'aes-128-cfb8', label: 'aes-128-cfb8' },
- { value: 'aes-128-cfb128', label: 'aes-128-cfb128' },
- { value: 'aes-256-cfb', label: 'aes-256-cfb' },
- { value: 'aes-256-cfb1', label: 'aes-256-cfb1' },
- { value: 'aes-256-cfb8', label: 'aes-256-cfb8' },
- { value: 'aes-256-cfb128', label: 'aes-256-cfb128' },
- { value: 'rc4', label: 'rc4' },
- { value: 'rc4-md5', label: 'rc4-md5' },
- { value: 'chacha20', label: 'chacha20' },
- { value: 'salsa20', label: 'salsa20' },
- { value: 'chacha20-ietf', label: 'chacha20-ietf' },
- { value: 'aes-128-gcm', label: 'aes-128-gcm' },
- { value: 'aes-256-gcm', label: 'aes-256-gcm' },
- { value: 'chacha20-ietf-poly1305', label: 'chacha20-ietf-poly1305' },
- { value: 'xchacha20-ietf-poly1305', label: 'xchacha20-ietf-poly1305' },
- { value: 'aes-128-pmac-siv', label: 'aes-128-pmac-siv' },
- { value: 'aes-256-pmac-siv', label: 'aes-256-pmac-siv' },
- ].sort((a, b) => a.label.localeCompare(b.label)),
- [],
- );
-
- // Report back to form component with the method values when all required values are set.
- useEffect(() => {
- if (ip !== '' && port !== undefined && cipher !== undefined) {
- props.onUpdate({
- type: 'shadowsocks',
- ip,
- port,
- password,
- cipher,
- });
- }
- }, [ip, port, password, cipher]);
-
- return (
- <SettingsGroup title={messages.pgettext('api-access-methods-view', 'Server details')}>
- <SettingsRow
- label={messages.pgettext('api-access-methods-view', 'Server')}
- errorMessage={messages.pgettext(
- 'api-access-methods-view',
- 'Please enter a valid IPv4 or IPv6 address.',
- )}>
- <SettingsTextInput
- value={ip}
- placeholder={messages.pgettext('api-access-methods-view', 'Enter IP')}
- onUpdate={setIp}
- validate={validateIp}
- />
- </SettingsRow>
-
- <SettingsRow
- label={messages.gettext('Port')}
- errorMessage={messages.pgettext(
- 'api-access-methods-view',
- 'Please enter a valid remote server port.',
- )}>
- <SettingsNumberInput
- value={port ?? ''}
- placeholder={messages.pgettext('api-access-methods-view', 'Enter port')}
- onUpdate={setPort}
- validate={validatePort}
- />
- </SettingsRow>
-
- <SettingsRow label={messages.gettext('Password')}>
- <SettingsTextInput
- value={password}
- placeholder={messages.gettext('Optional')}
- onUpdate={setPassword}
- optionalInForm
- />
- </SettingsRow>
-
- <SettingsRow label={messages.gettext('Cipher')}>
- <SettingsSelect
- data-testid="ciphers"
- direction="up"
- defaultValue={cipher}
- onUpdate={setCipher}
- items={ciphers}
- />
- </SettingsRow>
- </SettingsGroup>
- );
-}
-
-function EditSocks5Remote(props: EditMethodProps<Socks5RemoteAccessMethod>) {
- const [ip, setIp] = useState(props.method?.ip ?? '');
- const [port, setPort] = useState(props.method?.port);
- const [authentication, setAuthentication] = useState(props.method?.authentication !== undefined);
- const [username, setUsername] = useState(props.method?.authentication?.username ?? '');
- const [password, setPassword] = useState(props.method?.authentication?.password ?? '');
-
- // Report back to form component with the method values when all required values are set.
- useEffect(() => {
- if (
- ip !== '' &&
- port !== undefined &&
- (!authentication || (username !== '' && password !== ''))
- ) {
- props.onUpdate({
- type: 'socks5-remote',
- ip,
- port,
- authentication: authentication ? { username, password } : undefined,
- });
- }
- }, [ip, port, username, password]);
-
- return (
- <SettingsGroup title={messages.pgettext('api-access-methods-view', 'Remote Server')}>
- <SettingsRow
- label={messages.pgettext('api-access-methods-view', 'Server')}
- errorMessage={messages.pgettext(
- 'api-access-methods-view',
- 'Please enter a valid IPv4 or IPv6 address.',
- )}>
- <SettingsTextInput
- value={ip}
- placeholder={messages.pgettext('api-access-methods-view', 'Enter IP')}
- onUpdate={setIp}
- validate={validateIp}
- />
- </SettingsRow>
-
- <SettingsRow
- label={messages.gettext('Port')}
- errorMessage={messages.pgettext(
- 'api-access-methods-view',
- 'Please enter a valid remote server port.',
- )}>
- <SettingsNumberInput
- value={port ?? ''}
- placeholder={messages.pgettext('api-access-methods-view', 'Enter port')}
- onUpdate={setPort}
- validate={validatePort}
- />
- </SettingsRow>
-
- <SettingsRow label={messages.pgettext('api-access-methods-view', 'Authentication')}>
- <Cell.Switch isOn={authentication} onChange={setAuthentication} />
- </SettingsRow>
-
- {authentication && (
- <>
- <SettingsRow label={messages.gettext('Username')}>
- <SettingsTextInput
- value={username}
- placeholder={messages.gettext('Required')}
- onUpdate={setUsername}
- />
- </SettingsRow>
-
- <SettingsRow label={messages.gettext('Password')}>
- <SettingsTextInput
- value={password}
- placeholder={messages.gettext('Required')}
- onUpdate={setPassword}
- />
- </SettingsRow>
- </>
- )}
- </SettingsGroup>
- );
-}
-
-function EditSocks5Local(props: EditMethodProps<Socks5LocalAccessMethod>) {
- const [remoteIp, setRemoteIp] = useState(props.method?.remoteIp ?? '');
- const [remotePort, setRemotePort] = useState(props.method?.remotePort);
- const [remoteTransportProtocol, setRemoteTransportProtocol] = useState<RelayProtocol>(
- props.method?.remoteTransportProtocol ?? 'tcp',
- );
- const [localPort, setLocalPort] = useState(props.method?.localPort);
-
- const remoteTransportProtocols = useMemo<Array<SettingsSelectItem<RelayProtocol>>>(
- () => [
- { value: 'tcp', label: 'TCP' },
- { value: 'udp', label: 'UDP' },
- ],
- [],
- );
-
- useEffect(() => {
- if (remoteIp !== '' && remotePort !== undefined && localPort !== undefined) {
- props.onUpdate({
- type: 'socks5-local',
- remoteIp,
- remotePort,
- remoteTransportProtocol,
- localPort,
- });
- }
- }, [remoteIp, remotePort, localPort, remoteTransportProtocol]);
-
- return (
- <>
- <SettingsGroup
- title={messages.pgettext('api-access-methods-view', 'Local SOCKS5 server')}
- infoMessage={messages.pgettext(
- 'api-access-methods-view',
- 'The TCP port where your local SOCKS5 server is listening.',
- )}>
- <SettingsRow
- label={messages.gettext('Port')}
- errorMessage={messages.pgettext(
- 'api-access-methods-view',
- 'Please enter a valid localhost port.',
- )}>
- <SettingsNumberInput
- value={localPort}
- placeholder={messages.pgettext('api-access-methods-view', 'Enter port')}
- onUpdate={setLocalPort}
- validate={validatePort}
- />
- </SettingsRow>
- </SettingsGroup>
-
- <SettingsGroup
- title={messages.pgettext('api-access-methods-view', 'Remote Server')}
- infoMessage={[
- messages.pgettext(
- 'api-access-methods-view',
- 'The app needs the remote server details, where your local SOCKS5 server will forward your traffic.',
- ),
- messages.pgettext(
- 'api-access-methods-view',
- 'This is needed so our app can allow that traffic in the firewall.',
- ),
- ]}>
- <SettingsRow
- label={messages.pgettext('api-access-methods-view', 'Server')}
- errorMessage={messages.pgettext(
- 'api-access-methods-view',
- 'Please enter a valid IPv4 or IPv6 address.',
- )}>
- <SettingsTextInput
- value={remoteIp}
- placeholder={messages.pgettext('api-access-methods-view', 'Enter IP')}
- onUpdate={setRemoteIp}
- validate={validateIp}
- />
- </SettingsRow>
-
- <SettingsRow
- label={messages.gettext('Port')}
- errorMessage={messages.pgettext(
- 'api-access-methods-view',
- 'Please enter a valid remote server port.',
- )}>
- <SettingsNumberInput
- value={remotePort ?? ''}
- placeholder={messages.pgettext('api-access-methods-view', 'Enter port')}
- onUpdate={setRemotePort}
- validate={validatePort}
- />
- </SettingsRow>
-
- <SettingsRow label={messages.pgettext('api-access-methods-view', 'Transport protocol')}>
- <SettingsRadioGroup<'tcp' | 'udp'>
- defaultValue={remoteTransportProtocol}
- onUpdate={setRemoteTransportProtocol}
- items={remoteTransportProtocols}
- />
- </SettingsRow>
- </SettingsGroup>
- </>
- );
-}
-
-function validateIp(ip: string): boolean {
- try {
- void IpAddress.fromString(ip);
- return true;
- } catch {
- return false;
- }
-}
-
-function validatePort(port: number): boolean {
- return port > 0 && port <= 65535;
-}