2022-08-13 08:19:01 +00:00
|
|
|
import BaseProvider from "./baseProvider.js";
|
|
|
|
import {
|
|
|
|
BlockingResponse,
|
|
|
|
OnBeforeRequestDetailsType,
|
|
|
|
OnBeforeSendHeadersDetailsType,
|
|
|
|
OnHeadersReceivedDetailsType,
|
|
|
|
OnRequestDetailsType,
|
|
|
|
StreamFilter,
|
|
|
|
} from "../types.js";
|
2022-08-14 12:19:45 +00:00
|
|
|
import { getRelayProxies } from "../util.js";
|
2022-08-13 08:19:01 +00:00
|
|
|
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";
|
2022-08-15 13:50:44 +00:00
|
|
|
import { cacheDb } from "../databases.js";
|
2022-08-13 08:19:01 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-08-15 13:02:25 +00:00
|
|
|
const MAX_CACHE_SIZE = 1024 * 1024 * 1024 * 50;
|
|
|
|
|
2022-08-13 08:19:01 +00:00
|
|
|
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;
|
|
|
|
}
|
2022-08-13 17:16:53 +00:00
|
|
|
|
2022-08-13 08:19:01 +00:00
|
|
|
async handleProxy(details: OnRequestDetailsType): Promise<any> {
|
2022-08-14 12:19:45 +00:00
|
|
|
return getRelayProxies();
|
2022-08-13 08:19:01 +00:00
|
|
|
}
|
2022-08-13 17:16:53 +00:00
|
|
|
|
2022-08-13 08:19:01 +00:00
|
|
|
async handleReqHeaders(
|
|
|
|
details: OnBeforeSendHeadersDetailsType
|
|
|
|
): Promise<BlockingResponse | boolean> {
|
|
|
|
return {
|
|
|
|
requestHeaders: [
|
|
|
|
{ name: "x-status", value: this.getData(details, "status") },
|
|
|
|
],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async handleRequest(
|
|
|
|
details: OnBeforeRequestDetailsType
|
|
|
|
): Promise<BlockingResponse | boolean> {
|
2022-08-14 10:15:20 +00:00
|
|
|
let urlObj = new URL(details.url);
|
|
|
|
let urlPath = urlObj.pathname;
|
2022-08-13 08:19:01 +00:00
|
|
|
let hash = this.getData(details, "hash");
|
|
|
|
let resp: StatFileResponse | null = null;
|
|
|
|
let fetchMethod: typeof fetchIpfs | typeof fetchIpns;
|
|
|
|
let err;
|
2022-08-15 13:02:25 +00:00
|
|
|
let contentType: string;
|
2022-08-14 10:15:20 +00:00
|
|
|
if (urlObj.protocol == "https") {
|
|
|
|
urlObj.protocol = "http";
|
|
|
|
return { redirectUrl: urlObj.toString() };
|
|
|
|
}
|
|
|
|
|
2022-08-15 13:02:25 +00:00
|
|
|
let cachedPage: { contentType: string; data: Blob } | null = null;
|
|
|
|
|
2022-08-13 08:19:01 +00:00
|
|
|
try {
|
2022-08-15 13:02:25 +00:00
|
|
|
// @ts-ignore
|
|
|
|
cachedPage = await cacheDb.items.where("url").equals(details.url).first();
|
|
|
|
} catch {}
|
|
|
|
|
|
|
|
if (!cachedPage) {
|
|
|
|
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;
|
2022-08-13 08:19:01 +00:00
|
|
|
}
|
|
|
|
|
2022-08-15 13:02:25 +00:00
|
|
|
contentType = resp?.contentType as string;
|
|
|
|
if (contentType?.includes(";")) {
|
|
|
|
contentType = contentType?.split(";").shift() as string;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
contentType = cachedPage.contentType;
|
|
|
|
}
|
2022-08-13 08:19:01 +00:00
|
|
|
|
|
|
|
let status = "200";
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
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();
|
|
|
|
};
|
|
|
|
|
2022-08-15 13:02:25 +00:00
|
|
|
let buffer: Uint8Array[] = [];
|
|
|
|
let cacheBuffer: Uint8Array[] | Uint8Array = [];
|
2022-08-13 08:19:01 +00:00
|
|
|
|
|
|
|
const receiveUpdate = (chunk: Uint8Array) => {
|
2022-08-15 13:02:25 +00:00
|
|
|
if (!chunk.length && !chunk.byteLength) {
|
|
|
|
return filterPromise;
|
|
|
|
}
|
2022-08-13 08:19:01 +00:00
|
|
|
if (
|
|
|
|
Object.keys(contentModes).includes(contentType as string) &&
|
|
|
|
[CONTENT_MODE_CHUNKED, CONTENT_MODE_BUFFERED].includes(
|
|
|
|
contentModes[contentType as string]
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
buffer.push(chunk);
|
2022-08-15 13:02:25 +00:00
|
|
|
resp = resp as StatFileResponse;
|
|
|
|
cacheBuffer = cacheBuffer as Uint8Array[];
|
|
|
|
if (!cachedPage && resp.size <= MAX_CACHE_SIZE) {
|
|
|
|
cacheBuffer.push(chunk);
|
|
|
|
}
|
|
|
|
|
|
|
|
return filterPromise;
|
2022-08-13 08:19:01 +00:00
|
|
|
}
|
|
|
|
|
2022-08-15 13:02:25 +00:00
|
|
|
return filterPromise.then(() => {
|
2022-08-13 08:19:01 +00:00
|
|
|
streamPromise = streamPromise.then(() => {
|
|
|
|
filter.write(chunk);
|
2022-08-15 13:02:25 +00:00
|
|
|
cacheBuffer = cacheBuffer as Uint8Array[];
|
|
|
|
cacheBuffer.push(chunk);
|
2022-08-13 08:19:01 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
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);
|
|
|
|
receiveUpdate(new TextEncoder().encode(renderedDirectory));
|
|
|
|
filterPromise
|
|
|
|
.then(() => streamPromise)
|
|
|
|
.then(() => {
|
|
|
|
filter.close();
|
|
|
|
});
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
}
|
2022-08-15 13:02:25 +00:00
|
|
|
|
|
|
|
const handleBuffer = () => {
|
|
|
|
if (buffer.length) {
|
|
|
|
let mode = contentModes[contentType as string];
|
|
|
|
buffer = buffer.map((item: Uint8Array | ArrayBuffer) => {
|
|
|
|
if (item instanceof ArrayBuffer) {
|
|
|
|
return new Uint8Array(item);
|
|
|
|
}
|
|
|
|
return item;
|
|
|
|
});
|
|
|
|
if (mode === CONTENT_MODE_BUFFERED) {
|
|
|
|
let data: string | Uint8Array = Uint8Array.from(
|
|
|
|
buffer.reduce(
|
|
|
|
(previousValue: Uint8Array, currentValue: Uint8Array) => {
|
|
|
|
return Uint8Array.from([...previousValue, ...currentValue]);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
/* if (contentType === "text/html") {
|
|
|
|
data = new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
|
|
let htmlDoc = new DOMParser().parseFromString(
|
|
|
|
data as string,
|
|
|
|
contentType
|
|
|
|
);
|
|
|
|
let found = htmlDoc.documentElement.querySelectorAll(
|
|
|
|
'meta[http-equiv="Content-Security-Policy"]'
|
|
|
|
);
|
|
|
|
|
|
|
|
if (found.length) {
|
|
|
|
found.forEach((item) => item.remove());
|
|
|
|
data = htmlDoc.documentElement.outerHTML;
|
|
|
|
}
|
|
|
|
|
|
|
|
data = new TextEncoder().encode(data);
|
|
|
|
}*/
|
|
|
|
filter.write(data);
|
|
|
|
} else if (mode == CONTENT_MODE_CHUNKED) {
|
|
|
|
buffer.forEach((data) => filter.write(data));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
if (cachedPage) {
|
|
|
|
// @ts-ignore
|
|
|
|
cachedPage.data.arrayBuffer().then((data: ArrayBuffer) => {
|
|
|
|
// @ts-ignore
|
|
|
|
receiveUpdate(data)?.then(() => {
|
|
|
|
handleBuffer();
|
|
|
|
filterPromise.then(() => filter.close());
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2022-08-13 08:19:01 +00:00
|
|
|
// @ts-ignore
|
|
|
|
fetchMethod?.(hash, urlPath, receiveUpdate)
|
|
|
|
.then(() => streamPromise)
|
|
|
|
.then(() => {
|
2022-08-15 13:02:25 +00:00
|
|
|
handleBuffer();
|
|
|
|
resp = resp as StatFileResponse;
|
|
|
|
if (resp.size <= MAX_CACHE_SIZE) {
|
|
|
|
cacheBuffer = Uint8Array.from(
|
|
|
|
(cacheBuffer as Uint8Array[]).reduce(
|
|
|
|
(previousValue: Uint8Array, currentValue: Uint8Array) => {
|
|
|
|
return Uint8Array.from([...previousValue, ...currentValue]);
|
2022-08-14 10:15:20 +00:00
|
|
|
}
|
2022-08-15 13:02:25 +00:00
|
|
|
)
|
|
|
|
);
|
2022-08-14 10:15:20 +00:00
|
|
|
|
2022-08-15 13:02:25 +00:00
|
|
|
// @ts-ignore
|
|
|
|
return cacheDb.items.put({
|
|
|
|
url: details.url,
|
|
|
|
contentType,
|
|
|
|
data: new Blob([cacheBuffer.buffer], { type: contentType }),
|
|
|
|
timestamp: Date.now(),
|
|
|
|
});
|
2022-08-13 08:19:01 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch((e) => {
|
2022-08-13 17:16:53 +00:00
|
|
|
console.error("page error", urlPath, e.message);
|
2022-08-13 08:19:01 +00:00
|
|
|
/* if (
|
2022-08-13 17:16:53 +00:00
|
|
|
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);
|
|
|
|
}*/
|
2022-08-13 08:19:01 +00:00
|
|
|
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) {
|
2022-08-13 17:16:53 +00:00
|
|
|
headers.push({
|
|
|
|
name: "content-length",
|
|
|
|
value: contentLength,
|
|
|
|
});
|
|
|
|
}*/
|
2022-08-13 08:19:01 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
responseHeaders: headers,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|