203 lines
5.3 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|