From 0ef7f603d6bcf77881d67770ae7597d51666892f Mon Sep 17 00:00:00 2001 From: pldespaigne <5858657+pldespaigne@users.noreply.github.com> Date: Thu, 12 Dec 2019 20:28:07 +0100 Subject: [PATCH] Ipfs cid v1 base32 (#7362) add ipfs gateway to advanced settings use ipfs gateway from settings use ipfs.dweb.link as default CID gateway disallow gateway.ipfs.io as gateway --- app/_locales/en/messages.json | 16 +++- app/_locales/fr/messages.json | 12 +++ app/scripts/background.js | 6 +- app/scripts/controllers/preferences.js | 21 +++++ app/scripts/lib/ens-ipfs/resolver.js | 7 +- app/scripts/lib/ens-ipfs/setup.js | 5 +- app/scripts/metamask-controller.js | 15 +++ package.json | 2 +- .../advanced-tab/advanced-tab.component.js | 93 +++++++++++++++++++ .../advanced-tab/advanced-tab.container.js | 6 ++ .../tests/advanced-tab-component.test.js | 2 +- ui/app/selectors/selectors.js | 4 + ui/app/store/actions.js | 20 ++++ yarn.lock | 20 +--- 14 files changed, 205 insertions(+), 24 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c7e456b09181..4e14a887cd87 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -735,6 +735,18 @@ "invalidSeedPhrase": { "message": "Invalid seed phrase" }, + "ipfsGateway": { + "message": "IPFS Gateway" + }, + "ipfsGatewayDescription": { + "message": "Enter the URL of the IPFS CID gateway to use for ENS content resolution." + }, + "invalidIpfsGateway": { + "message": "Invalid IPFS Gateway: The value must be a valid URL" + }, + "forbiddenIpfsGateway": { + "message": "Forbidden IPFS Gateway: Please specify a CID gateway" + }, "jsonFile": { "message": "JSON File", "description": "format for importing an account" @@ -1336,7 +1348,7 @@ "message": "Sync data with 3Box (experimental)" }, "syncWithThreeBoxDescription": { - "message": "Turn on to have your settings backed up with 3Box. This feature is currenty experimental; use at your own risk." + "message": "Turn on to have your settings backed up with 3Box. This feature is currently experimental; use at your own risk." }, "syncWithThreeBoxDisabled": { "message": "3Box has been disabled due to an error during the initial sync" @@ -1360,7 +1372,7 @@ "message": "Make sure nobody else is looking at your screen when you scan this code" }, "syncWithMobileComplete": { - "message": "Your data has been synced succesfully. Enjoy the MetaMask mobile app!" + "message": "Your data has been synced successfully. Enjoy the MetaMask mobile app!" }, "terms": { "message": "Terms of Use" diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json index 1fc2a600d984..d43b9dad0fcb 100644 --- a/app/_locales/fr/messages.json +++ b/app/_locales/fr/messages.json @@ -599,6 +599,18 @@ "invalidSeedPhrase": { "message": "Phrase Seed invalide" }, + "ipfsGateway": { + "message": "IPFS Gateway" + }, + "ipfsGatewayDescription": { + "message": "Entrez l'URL de la gateway CID IPFS à utiliser pour résoudre les contenus ENS." + }, + "invalidIpfsGateway": { + "message": "IPFS Gateway Invalide: la valeur doit être une URL valide" + }, + "forbiddenIpfsGateway": { + "message": "IPFS Gateway Interdite: veuillez spécifier une gateway CID" + }, "jsonFile": { "message": "Fichier JSON", "description": "format for importing an account" diff --git a/app/scripts/background.js b/app/scripts/background.js index dfeec599d700..7dd13e083e15 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -252,8 +252,10 @@ function setupController (initState, initLangCode) { }, }) - const provider = controller.provider - setupEnsIpfsResolver({ provider }) + setupEnsIpfsResolver({ + getIpfsGateway: controller.preferencesController.getIpfsGateway.bind(controller.preferencesController), + provider: controller.provider, + }) // submit rpc requests to mesh-metrics controller.networkController.on('rpc-req', (data) => { diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 1de6fde3e176..780a41e80e93 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -60,6 +60,9 @@ class PreferencesController { completedOnboarding: false, metaMetricsId: null, metaMetricsSendCount: 0, + + // ENS decentralized website resolution + ipfsGateway: 'ipfs.dweb.link', }, opts.initState) this.diagnostics = opts.diagnostics @@ -608,6 +611,24 @@ class PreferencesController { return Promise.resolve(true) } + /** + * A getter for the `ipfsGateway` property + * @returns {string} The current IPFS gateway domain + */ + getIpfsGateway () { + return this.store.getState().ipfsGateway + } + + /** + * A setter for the `ipfsGateway` property + * @param {string} domain The new IPFS gateway domain + * @returns {Promise} A promise of the update IPFS gateway domain + */ + setIpfsGateway (domain) { + this.store.updateState({ ipfsGateway: domain }) + return Promise.resolve(domain) + } + // // PRIVATE METHODS // diff --git a/app/scripts/lib/ens-ipfs/resolver.js b/app/scripts/lib/ens-ipfs/resolver.js index 985f0378c233..08251cb5d206 100644 --- a/app/scripts/lib/ens-ipfs/resolver.js +++ b/app/scripts/lib/ens-ipfs/resolver.js @@ -32,8 +32,13 @@ async function resolveEnsToIpfsContentId ({ provider, name }) { if (isEIP1577Compliant[0]) { const contentLookupResult = await Resolver.contenthash(hash) const rawContentHash = contentLookupResult[0] - const decodedContentHash = contentHash.decode(rawContentHash) + let decodedContentHash = contentHash.decode(rawContentHash) const type = contentHash.getCodec(rawContentHash) + + if (type === 'ipfs-ns') { + decodedContentHash = contentHash.helpers.cidV0ToV1Base32(decodedContentHash) + } + return { type: type, hash: decodedContentHash } } if (isLegacyResolver[0]) { diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js index a703af5b50bf..4da1f3d315eb 100644 --- a/app/scripts/lib/ens-ipfs/setup.js +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -6,7 +6,7 @@ const supportedTopLevelDomains = ['eth'] module.exports = setupEnsIpfsResolver -function setupEnsIpfsResolver ({ provider }) { +function setupEnsIpfsResolver ({ provider, getIpfsGateway }) { // install listener const urlPatterns = supportedTopLevelDomains.map(tld => `*://*.${tld}/*`) @@ -40,12 +40,13 @@ function setupEnsIpfsResolver ({ provider }) { } async function attemptResolve ({ tabId, name, path, search, fragment }) { + const ipfsGateway = getIpfsGateway() extension.tabs.update(tabId, { url: `loading.html` }) let url = `https://app.ens.domains/name/${name}` try { const { type, hash } = await resolveEnsToIpfsContentId({ provider, name }) if (type === 'ipfs-ns') { - const resolvedUrl = `https://gateway.ipfs.io/ipfs/${hash}${path}${search || ''}${fragment || ''}` + const resolvedUrl = `https://${hash}.${ipfsGateway}${path}${search || ''}${fragment || ''}` try { // check if ipfs gateway has result const response = await fetch(resolvedUrl, { method: 'HEAD' }) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5dd7ee691555..5ffc61d615ee 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -439,6 +439,7 @@ module.exports = class MetamaskController extends EventEmitter { setCurrentCurrency: this.setCurrentCurrency.bind(this), setUseBlockie: this.setUseBlockie.bind(this), setUseNonceField: this.setUseNonceField.bind(this), + setIpfsGateway: this.setIpfsGateway.bind(this), setParticipateInMetaMetrics: this.setParticipateInMetaMetrics.bind(this), setMetaMetricsSendCount: this.setMetaMetricsSendCount.bind(this), setFirstTimeFlowType: this.setFirstTimeFlowType.bind(this), @@ -1841,6 +1842,20 @@ module.exports = class MetamaskController extends EventEmitter { } } + /** + * Sets the IPFS gateway to use for ENS content resolution. + * @param {string} val - the host of the gateway to set + * @param {Function} cb - A callback function called when complete. + */ + setIpfsGateway (val, cb) { + try { + this.preferencesController.setIpfsGateway(val) + cb(null) + } catch (err) { + cb(err) + } + } + /** * Sets whether or not the user will have usage data tracked with MetaMetrics * @param {boolean} bool - True for users that wish to opt-in, false for users that wish to remain out. diff --git a/package.json b/package.json index 985b08f3c2d3..27682c9f565b 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "c3": "^0.6.7", "classnames": "^2.2.5", "clone": "^2.1.2", - "content-hash": "^2.4.4", + "content-hash": "^2.5.0", "copy-to-clipboard": "^3.0.8", "currency-formatter": "^1.4.2", "d3": "^5.7.0", diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js index 5cd147e24c25..c8612060f744 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.component.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.component.js @@ -31,11 +31,15 @@ export default class AdvancedTab extends PureComponent { threeBoxSyncingAllowed: PropTypes.bool.isRequired, setThreeBoxSyncingPermission: PropTypes.func.isRequired, threeBoxDisabled: PropTypes.bool.isRequired, + setIpfsGateway: PropTypes.func.isRequired, + ipfsGateway: PropTypes.string.isRequired, } state = { autoLogoutTimeLimit: this.props.autoLogoutTimeLimit, logoutTimeError: '', + ipfsGateway: this.props.ipfsGateway, + ipfsGatewayError: '', } renderMobileSync () { @@ -354,6 +358,85 @@ export default class AdvancedTab extends PureComponent { ) } + handleIpfsGatewayChange (url) { + const { t } = this.context + + this.setState(() => { + let ipfsGatewayError = '' + + try { + + const urlObj = new URL(addUrlProtocolPrefix(url)) + if (!urlObj.host) { + throw new Error() + } + + // don't allow the use of this gateway + if (urlObj.host === 'gateway.ipfs.io') { + throw new Error('Forbidden gateway') + } + + } catch (error) { + ipfsGatewayError = ( + error.message === 'Forbidden gateway' + ? t('forbiddenIpfsGateway') + : t('invalidIpfsGateway') + ) + } + + return { + ipfsGateway: url, + ipfsGatewayError, + } + }) + } + + handleIpfsGatewaySave () { + + const url = new URL(addUrlProtocolPrefix(this.state.ipfsGateway)) + const host = url.host + + this.props.setIpfsGateway(host) + } + + renderIpfsGatewayControl () { + const { t } = this.context + const { ipfsGatewayError } = this.state + + return ( +
+
+ { t('ipfsGateway') } +
+ { t('ipfsGatewayDescription') } +
+
+
+
+ this.handleIpfsGatewayChange(e.target.value)} + error={ipfsGatewayError} + fullWidth + margin="dense" + /> + +
+
+
+ ) + } + renderContent () { const { warning } = this.props @@ -369,6 +452,7 @@ export default class AdvancedTab extends PureComponent { { this.renderUseNonceOptIn() } { this.renderAutoLogoutTimeLimit() } { this.renderThreeBoxControl() } + { this.renderIpfsGatewayControl() } ) } @@ -377,3 +461,12 @@ export default class AdvancedTab extends PureComponent { return this.renderContent() } } + +function addUrlProtocolPrefix (urlString) { + if (!urlString.match( + /(^http:\/\/)|(^https:\/\/)/ + )) { + return 'https://' + urlString + } + return urlString +} diff --git a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js index 46299916cc43..90a4861c7b5e 100644 --- a/ui/app/pages/settings/advanced-tab/advanced-tab.container.js +++ b/ui/app/pages/settings/advanced-tab/advanced-tab.container.js @@ -11,6 +11,7 @@ import { setThreeBoxSyncingPermission, turnThreeBoxSyncingOnAndInitialize, setUseNonceField, + setIpfsGateway, } from '../../../store/actions' import { preferencesSelector } from '../../../selectors/selectors' @@ -24,6 +25,7 @@ export const mapStateToProps = state => { threeBoxSyncingAllowed, threeBoxDisabled, useNonceField, + ipfsGateway, } = metamask const { showFiatInTestnets, autoLogoutTimeLimit } = preferencesSelector(state) @@ -36,6 +38,7 @@ export const mapStateToProps = state => { threeBoxSyncingAllowed, threeBoxDisabled, useNonceField, + ipfsGateway, } } @@ -59,6 +62,9 @@ export const mapDispatchToProps = dispatch => { dispatch(setThreeBoxSyncingPermission(newThreeBoxSyncingState)) } }, + setIpfsGateway: value => { + return dispatch(setIpfsGateway(value)) + }, } } diff --git a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js index 924602ace5e9..15e5306c2ae2 100644 --- a/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js +++ b/ui/app/pages/settings/advanced-tab/tests/advanced-tab-component.test.js @@ -16,7 +16,7 @@ describe('AdvancedTab Component', () => { } ) - assert.equal(root.find('.settings-page__content-row').length, 9) + assert.equal(root.find('.settings-page__content-row').length, 10) }) it('should update autoLogoutTimeLimit', () => { diff --git a/ui/app/selectors/selectors.js b/ui/app/selectors/selectors.js index f2b5f1f88363..4b79cdf79bac 100644 --- a/ui/app/selectors/selectors.js +++ b/ui/app/selectors/selectors.js @@ -543,3 +543,7 @@ export function getLastConnectedInfo (state) { }, {}) return lastConnectedInfoData } + +export function getIpfsGateway (state) { + return state.metamask.ipfsGateway +} diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index a9d213876072..70791f75cf90 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -304,6 +304,8 @@ const actions = { setUseNonceField, UPDATE_CUSTOM_NONCE: 'UPDATE_CUSTOM_NONCE', updateCustomNonce, + SET_IPFS_GATEWAY: 'SET_IPFS_GATEWAY', + setIpfsGateway, SET_PARTICIPATE_IN_METAMETRICS: 'SET_PARTICIPATE_IN_METAMETRICS', SET_METAMETRICS_SEND_COUNT: 'SET_METAMETRICS_SEND_COUNT', @@ -2660,6 +2662,24 @@ function setUseNonceField (val) { } } +function setIpfsGateway (val) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + log.debug(`background.setIpfsGateway`) + background.setIpfsGateway(val, (err) => { + dispatch(actions.hideLoadingIndication()) + if (err) { + return dispatch(actions.displayWarning(err.message)) + } else { + dispatch({ + type: actions.SET_IPFS_GATEWAY, + value: val, + }) + } + }) + } +} + function updateCurrentLocale (key) { return (dispatch) => { dispatch(actions.showLoadingIndication()) diff --git a/yarn.lock b/yarn.lock index 08e2b821e21e..e30d7570d9ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6826,16 +6826,6 @@ cids@^0.5.3, cids@~0.5.4, cids@~0.5.6: multicodec "~0.5.0" multihashes "~0.4.14" -cids@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cids/-/cids-0.6.0.tgz#0de7056a5246a7c7ebf3134eb4d83b3b8b841a06" - integrity sha512-34wuIeiBZOuvBwUuYR4XooVuXUQI2PYU9VmgM2eB3xkSmQYRlv2kh/dIbmGiLY2GuONlGR3lLtYdVkx1G9yXUg== - dependencies: - class-is "^1.1.0" - multibase "~0.6.0" - multicodec "~0.5.0" - multihashes "~0.4.14" - cids@^0.7.1, cids@~0.7.0, cids@~0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/cids/-/cids-0.7.1.tgz#d8bba49a35a0e82110879b5001abf1039c62347f" @@ -7413,12 +7403,12 @@ content-disposition@0.5.3, content-disposition@^0.5.2, content-disposition@~0.5. dependencies: safe-buffer "5.1.2" -content-hash@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/content-hash/-/content-hash-2.4.4.tgz#4bec87caecfcff8cf1a37645301cbef4728a083f" - integrity sha512-3FaUsqt7VR725pVxe0vIScGI5efmpryIXdVSeXafQ63fb5gRGparAQlAGxTSOiv0yRg7YeliseXuB20ByD1duQ== +content-hash@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/content-hash/-/content-hash-2.5.0.tgz#6f8a98feec0360f09213e951c69e57c727093e3a" + integrity sha512-Opn9YSjQQc3Wst/VPwGTdKcNONuQr4yc0dtvEbV82UGDIsc7dE2bdTz3FpLRxfnuOkHx8y9/94sZ3aytmUQ1HA== dependencies: - cids "^0.6.0" + cids "^0.7.1" multicodec "^0.5.5" multihashes "^0.4.15"