1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Allow user to turn off ENS DNS resolution (#20102)

* Allow user to turn off IPFS gateway resolution

* Add end to end test for toggle on and off

* Fix jest tests and snapshots

* Change variable name

* Implement provided content

* Use MetaMask eth instead

* Allow searching for ENS setting

* Fix jest

---------

Co-authored-by: Brad Decker <bhdecker84@gmail.com>
This commit is contained in:
David Walsh 2023-07-28 11:21:43 -05:00 committed by GitHub
parent 05b1b19c6f
commit 99c709ff8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 379 additions and 3 deletions

View File

@ -1426,6 +1426,24 @@
"enhancedTokenDetectionAlertMessage": {
"message": "Enhanced token detection is currently available on $1. $2"
},
"ensDomainsSettingDescriptionIntro": {
"message": "MetaMask lets you see ENS domains like \"https://metamask.eth\" right in your browser's address bar. Here's how it works:"
},
"ensDomainsSettingDescriptionOutro": {
"message": "Regular browsers don't usually handle ENS or IPFS addresses, but MetaMask helps with that. Using this feature might share your IP address with IPFS third-party services."
},
"ensDomainsSettingDescriptionPoint1": {
"message": "MetaMask checks with Ethereum's ENS contract to find the code connected to the ENS name."
},
"ensDomainsSettingDescriptionPoint2": {
"message": "If the code is linked to IPFS, it gets the content from the IPFS network."
},
"ensDomainsSettingDescriptionPoint3": {
"message": "Then, you can see the content, usually a website or something similar."
},
"ensDomainsSettingTitle": {
"message": "Show ENS domains in address bar"
},
"ensIllegalCharacter": {
"message": "Illegal character for ENS."
},

View File

@ -466,6 +466,9 @@ export function setupController(
getIpfsGateway: controller.preferencesController.getIpfsGateway.bind(
controller.preferencesController,
),
getUseAddressBarEnsResolution: () =>
controller.preferencesController.store.getState()
.useAddressBarEnsResolution,
provider: controller.provider,
});

View File

