Skip to content

Commit

Permalink
feat: add ens and cid support
Browse files Browse the repository at this point in the history
  • Loading branch information
Cafe137 committed Oct 29, 2023
1 parent 3ab8b9a commit 784119e
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 217 deletions.
105 changes: 16 additions & 89 deletions src/bzz-link.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,41 @@
import * as swarmCid from '@ethersphere/swarm-cid'
import { Request, Response } from 'express'
import { Strings } from 'cafe-utility'
import { Request } from 'express'
import { logger } from './logger'

export class NotEnabledError extends Error {}

export class RedirectCidError extends Error {
public newUrl: string

constructor(message: string, cid: string) {
super(message)
this.newUrl = cid
}
}

/**
* Function that evaluates if the request was made with subdomain.
*
* @param pathname
* @param req
*/
export function requestFilter(pathname: string, req: Request): boolean {
return req.subdomains.length >= 1
}

/**
* Closure that routes subdomain CID/ENS to /bzz endpoint.
*
* @param target
* @param isCidEnabled
* @param isEnsEnabled
*/
export function routerClosure(target: string, isCidEnabled: boolean, isEnsEnabled: boolean) {
return (req: Request): string => {
const bzzResource = subdomainToBzz(req, isCidEnabled, isEnsEnabled)

logger.debug(`bzz link proxy`, { hostname: req.hostname, bzzResource })

return `${target}/bzz/${bzzResource}`
}
}

