diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2022-07-20 13:28:34 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2022-07-25 11:40:06 +0200 |
| commit | c4ae17e338e78bfb0b22b1ba0bc25e6a8f6f28ad (patch) | |
| tree | 04d68c0dfa03fabeb8f295fc62b6774f573f2d30 /gui/src | |
| parent | a46df6ba2f6f3fedd79dab4980c7fc883f79536d (diff) | |
| download | mullvadvpn-c4ae17e338e78bfb0b22b1ba0bc25e6a8f6f28ad.tar.xz mullvadvpn-c4ae17e338e78bfb0b22b1ba0bc25e6a8f6f28ad.zip | |
Refactore OpenVpn settings view
Diffstat (limited to 'gui/src')
| -rw-r--r-- | gui/src/renderer/app.tsx | 8 | ||||
| -rw-r--r-- | gui/src/renderer/components/AppRouter.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/OpenVPNSettings.tsx | 719 | ||||
| -rw-r--r-- | gui/src/renderer/containers/OpenVPNSettingsPage.tsx | 136 |
4 files changed, 422 insertions, 445 deletions
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx index 9e2130ff1f..ec69b5d8a7 100644 --- a/gui/src/renderer/app.tsx +++ b/gui/src/renderer/app.tsx @@ -436,11 +436,11 @@ export default class AppRenderer { actions.settings.updateEnableIpv6(enableIpv6); }; - public async setBridgeState(bridgeState: BridgeState) { + public setBridgeState = async (bridgeState: BridgeState) => { const actions = this.reduxActions; await IpcRendererEventChannel.settings.setBridgeState(bridgeState); actions.settings.updateBridgeState(bridgeState); - } + }; public setBlockWhenDisconnected = async (blockWhenDisconnected: boolean) => { const actions = this.reduxActions; @@ -448,11 +448,11 @@ export default class AppRenderer { actions.settings.updateBlockWhenDisconnected(blockWhenDisconnected); }; - public async setOpenVpnMssfix(mssfix?: number) { + public setOpenVpnMssfix = async (mssfix?: number) => { const actions = this.reduxActions; actions.settings.updateOpenVpnMssfix(mssfix); await IpcRendererEventChannel.settings.setOpenVpnMssfix(mssfix); - } + }; public setWireguardMtu = async (mtu?: number) => { const actions = this.reduxActions; diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx index 4791faecb6..b6f2865299 100644 --- a/gui/src/renderer/components/AppRouter.tsx +++ b/gui/src/renderer/components/AppRouter.tsx @@ -4,7 +4,6 @@ import { Route, Switch } from 'react-router'; import AccountPage from '../containers/AccountPage'; import LoginPage from '../containers/LoginPage'; -import OpenVPNSettingsPage from '../containers/OpenVPNSettingsPage'; import ProblemReportPage from '../containers/ProblemReportPage'; import SelectLanguagePage from '../containers/SelectLanguagePage'; import SelectLocationPage from '../containers/SelectLocationPage'; @@ -23,6 +22,7 @@ import Focus, { IFocusHandle } from './Focus'; import InterfaceSettings from './InterfaceSettings'; import Launch from './Launch'; import MainView from './MainView'; +import OpenVPNSettings from './OpenVPNSettings'; import Settings from './Settings'; import SplitTunnelingSettings from './SplitTunnelingSettings'; import Support from './Support'; @@ -93,7 +93,7 @@ class AppRouter extends React.Component<IHistoryProps & IAppContext, IAppRoutesS <Route exact path={RoutePath.interfaceSettings} component={InterfaceSettings} /> <Route exact path={RoutePath.vpnSettings} component={VpnSettings} /> <Route exact path={RoutePath.wireguardSettings} component={WireguardSettings} /> - <Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettingsPage} /> + <Route exact path={RoutePath.openVpnSettings} component={OpenVPNSettings} /> <Route exact path={RoutePath.splitTunneling} component={SplitTunnelingSettings} /> <Route exact path={RoutePath.support} component={Support} /> <Route exact path={RoutePath.problemReport} component={ProblemReportPage} /> diff --git a/gui/src/renderer/components/OpenVPNSettings.tsx b/gui/src/renderer/components/OpenVPNSettings.tsx index 1b82c9971a..fce949c6a9 100644 --- a/gui/src/renderer/components/OpenVPNSettings.tsx +++ b/gui/src/renderer/components/OpenVPNSettings.tsx @@ -1,11 +1,17 @@ -import * as React from 'react'; +import { useCallback, useMemo } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { strings } from '../../config.json'; -import { BridgeState, RelayProtocol } from '../../shared/daemon-rpc-types'; +import { BridgeState, RelayProtocol, TunnelProtocol } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; +import log from '../../shared/logging'; +import RelaySettingsBuilder from '../../shared/relay-settings-builder'; +import { useAppContext } from '../context'; +import { useHistory } from '../lib/history'; +import { useBoolean } from '../lib/utilityHooks'; import { formatMarkdown } from '../markdown-formatter'; +import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; @@ -49,226 +55,86 @@ export const StyledSelectorContainer = styled.div({ flex: 0, }); -interface IProps { - bridgeModeAvailablity: BridgeModeAvailability; - openvpn: { - protocol?: RelayProtocol; - port?: number; - }; - mssfix?: number; - bridgeState: BridgeState; - setOpenVpnMssfix: (value: number | undefined) => void; - setOpenVpnRelayProtocolAndPort: (protocol?: RelayProtocol, port?: number) => void; - setBridgeState: (value: BridgeState) => void; - onClose: () => void; -} - -interface IState { - showBridgeStateConfirmationDialog: boolean; -} - -export default class OpenVpnSettings extends React.Component<IProps, IState> { - public state = { showBridgeStateConfirmationDialog: false }; +export default function OpenVpnSettings() { + const { pop } = useHistory(); - private portItems: { [key in RelayProtocol]: Array<ISelectorItem<OptionalPort>> }; + const relaySettings = useSelector((state) => state.settings.relaySettings); - constructor(props: IProps) { - super(props); + const protocol = useMemo(() => { + const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined; + return protocol === 'any' ? undefined : protocol; + }, [relaySettings]); - const automaticPort: ISelectorItem<OptionalPort> = { - label: messages.gettext('Automatic'), - value: undefined, - }; + return ( + <BackAction action={pop}> + <Layout> + <SettingsContainer> + <NavigationContainer> + <NavigationBar> + <NavigationItems> + <TitleBarItem> + {sprintf( + // TRANSLATORS: Title label in navigation bar + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openvpn)s - Will be replaced with "OpenVPN" + messages.pgettext('openvpn-settings-nav', '%(openvpn)s settings'), + { openvpn: strings.openvpn }, + )} + </TitleBarItem> + </NavigationItems> + </NavigationBar> - this.portItems = { - udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)), - tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)), - }; - } + <NavigationScrollbars> + <SettingsHeader> + <HeaderTitle> + {sprintf( + // TRANSLATORS: %(openvpn)s will be replaced with "OpenVPN" + messages.pgettext('openvpn-settings-view', '%(openvpn)s settings'), + { + openvpn: strings.openvpn, + }, + )} + </HeaderTitle> + </SettingsHeader> - public render() { - return ( - <BackAction action={this.props.onClose}> - <Layout> - <SettingsContainer> - <NavigationContainer> - <NavigationBar> - <NavigationItems> - <TitleBarItem> - {sprintf( - // TRANSLATORS: Title label in navigation bar - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(openvpn)s - Will be replaced with "OpenVPN" - messages.pgettext('openvpn-settings-nav', '%(openvpn)s settings'), - { openvpn: strings.openvpn }, - )} - </TitleBarItem> - </NavigationItems> - </NavigationBar> - - <StyledNavigationScrollbars> - <SettingsHeader> - <HeaderTitle> - {sprintf( - // TRANSLATORS: %(openvpn)s will be replaced with "OpenVPN" - messages.pgettext('openvpn-settings-view', '%(openvpn)s settings'), - { - openvpn: strings.openvpn, - }, - )} - </HeaderTitle> - </SettingsHeader> + <Cell.Group> + <TransportProtocolSelector /> + </Cell.Group> + {protocol ? ( <Cell.Group> - <StyledSelectorContainer> - <AriaInputGroup> - <Selector - title={messages.pgettext('openvpn-settings-view', 'Transport protocol')} - values={this.protocolItems(this.props.bridgeState !== 'on')} - value={this.props.openvpn.protocol} - onSelect={this.onSelectOpenvpnProtocol} - /> - {this.props.bridgeState === 'on' && ( - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {formatMarkdown( - // TRANSLATORS: This is used to instruct users how to make UDP mode - // TRANSLATORS: available. - messages.pgettext( - 'openvpn-settings-view', - 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.', - ), - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - )} - </AriaInputGroup> - </StyledSelectorContainer> + <PortSelector /> </Cell.Group> + ) : undefined} - {this.props.openvpn.protocol ? ( - <Cell.Group> - <StyledSelectorContainer> - <AriaInputGroup> - <Selector - title={sprintf( - // TRANSLATORS: The title for the port selector section. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) - messages.pgettext('openvpn-settings-view', '%(portType)s port'), - { - portType: this.props.openvpn.protocol.toUpperCase(), - }, - )} - values={this.portItems[this.props.openvpn.protocol]} - value={this.props.openvpn.port} - onSelect={this.onSelectOpenVpnPort} - /> - </AriaInputGroup> - </StyledSelectorContainer> - </Cell.Group> - ) : undefined} + <Cell.Group> + <BridgeModeSelector /> + </Cell.Group> - <Cell.Group> - <AriaInputGroup> - <StyledSelectorContainer> - <Selector - title={ - // TRANSLATORS: The title for the shadowsocks bridge selector section. - messages.pgettext('openvpn-settings-view', 'Bridge mode') - } - values={this.bridgeStateItems( - this.props.bridgeModeAvailablity === BridgeModeAvailability.available, - )} - value={this.props.bridgeState} - onSelect={this.onSelectBridgeState} - /> - </StyledSelectorContainer> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText>{this.bridgeModeFooterText()}</Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - </Cell.Group> + <Cell.Group> + <MssFixSetting /> + </Cell.Group> + </NavigationScrollbars> + </NavigationContainer> + </SettingsContainer> + </Layout> + </BackAction> + ); +} - <Cell.Group> - <AriaInputGroup> - <Cell.Container> - <AriaLabel> - <Cell.InputLabel> - {messages.pgettext('openvpn-settings-view', 'Mssfix')} - </Cell.InputLabel> - </AriaLabel> - <AriaInput> - <Cell.AutoSizingTextInput - value={this.props.mssfix ? this.props.mssfix.toString() : ''} - inputMode={'numeric'} - maxLength={4} - placeholder={messages.gettext('Default')} - onSubmitValue={this.onMssfixSubmit} - validateValue={OpenVpnSettings.mssfixIsValid} - submitOnBlur={true} - modifyValue={OpenVpnSettings.removeNonNumericCharacters} - /> - </AriaInput> - </Cell.Container> - <Cell.Footer> - <AriaDescription> - <Cell.FooterText> - {sprintf( - // TRANSLATORS: The hint displayed below the Mssfix input field. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(openvpn)s - will be replaced with "OpenVPN" - // TRANSLATORS: %(max)d - the maximum possible mssfix value - // TRANSLATORS: %(min)d - the minimum possible mssfix value - messages.pgettext( - 'openvpn-settings-view', - 'Set %(openvpn)s MSS value. Valid range: %(min)d - %(max)d.', - ), - { - openvpn: strings.openvpn, - min: MIN_MSSFIX_VALUE, - max: MAX_MSSFIX_VALUE, - }, - )} - </Cell.FooterText> - </AriaDescription> - </Cell.Footer> - </AriaInputGroup> - </Cell.Group> - </StyledNavigationScrollbars> - </NavigationContainer> - </SettingsContainer> +function TransportProtocolSelector() { + const relaySettings = useSelector((state) => state.settings.relaySettings); + const bridgeState = useSelector((state) => state.settings.bridgeState); - {this.renderBridgeStateConfirmation()} - </Layout> - </BackAction> - ); - } + const protocol = useMemo(() => { + const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined; + return protocol === 'any' ? undefined : protocol; + }, [relaySettings]); - private bridgeStateItems(onAvailable: boolean): Array<ISelectorItem<BridgeState>> { - return [ - { - label: messages.gettext('Automatic'), - value: 'auto', - }, - { - label: messages.gettext('On'), - value: 'on', - disabled: !onAvailable, - }, - { - label: messages.gettext('Off'), - value: 'off', - }, - ]; - } + const protocolAndPortUpdater = useProtocolAndPortUpdater(); - private protocolItems(udpAvailable: boolean): Array<ISelectorItem<OptionalRelayProtocol>> { - return [ + const items: ISelectorItem<OptionalRelayProtocol>[] = useMemo( + () => [ { label: messages.gettext('Automatic'), value: undefined, @@ -280,125 +146,372 @@ export default class OpenVpnSettings extends React.Component<IProps, IState> { { label: messages.gettext('UDP'), value: 'udp', - disabled: !udpAvailable, + disabled: bridgeState === 'on', }, - ]; - } + ], + [bridgeState], + ); - private onSelectOpenvpnProtocol = (protocol?: RelayProtocol) => { - this.props.setOpenVpnRelayProtocolAndPort(protocol); - }; + return ( + <StyledSelectorContainer> + <AriaInputGroup> + <Selector + title={messages.pgettext('openvpn-settings-view', 'Transport protocol')} + values={items} + value={protocol} + onSelect={protocolAndPortUpdater} + /> + {bridgeState === 'on' && ( + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {formatMarkdown( + // TRANSLATORS: This is used to instruct users how to make UDP mode + // TRANSLATORS: available. + messages.pgettext( + 'openvpn-settings-view', + 'To activate UDP, change **Bridge mode** to **Automatic** or **Off**.', + ), + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + )} + </AriaInputGroup> + </StyledSelectorContainer> + ); +} + +function useProtocolAndPortUpdater() { + const { updateRelaySettings } = useAppContext(); + + const updater = useCallback( + async (protocol?: RelayProtocol, port?: number) => { + const relayUpdate = RelaySettingsBuilder.normal() + .tunnel.openvpn((openvpn) => { + if (protocol) { + openvpn.protocol.exact(protocol); + } else { + openvpn.protocol.any(); + } + + if (port) { + openvpn.port.exact(port); + } else { + openvpn.port.any(); + } + }) + .build(); - private onSelectOpenVpnPort = (port?: number) => { - this.props.setOpenVpnRelayProtocolAndPort(this.props.openvpn.protocol, port); + try { + await updateRelaySettings(relayUpdate); + } catch (e) { + const error = e as Error; + log.error('Failed to update relay settings', error.message); + } + }, + [updateRelaySettings], + ); + + return updater; +} + +function PortSelector() { + const relaySettings = useSelector((state) => state.settings.relaySettings); + + const protocol = useMemo(() => { + const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined; + return protocol === 'any' ? undefined : protocol; + }, [relaySettings]); + + const port = useMemo(() => { + const port = 'normal' in relaySettings ? relaySettings.normal.openvpn.port : undefined; + return port === 'any' ? undefined : port; + }, [relaySettings]); + + const protocolAndPortUpdater = useProtocolAndPortUpdater(); + + const onSelect = useCallback( + async (port?: number) => { + await protocolAndPortUpdater(protocol, port); + }, + [protocolAndPortUpdater, protocol], + ); + + const automaticPort: ISelectorItem<OptionalPort> = { + label: messages.gettext('Automatic'), + value: undefined, }; - private onMssfixSubmit = (value: string) => { - const parsedValue = value === '' ? undefined : parseInt(value, 10); - if (OpenVpnSettings.mssfixIsValid(value)) { - this.props.setOpenVpnMssfix(parsedValue); - } + const portItems = { + udp: [automaticPort].concat(UDP_PORTS.map(mapPortToSelectorItem)), + tcp: [automaticPort].concat(TCP_PORTS.map(mapPortToSelectorItem)), }; - private static removeNonNumericCharacters(value: string) { - return value.replace(/[^0-9]/g, ''); + if (protocol === undefined) { + return null; } - private static mssfixIsValid(mssfix: string): boolean { - const parsedMssFix = mssfix ? parseInt(mssfix) : undefined; - return ( - parsedMssFix === undefined || - (parsedMssFix >= MIN_MSSFIX_VALUE && parsedMssFix <= MAX_MSSFIX_VALUE) - ); - } - - private bridgeModeFooterText() { - switch (this.props.bridgeModeAvailablity) { - case BridgeModeAvailability.blockedDueToTunnelProtocol: - return formatMarkdown( - sprintf( - // TRANSLATORS: This is used to instruct users how to make the bridge mode setting - // TRANSLATORS: available. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(tunnelProtocol)s - the name of the tunnel protocol setting - // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN - messages.pgettext( - 'openvpn-settings-view', - 'To activate Bridge mode, go back and change **%(tunnelProtocol)s** to **%(openvpn)s**.', - ), - { - tunnelProtocol: messages.pgettext('vpn-settings-view', 'Tunnel protocol'), - openvpn: strings.openvpn, - }, - ), - ); - case BridgeModeAvailability.blockedDueToTransportProtocol: - return formatMarkdown( - sprintf( - // TRANSLATORS: This is used to instruct users how to make the bridge mode setting - // TRANSLATORS: available. + return ( + <StyledSelectorContainer> + <AriaInputGroup> + <Selector + title={sprintf( + // TRANSLATORS: The title for the port selector section. // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(transportProtocol)s - the name of the transport protocol setting - // TRANSLATORS: %(automat)s - the translation of "Automatic" - // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN - messages.pgettext( - 'openvpn-settings-view', - 'To activate Bridge mode, change **%(transportProtocol)s** to **%(automatic)s** or **%(tcp)s**.', - ), + // TRANSLATORS: %(portType)s - a selected protocol (either TCP or UDP) + messages.pgettext('openvpn-settings-view', '%(portType)s port'), { - transportProtocol: messages.pgettext('openvpn-settings-view', 'Transport protocol'), - automatic: messages.gettext('Automatic'), - tcp: messages.gettext('TCP'), + portType: protocol.toUpperCase(), }, - ), - ); - case BridgeModeAvailability.available: - return sprintf( - // TRANSLATORS: This is used as a description for the bridge mode - // TRANSLATORS: setting. - // TRANSLATORS: Available placeholders: - // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN - messages.pgettext( - 'openvpn-settings-view', - 'Helps circumvent censorship, by routing your traffic through a bridge server before reaching an %(openvpn)s server. Obfuscation is added to make fingerprinting harder.', - ), - { openvpn: strings.openvpn }, - ); - } - } + )} + values={portItems[protocol]} + value={port} + onSelect={onSelect} + /> + </AriaInputGroup> + </StyledSelectorContainer> + ); +} + +function BridgeModeSelector() { + const { setBridgeState: setBridgeStateImpl } = useAppContext(); + const relaySettings = useSelector((state) => state.settings.relaySettings); + + const bridgeState = useSelector((state) => state.settings.bridgeState); + + const tunnelProtocol = useMemo(() => { + const protocol = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol : undefined; + return protocol === 'any' ? undefined : protocol; + }, [relaySettings]); + + const transportProtocol = useMemo(() => { + const protocol = 'normal' in relaySettings ? relaySettings.normal.openvpn.protocol : undefined; + return protocol === 'any' ? undefined : protocol; + }, [relaySettings]); + + const options: ISelectorItem<BridgeState>[] = useMemo( + () => [ + { + label: messages.gettext('Automatic'), + value: 'auto', + }, + { + label: messages.gettext('On'), + value: 'on', + disabled: tunnelProtocol !== 'openvpn' || transportProtocol === 'udp', + }, + { + label: messages.gettext('Off'), + value: 'off', + }, + ], + [tunnelProtocol, transportProtocol], + ); + + const [confirmationDialogVisible, showConfirmationDialog, hideConfirmationDialog] = useBoolean(); + + const setBridgeState = useCallback( + async (bridgeState: BridgeState) => { + try { + await setBridgeStateImpl(bridgeState); + } catch (e) { + const error = e as Error; + log.error(`Failed to update bridge state: ${error.message}`); + } + }, + [setBridgeStateImpl], + ); + + const onSelectBridgeState = useCallback( + async (newValue: BridgeState) => { + if (newValue === 'on') { + showConfirmationDialog(); + } else { + await setBridgeState(newValue); + } + }, + [showConfirmationDialog, setBridgeState], + ); + + const confirmBridgeState = useCallback(async () => { + hideConfirmationDialog(); + await setBridgeState('on'); + }, [hideConfirmationDialog, setBridgeState]); - private renderBridgeStateConfirmation = () => { - return ( + return ( + <> + <AriaInputGroup> + <StyledSelectorContainer> + <Selector + title={ + // TRANSLATORS: The title for the shadowsocks bridge selector section. + messages.pgettext('openvpn-settings-view', 'Bridge mode') + } + values={options} + value={bridgeState} + onSelect={onSelectBridgeState} + /> + </StyledSelectorContainer> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {bridgeModeFooterText(tunnelProtocol, transportProtocol)} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> <ModalAlert - isOpen={this.state.showBridgeStateConfirmationDialog} + isOpen={confirmationDialogVisible} type={ModalAlertType.info} message={messages.gettext('This setting increases latency. Use only if needed.')} buttons={[ - <AppButton.RedButton key="confirm" onClick={this.confirmBridgeState}> + <AppButton.RedButton key="confirm" onClick={confirmBridgeState}> {messages.gettext('Enable anyway')} </AppButton.RedButton>, - <AppButton.BlueButton key="back" onClick={this.hideBridgeStateConfirmationDialog}> + <AppButton.BlueButton key="back" onClick={hideConfirmationDialog}> {messages.gettext('Back')} </AppButton.BlueButton>, ]} - close={this.hideBridgeStateConfirmationDialog}></ModalAlert> + close={hideConfirmationDialog} + /> + </> + ); +} + +function bridgeModeFooterText(tunnelProtocol?: TunnelProtocol, transportProtocol?: RelayProtocol) { + if (tunnelProtocol !== 'openvpn') { + return formatMarkdown( + sprintf( + // TRANSLATORS: This is used to instruct users how to make the bridge mode setting + // TRANSLATORS: available. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(tunnelProtocol)s - the name of the tunnel protocol setting + // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN + messages.pgettext( + 'openvpn-settings-view', + 'To activate Bridge mode, go back and change **%(tunnelProtocol)s** to **%(openvpn)s**.', + ), + { + tunnelProtocol: messages.pgettext('vpn-settings-view', 'Tunnel protocol'), + openvpn: strings.openvpn, + }, + ), ); - }; + } else if (transportProtocol === 'udp') { + return formatMarkdown( + sprintf( + // TRANSLATORS: This is used to instruct users how to make the bridge mode setting + // TRANSLATORS: available. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(transportProtocol)s - the name of the transport protocol setting + // TRANSLATORS: %(automat)s - the translation of "Automatic" + // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN + messages.pgettext( + 'openvpn-settings-view', + 'To activate Bridge mode, change **%(transportProtocol)s** to **%(automatic)s** or **%(tcp)s**.', + ), + { + transportProtocol: messages.pgettext('openvpn-settings-view', 'Transport protocol'), + automatic: messages.gettext('Automatic'), + tcp: messages.gettext('TCP'), + }, + ), + ); + } else { + return sprintf( + // TRANSLATORS: This is used as a description for the bridge mode + // TRANSLATORS: setting. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openvpn)s - will be replaced with OpenVPN + messages.pgettext( + 'openvpn-settings-view', + 'Helps circumvent censorship, by routing your traffic through a bridge server before reaching an %(openvpn)s server. Obfuscation is added to make fingerprinting harder.', + ), + { openvpn: strings.openvpn }, + ); + } +} - private onSelectBridgeState = (newValue: BridgeState) => { - if (newValue === 'on') { - this.setState({ showBridgeStateConfirmationDialog: true }); - } else { - this.props.setBridgeState(newValue); - } - }; +function removeNonNumericCharacters(value: string) { + return value.replace(/[^0-9]/g, ''); +} - private hideBridgeStateConfirmationDialog = () => { - this.setState({ showBridgeStateConfirmationDialog: false }); - }; +function mssfixIsValid(mssfix: string): boolean { + const parsedMssFix = mssfix ? parseInt(mssfix) : undefined; + return ( + parsedMssFix === undefined || + (parsedMssFix >= MIN_MSSFIX_VALUE && parsedMssFix <= MAX_MSSFIX_VALUE) + ); +} - private confirmBridgeState = () => { - this.setState({ showBridgeStateConfirmationDialog: false }); - this.props.setBridgeState('on'); - }; +function MssFixSetting() { + const { setOpenVpnMssfix: setOpenVpnMssfixImpl } = useAppContext(); + const mssfix = useSelector((state) => state.settings.openVpn.mssfix); + + const setOpenVpnMssfix = useCallback( + async (mssfix?: number) => { + try { + await setOpenVpnMssfixImpl(mssfix); + } catch (e) { + const error = e as Error; + log.error('Failed to update mssfix value', error.message); + } + }, + [setOpenVpnMssfixImpl], + ); + + const onMssfixSubmit = useCallback( + async (value: string) => { + const parsedValue = value === '' ? undefined : parseInt(value, 10); + if (mssfixIsValid(value)) { + await setOpenVpnMssfix(parsedValue); + } + }, + [setOpenVpnMssfix], + ); + + return ( + <AriaInputGroup> + <Cell.Container> + <AriaLabel> + <Cell.InputLabel>{messages.pgettext('openvpn-settings-view', 'Mssfix')}</Cell.InputLabel> + </AriaLabel> + <AriaInput> + <Cell.AutoSizingTextInput + value={mssfix ? mssfix.toString() : ''} + inputMode={'numeric'} + maxLength={4} + placeholder={messages.gettext('Default')} + onSubmitValue={onMssfixSubmit} + validateValue={mssfixIsValid} + submitOnBlur={true} + modifyValue={removeNonNumericCharacters} + /> + </AriaInput> + </Cell.Container> + <Cell.Footer> + <AriaDescription> + <Cell.FooterText> + {sprintf( + // TRANSLATORS: The hint displayed below the Mssfix input field. + // TRANSLATORS: Available placeholders: + // TRANSLATORS: %(openvpn)s - will be replaced with "OpenVPN" + // TRANSLATORS: %(max)d - the maximum possible mssfix value + // TRANSLATORS: %(min)d - the minimum possible mssfix value + messages.pgettext( + 'openvpn-settings-view', + 'Set %(openvpn)s MSS value. Valid range: %(min)d - %(max)d.', + ), + { + openvpn: strings.openvpn, + min: MIN_MSSFIX_VALUE, + max: MAX_MSSFIX_VALUE, + }, + )} + </Cell.FooterText> + </AriaDescription> + </Cell.Footer> + </AriaInputGroup> + ); } diff --git a/gui/src/renderer/containers/OpenVPNSettingsPage.tsx b/gui/src/renderer/containers/OpenVPNSettingsPage.tsx deleted file mode 100644 index ab42f4a137..0000000000 --- a/gui/src/renderer/containers/OpenVPNSettingsPage.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { connect } from 'react-redux'; - -import { BridgeState, RelayProtocol } from '../../shared/daemon-rpc-types'; -import log from '../../shared/logging'; -import RelaySettingsBuilder from '../../shared/relay-settings-builder'; -import OpenVPNSettings, { BridgeModeAvailability } from '../components/OpenVPNSettings'; -import withAppContext, { IAppContext } from '../context'; -import { IHistoryProps, withHistory } from '../lib/history'; -import { RelaySettingsRedux } from '../redux/settings/reducers'; -import { IReduxState, ReduxDispatch } from '../redux/store'; - -const mapStateToProps = (state: IReduxState) => { - const protocolAndPort = mapRelaySettingsToProtocolAndPort(state.settings.relaySettings); - - let bridgeModeAvailablity = BridgeModeAvailability.available; - if (mapRelaySettingsToProtocol(state.settings.relaySettings) !== 'openvpn') { - bridgeModeAvailablity = BridgeModeAvailability.blockedDueToTunnelProtocol; - } else if (protocolAndPort.openvpn.protocol === 'udp') { - bridgeModeAvailablity = BridgeModeAvailability.blockedDueToTransportProtocol; - } - - return { - bridgeModeAvailablity, - mssfix: state.settings.openVpn.mssfix, - bridgeState: state.settings.bridgeState, - ...protocolAndPort, - }; -}; - -const mapRelaySettingsToProtocol = (relaySettings: RelaySettingsRedux) => { - if ('normal' in relaySettings) { - const { tunnelProtocol } = relaySettings.normal; - return tunnelProtocol === 'any' ? undefined : tunnelProtocol; - // since the GUI doesn't display custom settings, just display the default ones. - // If the user sets any settings, then those will be applied. - } else if ('customTunnelEndpoint' in relaySettings) { - return undefined; - } else { - throw new Error('Unknown type of relay settings.'); - } -}; - -const mapRelaySettingsToProtocolAndPort = (relaySettings: RelaySettingsRedux) => { - if ('normal' in relaySettings) { - const { openvpn } = relaySettings.normal; - return { - openvpn: { - protocol: openvpn.protocol === 'any' ? undefined : openvpn.protocol, - port: openvpn.port === 'any' ? undefined : openvpn.port, - }, - }; - // since the GUI doesn't display custom settings, just display the default ones. - // If the user sets any settings, then those will be applied. - } else if ('customTunnelEndpoint' in relaySettings) { - return { - openvpn: { - protocol: undefined, - port: undefined, - }, - }; - } else { - throw new Error('Unknown type of relay settings.'); - } -}; - -const mapDispatchToProps = (_dispatch: ReduxDispatch, props: IHistoryProps & IAppContext) => { - return { - onClose: () => { - props.history.pop(); - }, - setOpenVpnRelayProtocolAndPort: async (protocol?: RelayProtocol, port?: number) => { - const relayUpdate = RelaySettingsBuilder.normal() - .tunnel.openvpn((openvpn) => { - if (protocol) { - openvpn.protocol.exact(protocol); - } else { - openvpn.protocol.any(); - } - - if (port) { - openvpn.port.exact(port); - } else { - openvpn.port.any(); - } - }) - .build(); - - try { - await props.app.updateRelaySettings(relayUpdate); - } catch (e) { - const error = e as Error; - log.error('Failed to update relay settings', error.message); - } - }, - - setWireguardRelayPort: async (port?: number) => { - const relayUpdate = RelaySettingsBuilder.normal() - .tunnel.wireguard((wireguard) => { - if (port) { - wireguard.port.exact(port); - } else { - wireguard.port.any(); - } - }) - .build(); - try { - await props.app.updateRelaySettings(relayUpdate); - } catch (e) { - const error = e as Error; - log.error('Failed to update relay settings', error.message); - } - }, - - setBridgeState: async (bridgeState: BridgeState) => { - try { - await props.app.setBridgeState(bridgeState); - } catch (e) { - const error = e as Error; - log.error(`Failed to update bridge state: ${error.message}`); - } - }, - - setOpenVpnMssfix: async (mssfix?: number) => { - try { - await props.app.setOpenVpnMssfix(mssfix); - } catch (e) { - const error = e as Error; - log.error('Failed to update mssfix value', error.message); - } - }, - }; -}; - -export default withAppContext( - withHistory(connect(mapStateToProps, mapDispatchToProps)(OpenVPNSettings)), -); |
