@helia/verified-fetch 3.2.2 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/index.min.js +81 -71
- package/dist/index.min.js.map +4 -4
- package/dist/src/constants.d.ts +2 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +2 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/index.d.ts +57 -13
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +6 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-car.js +37 -27
- package/dist/src/plugins/plugin-handle-car.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor.js +5 -5
- package/dist/src/plugins/plugin-handle-dag-cbor.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.js +12 -12
- package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-walk.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-walk.js +5 -4
- package/dist/src/plugins/plugin-handle-dag-walk.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.js +13 -19
- package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
- package/dist/src/plugins/plugin-handle-json.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-json.js +5 -4
- package/dist/src/plugins/plugin-handle-json.js.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.js +18 -5
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
- package/dist/src/plugins/plugin-handle-tar.js +1 -1
- package/dist/src/plugins/plugin-handle-tar.js.map +1 -1
- package/dist/src/plugins/types.d.ts +10 -8
- package/dist/src/plugins/types.d.ts.map +1 -1
- package/dist/src/url-resolver.d.ts +21 -0
- package/dist/src/url-resolver.d.ts.map +1 -0
- package/dist/src/url-resolver.js +118 -0
- package/dist/src/url-resolver.js.map +1 -0
- package/dist/src/utils/byte-range-context.d.ts +1 -1
- package/dist/src/utils/content-type-parser.d.ts.map +1 -1
- package/dist/src/utils/content-type-parser.js +3 -0
- package/dist/src/utils/content-type-parser.js.map +1 -1
- package/dist/src/utils/get-content-type.d.ts +3 -3
- package/dist/src/utils/get-content-type.d.ts.map +1 -1
- package/dist/src/utils/get-content-type.js +1 -1
- package/dist/src/utils/get-content-type.js.map +1 -1
- package/dist/src/utils/get-e-tag.d.ts +1 -1
- package/dist/src/utils/get-offset-and-length.d.ts +6 -0
- package/dist/src/utils/get-offset-and-length.d.ts.map +1 -0
- package/dist/src/utils/get-offset-and-length.js +46 -0
- package/dist/src/utils/get-offset-and-length.js.map +1 -0
- package/dist/src/utils/get-resolved-accept-header.d.ts +2 -2
- package/dist/src/utils/get-resolved-accept-header.d.ts.map +1 -1
- package/dist/src/utils/handle-redirects.d.ts.map +1 -1
- package/dist/src/utils/handle-redirects.js +3 -3
- package/dist/src/utils/handle-redirects.js.map +1 -1
- package/dist/src/utils/ipfs-path-to-string.d.ts +6 -0
- package/dist/src/utils/ipfs-path-to-string.d.ts.map +1 -0
- package/dist/src/utils/ipfs-path-to-string.js +10 -0
- package/dist/src/utils/ipfs-path-to-string.js.map +1 -0
- package/dist/src/utils/is-accept-explicit.d.ts +6 -4
- package/dist/src/utils/is-accept-explicit.d.ts.map +1 -1
- package/dist/src/utils/is-accept-explicit.js +7 -4
- package/dist/src/utils/is-accept-explicit.js.map +1 -1
- package/dist/src/utils/parse-url-string.d.ts +1 -55
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +16 -217
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/response-headers.d.ts +1 -1
- package/dist/src/utils/response-headers.d.ts.map +1 -1
- package/dist/src/utils/responses.d.ts +1 -1
- package/dist/src/utils/select-output-type.d.ts +6 -2
- package/dist/src/utils/select-output-type.d.ts.map +1 -1
- package/dist/src/utils/select-output-type.js +28 -37
- package/dist/src/utils/select-output-type.js.map +1 -1
- package/dist/src/utils/server-timing.d.ts +5 -11
- package/dist/src/utils/server-timing.d.ts.map +1 -1
- package/dist/src/utils/server-timing.js +17 -15
- package/dist/src/utils/server-timing.js.map +1 -1
- package/dist/src/utils/walk-path.js +1 -1
- package/dist/src/utils/walk-path.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +3 -10
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +68 -57
- package/dist/src/verified-fetch.js.map +1 -1
- package/dist/typedoc-urls.json +13 -2
- package/package.json +35 -36
- package/src/constants.ts +1 -0
- package/src/index.ts +73 -22
- package/src/plugins/plugin-handle-car.ts +54 -30
- package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +2 -2
- package/src/plugins/plugin-handle-dag-cbor.ts +5 -5
- package/src/plugins/plugin-handle-dag-pb.ts +12 -12
- package/src/plugins/plugin-handle-dag-walk.ts +5 -4
- package/src/plugins/plugin-handle-ipns-record.ts +16 -19
- package/src/plugins/plugin-handle-json.ts +5 -4
- package/src/plugins/plugin-handle-raw.ts +21 -6
- package/src/plugins/plugin-handle-tar.ts +1 -1
- package/src/plugins/types.ts +12 -8
- package/src/url-resolver.ts +159 -0
- package/src/utils/byte-range-context.ts +1 -1
- package/src/utils/content-type-parser.ts +3 -0
- package/src/utils/get-content-type.ts +5 -4
- package/src/utils/get-e-tag.ts +1 -1
- package/src/utils/get-offset-and-length.ts +54 -0
- package/src/utils/get-resolved-accept-header.ts +2 -2
- package/src/utils/handle-redirects.ts +10 -3
- package/src/utils/ipfs-path-to-string.ts +9 -0
- package/src/utils/is-accept-explicit.ts +14 -7
- package/src/utils/parse-url-string.ts +20 -286
- package/src/utils/response-headers.ts +1 -1
- package/src/utils/responses.ts +1 -1
- package/src/utils/select-output-type.ts +38 -44
- package/src/utils/server-timing.ts +17 -30
- package/src/utils/walk-path.ts +1 -1
- package/src/verified-fetch.ts +78 -69
- package/dist/src/types.d.ts +0 -16
- package/dist/src/types.d.ts.map +0 -1
- package/dist/src/types.js +0 -2
- package/dist/src/types.js.map +0 -1
- package/dist/src/utils/parse-resource.d.ts +0 -18
- package/dist/src/utils/parse-resource.d.ts.map +0 -1
- package/dist/src/utils/parse-resource.js +0 -27
- package/dist/src/utils/parse-resource.js.map +0 -1
- package/src/types.ts +0 -17
- package/src/utils/parse-resource.ts +0 -42
|
@@ -37,19 +37,21 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
|
|
|
37
37
|
const forwardedHost = headers.get('x-forwarded-host')
|
|
38
38
|
const headerHost = headers.get('host')
|
|
39
39
|
const forwardedFor = headers.get('x-forwarded-for')
|
|
40
|
+
|
|
40
41
|
if (forwardedFor == null && forwardedHost == null && headerHost == null) {
|
|
41
42
|
log.trace('no redirect info found in headers')
|
|
42
43
|
return null
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
log.trace('checking for redirect info')
|
|
46
|
-
// if x-forwarded-host is passed, we need to set the location header to the
|
|
47
|
-
// so that the browser can redirect to the correct subdomain
|
|
47
|
+
// if x-forwarded-host is passed, we need to set the location header to the
|
|
48
|
+
// subdomain so that the browser can redirect to the correct subdomain
|
|
48
49
|
try {
|
|
49
50
|
const urlParts = matchURLString(resource)
|
|
50
51
|
const reqUrl = new URL(resource)
|
|
51
52
|
const actualHost = forwardedHost ?? reqUrl.host
|
|
52
53
|
const subdomainUrl = new URL(reqUrl)
|
|
54
|
+
|
|
53
55
|
if (urlParts.protocol === 'ipfs' && cid.version === 0) {
|
|
54
56
|
subdomainUrl.host = `${cid.toV1()}.ipfs.${actualHost}`
|
|
55
57
|
} else {
|
|
@@ -65,6 +67,7 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
|
|
|
65
67
|
log.trace('host header is not the same as the subdomain url host, not setting location header')
|
|
66
68
|
return null
|
|
67
69
|
}
|
|
70
|
+
|
|
68
71
|
if (reqUrl.host === subdomainUrl.host) {
|
|
69
72
|
log.trace('req url is the same as the subdomain url, not setting location header')
|
|
70
73
|
return null
|
|
@@ -75,6 +78,7 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
|
|
|
75
78
|
const pathUrl = new URL(reqUrl, `${reqUrl.protocol}//${actualHost}`)
|
|
76
79
|
pathUrl.pathname = maybeAddTrailingSlash(reqUrl.pathname)
|
|
77
80
|
log.trace('path url %s', pathUrl.href)
|
|
81
|
+
|
|
78
82
|
// try to query subdomain with HEAD request to see if it's supported
|
|
79
83
|
try {
|
|
80
84
|
const subdomainTest = await fetch(subdomainUrl, { method: 'HEAD' })
|
|
@@ -86,11 +90,14 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
|
|
|
86
90
|
throw new SubdomainNotSupportedError('subdomain not supported')
|
|
87
91
|
}
|
|
88
92
|
} catch (err: any) {
|
|
89
|
-
log('subdomain not supported', err)
|
|
93
|
+
log('subdomain not supported - %e', err)
|
|
94
|
+
|
|
90
95
|
if (pathUrl.href === reqUrl.href) {
|
|
91
96
|
log('path url is the same as the request url, not setting location header')
|
|
97
|
+
|
|
92
98
|
return null
|
|
93
99
|
}
|
|
100
|
+
|
|
94
101
|
// pathUrl is different from request URL (maybe even with just a trailing slash)
|
|
95
102
|
return movedPermanentlyResponse(resource.toString(), pathUrl.href)
|
|
96
103
|
}
|
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
import { FORMAT_TO_MIME_TYPE } from './select-output-type.js'
|
|
2
|
-
import type {
|
|
2
|
+
import type { UrlQuery } from '../index.ts'
|
|
3
3
|
|
|
4
4
|
export interface IsAcceptExplicitOptions {
|
|
5
|
-
query?:
|
|
5
|
+
query?: UrlQuery
|
|
6
6
|
headers: Headers
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function isExplicitAcceptHeader (headers: Headers): boolean {
|
|
10
10
|
const incomingAcceptHeader = headers.get('accept')
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
|
|
12
|
+
if (incomingAcceptHeader == null) {
|
|
13
|
+
return false
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
return Object.values(FORMAT_TO_MIME_TYPE)
|
|
17
|
+
.some(mimeType => incomingAcceptHeader.includes(mimeType))
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export function isExplicitFormatQuery (query?:
|
|
20
|
+
export function isExplicitFormatQuery (query?: UrlQuery): boolean {
|
|
18
21
|
const formatQuery = query?.format
|
|
22
|
+
|
|
19
23
|
if (formatQuery != null && Object.keys(FORMAT_TO_MIME_TYPE).includes(formatQuery)) {
|
|
20
24
|
return true
|
|
21
25
|
}
|
|
26
|
+
|
|
22
27
|
return false
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
|
-
* The user can provide an explicit `accept` header in the request headers or a
|
|
31
|
+
* The user can provide an explicit `accept` header in the request headers or a
|
|
32
|
+
* `format` query parameter in the URL.
|
|
33
|
+
*
|
|
27
34
|
* If either of these are provided, this function returns true.
|
|
28
35
|
*/
|
|
29
36
|
export function isExplicitIpldAcceptRequest ({ query, headers }: IsAcceptExplicitOptions): boolean {
|
|
@@ -1,68 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { TLRU } from './tlru.js'
|
|
6
|
-
import type { ServerTimingResult } from './server-timing.js'
|
|
7
|
-
import type { RequestFormatShorthand } from '../types.js'
|
|
8
|
-
import type { DNSLinkResolveResult, IPNS, IPNSResolveResult, IPNSRoutingEvents, ResolveDNSLinkProgressEvents, ResolveProgressEvents, ResolveResult } from '@helia/ipns'
|
|
9
|
-
import type { AbortOptions, ComponentLogger, PeerId } from '@libp2p/interface'
|
|
10
|
-
import type { ProgressOptions } from 'progress-events'
|
|
11
|
-
|
|
12
|
-
export const ipnsCache = new TLRU<DNSLinkResolveResult | IPNSResolveResult>(1000)
|
|
13
|
-
|
|
14
|
-
export interface ParseUrlStringInput {
|
|
15
|
-
urlString: string
|
|
16
|
-
ipns: IPNS
|
|
17
|
-
logger: ComponentLogger
|
|
18
|
-
withServerTiming?: boolean
|
|
19
|
-
}
|
|
20
|
-
export interface ParseUrlStringOptions extends ProgressOptions<ResolveProgressEvents | IPNSRoutingEvents | ResolveDNSLinkProgressEvents>, AbortOptions {
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ParsedUrlQuery extends Record<string, string | unknown> {
|
|
25
|
-
format?: RequestFormatShorthand
|
|
26
|
-
download?: boolean
|
|
27
|
-
filename?: string
|
|
28
|
-
'dag-scope'?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ParsedUrlStringResults extends ResolveResult {
|
|
32
|
-
protocol: 'ipfs' | 'ipns'
|
|
33
|
-
query: ParsedUrlQuery
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The value for the IPFS gateway spec compliant header `X-Ipfs-Path` on the
|
|
37
|
-
* response.
|
|
38
|
-
* The value of this header should be the original requested content path,
|
|
39
|
-
* prior to any path resolution or traversal.
|
|
40
|
-
*
|
|
41
|
-
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
|
|
42
|
-
*/
|
|
43
|
-
ipfsPath: string
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* seconds as a number
|
|
47
|
-
*/
|
|
48
|
-
ttl?: number
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* serverTiming items
|
|
52
|
-
*/
|
|
53
|
-
serverTimings: Array<ServerTimingResult<any>>
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
|
|
57
|
-
const PATH_REGEX = /^\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
|
|
58
|
-
const PATH_GATEWAY_REGEX = /^https?:\/\/(.*[^/])\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
|
|
59
|
-
const SUBDOMAIN_GATEWAY_REGEX = /^https?:\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.([^/?]+)\/?(?<path>[^?]*)\??(?<queryString>.*)$/
|
|
1
|
+
const URL_REGEX = /^(?<protocol>ip[fn]s):\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
|
|
2
|
+
const PATH_REGEX = /^\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
|
|
3
|
+
const PATH_GATEWAY_REGEX = /^https?:\/\/(.*[^/])\/(?<protocol>ip[fn]s)\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
|
|
4
|
+
const SUBDOMAIN_GATEWAY_REGEX = /^https?:\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.([^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
|
|
60
5
|
|
|
61
6
|
interface MatchUrlGroups {
|
|
62
7
|
protocol: 'ipfs' | 'ipns'
|
|
63
8
|
cidOrPeerIdOrDnsLink: string
|
|
64
9
|
path?: string
|
|
65
|
-
|
|
10
|
+
query?: string
|
|
66
11
|
}
|
|
67
12
|
|
|
68
13
|
function matchUrlGroupsGuard (groups?: null | { [key in string]: string; } | MatchUrlGroups): groups is MatchUrlGroups {
|
|
@@ -71,50 +16,38 @@ function matchUrlGroupsGuard (groups?: null | { [key in string]: string; } | Mat
|
|
|
71
16
|
const cidOrPeerIdOrDnsLink = groups?.cidOrPeerIdOrDnsLink
|
|
72
17
|
if (cidOrPeerIdOrDnsLink == null) { return false }
|
|
73
18
|
const path = groups?.path
|
|
74
|
-
const
|
|
19
|
+
const query = groups?.query
|
|
75
20
|
|
|
76
21
|
return ['ipns', 'ipfs'].includes(protocol) &&
|
|
77
22
|
typeof cidOrPeerIdOrDnsLink === 'string' &&
|
|
78
23
|
(path == null || typeof path === 'string') &&
|
|
79
|
-
(
|
|
24
|
+
(query == null || typeof query === 'string')
|
|
80
25
|
}
|
|
81
26
|
|
|
27
|
+
// TODO: can this be replaced with `new URL`?
|
|
82
28
|
export function matchURLString (urlString: string): MatchUrlGroups {
|
|
83
29
|
for (const pattern of [SUBDOMAIN_GATEWAY_REGEX, URL_REGEX, PATH_GATEWAY_REGEX, PATH_REGEX]) {
|
|
84
30
|
const match = urlString.match(pattern)
|
|
85
31
|
|
|
86
32
|
if (matchUrlGroupsGuard(match?.groups)) {
|
|
87
|
-
|
|
33
|
+
const groups = match.groups satisfies MatchUrlGroups
|
|
34
|
+
|
|
35
|
+
if (groups.path != null) {
|
|
36
|
+
groups.path = decodeURIComponent(groups.path)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// decode inline dnslink domain if present
|
|
40
|
+
if (pattern === SUBDOMAIN_GATEWAY_REGEX && groups.protocol === 'ipns' && isInlinedDnsLink(groups.cidOrPeerIdOrDnsLink)) {
|
|
41
|
+
groups.cidOrPeerIdOrDnsLink = dnsLinkLabelDecoder(groups.cidOrPeerIdOrDnsLink)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return groups
|
|
88
45
|
}
|
|
89
46
|
}
|
|
90
47
|
|
|
91
48
|
throw new TypeError(`Invalid URL: ${urlString}, please use ipfs://, ipns://, or gateway URLs only`)
|
|
92
49
|
}
|
|
93
50
|
|
|
94
|
-
/**
|
|
95
|
-
* determines the TTL for the resolved resource that will be used for the `Cache-Control` header's `max-age` directive.
|
|
96
|
-
* max-age is in seconds
|
|
97
|
-
*
|
|
98
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives
|
|
99
|
-
*
|
|
100
|
-
* If we have ipnsTtlNs, it will be a BigInt representing "nanoseconds". We need to convert it back to seconds.
|
|
101
|
-
*
|
|
102
|
-
* For more TTL nuances:
|
|
103
|
-
*
|
|
104
|
-
* @see https://github.com/ipfs/js-ipns/blob/16e0e10682fa9a663e0bb493a44d3e99a5200944/src/index.ts#L200
|
|
105
|
-
* @see https://github.com/ipfs/js-ipns/pull/308
|
|
106
|
-
* @returns the ttl in seconds
|
|
107
|
-
*/
|
|
108
|
-
function calculateTtl (resolveResult?: IPNSResolveResult | DNSLinkResolveResult): number | undefined {
|
|
109
|
-
if (resolveResult == null) {
|
|
110
|
-
return undefined
|
|
111
|
-
}
|
|
112
|
-
const dnsLinkTtl = (resolveResult as DNSLinkResolveResult).answer?.TTL
|
|
113
|
-
const ipnsTtlNs = (resolveResult as IPNSResolveResult).record?.ttl
|
|
114
|
-
const ipnsTtl = ipnsTtlNs != null ? Number(ipnsTtlNs / BigInt(1e9)) : undefined
|
|
115
|
-
return dnsLinkTtl ?? ipnsTtl
|
|
116
|
-
}
|
|
117
|
-
|
|
118
51
|
/**
|
|
119
52
|
* For DNSLink see https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
|
|
120
53
|
* DNSLink names include . which means they must be inlined into a single DNS label to provide unique origin and work with wildcard TLS certificates.
|
|
@@ -142,202 +75,3 @@ function isInlinedDnsLink (label: string): boolean {
|
|
|
142
75
|
function dnsLinkLabelDecoder (linkLabel: string): string {
|
|
143
76
|
return linkLabel.replace(/--/g, '%').replace(/-/g, '.').replace(/%/g, '-')
|
|
144
77
|
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* A function that parses ipfs:// and ipns:// URLs, returning an object with easily recognizable properties.
|
|
148
|
-
*
|
|
149
|
-
* After determining the protocol successfully, we process the cidOrPeerIdOrDnsLink:
|
|
150
|
-
* - If it's ipfs, it parses the CID or throws Error[]
|
|
151
|
-
* - If it's ipns, it attempts to resolve the PeerId and then the DNSLink. If both fail, Error[] is thrown.
|
|
152
|
-
*
|
|
153
|
-
* @todo we need to break out each step of this function (cid parsing, ipns resolving, dnslink resolving) into separate functions and then remove the eslint-disable comment
|
|
154
|
-
*
|
|
155
|
-
* @throws {Error[]}
|
|
156
|
-
*/
|
|
157
|
-
// eslint-disable-next-line complexity
|
|
158
|
-
export async function parseUrlString ({ urlString, ipns, logger, withServerTiming = false }: ParseUrlStringInput, options?: ParseUrlStringOptions): Promise<ParsedUrlStringResults> {
|
|
159
|
-
const log = logger.forComponent('helia:verified-fetch:parse-url-string')
|
|
160
|
-
const { protocol, cidOrPeerIdOrDnsLink, path: urlPath, queryString } = matchURLString(urlString)
|
|
161
|
-
|
|
162
|
-
let cid: CID | undefined
|
|
163
|
-
let resolvedPath: string | undefined
|
|
164
|
-
const errors: Error[] = []
|
|
165
|
-
let resolveResult: IPNSResolveResult | DNSLinkResolveResult | undefined
|
|
166
|
-
const serverTimings: Array<ServerTimingResult<any>> = []
|
|
167
|
-
|
|
168
|
-
if (protocol === 'ipfs') {
|
|
169
|
-
try {
|
|
170
|
-
cid = CID.parse(cidOrPeerIdOrDnsLink)
|
|
171
|
-
/**
|
|
172
|
-
* no ttl set. @link {setCacheControlHeader}
|
|
173
|
-
*/
|
|
174
|
-
} catch (err) {
|
|
175
|
-
log.error(err)
|
|
176
|
-
errors.push(new TypeError('Invalid CID for ipfs://<cid> URL'))
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
// protocol is ipns
|
|
180
|
-
resolveResult = ipnsCache.get(cidOrPeerIdOrDnsLink)
|
|
181
|
-
|
|
182
|
-
if (resolveResult != null) {
|
|
183
|
-
cid = resolveResult.cid
|
|
184
|
-
resolvedPath = resolveResult.path
|
|
185
|
-
log.trace('resolved %s to %c from cache', cidOrPeerIdOrDnsLink, cid)
|
|
186
|
-
} else {
|
|
187
|
-
log.trace('Attempting to resolve PeerId for %s', cidOrPeerIdOrDnsLink)
|
|
188
|
-
let peerId: PeerId | undefined
|
|
189
|
-
try {
|
|
190
|
-
// try resolving as an IPNS name
|
|
191
|
-
|
|
192
|
-
peerId = getPeerIdFromString(cidOrPeerIdOrDnsLink)
|
|
193
|
-
const pubKey = peerId?.publicKey
|
|
194
|
-
if (pubKey == null) {
|
|
195
|
-
throw new TypeError('cidOrPeerIdOrDnsLink contains no public key')
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (withServerTiming) {
|
|
199
|
-
const resolveIpns = async (): Promise<IPNSResolveResult> => {
|
|
200
|
-
return ipns.resolve(pubKey, options)
|
|
201
|
-
}
|
|
202
|
-
const resolveResultWithServerTiming = await serverTiming('ipns.resolve', `Resolve IPNS name ${cidOrPeerIdOrDnsLink}`, resolveIpns)
|
|
203
|
-
serverTimings.push(resolveResultWithServerTiming)
|
|
204
|
-
|
|
205
|
-
// eslint-disable-next-line max-depth
|
|
206
|
-
if (resolveResultWithServerTiming.error != null) {
|
|
207
|
-
throw resolveResultWithServerTiming.error
|
|
208
|
-
}
|
|
209
|
-
resolveResult = resolveResultWithServerTiming.result
|
|
210
|
-
} else {
|
|
211
|
-
resolveResult = await ipns.resolve(pubKey, options)
|
|
212
|
-
}
|
|
213
|
-
cid = resolveResult?.cid
|
|
214
|
-
resolvedPath = resolveResult?.path
|
|
215
|
-
log.trace('resolved %s to %c', cidOrPeerIdOrDnsLink, cid)
|
|
216
|
-
} catch (err) {
|
|
217
|
-
if (options?.signal?.aborted) {
|
|
218
|
-
throw new AbortError(options?.signal?.reason)
|
|
219
|
-
}
|
|
220
|
-
if (peerId == null) {
|
|
221
|
-
log.error('could not parse PeerId string "%s"', cidOrPeerIdOrDnsLink, err)
|
|
222
|
-
errors.push(new TypeError(`Could not parse PeerId in ipns url "${cidOrPeerIdOrDnsLink}", ${(err as Error).message}`))
|
|
223
|
-
} else {
|
|
224
|
-
log.error('could not resolve PeerId %c', peerId, err)
|
|
225
|
-
errors.push(new TypeError(`Could not resolve PeerId "${cidOrPeerIdOrDnsLink}": ${(err as Error).message}`))
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (cid == null) {
|
|
230
|
-
// cid is still null, try resolving as a DNSLink
|
|
231
|
-
let decodedDnsLinkLabel = cidOrPeerIdOrDnsLink
|
|
232
|
-
if (isInlinedDnsLink(cidOrPeerIdOrDnsLink)) {
|
|
233
|
-
decodedDnsLinkLabel = dnsLinkLabelDecoder(cidOrPeerIdOrDnsLink)
|
|
234
|
-
log.trace('decoded dnslink from "%s" to "%s"', cidOrPeerIdOrDnsLink, decodedDnsLinkLabel)
|
|
235
|
-
}
|
|
236
|
-
log.trace('Attempting to resolve DNSLink for %s', decodedDnsLinkLabel)
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
// eslint-disable-next-line max-depth
|
|
240
|
-
if (withServerTiming) {
|
|
241
|
-
const resolveResultWithServerTiming = await serverTiming('ipns.resolveDNSLink', `Resolve DNSLink ${decodedDnsLinkLabel}`, ipns.resolveDNSLink.bind(ipns, decodedDnsLinkLabel, options))
|
|
242
|
-
serverTimings.push(resolveResultWithServerTiming)
|
|
243
|
-
// eslint-disable-next-line max-depth
|
|
244
|
-
if (resolveResultWithServerTiming.error != null) {
|
|
245
|
-
throw resolveResultWithServerTiming.error
|
|
246
|
-
}
|
|
247
|
-
resolveResult = resolveResultWithServerTiming.result
|
|
248
|
-
} else {
|
|
249
|
-
resolveResult = await ipns.resolveDNSLink(decodedDnsLinkLabel, options)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
cid = resolveResult?.cid
|
|
253
|
-
resolvedPath = resolveResult?.path
|
|
254
|
-
log.trace('resolved %s to %c', decodedDnsLinkLabel, cid)
|
|
255
|
-
} catch (err: any) {
|
|
256
|
-
// eslint-disable-next-line max-depth
|
|
257
|
-
if (options?.signal?.aborted) {
|
|
258
|
-
throw new AbortError(options?.signal?.reason)
|
|
259
|
-
}
|
|
260
|
-
log.error('could not resolve DnsLink for "%s"', cidOrPeerIdOrDnsLink, err)
|
|
261
|
-
errors.push(err)
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (cid == null) {
|
|
268
|
-
if (errors.length === 1) {
|
|
269
|
-
throw errors[0]
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
errors.push(new Error(`Invalid resource. Cannot determine CID from URL "${urlString}".`))
|
|
273
|
-
|
|
274
|
-
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
275
|
-
throw errors
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
let ttl = calculateTtl(resolveResult)
|
|
279
|
-
|
|
280
|
-
if (resolveResult != null) {
|
|
281
|
-
// use the ttl for the resolved resource for the cache, but fallback to 2 minutes if not available
|
|
282
|
-
ttl = ttl ?? 60 * 2
|
|
283
|
-
log.trace('caching %s resolved to %s with TTL: %s', cidOrPeerIdOrDnsLink, cid, ttl)
|
|
284
|
-
// convert ttl from seconds to ms for the cache
|
|
285
|
-
ipnsCache.set(cidOrPeerIdOrDnsLink, resolveResult, ttl * 1000)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// parse query string
|
|
289
|
-
const query: Record<string, any> = {}
|
|
290
|
-
|
|
291
|
-
if (queryString != null && queryString.length > 0) {
|
|
292
|
-
const queryParts = queryString.split('&')
|
|
293
|
-
for (const part of queryParts) {
|
|
294
|
-
const [key, value] = part.split('=')
|
|
295
|
-
query[key] = decodeURIComponent(value)
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (query.download != null) {
|
|
299
|
-
query.download = query.download === 'true'
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (query.filename != null) {
|
|
303
|
-
query.filename = query.filename.toString()
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return {
|
|
308
|
-
protocol,
|
|
309
|
-
cid,
|
|
310
|
-
path: joinPaths(resolvedPath, urlPath ?? ''),
|
|
311
|
-
query,
|
|
312
|
-
ttl,
|
|
313
|
-
ipfsPath: `/${protocol}/${cidOrPeerIdOrDnsLink}${urlPath != null && urlPath !== '' ? `/${urlPath}` : ''}`,
|
|
314
|
-
serverTimings
|
|
315
|
-
} satisfies ParsedUrlStringResults
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* join the path from resolve result & given path.
|
|
320
|
-
* e.g. /ipns/<peerId>/ that is resolved to /ipfs/<cid>/<path1>, when requested as /ipns/<peerId>/<path2>, should be
|
|
321
|
-
* resolved to /ipfs/<cid>/<path1>/<path2>
|
|
322
|
-
*/
|
|
323
|
-
function joinPaths (resolvedPath: string | undefined, urlPath: string): string {
|
|
324
|
-
let path = ''
|
|
325
|
-
|
|
326
|
-
if (resolvedPath != null) {
|
|
327
|
-
path += resolvedPath
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (urlPath.length > 0) {
|
|
331
|
-
path = `${path.length > 0 ? `${path}/` : path}${urlPath}`
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// replace duplicate forward slashes
|
|
335
|
-
path = path.replace(/\/(\/)+/g, '/')
|
|
336
|
-
|
|
337
|
-
// strip trailing forward slash if present
|
|
338
|
-
if (path.startsWith('/')) {
|
|
339
|
-
path = path.substring(1)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return path.split('/').map(decodeURIComponent).join('/')
|
|
343
|
-
}
|
package/src/utils/responses.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ByteRangeContext } from './byte-range-context.js'
|
|
2
|
-
import type { SupportedBodyTypes } from '../
|
|
2
|
+
import type { SupportedBodyTypes } from '../index.js'
|
|
3
3
|
import type { Logger } from '@libp2p/interface'
|
|
4
4
|
|
|
5
5
|
function setField (response: Response, name: string, value: string | boolean): void {
|
|
@@ -3,7 +3,7 @@ import { code as dagJsonCode } from '@ipld/dag-json'
|
|
|
3
3
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
4
4
|
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
5
5
|
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
6
|
-
import type { RequestFormatShorthand } from '../
|
|
6
|
+
import type { RequestFormatShorthand } from '../index.js'
|
|
7
7
|
import type { CID } from 'multiformats/cid'
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -62,10 +62,15 @@ const CID_TYPE_MAP: Record<number, string[]> = {
|
|
|
62
62
|
]
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export interface AcceptHeader {
|
|
66
|
+
mimeType: string
|
|
67
|
+
options: Record<string, string>
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
/**
|
|
66
71
|
* Selects an output mime-type based on the CID and a passed `Accept` header
|
|
67
72
|
*/
|
|
68
|
-
export function selectOutputType (cid: CID, accept?: string):
|
|
73
|
+
export function selectOutputType (cid: CID, accept?: string): AcceptHeader | undefined {
|
|
69
74
|
const cidMimeTypes = CID_TYPE_MAP[cid.code]
|
|
70
75
|
|
|
71
76
|
if (accept != null) {
|
|
@@ -73,80 +78,69 @@ export function selectOutputType (cid: CID, accept?: string): string | undefined
|
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
function chooseMimeType (accept: string, validMimeTypes: string[]):
|
|
81
|
+
function chooseMimeType (accept: string, validMimeTypes: string[]): AcceptHeader | undefined {
|
|
77
82
|
const requestedMimeTypes = accept
|
|
78
83
|
.split(',')
|
|
79
84
|
.map(s => {
|
|
80
85
|
const parts = s.trim().split(';')
|
|
81
86
|
|
|
87
|
+
const options: Record<string, string> = {
|
|
88
|
+
q: '0'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (let i = 1; i < parts.length; i++) {
|
|
92
|
+
const [key, value] = parts[i].split('=').map(s => s.trim())
|
|
93
|
+
|
|
94
|
+
options[key] = value
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
return {
|
|
83
98
|
mimeType: `${parts[0]}`.trim(),
|
|
84
|
-
|
|
99
|
+
options
|
|
85
100
|
}
|
|
86
101
|
})
|
|
87
102
|
.sort((a, b) => {
|
|
88
|
-
if (a.
|
|
103
|
+
if (a.options.q === b.options.q) {
|
|
89
104
|
return 0
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
if (a.
|
|
107
|
+
if (a.options.q > b.options.q) {
|
|
93
108
|
return -1
|
|
94
109
|
}
|
|
95
110
|
|
|
96
111
|
return 1
|
|
97
112
|
})
|
|
98
|
-
.map(s => s.mimeType)
|
|
99
113
|
|
|
100
114
|
for (const headerFormat of requestedMimeTypes) {
|
|
101
115
|
for (const mimeType of validMimeTypes) {
|
|
102
|
-
if (headerFormat.includes(mimeType)) {
|
|
103
|
-
return
|
|
116
|
+
if (headerFormat.mimeType.includes(mimeType)) {
|
|
117
|
+
return headerFormat
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
if (headerFormat === '*/*') {
|
|
107
|
-
return
|
|
120
|
+
if (headerFormat.mimeType === '*/*') {
|
|
121
|
+
return {
|
|
122
|
+
mimeType,
|
|
123
|
+
options: headerFormat.options
|
|
124
|
+
}
|
|
108
125
|
}
|
|
109
126
|
|
|
110
|
-
if (headerFormat.startsWith('*/') && mimeType.split('/')[1] === headerFormat.split('/')[1]) {
|
|
111
|
-
return
|
|
127
|
+
if (headerFormat.mimeType.startsWith('*/') && mimeType.split('/')[1] === headerFormat.mimeType.split('/')[1]) {
|
|
128
|
+
return {
|
|
129
|
+
mimeType,
|
|
130
|
+
options: headerFormat.options
|
|
131
|
+
}
|
|
112
132
|
}
|
|
113
133
|
|
|
114
|
-
if (headerFormat.endsWith('/*') && mimeType.split('/')[0] === headerFormat.split('/')[0]) {
|
|
115
|
-
return
|
|
134
|
+
if (headerFormat.mimeType.endsWith('/*') && mimeType.split('/')[0] === headerFormat.mimeType.split('/')[0]) {
|
|
135
|
+
return {
|
|
136
|
+
mimeType,
|
|
137
|
+
options: headerFormat.options
|
|
138
|
+
}
|
|
116
139
|
}
|
|
117
140
|
}
|
|
118
141
|
}
|
|
119
142
|
}
|
|
120
143
|
|
|
121
|
-
/**
|
|
122
|
-
* Parses q-factor weighting from the accept header to allow letting some mime
|
|
123
|
-
* types take precedence over others.
|
|
124
|
-
*
|
|
125
|
-
* If the q-factor for an acceptable mime representation is omitted it defaults
|
|
126
|
-
* to `1`.
|
|
127
|
-
*
|
|
128
|
-
* All specified values should be in the range 0-1.
|
|
129
|
-
*
|
|
130
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept#q
|
|
131
|
-
*/
|
|
132
|
-
function parseQFactor (str?: string): number {
|
|
133
|
-
if (str != null) {
|
|
134
|
-
str = str.trim()
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (str?.startsWith('q=') !== true) {
|
|
138
|
-
return 1
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const factor = parseFloat(str.replace('q=', ''))
|
|
142
|
-
|
|
143
|
-
if (isNaN(factor)) {
|
|
144
|
-
return 0
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return factor
|
|
148
|
-
}
|
|
149
|
-
|
|
150
144
|
export const FORMAT_TO_MIME_TYPE: Record<RequestFormatShorthand, string> = {
|
|
151
145
|
raw: 'application/vnd.ipld.raw',
|
|
152
146
|
car: 'application/vnd.ipld.car',
|
|
@@ -1,37 +1,24 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
result: T
|
|
4
|
-
header: string
|
|
5
|
-
}
|
|
6
|
-
export interface ServerTimingError {
|
|
7
|
-
result: null
|
|
8
|
-
error: Error
|
|
9
|
-
header: string
|
|
10
|
-
}
|
|
11
|
-
export type ServerTimingResult<T> = ServerTimingSuccess<T> | ServerTimingError
|
|
1
|
+
export class ServerTiming {
|
|
2
|
+
private headers: string[]
|
|
12
3
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
fn: () => Promise<T>
|
|
17
|
-
): Promise<ServerTimingResult<T>> {
|
|
18
|
-
const startTime = performance.now()
|
|
4
|
+
constructor () {
|
|
5
|
+
this.headers = []
|
|
6
|
+
}
|
|
19
7
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
getHeader (): string {
|
|
9
|
+
return this.headers.join(',')
|
|
10
|
+
}
|
|
23
11
|
|
|
24
|
-
|
|
12
|
+
async time <T> (name: string, description: string, promise: Promise<T>): Promise<T> {
|
|
13
|
+
const startTime = performance.now()
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const duration = (endTime - startTime).toFixed(1)
|
|
15
|
+
try {
|
|
16
|
+
return await promise // Execute the function
|
|
17
|
+
} finally {
|
|
18
|
+
const endTime = performance.now()
|
|
19
|
+
const duration = (endTime - startTime).toFixed(1) // Duration in milliseconds
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return { result: null, error, header } // Pass error with timing info
|
|
21
|
+
this.headers.push(`${name};dur=${duration};desc="${description}"`)
|
|
22
|
+
}
|
|
36
23
|
}
|
|
37
24
|
}
|
package/src/utils/walk-path.ts
CHANGED
|
@@ -62,7 +62,7 @@ export async function handlePathWalking ({ cid, path, resource, options, blockst
|
|
|
62
62
|
return notFoundResponse(resource)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
log.error('error walking path %s', path, err)
|
|
65
|
+
log.error('error walking path "%s" - %e', path, err)
|
|
66
66
|
return badGatewayResponse(resource, 'Error walking path')
|
|
67
67
|
}
|
|
68
68
|
}
|