@helia/verified-fetch 4.0.1 → 4.0.3
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/dist/index.min.js +48 -48
- package/dist/index.min.js.map +4 -4
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.d.ts +3 -3
- 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 +5 -5
- package/dist/src/plugins/plugin-handle-dag-cbor-html-preview.js.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-dag-pb.js +35 -25
- package/dist/src/plugins/plugin-handle-dag-pb.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-ipns-record.js +2 -2
- package/dist/src/plugins/plugin-handle-ipns-record.js.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.js +1 -1
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -1
- package/dist/src/plugins/types.d.ts +1 -4
- package/dist/src/plugins/types.d.ts.map +1 -1
- package/dist/src/url-resolver.d.ts +4 -3
- package/dist/src/url-resolver.d.ts.map +1 -1
- package/dist/src/url-resolver.js +35 -47
- package/dist/src/url-resolver.js.map +1 -1
- package/dist/src/utils/dnslink-label.d.ts +26 -0
- package/dist/src/utils/dnslink-label.d.ts.map +1 -0
- package/dist/src/utils/dnslink-label.js +35 -0
- package/dist/src/utils/dnslink-label.js.map +1 -0
- package/dist/src/utils/get-content-type.d.ts +1 -1
- package/dist/src/utils/get-content-type.d.ts.map +1 -1
- package/dist/src/utils/get-content-type.js +2 -2
- package/dist/src/utils/get-content-type.js.map +1 -1
- package/dist/src/utils/get-stream-from-async-iterable.d.ts +1 -2
- package/dist/src/utils/get-stream-from-async-iterable.d.ts.map +1 -1
- package/dist/src/utils/get-stream-from-async-iterable.js +1 -3
- package/dist/src/utils/get-stream-from-async-iterable.js.map +1 -1
- package/dist/src/utils/handle-redirects.js +2 -2
- package/dist/src/utils/ipfs-path-to-url.d.ts +16 -0
- package/dist/src/utils/ipfs-path-to-url.d.ts.map +1 -0
- package/dist/src/utils/ipfs-path-to-url.js +45 -0
- package/dist/src/utils/ipfs-path-to-url.js.map +1 -0
- package/dist/src/utils/parse-url-string.d.ts +18 -5
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +126 -44
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/resource-to-cache-key.js +2 -2
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +4 -0
- package/dist/src/utils/responses.js.map +1 -1
- package/dist/src/utils/walk-path.js +1 -1
- package/dist/src/utils/walk-path.js.map +1 -1
- package/package.json +10 -10
- package/src/index.ts +4 -2
- package/src/plugins/plugin-handle-dag-cbor-html-preview.ts +8 -8
- package/src/plugins/plugin-handle-dag-pb.ts +42 -24
- package/src/plugins/plugin-handle-ipns-record.ts +2 -2
- package/src/plugins/plugin-handle-raw.ts +1 -1
- package/src/plugins/types.ts +1 -4
- package/src/url-resolver.ts +37 -56
- package/src/utils/dnslink-label.ts +38 -0
- package/src/utils/get-content-type.ts +3 -3
- package/src/utils/get-stream-from-async-iterable.ts +1 -4
- package/src/utils/handle-redirects.ts +2 -2
- package/src/utils/ipfs-path-to-url.ts +54 -0
- package/src/utils/parse-url-string.ts +166 -49
- package/src/utils/resource-to-cache-key.ts +2 -2
- package/src/utils/responses.ts +6 -0
- package/src/utils/walk-path.ts +1 -1
package/src/url-resolver.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { peerIdFromCID, peerIdFromString } from '@libp2p/peer-id'
|
|
2
2
|
import { CID } from 'multiformats/cid'
|
|
3
|
-
import {
|
|
3
|
+
import { parseURLString } from './utils/parse-url-string.ts'
|
|
4
4
|
import type { ResolveURLOptions, ResolveURLResult, Resource, URLResolver as URLResolverInterface } from './index.ts'
|
|
5
|
+
import type { ParsedURL } from './utils/parse-url-string.ts'
|
|
5
6
|
import type { ServerTiming } from './utils/server-timing.ts'
|
|
6
7
|
import type { DNSLink } from '@helia/dnslink'
|
|
7
8
|
import type { IPNSResolver } from '@helia/ipns'
|
|
@@ -15,29 +16,6 @@ export interface URLResolverComponents {
|
|
|
15
16
|
timing: ServerTiming
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function toQuery (query?: string): Record<string, any> {
|
|
19
|
-
if (query == null) {
|
|
20
|
-
return {}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const params = new URLSearchParams(query)
|
|
24
|
-
const output: Record<string, any> = {}
|
|
25
|
-
|
|
26
|
-
for (const [key, value] of params.entries()) {
|
|
27
|
-
output[key] = value
|
|
28
|
-
|
|
29
|
-
if (value === 'true') {
|
|
30
|
-
output[key] = true
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (value === 'false') {
|
|
34
|
-
output[key] = false
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return output
|
|
39
|
-
}
|
|
40
|
-
|
|
41
19
|
export class URLResolver implements URLResolverInterface {
|
|
42
20
|
private readonly components: URLResolverComponents
|
|
43
21
|
|
|
@@ -53,56 +31,60 @@ export class URLResolver implements URLResolverInterface {
|
|
|
53
31
|
const cid = CID.asCID(resource)
|
|
54
32
|
|
|
55
33
|
if (cid != null) {
|
|
56
|
-
return this.resolveCIDResource(cid,
|
|
34
|
+
return this.resolveCIDResource(cid, {
|
|
35
|
+
url: new URL(`ipfs://${cid}`)
|
|
36
|
+
}, options)
|
|
57
37
|
}
|
|
58
38
|
|
|
59
39
|
throw new TypeError(`Invalid resource. Cannot determine CID from resource: ${resource}`)
|
|
60
40
|
}
|
|
61
41
|
|
|
62
42
|
async parseUrlString (urlString: string, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
|
|
63
|
-
const
|
|
43
|
+
const result = parseURLString(urlString)
|
|
64
44
|
|
|
65
|
-
if (protocol === 'ipfs') {
|
|
66
|
-
const cid = CID.parse(cidOrPeerIdOrDnsLink)
|
|
45
|
+
if (result.protocol === 'ipfs') {
|
|
46
|
+
const cid = CID.parse(result.cidOrPeerIdOrDnsLink)
|
|
67
47
|
|
|
68
|
-
return this.resolveCIDResource(cid,
|
|
48
|
+
return this.resolveCIDResource(cid, result, options)
|
|
69
49
|
}
|
|
70
50
|
|
|
71
|
-
if (protocol === 'ipns') {
|
|
51
|
+
if (result.protocol === 'ipns') {
|
|
72
52
|
// try to parse target as peer id
|
|
73
53
|
let peerId: PeerId
|
|
74
54
|
|
|
75
55
|
try {
|
|
76
|
-
peerId = peerIdFromString(cidOrPeerIdOrDnsLink)
|
|
56
|
+
peerId = peerIdFromString(result.cidOrPeerIdOrDnsLink)
|
|
77
57
|
} catch {
|
|
78
58
|
// fall back to DNSLink (e.g. /ipns/example.com)
|
|
79
|
-
return this.resolveDNSLink(cidOrPeerIdOrDnsLink,
|
|
59
|
+
return this.resolveDNSLink(result.cidOrPeerIdOrDnsLink, result, options)
|
|
80
60
|
}
|
|
81
61
|
|
|
82
62
|
// parse multihash from string (e.g. /ipns/QmFoo...)
|
|
83
|
-
return this.resolveIPNSName(cidOrPeerIdOrDnsLink, peerId,
|
|
63
|
+
return this.resolveIPNSName(result.cidOrPeerIdOrDnsLink, peerId, result, options)
|
|
84
64
|
}
|
|
85
65
|
|
|
86
66
|
throw new TypeError(`Invalid resource. Cannot determine CID from resource: ${urlString}`)
|
|
87
67
|
}
|
|
88
68
|
|
|
89
|
-
async resolveCIDResource (cid: CID,
|
|
69
|
+
async resolveCIDResource (cid: CID, parsed: Partial<ParsedURL> & Pick<ParsedURL, 'url'>, options: ResolveURLOptions = {}): Promise<ResolveURLResult> {
|
|
90
70
|
if (cid.code === CODEC_LIBP2P_KEY) {
|
|
91
71
|
// special case - peer id encoded as a CID
|
|
92
|
-
return this.resolveIPNSName(cid.toString(), peerIdFromCID(cid),
|
|
72
|
+
return this.resolveIPNSName(cid.toString(), peerIdFromCID(cid), parsed, options)
|
|
93
73
|
}
|
|
94
74
|
|
|
95
75
|
return {
|
|
76
|
+
url: parsed.url,
|
|
96
77
|
cid,
|
|
97
78
|
protocol: 'ipfs',
|
|
98
|
-
query,
|
|
99
|
-
path,
|
|
79
|
+
query: parsed.query ?? {},
|
|
80
|
+
path: parsed.path ?? [],
|
|
81
|
+
fragment: parsed.fragment ?? '',
|
|
100
82
|
ttl: 29030400, // 1 year for ipfs content
|
|
101
|
-
ipfsPath: `/ipfs/${cid}${
|
|
83
|
+
ipfsPath: `/ipfs/${cid}${parsed.url.pathname}`
|
|
102
84
|
}
|
|
103
85
|
}
|
|
104
86
|
|
|
105
|
-
async resolveDNSLink (domain: string,
|
|
87
|
+
async resolveDNSLink (domain: string, parsed: ParsedURL, options?: ResolveURLOptions): Promise<ResolveURLResult> {
|
|
106
88
|
const results = await this.components.timing.time('dnsLink.resolve', `Resolve DNSLink ${domain}`, this.components.dnsLink.resolve(domain, options))
|
|
107
89
|
const result = results?.[0]
|
|
108
90
|
|
|
@@ -112,7 +94,7 @@ export class URLResolver implements URLResolverInterface {
|
|
|
112
94
|
|
|
113
95
|
// dnslink resolved to IPNS name
|
|
114
96
|
if (result.namespace === 'ipns') {
|
|
115
|
-
return this.resolveIPNSName(domain, result.peerId,
|
|
97
|
+
return this.resolveIPNSName(domain, result.peerId, parsed, options)
|
|
116
98
|
}
|
|
117
99
|
|
|
118
100
|
// dnslink resolved to CID
|
|
@@ -122,38 +104,37 @@ export class URLResolver implements URLResolverInterface {
|
|
|
122
104
|
}
|
|
123
105
|
|
|
124
106
|
return {
|
|
107
|
+
url: parsed.url,
|
|
125
108
|
cid: result.cid,
|
|
126
|
-
path: concatPaths(result.path, path),
|
|
109
|
+
path: concatPaths(...(result.path ?? '').split('/'), ...(parsed.path ?? [])),
|
|
110
|
+
fragment: parsed.fragment,
|
|
127
111
|
// dnslink is mutable so return 'ipns' protocol so we do not include immutable in cache-control header
|
|
128
112
|
protocol: 'ipns',
|
|
129
113
|
ttl: result.answer.TTL,
|
|
130
|
-
query,
|
|
131
|
-
ipfsPath: `/ipns/${domain}${
|
|
114
|
+
query: parsed.query,
|
|
115
|
+
ipfsPath: `/ipns/${domain}${parsed.url.pathname}`
|
|
132
116
|
}
|
|
133
117
|
}
|
|
134
118
|
|
|
135
|
-
async resolveIPNSName (resource: string, key: PeerId,
|
|
119
|
+
async resolveIPNSName (resource: string, key: PeerId, parsed: Partial<ParsedURL> & Pick<ParsedURL, 'url'>, options?: AbortOptions): Promise<ResolveURLResult> {
|
|
136
120
|
const result = await this.components.timing.time('ipns.resolve', `Resolve IPNS name ${key}`, this.components.ipnsResolver.resolve(key, options))
|
|
137
121
|
|
|
138
122
|
return {
|
|
123
|
+
url: parsed.url,
|
|
139
124
|
cid: result.cid,
|
|
140
|
-
path: concatPaths(result.path, path),
|
|
141
|
-
query,
|
|
125
|
+
path: concatPaths(...(result.path ?? '').split('/'), ...(parsed.path ?? [])),
|
|
126
|
+
query: parsed.query ?? {},
|
|
127
|
+
fragment: parsed.fragment ?? '',
|
|
142
128
|
protocol: 'ipns',
|
|
143
129
|
// IPNS ttl is in nanoseconds, convert to seconds
|
|
144
130
|
ttl: Number((result.record.ttl ?? 0n) / BigInt(1e9)),
|
|
145
|
-
ipfsPath: `/ipns/${resource}${
|
|
131
|
+
ipfsPath: `/ipns/${resource}${parsed.url.pathname}`
|
|
146
132
|
}
|
|
147
133
|
}
|
|
148
134
|
}
|
|
149
135
|
|
|
150
|
-
function concatPaths (...paths: Array<string | undefined>): string {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.join('/')
|
|
155
|
-
.replaceAll(/(\/+)/g, '/')
|
|
156
|
-
.replace(/^(\/)+/, '')
|
|
157
|
-
.replace(/(\/)+$/, '/')
|
|
158
|
-
}`
|
|
136
|
+
function concatPaths (...paths: Array<string | undefined>): string[] {
|
|
137
|
+
// @ts-expect-error undefined is filtered out
|
|
138
|
+
return paths
|
|
139
|
+
.filter(p => p != null && p !== '')
|
|
159
140
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* For DNSLink see https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
|
|
3
|
+
* DNSLink names include . which means they must be inlined into a single DNS label to provide unique origin and work with wildcard TLS certificates.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// DNS label can have up to 63 characters, consisting of alphanumeric
|
|
7
|
+
// characters or hyphens -, but it must not start or end with a hyphen.
|
|
8
|
+
const dnsLabelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks if label looks like inlined DNSLink.
|
|
12
|
+
* (https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header)
|
|
13
|
+
*/
|
|
14
|
+
export function isInlinedDnsLink (label: string): boolean {
|
|
15
|
+
return dnsLabelRegex.test(label) && label.includes('-') && !label.includes('.')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* DNSLink label decoding
|
|
20
|
+
* - Every standalone - is replaced with .
|
|
21
|
+
* - Every remaining -- is replaced with -
|
|
22
|
+
*
|
|
23
|
+
* @example en-wikipedia--on--ipfs-org -> en.wikipedia-on-ipfs.org
|
|
24
|
+
*/
|
|
25
|
+
export function decodeDNSLinkLabel (label: string): string {
|
|
26
|
+
return label.replace(/--/g, '%').replace(/-/g, '.').replace(/%/g, '-')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* DNSLink label encoding
|
|
31
|
+
* - Every - is replaced with --
|
|
32
|
+
* - Every . is replaced with -
|
|
33
|
+
*
|
|
34
|
+
* @example en.wikipedia-on-ipfs.org -> en-wikipedia--on--ipfs-org
|
|
35
|
+
*/
|
|
36
|
+
export function encodeDNSLinkLabel (name: string): string {
|
|
37
|
+
return name.replace(/-/g, '--').replace(/\./g, '-')
|
|
38
|
+
}
|
|
@@ -5,7 +5,7 @@ import type { Logger } from '@libp2p/interface'
|
|
|
5
5
|
|
|
6
6
|
export interface GetContentTypeOptions {
|
|
7
7
|
bytes: Uint8Array
|
|
8
|
-
path?: string
|
|
8
|
+
path?: string[]
|
|
9
9
|
defaultContentType?: string
|
|
10
10
|
contentTypeParser?: ContentTypeParser
|
|
11
11
|
log: Logger
|
|
@@ -25,7 +25,7 @@ export async function getContentType ({ bytes, path, contentTypeParser, log, def
|
|
|
25
25
|
try {
|
|
26
26
|
let fileName
|
|
27
27
|
if (filenameParam == null) {
|
|
28
|
-
fileName = path?.
|
|
28
|
+
fileName = path?.[path.length - 1]?.trim()
|
|
29
29
|
fileName = (fileName === '' || fileName?.split('.').length === 1) ? undefined : fileName
|
|
30
30
|
} else {
|
|
31
31
|
fileName = filenameParam
|
|
@@ -41,7 +41,7 @@ export async function getContentType ({ bytes, path, contentTypeParser, log, def
|
|
|
41
41
|
} else if (parsed != null) {
|
|
42
42
|
contentType = parsed
|
|
43
43
|
}
|
|
44
|
-
log.trace('contentTypeParser returned %s', contentType)
|
|
44
|
+
log.trace('contentTypeParser returned %s for file with name %s', contentType, fileName)
|
|
45
45
|
} catch (err) {
|
|
46
46
|
log.error('error parsing content type', err)
|
|
47
47
|
}
|
|
@@ -2,18 +2,15 @@ import { AbortError } from '@libp2p/interface'
|
|
|
2
2
|
import { CustomProgressEvent } from 'progress-events'
|
|
3
3
|
import { NoContentError } from '../errors.js'
|
|
4
4
|
import type { VerifiedFetchInit } from '../index.js'
|
|
5
|
-
import type { Logger } from '@libp2p/interface'
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Converts an async iterator of Uint8Array bytes to a stream and returns the first chunk of bytes
|
|
9
8
|
*/
|
|
10
|
-
export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>,
|
|
11
|
-
const log = logger.newScope('get-stream-from-async-iterable')
|
|
9
|
+
export async function getStreamFromAsyncIterable (iterator: AsyncIterable<Uint8Array>, options?: Pick<VerifiedFetchInit, 'onProgress' | 'signal'>): Promise<{ stream: ReadableStream<Uint8Array>, firstChunk: Uint8Array }> {
|
|
12
10
|
const reader = iterator[Symbol.asyncIterator]()
|
|
13
11
|
const { value: firstChunk, done } = await reader.next()
|
|
14
12
|
|
|
15
13
|
if (done === true) {
|
|
16
|
-
log.error('no content found for path "%s"', path)
|
|
17
14
|
throw new NoContentError()
|
|
18
15
|
}
|
|
19
16
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SubdomainNotSupportedError } from '../errors.js'
|
|
2
|
-
import {
|
|
2
|
+
import { parseURLString } from './parse-url-string.js'
|
|
3
3
|
import { movedPermanentlyResponse } from './responses.js'
|
|
4
4
|
import type { VerifiedFetchInit, Resource } from '../index.js'
|
|
5
5
|
import type { AbortOptions, ComponentLogger } from '@libp2p/interface'
|
|
@@ -47,7 +47,7 @@ export async function getRedirectResponse ({ resource, options, logger, cid, fet
|
|
|
47
47
|
// if x-forwarded-host is passed, we need to set the location header to the
|
|
48
48
|
// subdomain so that the browser can redirect to the correct subdomain
|
|
49
49
|
try {
|
|
50
|
-
const urlParts =
|
|
50
|
+
const urlParts = parseURLString(resource)
|
|
51
51
|
const reqUrl = new URL(resource)
|
|
52
52
|
const actualHost = forwardedHost ?? reqUrl.host
|
|
53
53
|
const subdomainUrl = new URL(reqUrl)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { InvalidParametersError } from '@libp2p/interface'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Turns an IPFS or IPNS path into a HTTP URL. Path gateway syntax is used to
|
|
5
|
+
* preserve any case sensitivity
|
|
6
|
+
*
|
|
7
|
+
* - `/ipfs/cid` -> `https://example.org/ipfs/cid`
|
|
8
|
+
* - `/ipns/name` -> `https://example.org/ipns/name`
|
|
9
|
+
*/
|
|
10
|
+
export function ipfsPathToUrl (path: string): string {
|
|
11
|
+
if (!path.startsWith('/ipfs/') && !path.startsWith('/ipns/')) {
|
|
12
|
+
throw new InvalidParametersError(`Path ${path} did not start with /ipfs/ or /ipns/`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// trim fragment
|
|
16
|
+
const fragmentIndex = path.indexOf('#')
|
|
17
|
+
let fragment = ''
|
|
18
|
+
|
|
19
|
+
if (fragmentIndex > -1) {
|
|
20
|
+
fragment = path.substring(fragmentIndex)
|
|
21
|
+
path = path.substring(0, fragmentIndex)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// trim query
|
|
25
|
+
const queryIndex = path.indexOf('?')
|
|
26
|
+
let query = ''
|
|
27
|
+
|
|
28
|
+
if (queryIndex > -1) {
|
|
29
|
+
query = path.substring(queryIndex)
|
|
30
|
+
path = path.substring(0, queryIndex)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const type = path.substring(1, 5)
|
|
34
|
+
const rest = path.substring(6)
|
|
35
|
+
|
|
36
|
+
return `https://example.org/${type}/${rest}${query}${fragment}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Turns an IPFS or IPNS URL into a HTTP URL. Path gateway syntax is used to
|
|
41
|
+
* preserve and case sensitivity
|
|
42
|
+
*
|
|
43
|
+
* `ipfs://cid` -> `https://example.org/ipfs/cid`
|
|
44
|
+
*/
|
|
45
|
+
export function ipfsUrlToUrl (url: string): string {
|
|
46
|
+
if (!url.startsWith('ipfs://') && !url.startsWith('ipns://')) {
|
|
47
|
+
throw new InvalidParametersError(`URL ${url} did not start with ipfs:// or ipns://`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const type = url.substring(0, 4)
|
|
51
|
+
const rest = url.substring(7)
|
|
52
|
+
|
|
53
|
+
return `https://example.org/${type}/${rest}`
|
|
54
|
+
}
|
|
@@ -1,77 +1,194 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const SUBDOMAIN_GATEWAY_REGEX = /^https?:\/\/(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.([^/?]+)\/?(?<path>[^?]*)\??(?<query>.*)$/
|
|
1
|
+
import { InvalidParametersError } from '@libp2p/interface'
|
|
2
|
+
import { decodeDNSLinkLabel, isInlinedDnsLink } from './dnslink-label.ts'
|
|
3
|
+
import { ipfsPathToUrl, ipfsUrlToUrl } from './ipfs-path-to-url.ts'
|
|
5
4
|
|
|
6
|
-
interface
|
|
5
|
+
interface SubdomainMatchGroups {
|
|
7
6
|
protocol: 'ipfs' | 'ipns'
|
|
8
7
|
cidOrPeerIdOrDnsLink: string
|
|
9
|
-
path?: string
|
|
10
|
-
query?: string
|
|
11
8
|
}
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
const SUBDOMAIN_GATEWAY_REGEX = /^(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.([^/?]+)$/
|
|
11
|
+
|
|
12
|
+
function matchSubdomainGroupsGuard (groups?: null | { [key in string]: string; } | SubdomainMatchGroups): groups is SubdomainMatchGroups {
|
|
14
13
|
const protocol = groups?.protocol
|
|
15
|
-
|
|
14
|
+
|
|
15
|
+
if (protocol !== 'ipfs' && protocol !== 'ipns') {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
16
19
|
const cidOrPeerIdOrDnsLink = groups?.cidOrPeerIdOrDnsLink
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
(path == null || typeof path === 'string') &&
|
|
24
|
-
(query == null || typeof query === 'string')
|
|
20
|
+
|
|
21
|
+
if (cidOrPeerIdOrDnsLink == null) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
export interface ParsedURL {
|
|
29
|
+
url: URL,
|
|
30
|
+
protocol: 'ipfs' | 'ipns'
|
|
31
|
+
cidOrPeerIdOrDnsLink: string
|
|
32
|
+
path: string[]
|
|
33
|
+
query: Record<string, any>
|
|
34
|
+
fragment: string
|
|
35
|
+
}
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
function toQuery (query?: URLSearchParams): Record<string, any> {
|
|
38
|
+
if (query == null) {
|
|
39
|
+
return {}
|
|
40
|
+
}
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
groups.path = decodeURIComponent(groups.path)
|
|
37
|
-
}
|
|
42
|
+
const output: Record<string, any> = {}
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
for (const [key, value] of query.entries()) {
|
|
45
|
+
output[key] = value
|
|
46
|
+
|
|
47
|
+
if (value === 'true') {
|
|
48
|
+
output[key] = true
|
|
49
|
+
}
|
|
43
50
|
|
|
44
|
-
|
|
51
|
+
if (value === 'false') {
|
|
52
|
+
output[key] = false
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
|
|
48
|
-
|
|
56
|
+
return output
|
|
49
57
|
}
|
|
50
58
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*/
|
|
59
|
+
function stripLeadingHash (pathname: string): string {
|
|
60
|
+
return stripLeading(pathname, '#')
|
|
61
|
+
}
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
function stripLeading (str: string, char: string): string {
|
|
64
|
+
while (str.startsWith(char)) {
|
|
65
|
+
str = str.substring(1)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return str
|
|
69
|
+
}
|
|
59
70
|
|
|
60
71
|
/**
|
|
61
|
-
*
|
|
62
|
-
* (
|
|
72
|
+
* If the caller has passed a case-sensitive identifier (like a base58btc
|
|
73
|
+
* encoded CID or PeerId) in a case-insensitive location (like a subdomain),
|
|
74
|
+
* be nice and return the original identifier from the passed string
|
|
63
75
|
*/
|
|
64
|
-
function
|
|
65
|
-
|
|
76
|
+
function findOriginalCidOrPeer (needle: string, haystack: string): string {
|
|
77
|
+
const start = haystack.toLowerCase().indexOf(needle)
|
|
78
|
+
|
|
79
|
+
if (start === -1) {
|
|
80
|
+
return needle
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return haystack.substring(start, start + needle.length)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toUrl (urlString: string): URL {
|
|
87
|
+
// turn IPFS/IPNS path into gateway URL string
|
|
88
|
+
if (urlString.startsWith('/ipfs/') || urlString.startsWith('/ipns/')) {
|
|
89
|
+
urlString = ipfsPathToUrl(urlString)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// turn IPFS/IPNS URL into gateway URL string
|
|
93
|
+
if (urlString.startsWith('ipfs://') || urlString.startsWith('ipns://')) {
|
|
94
|
+
urlString = ipfsUrlToUrl(urlString)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (urlString.startsWith('http://') || urlString.startsWith('https://')) {
|
|
98
|
+
return new URL(urlString)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
throw new InvalidParametersError(`Invalid URL: ${urlString}`)
|
|
66
102
|
}
|
|
67
103
|
|
|
68
104
|
/**
|
|
69
|
-
*
|
|
70
|
-
* - Every standalone - is replaced with .
|
|
71
|
-
* - Every remaining -- is replaced with -
|
|
105
|
+
* Accepts the following url strings:
|
|
72
106
|
*
|
|
73
|
-
*
|
|
107
|
+
* - /ipfs/Qmfoo/path
|
|
108
|
+
* - /ipns/Qmfoo/path
|
|
109
|
+
* - ipfs://cid/path
|
|
110
|
+
* - ipns://name/path
|
|
111
|
+
* - http://cid.ipfs.example.com/path
|
|
112
|
+
* - http://name.ipns.example.com/path
|
|
113
|
+
* - http://example.com/ipfs/cid/path
|
|
114
|
+
* - http://example.com/ipns/name/path
|
|
74
115
|
*/
|
|
75
|
-
function
|
|
76
|
-
|
|
116
|
+
export function parseURLString (urlString: string): ParsedURL {
|
|
117
|
+
// validate url
|
|
118
|
+
const url = toUrl(urlString)
|
|
119
|
+
|
|
120
|
+
// test for subdomain gateway URL
|
|
121
|
+
const subdomainMatch = url.hostname.match(SUBDOMAIN_GATEWAY_REGEX)
|
|
122
|
+
|
|
123
|
+
if (matchSubdomainGroupsGuard(subdomainMatch?.groups)) {
|
|
124
|
+
const groups = subdomainMatch.groups
|
|
125
|
+
|
|
126
|
+
if (groups.protocol === 'ipns' && isInlinedDnsLink(groups.cidOrPeerIdOrDnsLink)) {
|
|
127
|
+
// decode inline dnslink domain if present
|
|
128
|
+
groups.cidOrPeerIdOrDnsLink = decodeDNSLinkLabel(groups.cidOrPeerIdOrDnsLink)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cidOrPeerIdOrDnsLink = findOriginalCidOrPeer(groups.cidOrPeerIdOrDnsLink, urlString)
|
|
132
|
+
|
|
133
|
+
// parse url as not http(s):// - this is necessary because URL makes
|
|
134
|
+
// `.pathname` default to `/` for http URLs, even if no trailing slash was
|
|
135
|
+
// present in the string URL and we need to be able to round-trip the user's
|
|
136
|
+
// input while also maintaining a sane canonical URL for the resource. Phew.
|
|
137
|
+
const wat = new URL(`not-${urlString}`)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
url: new URL(`${groups.protocol}://${cidOrPeerIdOrDnsLink}${wat.pathname}${url.search}${url.hash}`),
|
|
141
|
+
protocol: groups.protocol,
|
|
142
|
+
cidOrPeerIdOrDnsLink,
|
|
143
|
+
path: url.pathname.split('/')
|
|
144
|
+
.filter(str => str !== '')
|
|
145
|
+
.map(str => decodeURIComponent(str)),
|
|
146
|
+
query: toQuery(url.searchParams),
|
|
147
|
+
fragment: stripLeadingHash(url.hash)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// test for IPFS path gateway URL
|
|
152
|
+
if (url.pathname.startsWith('/ipfs/')) {
|
|
153
|
+
const parts = url.pathname.substring(6).split('/')
|
|
154
|
+
const cid = parts.shift()
|
|
155
|
+
|
|
156
|
+
if (cid == null) {
|
|
157
|
+
throw new InvalidParametersError(`Path gateway URL ${urlString} had no CID`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
url: new URL(`ipfs://${cid}${url.pathname.replace(`/ipfs/${cid}`, '')}${url.search}${url.hash}`),
|
|
162
|
+
protocol: 'ipfs',
|
|
163
|
+
cidOrPeerIdOrDnsLink: cid,
|
|
164
|
+
path: parts
|
|
165
|
+
.filter(str => str !== '')
|
|
166
|
+
.map(str => decodeURIComponent(str)),
|
|
167
|
+
query: toQuery(url.searchParams),
|
|
168
|
+
fragment: stripLeadingHash(url.hash)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// test for IPNS path gateway URL
|
|
173
|
+
if (url.pathname.startsWith('/ipns/')) {
|
|
174
|
+
const parts = url.pathname.substring(6).split('/')
|
|
175
|
+
const name = parts.shift()
|
|
176
|
+
|
|
177
|
+
if (name == null) {
|
|
178
|
+
throw new InvalidParametersError(`Path gateway URL ${urlString} had no name`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
url: new URL(`ipns://${name}${url.pathname.replace(`/ipns/${name}`, '')}${url.search}${url.hash}`),
|
|
183
|
+
protocol: 'ipns',
|
|
184
|
+
cidOrPeerIdOrDnsLink: name,
|
|
185
|
+
path: parts
|
|
186
|
+
.filter(str => str !== '')
|
|
187
|
+
.map(str => decodeURIComponent(str)),
|
|
188
|
+
query: toQuery(url.searchParams),
|
|
189
|
+
fragment: stripLeadingHash(url.hash)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new TypeError(`Invalid URL: ${urlString}, please use ipfs://, ipns://, or gateway URLs only`)
|
|
77
194
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CID } from 'multiformats/cid'
|
|
2
|
-
import {
|
|
2
|
+
import { parseURLString } from './parse-url-string.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Takes a resource and returns a session cache key as an IPFS or IPNS path with
|
|
@@ -24,7 +24,7 @@ export function resourceToSessionCacheKey (url: string | CID): string {
|
|
|
24
24
|
return `ipfs://${CID.parse(url.toString())}`
|
|
25
25
|
} catch {}
|
|
26
26
|
|
|
27
|
-
const { protocol, cidOrPeerIdOrDnsLink } =
|
|
27
|
+
const { protocol, cidOrPeerIdOrDnsLink } = parseURLString(url.toString())
|
|
28
28
|
|
|
29
29
|
return `${protocol}://${cidOrPeerIdOrDnsLink}`
|
|
30
30
|
}
|
package/src/utils/responses.ts
CHANGED
|
@@ -16,6 +16,12 @@ function setType (response: Response, value: 'basic' | 'cors' | 'error' | 'opaqu
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function setUrl (response: Response, value: string): void {
|
|
19
|
+
const fragmentStart = value.indexOf('#')
|
|
20
|
+
|
|
21
|
+
if (fragmentStart > -1) {
|
|
22
|
+
value = value.substring(0, fragmentStart)
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
setField(response, 'url', value)
|
|
20
26
|
}
|
|
21
27
|
|
package/src/utils/walk-path.ts
CHANGED
|
@@ -52,7 +52,7 @@ export function isObjectNode (node: UnixFSEntry): node is ObjectNode {
|
|
|
52
52
|
*/
|
|
53
53
|
export async function handlePathWalking ({ cid, path, resource, options, blockstore, log }: PluginContext & { blockstore: Blockstore, log: Logger }): Promise<PathWalkerResponse | Response> {
|
|
54
54
|
try {
|
|
55
|
-
return await walkPath(blockstore, `${cid}/${path}`, options)
|
|
55
|
+
return await walkPath(blockstore, `${cid}/${path.join('/')}`, options)
|
|
56
56
|
} catch (err: any) {
|
|
57
57
|
if (options?.signal?.aborted) {
|
|
58
58
|
throw new AbortError(options?.signal?.reason)
|