This repository has been archived on 2023-12-17. You can view files and clone it, but cannot push or open issues or pull requests.
extension/src/contentProviders/ipfsProvider.ts

203 lines
5.3 KiB
TypeScript

import BaseProvider from "./baseProvider.js";
import type {
BlockingResponse,
OnBeforeRequestDetailsType,
OnBeforeSendHeadersDetailsType,
OnHeadersReceivedDetailsType,
OnRequestDetailsType,
StreamFilter,
} from "../types.js";
import { getRelayProxies } from "../util.js";
import { ipfsPath, ipnsPath, path as checkPath } from "is-ipfs";
import { createClient } from "@lumeweb/kernel-ipfs-client";
import { DNS_RECORD_TYPE } from "@lumeweb/libresolver";
import type { DNSResult } from "@lumeweb/libresolver";
import RequestStream from "../requestStream.js";
import type { UnixFSStats } from "@helia/unixfs";
import * as path from "path";
import { CID } from "multiformats/cid";
import { fileTypeFromBuffer } from "file-type";
import extToMimes from "../mimes.js";
export default class IpfsProvider extends BaseProvider {
private _client = createClient();
async shouldHandleRequest(
details: OnBeforeRequestDetailsType,
): Promise<boolean> {
let dnsResult: DNSResult | boolean | string = await this.resolveDns(
details,
[DNS_RECORD_TYPE.CONTENT, DNS_RECORD_TYPE.TEXT],
);
if (!dnsResult) {
return false;
}
let contentRecords = (dnsResult as DNSResult).records.map(
(item: { value: string }) =>
"/" + item.value.replace("://", "/").replace(/^\+/, "/"),
);
contentRecords = contentRecords.filter((item) => checkPath(item));
if (!contentRecords.length) {
return false;
}
this.setData(details, "cid", contentRecords.shift());
return true;
}
async handleProxy(details: OnRequestDetailsType): Promise<any> {
return getRelayProxies();
}
async handleReqHeaders(
details: OnBeforeSendHeadersDetailsType,
): Promise<BlockingResponse | boolean> {
return {
requestHeaders: [
{ name: "x-status", value: this.getData(details, "status") },
],
};
}
async handleRequest(
details: OnBeforeRequestDetailsType,
): Promise<BlockingResponse | boolean> {
let urlObj = new URL(details.url);
let urlPath = urlObj.pathname;
let cid = this.getData(details, "cid");
let err;
let stat: UnixFSStats | null = null;
const parsedPath = path.parse(urlPath);
cid = cid.replace("ipns://", "/ipns/");
cid = cid.replace("ipfs://", "/ipfs/");
try {
if (ipnsPath(cid)) {
const cidHash = cid.replace("/ipns/", "");
cid = await this._client.ipns(cidHash);
cid = `/ipfs/${cid}`;
}
if (ipfsPath(cid)) {
cid = CID.parse(cid.replace("/ipfs/", "")).toV1().toString();
stat = await this._client.stat(cid);
}
} catch (e) {
err = (e as Error).message;
}
if (err) {
err = "404";
}
if (!err && stat?.type === "directory") {
if (!parsedPath.base.length || !parsedPath.ext.length) {
let found = false;
for (const indexFile of ["index.html", "index.htm"]) {
try {
const subPath = path.join(urlPath, indexFile);
await this._client.stat(cid, {
path: subPath,
});
urlPath = subPath;
found = true;
break;
} catch {}
}
if (!found) {
err = "404";
}
}
}
const reqStream = new RequestStream(details);
reqStream.start();
if (err) {
reqStream.close();
return {};
}
let reader = await this._client.cat(cid, { path: urlPath });
let bufferRead = 0;
const fileTypeBufferLength = 4100;
const mimeBuffer: Uint8Array[] = [];
for await (const chunk of reader.iterable()) {
if (bufferRead < fileTypeBufferLength) {
if (chunk.length >= fileTypeBufferLength) {
mimeBuffer.push(chunk.slice(0, fileTypeBufferLength));
bufferRead += fileTypeBufferLength;
} else {
mimeBuffer.push(chunk);
bufferRead += chunk.length;
}
if (bufferRead >= fileTypeBufferLength) {
reader.abort();
break;
}
} else {
reader.abort();
break;
}
}
if (bufferRead >= fileTypeBufferLength) {
const mime = await fileTypeFromBuffer(
mimeBuffer.reduce((acc, val) => {
return new Uint8Array([...acc, ...val]);
}, new Uint8Array()),
);
if (mime) {
this.setData(details, "contentType", mime.mime);
}
if (!mime) {
const ext = path.parse(urlPath).ext.replace(".", "");
if (extToMimes.has(ext)) {
this.setData(details, "contentType", extToMimes.get(ext));
}
}
}
reader = await this._client.cat(cid, { path: urlPath });
const streamWriter = reqStream.stream.writable.getWriter();
let streaming = (async function () {
try {
// @ts-ignore
for await (const chunk of reader.iterable()) {
streamWriter.write(chunk);
}
} catch (e) {
streamWriter.releaseLock();
reqStream.close();
return;
}
streamWriter.releaseLock();
reqStream.close();
})();
return {};
}
async handleHeaders(
details: OnHeadersReceivedDetailsType,
): Promise<BlockingResponse | boolean> {
let headers: any = [];
headers.push({
name: "Content-Type",
value: this.getData(details, "contentType"),
});
return {
responseHeaders: headers,
};
}
}