*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 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");
|
||||
|
|
|
@ -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