Skip to content

Commit

Permalink
fix: redirect page and first-hit normalization (#201)
Browse files Browse the repository at this point in the history
* fix: helia-sw? and other first-hits are handled the same

* fix: iframe config is loaded on first-hit

* chore: apply suggestions from code review

* fix: build after preact migration
  • Loading branch information
SgtPooki authored Apr 17, 2024
1 parent 25df402 commit 8c85b6f
Show file tree
Hide file tree
Showing 9 changed files with 407 additions and 1,154 deletions.
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 },
{ 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</>)
}

/**
* 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
}
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

0 comments on commit 8c85b6f

Please sign in to comment.