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

feat(server): ⚡️ Subdomain Gateway Using Fastify #31

Merged
merged 10 commits into from
Oct 25, 2023
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ $ docker run -it -p 8080:8080 -e DEBUG="helia-http-gateway" helia
| `DEBUG` | Debug level | `''`|
| `PORT` | Port to listen on | `8080` |
| `HOST` | Host to listen on | `0.0.0.0` |
| `METRICS` | Whether to enable prometheus metrics. Any value other than 'true' will disable metrics. | `true` |
| `USE_BITSWAP` | Use bitswap to fetch content from IPFS | `true` |
| `USE_TRUSTLESS_GATEWAYS` | Whether to fetch content from trustless-gateways or not | `true` |
| `TRUSTLESS_GATEWAYS` | Comma separated list of trusted gateways to fetch content from | [Defined in Helia](https://github.com/ipfs/helia/blob/main/packages/helia/src/block-brokers/trustless-gateway/index.ts) |
Expand Down
1,145 changes: 487 additions & 658 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 6 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"test": "echo \"Error: no test specified\" && exit 1",
"test:e2e": "playwright test",
"test:http-e2e": "cross-env USE_BITSWAP=false USE_LIBP2P=false playwright test",
"test:e2e-flame": "concurrently -k --ks SIGINT -s all -n \"gateway,playwright\" -c \"magenta,blue\" \"npm run start:dev-flame\" \"wait-on 'http://127.0.0.1:$PORT' && npm run test:e2e\"",
"test:e2e-doctor": "concurrently -k --ks SIGINT -s all -n \"gateway,playwright\" -c \"magenta,blue\" \"npm run start:dev-doctor\" \"wait-on 'http://127.0.0.1:$PORT' && npm run test:e2e\"",
"test:e2e-flame": "concurrently -k --ks SIGINT -s all -n \"gateway,playwright\" -c \"magenta,blue\" \"npm run start:dev-flame\" \"wait-on 'tcp:$PORT' && npm run test:e2e\"",
"test:e2e-doctor": "concurrently -k --ks SIGINT -s all -n \"gateway,playwright\" -c \"magenta,blue\" \"npm run start:dev-doctor\" \"wait-on 'tcp:$PORT' && npm run test:e2e\"",
"healthcheck": "node dist/src/healthcheck.js"
},
"type": "module",
Expand Down Expand Up @@ -46,8 +46,6 @@
"homepage": "https://github.com/ipfs/helia-http-gateway#readme",
"devDependencies": {
"@playwright/test": "^1.39.0",
"@types/express": "4.x",
"@types/express-session": "1.17.9",
"@types/mime-types": "2.x",
"@types/node": "20.x",
"aegir": "40.x",
Expand All @@ -62,13 +60,13 @@
"@helia/unixfs": "1.x",
"blockstore-level": "^1.1.4",
"datastore-level": "^10.1.4",
"express": "4.x",
"express-prom-bundle": "6.x",
"express-session": "1.17.3",
"fastify": "4.24.3",
"fastify-metrics": "10.3.2",
"file-type": "18.x",
"helia": "next",
"lru-cache": "10.x",
"mime-types": "2.x",
"p-try-each": "1.x"
"p-try-each": "1.x",
"pino-pretty": "10.2.3"
}
}
4 changes: 2 additions & 2 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig, devices } from '@playwright/test'
import { HOST, PORT } from './src/constants.js'
import { PORT } from './src/constants.js'

/**
* See https://playwright.dev/docs/test-configuration.
Expand Down Expand Up @@ -51,7 +51,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: (process.env.DOCTOR != null) ? 'npm run start:dev-doctor' : 'npm run start:dev',
url: `http://${HOST}:${PORT}`,
port: PORT,
// Tiros does not re-use the existing server.
reuseExistingServer: process.env.CI == null
}
Expand Down
9 changes: 9 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ export const PORT = Number(process.env.PORT ?? 8080)

export const HOST = process.env.HOST ?? '0.0.0.0'

export const DEBUG = process.env.DEBUG ?? ''

/**
* If set to any value other than 'true', we will disable prometheus metrics.
*
* @default 'true'
*/
export const METRICS = process.env.METRICS ?? 'true'