@ -67,6 +67,7 @@ export default class PreferencesController {
},
// ENS decentralized website resolution
ipfsGateway: IPFS_DEFAULT_GATEWAY_URL,
useAddressBarEnsResolution: true,
infuraBlocked: null,
ledgerTransportType: window.navigator.hid
? LedgerTransportTypes.webhid
@ -480,6 +481,15 @@ export default class PreferencesController {
return domain;
}
/**
* A setter for the `useAddressBarEnsResolution` property
*
* @param {boolean} useAddressBarEnsResolution - Whether or not user prefers IPFS resolution for domains
*/
async setUseAddressBarEnsResolution(useAddressBarEnsResolution) {
this.store.updateState({ useAddressBarEnsResolution });
}
/**
* A setter for the `ledgerTransportType` property.
*

View File

@ -13,6 +13,7 @@ export default function setupEnsIpfsResolver({
provider,
getCurrentChainId,
getIpfsGateway,
getUseAddressBarEnsResolution,
}) {
// install listener
const urlPatterns = supportedTopLevelDomains.map((tld) => `*://*.${tld}/*`);
@ -33,7 +34,12 @@ export default function setupEnsIpfsResolver({
const { tabId, url } = details;
// ignore requests that are not associated with tabs
// only attempt ENS resolution on mainnet
if (tabId === -1 || getCurrentChainId() !== '0x1') {
if (
(tabId === -1 || getCurrentChainId() !== '0x1') &&
// E2E tests use a chain other than 0x1, so for testing,
// allow the reuqest to pass through
!process.env.IN_TEST
) {
return;
}
// parse ens name
@ -50,9 +56,23 @@ export default function setupEnsIpfsResolver({
async function attemptResolve({ tabId, name, pathname, search, fragment }) {
const ipfsGateway = getIpfsGateway();
const useAddressBarEnsResolution = getUseAddressBarEnsResolution();
if (!useAddressBarEnsResolution || ipfsGateway === '') {
return;
}
browser.tabs.update(tabId, { url: `loading.html` });
let url = `https://app.ens.domains/name/${name}`;
// If we're testing ENS domain resolution support,
// we assume the ENS domains URL
if (process.env.IN_TEST) {
browser.tabs.update(tabId, { url });
return;
}
try {
const { type, hash } = await resolveEnsToIpfsContentId({
provider,

View File

@ -2233,6 +2233,10 @@ export default class MetamaskController extends EventEmitter {
setIpfsGateway: preferencesController.setIpfsGateway.bind(
preferencesController,
),
setUseAddressBarEnsResolution:
preferencesController.setUseAddressBarEnsResolution.bind(
preferencesController,
),
setParticipateInMetaMetrics:
metaMetricsController.setParticipateInMetaMetrics.bind(
metaMetricsController,

View File

@ -190,7 +190,14 @@ async function withFixtures(options, testSuite) {
if (phishingPageServer.isRunning()) {
await phishingPageServer.quit();
}
await mockServer.stop();
// Since mockServer could be stop'd at another location,
// use a try/catch to avoid an error
try {
await mockServer.stop();
} catch (e) {
console.log('mockServer already stopped');
}
}
}
}

View File

@ -0,0 +1,79 @@
const { strict: assert } = require('assert');
const { buildWebDriver } = require('../webdriver');
const { withFixtures } = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
describe('Settings', function () {
const ENS_NAME = 'metamask.eth';
const ENS_NAME_URL = `https://${ENS_NAME}/`;
const ENS_DESTINATION_URL = `https://app.ens.domains/name/${ENS_NAME}`;
it('Redirects to ENS domains when user inputs ENS into address bar', async function () {
// Using proxy port that doesn't resolve so that the browser can error out properly
// on the ".eth" hostname. The proxy does too much interference with 8000.
const { driver } = await buildWebDriver({ proxyUrl: '127.0.0.1:8001' });
await driver.navigate();
// The setting defaults to "on" so we can simply enter an ENS address
// into the address bar and listen for address change
try {
await driver.openNewPage(ENS_NAME_URL);
} catch (e) {
// Ignore ERR_PROXY_CONNECTION_FAILED error
// since all we care about is getting to the correct URL
}
// Ensure that the redirect to ENS Domains has happened
const currentUrl = await driver.getCurrentUrl();
assert.equal(currentUrl, ENS_DESTINATION_URL);
await driver.quit();
});
it('Does not lookup IPFS data for ENS Domain when switched off', async function () {
let server;
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
title: this.test.title,
testSpecificMock: (mockServer) => {
server = mockServer;
},
},
async ({ driver }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
// goes to the settings screen
await driver.clickElement(
'[data-testid="account-options-menu-button"]',
);
await driver.clickElement({ text: 'Settings', tag: 'div' });
await driver.clickElement({ text: 'Security & privacy', tag: 'div' });
// turns off IPFS domain resolution
await driver.clickElement(
'[data-testid="ipfs-gateway-resolution-container"] .toggle-button',
);
// Now that we no longer need the MetaMask UI, and want the browser
// to handle the request error, we need to stop the server
await server.stop();
try {
await driver.openNewPage(ENS_NAME_URL);
} catch (e) {
// Ignore ERR_PROXY_CONNECTION_FAILED error
// since all we care about is getting to the correct URL
}
// Ensure that the redirect to ENS Domains does not happen
// Instead, the domain will be kept which is a 404
const currentUrl = await driver.getCurrentUrl();
assert.equal(currentUrl, ENS_NAME_URL);
},
);
});
});

View File

@ -205,6 +205,13 @@ export const SETTINGS_CONSTANTS = [
route: `${SECURITY_ROUTE}#price-checker`,
icon: 'fa fa-lock',
},
{
tabMessage: (t) => t('securityAndPrivacy'),
sectionMessage: (t) => t('ensDomainsSettingTitle'),
descriptionMessage: (t) => t('ensDomainsSettingDescriptionIntro'),
route: `${SECURITY_ROUTE}#ens-domains`,
icon: 'fa fa-lock',
},
{
tabMessage: (t) => t('alerts'),
sectionMessage: (t) => t('alertSettingsUnconnectedAccount'),

View File

@ -165,7 +165,7 @@ describe('Settings Search Utils', () => {
it('should get good security & privacy section number', () => {
expect(
getNumberOfSettingsInSection(t, t('securityAndPrivacy')),
).toStrictEqual(9);
).toStrictEqual(10);
});
it('should get good alerts section number', () => {

View File

@ -13,6 +13,7 @@ import {
} from '../../../../shared/lib/ui-utils';
import {
PickerNetwork,
Text,
TextField,
} from '../../../components/component-library';
import Box from '../../../components/ui/box/box';
@ -22,6 +23,7 @@ import { MetaMetricsContext } from '../../../contexts/metametrics';
import {
FONT_WEIGHT,
TextColor,
TextVariant,
TypographyVariant,
} from '../../../helpers/constants/design-system';
import { ONBOARDING_PIN_EXTENSION_ROUTE } from '../../../helpers/constants/routes';
@ -35,6 +37,7 @@ import {
setUseMultiAccountBalanceChecker,
setUsePhishDetect,
setUseTokenDetection,
setUseAddressBarEnsResolution,
showModal,
toggleNetworkMenu,
} from '../../../store/actions';
@ -54,6 +57,7 @@ export default function PrivacySettings() {
setMultiAccountBalanceCheckerEnabled,
] = useState(true);
const [ipfsURL, setIPFSURL] = useState('');
const [addressBarResolution, setAddressBarResolution] = useState(true);
const [ipfsError, setIPFSError] = useState(null);
const trackEvent = useContext(MetaMetricsContext);
@ -70,6 +74,7 @@ export default function PrivacySettings() {
);
dispatch(setUseCurrencyRateCheck(turnOnCurrencyRateCheck));
dispatch(setCompletedOnboarding());
dispatch(setUseAddressBarEnsResolution(addressBarResolution));
if (ipfsURL && !ipfsError) {
const { host } = new URL(addUrlProtocolPrefix(ipfsURL));
@ -252,6 +257,38 @@ export default function PrivacySettings() {
</>
}
/>
<Setting
value={addressBarResolution}
setValue={setAddressBarResolution}
title={t('ensDomainsSettingTitle')}
description={
<>
<Text variant={TextVariant.inherit}>
{t('ensDomainsSettingDescriptionIntro')}
</Text>
<Box
as="ul"
marginTop={4}
marginBottom={4}
paddingInlineStart={4}
style={{ listStyleType: 'circle' }}
>
<Text variant={TextVariant.inherit} as="li">
{t('ensDomainsSettingDescriptionPoint1')}
</Text>
<Text variant={TextVariant.inherit} as="li">
{t('ensDomainsSettingDescriptionPoint2')}
</Text>
<Text variant={TextVariant.inherit} as="li">
{t('ensDomainsSettingDescriptionPoint3')}
</Text>
</Box>
<Text variant={TextVariant.inherit}>
{t('ensDomainsSettingDescriptionOutro')}
</Text>
</>
}
/>
<Setting
value={turnOnCurrencyRateCheck}
setValue={setTurnOnCurrencyRateCheck}

View File

@ -28,6 +28,7 @@ describe('Privacy Settings Onboarding View', () => {
.fn()
.mockImplementation(() => Promise.resolve());
const setUseMultiAccountBalanceCheckerStub = jest.fn();
const setUseAddressBarEnsResolutionStub = jest.fn();
setBackgroundConnection({
setFeatureFlag: setFeatureFlagStub,
@ -37,6 +38,7 @@ describe('Privacy Settings Onboarding View', () => {
setIpfsGateway: setIpfsGatewayStub,
completeOnboarding: completeOnboardingStub,
setUseMultiAccountBalanceChecker: setUseMultiAccountBalanceCheckerStub,
setUseAddressBarEnsResolution: setUseAddressBarEnsResolutionStub,
});
it('should update preferences', () => {
@ -50,6 +52,7 @@ describe('Privacy Settings Onboarding View', () => {
expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(0);
expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(0);
expect(setUseCurrencyRateCheckStub).toHaveBeenCalledTimes(0);
expect(setUseAddressBarEnsResolutionStub).toHaveBeenCalledTimes(0);
const toggles = container.querySelectorAll('input[type=checkbox]');
const submitButton = getByText('Done');
@ -60,6 +63,7 @@ describe('Privacy Settings Onboarding View', () => {
fireEvent.click(toggles[2]);
fireEvent.click(toggles[3]);
fireEvent.click(toggles[4]);
fireEvent.click(toggles[5]);
fireEvent.click(submitButton);
expect(setFeatureFlagStub).toHaveBeenCalledTimes(1);
@ -67,6 +71,7 @@ describe('Privacy Settings Onboarding View', () => {
expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(1);
expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(1);
expect(setUseCurrencyRateCheckStub).toHaveBeenCalledTimes(1);
expect(setUseAddressBarEnsResolutionStub).toHaveBeenCalledTimes(1);
expect(setFeatureFlagStub.mock.calls[0][1]).toStrictEqual(false);
expect(setUsePhishDetectStub.mock.calls[0][0]).toStrictEqual(false);
@ -75,6 +80,9 @@ describe('Privacy Settings Onboarding View', () => {
false,
);
expect(setUseCurrencyRateCheckStub.mock.calls[0][0]).toStrictEqual(false);
expect(setUseAddressBarEnsResolutionStub.mock.calls[0][0]).toStrictEqual(
false,
);
// toggle back to true
fireEvent.click(toggles[0]);
@ -82,12 +90,14 @@ describe('Privacy Settings Onboarding View', () => {
fireEvent.click(toggles[2]);
fireEvent.click(toggles[3]);
fireEvent.click(toggles[4]);
fireEvent.click(toggles[5]);
fireEvent.click(submitButton);
expect(setFeatureFlagStub).toHaveBeenCalledTimes(2);
expect(setUsePhishDetectStub).toHaveBeenCalledTimes(2);
expect(setUseTokenDetectionStub).toHaveBeenCalledTimes(2);
expect(setUseMultiAccountBalanceCheckerStub).toHaveBeenCalledTimes(2);
expect(setUseCurrencyRateCheckStub).toHaveBeenCalledTimes(2);
expect(setUseAddressBarEnsResolutionStub).toHaveBeenCalledTimes(2);
expect(setFeatureFlagStub.mock.calls[1][1]).toStrictEqual(true);
expect(setUsePhishDetectStub.mock.calls[1][0]).toStrictEqual(true);
@ -96,6 +106,9 @@ describe('Privacy Settings Onboarding View', () => {
true,
);
expect(setUseCurrencyRateCheckStub.mock.calls[1][0]).toStrictEqual(true);
expect(setUseAddressBarEnsResolutionStub.mock.calls[1][0]).toStrictEqual(
true,
);
});
describe('IPFS', () => {

View File

@ -405,6 +405,100 @@ exports[`Security Tab should match snapshot 1`] = `
</div>
</div>
</div>
<div
class="settings-page__content-item"
id="ens-domains"
>
Show ENS domains in address bar
<div
class="settings-page__content-description"
>
<span
class="mm-box mm-text mm-text--inherit mm-box--color-inherit"
>
MetaMask lets you see ENS domains like "https://metamask.eth" right in your browser's address bar. Here's how it works:
</span>
<ul
class="mm-box mm-box--margin-top-4 mm-box--margin-bottom-4 mm-box--padding-inline-start-4"
style="list-style-type: circle;"
>
<li
class="mm-box mm-text mm-text--inherit mm-box--color-inherit"
>
MetaMask checks with Ethereum's ENS contract to find the code connected to the ENS name.
</li>
<li
class="mm-box mm-text mm-text--inherit mm-box--color-inherit"
>
If the code is linked to IPFS, it gets the content from the IPFS network.
</li>
<li
class="mm-box mm-text mm-text--inherit mm-box--color-inherit"
>
Then, you can see the content, usually a website or something similar.
</li>
</ul>
<span
class="mm-box mm-text mm-text--inherit mm-box--color-inherit"
>
Regular browsers don't usually handle ENS or IPFS addresses, but MetaMask helps with that. Using this feature might share your IP address with IPFS third-party services.
</span>
</div>
</div>
<div
class="settings-page__content-item"
>
<div
class="settings-page__content-item-col"
data-testid="ipfs-gateway-resolution-container"
>
<label
class="toggle-button toggle-button--off"
tabindex="0"
>
<div
style="display: flex; width: 52px; align-items: center; justify-content: flex-start; position: relative; cursor: pointer; background-color: transparent; border: 0px; padding: 0px; user-select: none;"
>
<div
style="width: 40px; height: 24px; padding: 0px; border-radius: 26px; display: flex; align-items: center; justify-content: center; background-color: rgb(242, 244, 246);"
>
<div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgb(250, 250, 250); margin-top: auto; margin-bottom: auto; line-height: 0; opacity: 0; width: 26px; height: 20px; left: 4px;"
/>
<div
style="font-size: 11px; display: flex; align-items: center; justify-content: center; font-family: 'Helvetica Neue', Helvetica, sans-serif; position: relative; color: rgba(255, 255, 255, 0.6); bottom: 0px; margin-top: auto; margin-bottom: auto; padding-right: 5px; line-height: 0; width: 26px; height: 20px; opacity: 1;"
/>
</div>
<div
style="position: absolute; height: 100%; top: 0px; left: 0px; display: flex; flex: 1; align-self: stretch; align-items: center; justify-content: flex-start;"
>
<div
style="width: 18px; height: 18px; display: flex; align-self: center; box-shadow: none; border-radius: 50%; box-sizing: border-box; position: relative; background-color: rgb(106, 115, 125); left: 3px;"
/>
</div>
<input
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px;"
type="checkbox"
value="false"
/>
</div>
<div
class="toggle-button__status"
>
<span
class="toggle-button__label-off"
>
Off
</span>
<span
class="toggle-button__label-on"
>
On
</span>
</div>
</label>
</div>
</div>
</div>
</div>
<span

View File

@ -31,6 +31,11 @@ import {
getNumberOfSettingsInSection,
handleSettingsRefs,
} from '../../../helpers/utils/settings-search';
import { Box, Text } from '../../../components/component-library';
import {
TextColor,
TextVariant,
} from '../../../helpers/constants/design-system';
export default class SecurityTab extends PureComponent {
static contextTypes = {
@ -55,6 +60,8 @@ export default class SecurityTab extends PureComponent {
setUseMultiAccountBalanceChecker: PropTypes.func.isRequired,
useCurrencyRateCheck: PropTypes.bool.isRequired,
setUseCurrencyRateCheck: PropTypes.func.isRequired,
useAddressBarEnsResolution: PropTypes.bool.isRequired,
setUseAddressBarEnsResolution: PropTypes.func.isRequired,
};
state = {
@ -309,6 +316,8 @@ export default class SecurityTab extends PureComponent {
renderIpfsGatewayControl() {
const { t } = this.context;
const { ipfsGatewayError } = this.state;
const { useAddressBarEnsResolution, setUseAddressBarEnsResolution } =
this.props;
const handleIpfsGatewaySave = (gateway) => {
const url = new URL(addUrlProtocolPrefix(gateway));
@ -370,6 +379,63 @@ export default class SecurityTab extends PureComponent {
/>
</div>
</div>
<div
className="settings-page__content-item"
ref={this.settingsRefs[9]}
id="ens-domains"
>
{t('ensDomainsSettingTitle')}
<div className="settings-page__content-description">
<Text color={TextColor.inherit} variant={TextVariant.inherit}>
{t('ensDomainsSettingDescriptionIntro')}
</Text>
<Box
as="ul"
marginTop={4}
marginBottom={4}
paddingInlineStart={4}
style={{ listStyleType: 'circle' }}
>
<Text
as="li"
color={TextColor.inherit}
variant={TextVariant.inherit}
>
{t('ensDomainsSettingDescriptionPoint1')}
</Text>
<Text
as="li"
color={TextColor.inherit}
variant={TextVariant.inherit}
>
{t('ensDomainsSettingDescriptionPoint2')}
</Text>
<Text
as="li"
color={TextColor.inherit}
variant={TextVariant.inherit}
>
{t('ensDomainsSettingDescriptionPoint3')}
</Text>
</Box>
<Text color={TextColor.inherit} variant={TextVariant.inherit}>
{t('ensDomainsSettingDescriptionOutro')}
</Text>
</div>
</div>
<div className="settings-page__content-item">
<div
className="settings-page__content-item-col"
data-testid="ipfs-gateway-resolution-container"
>
<ToggleButton
value={useAddressBarEnsResolution}
onToggle={(value) => setUseAddressBarEnsResolution(!value)}
offLabel={t('off')}
onLabel={t('on')}
/>
</div>
</div>
</div>
);
}

View File

@ -9,6 +9,7 @@ import {
setUseMultiAccountBalanceChecker,
setUsePhishDetect,
setUseTokenDetection,
setUseAddressBarEnsResolution,
} from '../../../store/actions';
import SecurityTab from './security-tab.component';
@ -25,6 +26,7 @@ const mapStateToProps = (state) => {
ipfsGateway,
useMultiAccountBalanceChecker,
useCurrencyRateCheck,
useAddressBarEnsResolution,
} = metamask;
return {
@ -36,6 +38,7 @@ const mapStateToProps = (state) => {
ipfsGateway,
useMultiAccountBalanceChecker,
useCurrencyRateCheck,
useAddressBarEnsResolution,
};
};
@ -56,6 +59,8 @@ const mapDispatchToProps = (dispatch) => {
setUseMultiAccountBalanceChecker: (value) => {
return dispatch(setUseMultiAccountBalanceChecker(value));
},
setUseAddressBarEnsResolution: (value) =>
dispatch(setUseAddressBarEnsResolution(value)),
};
};

View File

@ -3095,6 +3095,19 @@ export function setIpfsGateway(
};
}
export function setUseAddressBarEnsResolution(
val: string,
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {
return (dispatch: MetaMaskReduxDispatch) => {
log.debug(`background.setUseAddressBarEnsResolution`);
callBackgroundMethod('setUseAddressBarEnsResolution', [val], (err) => {
if (err) {
dispatch(displayWarning(err));
}
});
};
}
export function updateCurrentLocale(
key: string,
): ThunkAction<void, MetaMaskReduxState, unknown, AnyAction> {