Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: view in gateway url now validates if url is acessible #1591

Merged
merged 11 commits into from
Sep 1, 2020
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@
"it-all": "^1.0.2",
"it-last": "^1.0.2",
"it-map": "^1.0.2",
"memoizee": "^0.4.14",
"milliseconds": "^1.0.3",
"money-clip": "^3.0.2",
"multiaddr": "^7.5.0",
"multiaddr-to-uri": "^5.1.0",
"p-memoize": "^4.0.0",
"p-queue": "^6.6.0",
"prop-types": "^15.7.2",
"react": "^16.13.1",
Expand Down
72 changes: 65 additions & 7 deletions src/bundles/config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import memoize from 'p-memoize'
import toUri from 'multiaddr-to-uri'
import { createAsyncResourceBundle, createSelector } from 'redux-bundler'

const DEFAULT_URI = 'https://ipfs.io'
const LOCAL_HOSTNAMES = ['127.0.0.1', '[::1]', '0.0.0.0', '[::]']

const bundle = createAsyncResourceBundle({
name: 'config',
getPromise: async ({ getIpfs }) => {
staleAfter: 60000,
persist: false,
checkIfOnline: false,

getPromise: async ({ getIpfs, store }) => {
const rawConf = await getIpfs().config.getAll()
let conf

Expand All @@ -13,12 +21,29 @@ const bundle = createAsyncResourceBundle({
conf = JSON.stringify(rawConf, null, '\t')
}

const config = JSON.parse(conf)
const url = getURLFromAddress('Gateway', config) || DEFAULT_URI

// Normalize local hostnames to localhost
// to leverage subdomain gateway, if present
// https://github.com/ipfs-shipyard/ipfs-webui/issues/1490
const gw = new URL(url)
if (LOCAL_HOSTNAMES.includes(gw.hostname)) {
gw.hostname = 'localhost'
const localUrl = gw.toString().replace(/\/+$/, '') // no trailing slashes
if (await checkIfSubdomainGatewayUrlIsAccessible(localUrl)) {
store.doSetAvailableGateway(localUrl)
return conf
}
}

if (!await checkIfGatewayUrlIsAccessible(url)) {
store.doSetAvailableGateway(DEFAULT_URI)
}

// stringy json for quick compares
return conf
},
staleAfter: 60000,
persist: false,
checkIfOnline: false
}
})

// derive the object from the stringy json
Expand All @@ -29,12 +54,18 @@ bundle.selectConfigObject = createSelector(

bundle.selectApiUrl = createSelector(
'selectConfigObject',
(config) => getURLFromAddress('API', config) || 'https://ipfs.io'
(config) => getURLFromAddress('API', config) || DEFAULT_URI
)

bundle.selectGatewayUrl = createSelector(
'selectConfigObject',
(config) => getURLFromAddress('Gateway', config) || 'https://ipfs.io'
(config) => getURLFromAddress('Gateway', config) || DEFAULT_URI
)

bundle.selectAvailableGatewayUrl = createSelector(
'selectAvailableGateway',
'selectGatewayUrl',
(availableGateway, gatewayUrl) => availableGateway || gatewayUrl
)

bundle.selectBootstrapPeers = createSelector(
Expand Down Expand Up @@ -74,4 +105,31 @@ function getURLFromAddress (name, config) {
}
}

const checkIfGatewayUrlIsAccessible = memoize(async (url) => {
try {
const { status } = await fetch(
`${url}/ipfs/bafkqaaa`
)
return status === 200
} catch (e) {
console.error(`Unable to use the gateway at ${url}. The public gateway will be used as a fallback`, e)
return false
}
})

// Separate test is necessary to see if subdomain mode is possible,
// because some browser+OS combinations won't resolve them:
// https://github.com/ipfs/go-ipfs/issues/7527
const checkIfSubdomainGatewayUrlIsAccessible = memoize(async (url) => {
try {
url = new URL(url)
url.hostname = `bafkqaaa.ipfs.${url.hostname}`
const { status } = await fetch(url.toString())
return status === 200
} catch (e) {
console.error(`Unable to use the subdomain gateway at ${url}. Regular gateway will be used as a fallback`, e)
return false
}
})

export default bundle
17 changes: 17 additions & 0 deletions src/bundles/gateway.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const bundle = {
name: 'gateway',

reducer: (state = { availableGateway: null }, action) => {
if (action.type === 'SET_AVAILABLE_GATEWAY') {
return { ...state, availableGateway: action.payload }
}

return state
},

doSetAvailableGateway: url => ({ dispatch }) => dispatch({ type: 'SET_AVAILABLE_GATEWAY', payload: url }),

selectAvailableGateway: (state) => state?.gateway?.availableGateway
}

export default bundle
2 changes: 2 additions & 0 deletions src/bundles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import repoStats from './repo-stats'
import createAnalyticsBundle from './analytics'
import experimentsBundle from './experiments'
import cliTutorModeBundle from './cli-tutor-mode'
import gatewayBundle from './gateway'

export default composeBundles(
createCacheBundle({
Expand All @@ -37,6 +38,7 @@ export default composeBundles(
exploreBundle(),
configBundle,
configSaveBundle,
gatewayBundle,
nodeBandwidthBundle,
nodeBandwidthChartBundle(),
peersBundle,
Expand Down
4 changes: 2 additions & 2 deletions src/bundles/peer-locations.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import HLRU from 'hashlru'
import Multiaddr from 'multiaddr'
import ms from 'milliseconds'
import ip from 'ip'
import memoizee from 'memoizee'
import memoize from 'p-memoize'

// After this time interval, we re-check the locations for each peer
// once again through PeerLocationResolver.
Expand Down Expand Up @@ -146,7 +146,7 @@ const parseLatency = (latency) => {
return value
}

const getPublicIP = memoizee((identity) => {
const getPublicIP = memoize((identity) => {
if (!identity) return

for (const maddr of identity.addresses) {
Expand Down
10 changes: 8 additions & 2 deletions src/explore/ExploreContainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import withTour from '../components/tour/withTour'

const ExploreContainer = ({
toursEnabled,
handleJoyrideCallback
handleJoyrideCallback,
availableGatewayUrl
}) => (
<ExplorePage runTour={toursEnabled} joyrideCallback={handleJoyrideCallback} />
<ExplorePage
runTour={toursEnabled}
joyrideCallback={handleJoyrideCallback}
gatewayUrl={availableGatewayUrl}
/>
)

export default connect(
'selectToursEnabled',
'selectAvailableGatewayUrl',
withTour(ExploreContainer)
)
1 change: 0 additions & 1 deletion src/files/FilesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,6 @@ export default connect(
'doFilesPin',
'doFilesUnpin',
'doFilesUpdateSorting',
'selectGatewayUrl',
'selectFilesSorting',
'selectToursEnabled',
'doFilesWrite',
Expand Down
6 changes: 3 additions & 3 deletions src/files/file-preview/FilePreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Preview extends React.Component {
}

render () {
const { t, name, cid, size, gatewayUrl } = this.props
const { t, name, cid, size, availableGatewayUrl: gatewayUrl } = this.props

const type = typeFromExt(name)
const src = `${gatewayUrl}/ipfs/${cid}`
Expand Down Expand Up @@ -88,14 +88,14 @@ Preview.propTypes = {
name: PropTypes.string.isRequired,
hash: PropTypes.instanceOf(CID),
size: PropTypes.number.isRequired,
gatewayUrl: PropTypes.string.isRequired,
availableGatewayUrl: PropTypes.string.isRequired,
read: PropTypes.func.isRequired,
content: PropTypes.object,
t: PropTypes.func.isRequired,
tReady: PropTypes.bool.isRequired
}

export default connect(
'selectGatewayUrl',
'selectAvailableGatewayUrl',
withTranslation('files')(Preview)
)