diff --git a/src/contentProviders/ipfsProvider.ts b/src/contentProviders/ipfsProvider.ts
new file mode 100644
index 0000000..80bd8ea
--- /dev/null
+++ b/src/contentProviders/ipfsProvider.ts
@@ -0,0 +1,307 @@
+import BaseProvider from "./baseProvider.js";
+import {
+ BlockingResponse,
+ OnBeforeRequestDetailsType,
+ OnBeforeSendHeadersDetailsType,
+ OnHeadersReceivedDetailsType,
+ OnRequestDetailsType,
+ StreamFilter,
+} from "../types.js";
+import { requestProxies } from "../util.js";
+import browser from "@lumeweb/webextension-polyfill";
+import { ipfsPath, ipnsPath, path } from "is-ipfs";
+import {
+ fetchIpfs,
+ fetchIpns,
+ statIpfs,
+ statIpns,
+} from "@lumeweb/kernel-ipfs-client";
+import ejs from "ejs";
+import {
+ CONTENT_MODE_BUFFERED,
+ CONTENT_MODE_CHUNKED,
+ contentModes,
+} from "../mimes.js";
+
+const INDEX_HTML_FILES = ["index.html", "index.htm", "index.shtml"];
+
+const DIRECTORY_TEMPLATE = ejs.compile(`
+
+
+
+
+ <%= path %>
+
+
+
+
+
+
+
+
+ Index of <%= path %>
+
+
+
+
+
+
+ |
+
+ ..
+ |
+ |
+
+ <% links.forEach(function (link) { %>
+
+ |
+ <%= link.name %>
+ | <%= link.size %> |
+
+
+ <% }) %>
+
+
+
+
+
+`);
+
+interface StatFileResponse {
+ exists: boolean;
+ contentType: string | null;
+ error: any;
+ directory: boolean;
+ files: StatFileSubfile[];
+ timeout: boolean;
+ size: number;
+}
+
+interface StatFileSubfile {
+ name: string;
+ size: number;
+}
+
+export default class IpfsProvider extends BaseProvider {
+ async shouldHandleRequest(
+ details: OnBeforeRequestDetailsType
+ ): Promise {
+ let dns = await this.resolveDns(details);
+ if (dns) {
+ dns = "/" + dns.replace("://", "/");
+ dns = dns.replace(/^\+/, "/");
+ }
+
+ if (dns && path(dns)) {
+ this.setData(details, "hash", dns);
+ return true;
+ }
+
+ return false;
+ }
+ async handleProxy(details: OnRequestDetailsType): Promise {
+ return requestProxies;
+ }
+ async handleReqHeaders(
+ details: OnBeforeSendHeadersDetailsType
+ ): Promise {
+ return {
+ requestHeaders: [
+ { name: "x-status", value: this.getData(details, "status") },
+ ],
+ };
+ }
+
+ async handleRequest(
+ details: OnBeforeRequestDetailsType
+ ): Promise {
+ let urlPath = new URL(details.url).pathname;
+ let hash = this.getData(details, "hash");
+ let resp: StatFileResponse | null = null;
+ let fetchMethod: typeof fetchIpfs | typeof fetchIpns;
+ let err;
+ try {
+ if (ipfsPath(hash)) {
+ hash = hash.replace("/ipfs/", "");
+ resp = await statIpfs(hash.replace("/ipfs/", ""), urlPath);
+ fetchMethod = fetchIpfs;
+ } else if (ipnsPath(hash)) {
+ hash = hash.replace("/ipns/", "");
+ resp = await statIpns(hash.replace("/ipns/", ""), urlPath);
+ fetchMethod = fetchIpns;
+ } else {
+ err = "invalid content";
+ }
+ } catch (e: any) {
+ err = (e as Error).message;
+ }
+
+ let contentType = resp?.contentType;
+ let contentLength = resp?.size;
+
+ let status = "200";
+
+ if (contentType?.includes(";")) {
+ contentType = contentType?.split(";").shift();
+ }
+
+ if (resp) {
+ if (!resp.exists) {
+ err = "404";
+ }
+ if (resp.directory) {
+ contentType = "text/html";
+ }
+ }
+
+ this.setData(details, "contentType", contentType);
+
+ if (err) {
+ if (err === "NOT_FOUND") {
+ err = "404";
+ }
+ if (err === "timeout") {
+ err = "408";
+ }
+ if (err.includes("no link")) {
+ err = "404";
+ }
+
+ this.setData(details, "error", err);
+
+ if (!isNaN(parseInt(err))) {
+ status = err;
+ }
+ }
+
+ this.setData(details, "status", status);
+ this.setData(details, "contentLength", contentLength);
+
+ let filterPromiseResolve: any;
+ let filterPromise = new Promise((resolve) => {
+ filterPromiseResolve = resolve;
+ });
+ let streamPromise = Promise.resolve();
+ const filter: StreamFilter = browser.webRequest.filterResponseData(
+ details.requestId
+ );
+ filter.ondata = () => {};
+ filter.onstop = () => {
+ filterPromiseResolve();
+ };
+
+ const buffer: Uint8Array[] = [];
+
+ const receiveUpdate = (chunk: Uint8Array) => {
+ if (
+ Object.keys(contentModes).includes(contentType as string) &&
+ [CONTENT_MODE_CHUNKED, CONTENT_MODE_BUFFERED].includes(
+ contentModes[contentType as string]
+ )
+ ) {
+ buffer.push(chunk);
+ return;
+ }
+
+ filterPromise.then(() => {
+ streamPromise = streamPromise.then(() => {
+ filter.write(chunk);
+ });
+ });
+ };
+
+ if (err) {
+ // receiveUpdate(new TextEncoder().encode(serverErrorTemplate()));
+ filterPromise.then(() => streamPromise).then(() => filter.close());
+ return {};
+ }
+
+ if (resp?.directory) {
+ let indexFiles =
+ resp?.files.filter((item) => INDEX_HTML_FILES.includes(item.name)) ||
+ [];
+
+ if (indexFiles.length > 0) {
+ urlPath += `/${indexFiles[0].name}`;
+ } else {
+ const renderedDirectory = DIRECTORY_TEMPLATE(resp?.files);
+ contentLength = renderedDirectory.length;
+ receiveUpdate(new TextEncoder().encode(renderedDirectory));
+ filterPromise
+ .then(() => streamPromise)
+ .then(() => {
+ filter.close();
+ });
+ this.setData(details, "contentLength", contentLength);
+ return {};
+ }
+ }
+ // @ts-ignore
+ fetchMethod?.(hash, urlPath, receiveUpdate)
+ .then(() => streamPromise)
+ .then(() => {
+ if (buffer.length) {
+ let mode = contentModes[contentType as string];
+ if (mode === CONTENT_MODE_BUFFERED) {
+ filter.write(
+ Uint8Array.from(
+ buffer.reduce(
+ (previousValue: Uint8Array, currentValue: Uint8Array) => {
+ return Uint8Array.from([...previousValue, ...currentValue]);
+ }
+ )
+ )
+ );
+ } else if (mode == CONTENT_MODE_CHUNKED) {
+ buffer.forEach(filter.write);
+ }
+ }
+ filter.close();
+ })
+ .catch((e) => {
+ console.log("page error", urlPath, e.message);
+ /* if (
+ urlPath.endsWith(".html") ||
+ urlPath.endsWith(".htm") ||
+ urlPath.endsWith(".xhtml") ||
+ urlPath.endsWith(".shtml")
+ ) {
+ this.setData(details, "contentType", "text/html");
+ let template = serverErrorTemplate();
+ contentLength = template.length;
+ receiveUpdate(new TextEncoder().encode(template));
+ this.setData(details, "contentLength", contentLength);
+ }*/
+ filterPromise.then(() => streamPromise).then(() => filter.close());
+ });
+
+ return {};
+ }
+
+ async handleHeaders(
+ details: OnHeadersReceivedDetailsType
+ ): Promise {
+ let headers = [];
+ // const contentLength = this.getData(details, "contentLength");
+
+ headers.push({
+ name: "Content-Type",
+ value: this.getData(details, "contentType"),
+ });
+
+ /* if (contentLength) {
+ headers.push({
+ name: "content-length",
+ value: contentLength,
+ });
+ }*/
+
+ return {
+ responseHeaders: headers,
+ };
+ }
+}
diff --git a/src/main/background.ts b/src/main/background.ts
index 86ad60e..fb2a5bd 100644
--- a/src/main/background.ts
+++ b/src/main/background.ts
@@ -5,6 +5,7 @@ import InternalProvider from "../contentProviders/internalProvider.js";
import SkynetProvider from "../contentProviders/skynetProvider.js";
import ServerProvider from "../contentProviders/serverProvider.js";
import { init } from "libkernel";
+import IpfsProvider from "../contentProviders/ipfsProvider.js";
declare var browser: any; // tsc
let queriesNonce = 1;
@@ -191,6 +192,7 @@ const engine = new WebEngine();
engine.registerContentProvider(new InternalProvider(engine));
engine.registerContentProvider(new ServerProvider(engine));
engine.registerContentProvider(new SkynetProvider(engine));
+engine.registerContentProvider(new IpfsProvider(engine));
// @ts-ignore
let kernelFrame: HTMLIFrameElement = document.createElement("iframe");
diff --git a/src/mimes.ts b/src/mimes.ts
new file mode 100644
index 0000000..b277d2c
--- /dev/null
+++ b/src/mimes.ts
@@ -0,0 +1,15 @@
+export const CONTENT_MODE_BUFFERED = "buffered";
+export const CONTENT_MODE_CHUNKED = "chunked";
+//export const CONTENT_MODE_STREAMED = "streamed";
+
+export const contentModes: { [mimeType: string]: string } = {
+ // Images
+ "image/png": CONTENT_MODE_BUFFERED,
+ "image/jpeg": CONTENT_MODE_BUFFERED,
+ "image/x-citrix-jpeg": CONTENT_MODE_BUFFERED,
+ "image/gif": CONTENT_MODE_BUFFERED,
+ "image/webp": CONTENT_MODE_BUFFERED,
+
+ //JS
+ "application/javascript": CONTENT_MODE_CHUNKED,
+};