/**
* Express error handler that handles BZZ link error cases.
*
* @param err
* @param req
* @param res
* @param next
*/
export function errorHandler(err: Error, req: Request, res: Response, next: (e: Error) => void): void {
if (res.headersSent) {
next(err)

return
}

if (err instanceof NotEnabledError) {
res.writeHead(500).end(`Error 500: ${err.message}`)

return
}

if (err instanceof RedirectCidError) {
// Using Permanently Moved HTTP code for redirection
res.redirect(301, err.newUrl)

return
}

next(err)
export function requestFilter(req: Request): boolean {
return req.method === 'GET' && req.subdomains.length >= 1
}

/**
* Helper function that translates subdomain (CID/ENS) into Bzz resource
*
* @param req
* @param isCidEnabled
* @param isEnsEnabled
*/
function subdomainToBzz(req: Request, isCidEnabled: boolean, isEnsEnabled: boolean): string {
const host = req.hostname.split('.')
const subdomain = [...req.subdomains].reverse().join('.')
export function subdomainToBzz(
requestHostname: string,
appHostname: string,
isCidEnabled: boolean,
isEnsEnabled: boolean,
): string {
const relevantSubdomain = Strings.before(requestHostname, appHostname)

try {
const result = swarmCid.decodeCid(subdomain)
const result = swarmCid.decodeCid(relevantSubdomain)

if (!isCidEnabled) {
logger.warn('cid subdomain support disabled, but got cid', { subdomain })
logger.warn('cid subdomain support disabled, but got cid', { relevantSubdomain })
throw new NotEnabledError('CID subdomain support is disabled, but got a CID!')
}

// We got old CID redirect to new one with proper multicodec
if (result.type === undefined) {
host[0] = swarmCid.encodeManifestReference(result.reference).toString()
const newUrl = `${req.protocol}://${host.join('.')}${req.originalUrl}`
logger.info(`redirecting to new cid`, newUrl)
throw new RedirectCidError('old CID format, redirect to new one', newUrl)
}

return result.reference
} catch (e) {
if (e instanceof NotEnabledError || e instanceof RedirectCidError) {
if (e instanceof NotEnabledError) {
throw e
}

if (!isEnsEnabled) {
logger.warn('ens subdomain support disabled, but got ens', { subdomain })
logger.warn('ens subdomain support disabled, but got ens', { relevantSubdomain })
throw new NotEnabledError('ENS subdomain support is disabled, but got an ENS domain!')
}

return `${subdomain}.eth`
return `${relevantSubdomain}.eth`
}
}
56 changes: 41 additions & 15 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import axios from 'axios'
import { Dates, Objects, Strings } from 'cafe-utility'
import { Application, Request, Response } from 'express'
import { Application, Response } from 'express'
import { IncomingHttpHeaders } from 'http'
import { requestFilter, subdomainToBzz } from './bzz-link'
import { logger } from './logger'
import { StampsManager } from './stamps'
import { getErrorMessage } from './utils'
Expand All @@ -16,32 +18,61 @@ interface Options {
removePinHeader: boolean
stampManager: StampsManager | null
allowlist?: string[]
hostname?: string
cidSubdomains?: boolean
ensSubdomains?: boolean
}

export function createProxyEndpoints(app: Application, options: Options) {
app.use(async (req, res, next) => {
if (!options.hostname || !requestFilter(req)) {
next()
}
const newUrl = subdomainToBzz(
req.hostname,
options.hostname!,
options.cidSubdomains ?? false,
options.ensSubdomains ?? false,
)
await fetchAndRespond(
'GET',
Strings.joinUrl('bzz', newUrl, req.path),
req.query,
req.headers,
req.body,
res,
options,
)
})
app.get(GET_PROXY_ENDPOINTS, async (req, res) => {
await fetchAndRespond(req, res, options)
await fetchAndRespond('GET', req.path, req.query, req.headers, req.body, res, options)
})
app.post(POST_PROXY_ENDPOINTS, async (req, res) => {
await fetchAndRespond(req, res, options)
await fetchAndRespond('POST', req.path, req.query, req.headers, req.body, res, options)
})
}

async function fetchAndRespond(req: Request, res: Response, options: Options) {
const { headers, path } = req

async function fetchAndRespond(
method: 'GET' | 'POST',
path: string,
query: Record<string, unknown>,
headers: IncomingHttpHeaders,
body: any,

Check warning on line 60 in src/proxy.ts

View workflow job for this annotation

GitHub Actions / check (14.x)

Unexpected any. Specify a different type
res: Response,
options: Options,
) {
if (options.removePinHeader) {
delete headers[SWARM_PIN_HEADER]
}

if (req.method === 'POST' && options.stampManager) {
if (method === 'POST' && options.stampManager) {
headers[SWARM_STAMP_HEADER] = options.stampManager.postageStamp
}
try {
const response = await axios({
method: req.method,
url: Strings.joinUrl(options.beeApiUrl, path) + Objects.toQueryString(req.query, true),
data: req.body,
method,
url: Strings.joinUrl(options.beeApiUrl, path) + Objects.toQueryString(query, true),
data: body,
headers,
timeout: Dates.minutes(20),
validateStatus: status => status < 500,
Expand All @@ -63,11 +94,6 @@ async function fetchAndRespond(req: Request, res: Response, options: Options) {
}
}

if (Array.isArray(response.headers['set-cookie'])) {
response.headers['set-cookie'] = response.headers['set-cookie'].map((cookie: string) => {
return cookie.replace(/Domain=.*?(;|$)/, `Domain=${req.hostname};`)
})
}
res.set(response.headers).status(response.status).send(response.data)
} catch (error) {
res.status(500).send('Internal server error')
Expand Down
6 changes: 5 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,16 @@ export const createApp = (

createProxyEndpoints(app, {
beeApiUrl,
removePinHeader: removePinHeader ?? false,
removePinHeader: removePinHeader ?? true,
stampManager: stampManager ?? null,
allowlist,
cidSubdomains,
ensSubdomains,
hostname,
})

app.use(express.static('public'))

app.use((_req, res) => res.sendStatus(404))

return app
Expand Down
16 changes: 0 additions & 16 deletions test/bzz-link.mockserver.ts

This file was deleted.

Loading

0 comments on commit 784119e

Please sign in to comment.