diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2024-04-08 07:57:05 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2024-04-11 17:21:23 +0200 |
| commit | 41a01db5963e0f499c0dc23419af8ed6bd8bf772 (patch) | |
| tree | 4824fd3a7818cdb9cbfb1241b29334ca0336ce7b /gui/src/renderer/components/EditApiAccessMethod.tsx | |
| parent | 9e7b4bf6638c1b57d5291305a838367865ce8f76 (diff) | |
| download | mullvadvpn-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.tsx | 453 |
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; -} |