/**
* If not set, we will enable bitswap by default.
*/
Expand Down
37 changes: 28 additions & 9 deletions src/heliaFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ const ROOT_FILE_PATTERNS = [

const DELEGATED_ROUTING_API = 'https://node3.delegate.ipfs.io/api/v0/name/resolve/'

interface HeliaPathParts {
namespace: string
address: string
relativePath: string
}

/**
* Fetches files from IPFS or IPNS
*/
export class HeliaFetch {
private fs!: UnixFS
private readonly delegatedRoutingApi: string
private readonly log: debug.Debugger
private readonly PARSE_PATH_REGEX = /^\/(?<namespace>ip[fn]s)\/(?<address>[^/$]+)(?<relativePath>[^$]*)/
private readonly rootFilePatterns: string[]
public node!: Helia
public ready: Promise<void>
Expand Down Expand Up @@ -73,19 +80,18 @@ export class HeliaFetch {
/**
* Parse a path into its namespace, address, and relative path
*/
public parsePath (path: string): { namespace: string, address: string, relativePath: string } {
public parsePath (path: string): HeliaPathParts {
if (path === undefined) {
throw new Error('Path is empty')
}
this.log(`Parsing path: ${path}`)
const regex = /^\/(?<namespace>ip[fn]s)\/(?<address>[^/$]+)(?<relativePath>[^$]*)/
const result = path.match(regex)
const result = path.match(this.PARSE_PATH_REGEX)
if (result == null || result?.groups == null) {
this.log(`Error parsing path: ${path}:`, result)
throw new Error(`Path: ${path} is not valid, provide path as /ipfs/<cid> or /ipns/<path>`)
}
this.log('Parsed path:', result?.groups)
return result.groups as { namespace: string, address: string, relativePath: string }
return result.groups as unknown as HeliaPathParts
}

/**
Expand All @@ -96,13 +102,11 @@ export class HeliaFetch {
}

/**
* fetch a path from IPFS or IPNS
* fetch a path from a given namespace and address.
*/
public async fetch (path: string): Promise<AsyncIterable<Uint8Array>> {
public async fetch ({ namespace, address, relativePath }: HeliaPathParts): Promise<AsyncIterable<Uint8Array>> {
try {
await this.ready
this.log('Fetching:', path)
const { namespace, address, relativePath } = this.parsePath(path)
this.log('Processing Fetch:', { namespace, address, relativePath })
switch (namespace) {
case 'ipfs':
Expand All @@ -112,6 +116,21 @@ export class HeliaFetch {
default:
throw new Error('Namespace is not valid, provide path as /ipfs/<cid> or /ipns/<path>')
}
} catch (error) {
// eslint-disable-next-line no-console
this.log(`Error fetching: ${namespace}/${address}${relativePath}`, error)
throw error
}
}

/**
* fetch a path as string from IPFS or IPNS
*/
public async fetchPath (path: string): Promise<AsyncIterable<Uint8Array>> {
try {
this.log('Fetching:', path)
const { namespace, address, relativePath } = this.parsePath(path)
return await this.fetch({ namespace, address, relativePath })
} catch (error) {
// eslint-disable-next-line no-console
this.log(`Error fetching: ${path}`, error)
Expand Down Expand Up @@ -151,7 +170,7 @@ export class HeliaFetch {
}
const finalPath = `${this.ipnsResolutionCache.get(address)}${options?.path ?? ''}`
this.log('Final IPFS path:', finalPath)
return this.fetch(finalPath)
return this.fetchPath(finalPath)
}

/**
Expand Down
Loading
Loading