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: redirect page and first-hit normalization #201

Merged
merged 6 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,427 changes: 318 additions & 1,109 deletions package-lock.json

Large diffs are not rendered by default.

31 changes: 7 additions & 24 deletions src/context/service-worker-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,13 @@
*
* Before the Service Worker is registered (e.g. first requests to root hosted domain or subdomains):
*
* 1. Being redirected from _redirects file to a ?helia-sw= url
* 2. The app is loaded because service worker is not yet registered, we need to reload the page so the service worker intercepts the request
* 1. The app is loaded because service worker is not yet registered, we need to reload the page so the service worker intercepts the request
*
* After the service worker is loaded. Usually any react code isn't loaded, but some edge cases are:
* 1. The page being loaded using some /ip[fn]s/<path> url, but subdomain isolation is supported, so we need to redirect to the isolated origin
*/
import React, { createContext, useEffect, useState } from 'preact/compat'
import { getRedirectUrl, isDeregisterRequest } from '../lib/deregister-request.js'
import { translateIpfsRedirectUrl } from '../lib/ipfs-hosted-redirect-utils.js'
import { error, trace } from '../lib/logger.js'
import { findOriginIsolationRedirect } from '../lib/path-or-subdomain.js'
import { registerServiceWorker } from '../service-worker-utils.js'
Expand All @@ -26,29 +24,14 @@ export const ServiceWorkerContext = createContext({
export const ServiceWorkerProvider = ({ children }): React.JSX.Element => {
const [isServiceWorkerRegistered, setIsServiceWorkerRegistered] = useState(false)

const windowLocation = translateIpfsRedirectUrl(window.location.href)

useEffect(() => {
if (isServiceWorkerRegistered) {
/**
* The service worker is registered, now we need to check for "helia-sw" and origin isolation support
*/
if (windowLocation.href !== window.location.href) {
/**
* We're at a domain with ?helia-sw=, we can reload the page so the service worker will
* capture the request
*/
window.location.replace(windowLocation.href)
} else {
/**
* ?helia-sw= url handling is done, now we can check for origin isolation redirects
*/
void findOriginIsolationRedirect(windowLocation).then((originRedirect) => {
if (originRedirect !== null) {
window.location.replace(originRedirect)
}
})
}
void findOriginIsolationRedirect(window.location).then((originRedirect) => {
if (originRedirect !== null) {
window.location.replace(originRedirect)
}
})

/**
* The service worker is registered, we don't need to do any more work
*/
Expand Down
6 changes: 4 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ if (container == null) {
const LazyConfig = React.lazy(async () => import('./pages/config.jsx'))
const LazyHelperUi = React.lazy(async () => import('./pages/helper-ui.jsx'))
const LazyRedirectPage = React.lazy(async () => import('./pages/redirect-page.jsx'))
const LazyInterstitial = React.lazy(async () => import('./pages/redirects-interstitial.jsx'))

const routes: Route[] = [
{ default: true, component: LazyHelperUi },
{ path: '#/ipfs-sw-config', shouldRender: async () => (await import('./lib/routing-render-checks')).shouldRenderConfigPage(), component: LazyConfig },
{ shouldRender: async () => (await import('./lib/routing-render-checks')).shouldRenderRedirectPage(), component: LazyRedirectPage }
{ shouldRender: async () => (await import('./lib/routing-render-checks.js')).shouldRenderRedirectsInterstitial(), component: LazyInterstitial },
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably import this check function immediately since it's tiny but this is consistent at least.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a reasonable way to avoid an additional round trip.

{ path: '#/ipfs-sw-config', shouldRender: async () => (await import('./lib/routing-render-checks.js')).shouldRenderConfigPage(), component: LazyConfig },
{ shouldRender: async () => (await import('./lib/routing-render-checks.js')).shouldRenderRedirectPage(), component: LazyRedirectPage }
]

render(
Expand Down
15 changes: 0 additions & 15 deletions src/lib/ipfs-hosted-redirect-utils.ts

This file was deleted.

6 changes: 6 additions & 0 deletions src/lib/routing-render-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ export async function shouldRenderConfigPage (): Promise<boolean> {
const isRequestToViewConfigPage = isConfigPage(window.location.hash)
return isRequestToViewConfigPage
}

export async function shouldRenderRedirectsInterstitial (): Promise<boolean> {
const url = new URL(window.location.href)
const heliaSw = url.searchParams.get('helia-sw')
return heliaSw != null
}
13 changes: 10 additions & 3 deletions src/pages/redirect-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ import { error, trace } from '../lib/logger.js'

const ConfigIframe = (): React.JSX.Element => {
const { parentDomain } = getSubdomainParts(window.location.href)

const portString = window.location.port === '' ? '' : `:${window.location.port}`
const iframeSrc = `${window.location.protocol}//${parentDomain}${portString}/#/ipfs-sw-config@origin=${encodeURIComponent(window.location.origin)}`
let iframeSrc
if (parentDomain == null || parentDomain === window.location.href) {
const url = new URL(window.location.href)
url.pathname = '/'
url.hash = `#/ipfs-sw-config@origin=${encodeURIComponent(window.location.origin)}`
iframeSrc = url.href
} else {
const portString = window.location.port === '' ? '' : `:${window.location.port}`
iframeSrc = `${window.location.protocol}//${parentDomain}${portString}/#/ipfs-sw-config@origin=${encodeURIComponent(window.location.origin)}`
}

return (
<iframe id="redirect-config-iframe" src={iframeSrc} style={{ width: '100vw', height: '100vh', border: 'none' }} />
Expand Down
34 changes: 34 additions & 0 deletions src/pages/redirects-interstitial.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'preact'

/**
* This page is only used to capture the ?helia-sw=/ip[fn]s/blah query parameter that
* is used by IPFS hosted versions of the service-worker-gateway when non-existent paths are requested.
*/
export default function RedirectsInterstitial (): React.JSX.Element {
const windowLocation = translateIpfsRedirectUrl(window.location.href)
if (windowLocation.href !== window.location.href) {
/**
* We're at a domain with ?helia-sw=, we can reload the page so the service worker will
* capture the request
*/
window.location.replace(windowLocation.href)
}

return (<>First-hit on IPFS hosted service-worker-gateway. Reloading</>)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could probably add a spinner or something better here.

}

/**
* If you host helia-service-worker-gateway on an IPFS domain, the redirects file will route some requests from
* `<domain>/<wildcard-splat>` to `https://<domain>/?helia-sw=<wildcard-splat>`.
*
* This function will check for "?helia-sw=" in the URL and modify the URL so that it works with the rest of our logic
*/
function translateIpfsRedirectUrl (urlString: string): URL {
const url = new URL(urlString)
const heliaSw = url.searchParams.get('helia-sw')
if (heliaSw != null) {
url.searchParams.delete('helia-sw')
url.pathname = heliaSw
}
return url
}
Comment on lines +20 to +34
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was moved from ipfs-hosted-redirect-utils.ts without modification

23 changes: 22 additions & 1 deletion test-e2e/first-hit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ test.describe('first-hit ipfs-hosted', () => {
// then we should be redirected to the IPFS path
await page.waitForURL('http://127.0.0.1:3333/ipfs/bafkqablimvwgy3y')

// and then the normal redirectPage logic:
await waitForServiceWorker(page)
const bodyTextLocator = page.locator('body')
await expect(bodyTextLocator).toContainText('Please save your changes to the config to apply them')

// it should render the config iframe
await expect(page.locator('#redirect-config-iframe')).toBeAttached({ timeout: 1 })

// wait for the service worker to be registered, and click load content.
const loadContent = await page.waitForSelector('#load-content', { state: 'visible' })
await loadContent.click()

// and we verify the content was returned
const text = await page.innerText('body')
expect(text).toBe('hello')
Expand All @@ -40,13 +52,16 @@ test.describe('first-hit ipfs-hosted', () => {
expect(headers?.['content-type']).toContain('text/html')

// then we should be redirected to the IPFS path
await page.waitForURL(`${protocol}//bafkqablimvwgy3y.ipfs.${rootDomain}`)
const bodyTextLocator = page.locator('body')
await page.waitForURL(`${protocol}//bafkqablimvwgy3y.ipfs.${rootDomain}`)
await expect(bodyTextLocator).toContainText('Registering Helia service worker')

await waitForServiceWorker(page)
await expect(bodyTextLocator).toContainText('Please save your changes to the config to apply them')

// it should render the config iframe
await expect(page.locator('#redirect-config-iframe')).toBeAttached({ timeout: 1 })

await page.reload()

// and we verify the content was returned
Expand Down Expand Up @@ -81,6 +96,9 @@ test.describe('first-hit direct-hosted', () => {
const bodyTextLocator = page.locator('body')
await expect(bodyTextLocator).toContainText('Please save your changes to the config to apply them')

// it should render the config iframe
await expect(page.locator('#redirect-config-iframe')).toBeAttached({ timeout: 1 })

// wait for the service worker to be registered, and click load content.
const loadContent = await page.waitForSelector('#load-content', { state: 'visible' })
await loadContent.click()
Expand Down Expand Up @@ -110,6 +128,9 @@ test.describe('first-hit direct-hosted', () => {
await waitForServiceWorker(page)
await expect(bodyTextLocator).toContainText('Please save your changes to the config to apply them')

// it should render the config iframe
await expect(page.locator('#redirect-config-iframe')).toBeAttached({ timeout: 1 })

const loadContent = await page.waitForSelector('#load-content', { state: 'visible' })
await loadContent.click()

Expand Down
6 changes: 6 additions & 0 deletions test-e2e/fixtures/locators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export interface GetFrameLocator {
(page: Page | FrameLocator): FrameLocator
}

/**
* Page parts
*/
export const getHeader: GetLocator = (page) => page.locator('.e2e-header')
export const getHeaderTitle: GetLocator = (page) => page.locator('.e2e-header-title')
export const getConfigButton: GetLocator = (page) => page.locator('.e2e-header-config-button')
Expand All @@ -18,6 +21,9 @@ export const getConfigGatewaysInput: GetLocator = (page) => page.locator('.e2e-c
export const getConfigRoutersInput: GetLocator = (page) => page.locator('.e2e-config-page-input-routers')
export const getConfigAutoReloadInput: GetLocator = (page) => page.locator('.e2e-config-page-input-autoreload')

/**
* Iframe page parts
*/
export const getConfigButtonIframe: GetLocator = (page) => getIframeLocator(page).locator('.e2e-collapsible-button')
export const getConfigGatewaysInputIframe: GetLocator = (page) => getConfigGatewaysInput(getIframeLocator(page))
export const getConfigRoutersInputIframe: GetLocator = (page) => getConfigRoutersInput(getIframeLocator(page))
Expand Down
Loading