UNPKG

@helia/verified-fetch

Version:

A fetch-like API for obtaining verified & trustless IPFS content on the web

166 lines (130 loc) 5.05 kB
import { InvalidParametersError } from '@libp2p/interface' import { peerIdFromCID, peerIdFromString } from '@libp2p/peer-id' import { CID } from 'multiformats/cid' import { decodeDNSLinkLabel, isInlinedDnsLink } from './dnslink-label.ts' import { ipfsPathToUrl, ipfsUrlToUrl } from './ipfs-path-to-url.ts' interface SubdomainMatchGroups { protocol: 'ipfs' | 'ipns' cidOrPeerIdOrDnsLink: string } export const SUBDOMAIN_GATEWAY_REGEX = /^(?<cidOrPeerIdOrDnsLink>[^/?]+)\.(?<protocol>ip[fn]s)\.([^/?]+)$/ const CODEC_LIBP2P_KEY = 0x72 function matchSubdomainGroupsGuard (groups?: null | { [key in string]: string; } | SubdomainMatchGroups): groups is SubdomainMatchGroups { const protocol = groups?.protocol if (protocol !== 'ipfs' && protocol !== 'ipns') { return false } const cidOrPeerIdOrDnsLink = groups?.cidOrPeerIdOrDnsLink if (cidOrPeerIdOrDnsLink == null) { return false } return true } export interface ParsedURL { url: URL, protocol: 'ipfs' | 'ipns' cidOrPeerIdOrDnsLink: string path: string[] query: Record<string, any> fragment: string } /** * If the caller has passed a case-sensitive identifier (like a base58btc * encoded CID or PeerId) in a case-insensitive location (like a subdomain), * be nice and return the original identifier from the passed string */ function findOriginalCidOrPeer (needle: string, haystack: string): string { const start = haystack.toLowerCase().indexOf(needle) if (start === -1) { return needle } return haystack.substring(start, start + needle.length) } function stringToUrl (urlString: string | URL): URL { if (urlString instanceof URL) { return urlString } // turn IPFS/IPNS path into gateway URL string if (urlString.startsWith('/ipfs/') || urlString.startsWith('/ipns/')) { urlString = ipfsPathToUrl(urlString) } // turn IPFS/IPNS URL into gateway URL string if (urlString.startsWith('ipfs://') || urlString.startsWith('ipns://')) { urlString = ipfsUrlToUrl(urlString) } if (urlString.startsWith('http://') || urlString.startsWith('https://')) { return new URL(urlString) } throw new InvalidParametersError(`Invalid URL: ${urlString}`) } function toURL (urlString: string): URL { // validate url const url = stringToUrl(urlString) // test for subdomain gateway URL const subdomainMatch = url.hostname.match(SUBDOMAIN_GATEWAY_REGEX) if (matchSubdomainGroupsGuard(subdomainMatch?.groups)) { const groups = subdomainMatch.groups if (groups.protocol === 'ipns' && isInlinedDnsLink(groups.cidOrPeerIdOrDnsLink)) { // decode inline dnslink domain if present groups.cidOrPeerIdOrDnsLink = decodeDNSLinkLabel(groups.cidOrPeerIdOrDnsLink) } const cidOrPeerIdOrDnsLink = findOriginalCidOrPeer(groups.cidOrPeerIdOrDnsLink, urlString) // parse url as not http(s):// - this is necessary because URL makes // `.pathname` default to `/` for http URLs, even if no trailing slash was // present in the string URL and we need to be able to round-trip the user's // input while also maintaining a sane canonical URL for the resource. Phew. const wat = new URL(`not-${urlString}`) return new URL(`${groups.protocol}://${cidOrPeerIdOrDnsLink}${wat.pathname}${url.search}${url.hash}`) } // test for IPFS path gateway URL if (url.pathname.startsWith('/ipfs/')) { const parts = url.pathname.substring(6).split('/') const cid = parts.shift() if (cid == null) { throw new InvalidParametersError(`Path gateway URL ${urlString} had no CID`) } return new URL(`ipfs://${cid}${url.pathname.replace(`/ipfs/${cid}`, '')}${url.search}${url.hash}`) } // test for IPNS path gateway URL if (url.pathname.startsWith('/ipns/')) { const parts = url.pathname.substring(6).split('/') const name = parts.shift() if (name == null) { throw new InvalidParametersError(`Path gateway URL ${urlString} had no name`) } return new URL(`ipns://${name}${url.pathname.replace(`/ipns/${name}`, '')}${url.search}${url.hash}`) } throw new TypeError(`Invalid URL: ${urlString}, please use ipfs://, ipns://, or gateway URLs only`) } /** * Accepts the following url strings: * * - /ipfs/Qmfoo/path * - /ipns/Qmfoo/path * - ipfs://cid/path * - ipns://name/path * - http://cid.ipfs.example.com/path * - http://name.ipns.example.com/path * - http://example.com/ipfs/cid/path * - http://example.com/ipns/name/path */ export function parseURLString (urlString: string): URL { // validate url const url = toURL(urlString) // treat IPNS keys that do not parse as PeerIds as DNSLink if (url.protocol === 'ipns:') { try { peerIdFromString(url.hostname) } catch { url.protocol = 'dnslink:' } } if (url.protocol === 'ipfs:') { const cid = CID.parse(url.hostname) // special case - peer id encoded as a CID if (cid.code === CODEC_LIBP2P_KEY) { return new URL(`ipns://${peerIdFromCID(cid)}${url.pathname}${url.search}${url.hash}`) } } return url }