1+ import { isIP } from '@chainsafe/is-ip'
2+ import { InvalidParametersError } from '@libp2p/interface'
13import { peerIdFromString } from '@libp2p/peer-id'
4+ import { base16 , base16upper } from 'multiformats/bases/base16'
25import { base32 } from 'multiformats/bases/base32'
36import { base36 } from 'multiformats/bases/base36'
7+ import { base58btc } from 'multiformats/bases/base58'
48import { CID } from 'multiformats/cid'
59import { dnsLinkLabelEncoder } from './dns-link-labels.js'
610import { getHeliaSwRedirectUrl } from './first-hit-helpers.js'
711import { pathRegex , subdomainRegex } from './regex.js'
812import type { Logger } from '@libp2p/interface'
13+ import type { MultibaseDecoder } from 'multiformats/cid'
914
1015export const isPathOrSubdomainRequest = ( location : Pick < Location , 'host' | 'pathname' > ) : boolean => {
1116 return isPathGatewayRequest ( location ) || isSubdomainGatewayRequest ( location )
@@ -28,25 +33,40 @@ export const isPathGatewayRequest = (location: Pick<Location, 'host' | 'pathname
2833export const findOriginIsolationRedirect = ( location : Pick < Location , 'protocol' | 'host' | 'pathname' | 'search' | 'hash' | 'href' | 'origin' > , logger : Logger , supportsSubdomains : boolean | null ) : string | null => {
2934 const log = logger ?. newScope ( 'find-origin-isolation-redirect' )
3035
31- if ( isSubdomainGatewayRequest ( location ) ) {
32- // already on an isolated origin
33- return null
34- }
36+ try {
37+ if ( isSubdomainGatewayRequest ( location ) ) {
38+ // already on an isolated origin
39+ return null
40+ }
3541
36- if ( isPathGatewayRequest ( location ) ) {
37- log ?. trace ( 'checking for subdomain support' )
42+ if ( isPathGatewayRequest ( location ) ) {
43+ log ?. trace ( 'checking for subdomain support' )
3844
39- if ( supportsSubdomains === true ) {
40- log ?. trace ( 'subdomain support is enabled' )
41- return toSubdomainRequest ( location )
45+ if ( supportsSubdomains === true ) {
46+ log ?. trace ( 'subdomain support is enabled' )
47+ return toSubdomainRequest ( location )
48+ }
4249 }
50+ } catch ( err ) {
51+ log . error ( 'could not parse gateway request from URL - %e' , err )
4352 }
4453
4554 // not path/subdomain request - we are showing UI
4655 return null
4756}
4857
58+ /**
59+ * Turns a path gateway url into a subdomain gateway url.
60+ *
61+ * Throws an `InvalidParametersError` if the user supplied a bad CID or path
62+ * or `Error` if subdomains are unsupported because (for example) the gateway
63+ * is accessed via an IP address instead of a domain.
64+ */
4965export const toSubdomainRequest = ( location : Pick < Location , 'protocol' | 'host' | 'pathname' | 'search' | 'hash' | 'href' | 'origin' > ) : string => {
66+ if ( isIP ( location . host ) ) {
67+ throw new Error ( 'Host was an IP address so subdomains are unsupported' )
68+ }
69+
5070 const segments = location . pathname
5171 . split ( '/' )
5272 . filter ( segment => segment !== '' )
@@ -57,7 +77,7 @@ export const toSubdomainRequest = (location: Pick<Location, 'protocol' | 'host'
5777 }
5878
5979 if ( segments . length < 2 ) {
60- throw new Error ( `Invalid location ${ location } ` )
80+ throw new InvalidParametersError ( `Invalid location ${ location } ` )
6181 }
6282
6383 const ns = segments [ 0 ]
@@ -66,40 +86,38 @@ export const toSubdomainRequest = (location: Pick<Location, 'protocol' | 'host'
6686 // DNS labels are case-insensitive, and the length limit is 63.
6787 // We ensure base32 if CID, base36 if ipns,
6888 // or inlined according to https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header if DNSLink name
69- try {
70- switch ( ns ) {
71- case 'ipfs' :
72- // Base32 is case-insensitive and allows CID with popular hashes like
73- // sha2-256 to fit in a single DNS label
74- id = CID . parse ( id ) . toV1 ( ) . toString ( base32 )
75- break
76- case 'ipns' :
77- if ( id . startsWith ( 'Q' ) || id . startsWith ( '1' ) ) {
78- // possibly a PeerId - non-standard but try converting to a CID
79- try {
80- const peerId = peerIdFromString ( id )
81- id = peerId . toCID ( ) . toString ( )
82- } catch { }
83- }
84-
89+ switch ( ns ) {
90+ case 'ipfs' :
91+ // Base32 is case-insensitive and allows CID with popular hashes like
92+ // sha2-256 to fit in a single DNS label
93+ id = parseCid ( id ) . toString ( base32 )
94+ break
95+ case 'ipns' :
96+ if ( id . startsWith ( 'Q' ) || id . startsWith ( '1' ) ) {
97+ // possibly a PeerId - non-standard but try converting to a CID
98+ try {
99+ const peerId = peerIdFromString ( id )
100+ id = peerId . toCID ( ) . toString ( )
101+ } catch { }
102+ }
103+
104+ try {
85105 // IPNS Names are represented as Base36 CIDv1 with libp2p-key codec
86106 // https://specs.ipfs.tech/ipns/ipns-record/#ipns-name
87- // eslint-disable-next-line no-case-declarations
88- const ipnsName = CID . parse ( id ) . toV1 ( )
107+
89108 // /ipns/ namespace uses Base36 instead of 32 because ED25519 keys need
90109 // to fit in DNS label of max length 63
91- id = ipnsName . toString ( base36 )
92- break
93- default :
94- throw new Error ( 'Unknown namespace: ' + ns )
95- }
96- } catch {
97- // not a CID, so we assume a DNSLink name and inline it according to
98- // https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
99- if ( id ?. includes ( '.' ) === true ) {
100- id = dnsLinkLabelEncoder ( id )
101- }
110+ id = parseCid ( id ) . toString ( base36 )
111+ } catch {
112+ // not a CID, so we assume a DNSLink name and inline it according to
113+ // https://specs.ipfs.tech/http-gateways/subdomain-gateway/#host-request-header
114+ id = dnsLinkLabelEncoder ( id )
115+ }
116+ break
117+ default :
118+ throw new InvalidParametersError ( 'Unknown namespace: "' + ns + '"' )
102119 }
120+
103121 const remainingPath = `/${ segments . slice ( 2 ) . join ( '/' ) } `
104122
105123 // create new URL with the subdomain but without the path
@@ -108,10 +126,40 @@ export const toSubdomainRequest = (location: Pick<Location, 'protocol' | 'host'
108126 const modifiedOriginalUrl = new URL ( location . href )
109127 modifiedOriginalUrl . pathname = remainingPath
110128 modifiedOriginalUrl . hash = location . hash
129+
111130 const originalSearchParams = new URLSearchParams ( location . search )
112131 originalSearchParams . forEach ( ( value , key ) => {
113132 modifiedOriginalUrl . searchParams . set ( key , value )
114133 } )
115134
116135 return getHeliaSwRedirectUrl ( modifiedOriginalUrl , newLocation ) . href
117136}
137+
138+ /**
139+ * Parses the string as a CID and converts it to v1. Translates any thrown error
140+ * to an `InvalidParametersError` so we can show a 400 screen to the user.
141+ */
142+ function parseCid ( str : string ) : CID < any , any , any , 1 > {
143+ try {
144+ return CID . parse ( str , findMultibaseDecoder ( str ) ) . toV1 ( )
145+ } catch ( err : any ) {
146+ throw new InvalidParametersError ( `Could not parse CID from string "${ str } " - ${ err . message } ` )
147+ }
148+ }
149+
150+ function findMultibaseDecoder ( str : string ) : MultibaseDecoder < string > | undefined {
151+ const prefix = str . substring ( 0 , 1 )
152+
153+ switch ( prefix ) {
154+ case 'b' :
155+ return base32
156+ case 'f' :
157+ return base16
158+ case 'F' :
159+ return base16upper
160+ case 'z' :
161+ return base58btc
162+ default :
163+ // do nothing
164+ }
165+ }
0 commit comments