*Add initial version of IPFS provider
This commit is contained in:
parent
fc41eb645a
commit
ab278d20f3
|
@ -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(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title><%= path %></title>
|
||||||
|
<style></style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="header" class="row">
|
||||||
|
<div class="col-xs-2">
|
||||||
|
<div id="logo" class="ipfs-logo"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Index of <%= path %></strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="narrow">
|
||||||
|
<div class="ipfs-icon ipfs-_blank"> </div>
|
||||||
|
</td>
|
||||||
|
<td class="padding">
|
||||||
|
<a href="<%= parentHref %>">..</a>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
<% links.forEach(function (link) { %>
|
||||||
|
<tr>
|
||||||
|
<td><div class="ipfs-icon ipfs-_blank"> </div></td>
|
||||||
|
<td><a href="<%= link.link %>"><%= link.name %></a></t>
|
||||||
|
<td><%= link.size %></td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% }) %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`);
|
||||||
|
|
||||||
|
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<boolean> {
|
||||||
|
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<any> {
|
||||||
|
return requestProxies;
|
||||||
|
}
|
||||||
|
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 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<BlockingResponse | boolean> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import InternalProvider from "../contentProviders/internalProvider.js";
|
||||||
import SkynetProvider from "../contentProviders/skynetProvider.js";
|
import SkynetProvider from "../contentProviders/skynetProvider.js";
|
||||||
import ServerProvider from "../contentProviders/serverProvider.js";
|
import ServerProvider from "../contentProviders/serverProvider.js";
|
||||||
import { init } from "libkernel";
|
import { init } from "libkernel";
|
||||||
|
import IpfsProvider from "../contentProviders/ipfsProvider.js";
|
||||||
|
|
||||||
declare var browser: any; // tsc
|
declare var browser: any; // tsc
|
||||||
let queriesNonce = 1;
|
let queriesNonce = 1;
|
||||||
|
@ -191,6 +192,7 @@ const engine = new WebEngine();
|
||||||
engine.registerContentProvider(new InternalProvider(engine));
|
engine.registerContentProvider(new InternalProvider(engine));
|
||||||
engine.registerContentProvider(new ServerProvider(engine));
|
engine.registerContentProvider(new ServerProvider(engine));
|
||||||
engine.registerContentProvider(new SkynetProvider(engine));
|
engine.registerContentProvider(new SkynetProvider(engine));
|
||||||
|
engine.registerContentProvider(new IpfsProvider(engine));
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let kernelFrame: HTMLIFrameElement = document.createElement("iframe");
|
let kernelFrame: HTMLIFrameElement = document.createElement("iframe");
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
Reference in New Issue