Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
Derrick Hammer | 4228d834cd | |
Derrick Hammer | 00d1bd2ab5 |
|
@ -0,0 +1,68 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#browser-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#address-bar-container {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#address-bar {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#go-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#web-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#booting {
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title></title>
|
||||||
|
<script type="application/javascript" src="./index.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="browser-container">
|
||||||
|
<div id="address-bar-container">
|
||||||
|
<span id="booting">Booting</span>
|
||||||
|
<input type="text" id="address-bar" placeholder="Enter URL..." disabled>
|
||||||
|
<button id="go-button" disabled>Go</button>
|
||||||
|
</div>
|
||||||
|
<iframe id="web-content" src="about:blank" frameborder="0" style="width: 100%; height: 100%;"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"name": "browser-webapp",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@lumeweb/presetter-kernel-module-preset": "^0.1.0-develop.1",
|
||||||
|
"presetter": "*"
|
||||||
|
},
|
||||||
|
"readme": "ERROR: No README data found!",
|
||||||
|
"_id": "browser-webapp@0.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "presetter bootstrap",
|
||||||
|
"build": "run build",
|
||||||
|
"semantic-release": "semantic-release"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@helia/unixfs": "^1.4.2",
|
||||||
|
"@lumeweb/kernel-dns-client": "^0.1.0-develop.7",
|
||||||
|
"@lumeweb/kernel-eth-client": "^0.1.0-develop.16",
|
||||||
|
"@lumeweb/kernel-handshake-client": "^0.1.0-develop.8",
|
||||||
|
"@lumeweb/kernel-ipfs-client": "^0.1.0-develop.24",
|
||||||
|
"@lumeweb/kernel-network-registry-client": "^0.1.0-develop.9",
|
||||||
|
"@lumeweb/kernel-peer-discovery-client": "^0.0.2-develop.16",
|
||||||
|
"@lumeweb/kernel-swarm-client": "^0.1.0-develop.10",
|
||||||
|
"@lumeweb/libkernel": "^0.1.0-develop.63",
|
||||||
|
"@lumeweb/tld-enum": "^0.1.0-develop.1",
|
||||||
|
"cheerio": "^1.0.0-rc.12",
|
||||||
|
"file-type": "^18.5.0",
|
||||||
|
"is-ipfs": "^8.0.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { createClient as createDnsClient } from "@lumeweb/kernel-dns-client";
|
||||||
|
import { createClient as createIpfsClient } from "@lumeweb/kernel-ipfs-client";
|
||||||
|
import { createClient as createSwarmClient } from "@lumeweb/kernel-swarm-client";
|
||||||
|
import { createClient as createPeerDiscoveryClient } from "@lumeweb/kernel-peer-discovery-client";
|
||||||
|
import { createClient as createNetworkRegistryClient } from "@lumeweb/kernel-network-registry-client";
|
||||||
|
import { createClient as createHandshakeClient } from "@lumeweb/kernel-handshake-client";
|
||||||
|
import { createClient as createEthClient } from "@lumeweb/kernel-eth-client";
|
||||||
|
|
||||||
|
const dnsClient = createDnsClient();
|
||||||
|
const ipfsClient = createIpfsClient();
|
||||||
|
const swarmClient = createSwarmClient();
|
||||||
|
const peerDiscoveryClient = createPeerDiscoveryClient();
|
||||||
|
const networkRegistryClient = createNetworkRegistryClient();
|
||||||
|
const handshakeClient = createHandshakeClient();
|
||||||
|
const ethClient = createEthClient();
|
||||||
|
|
||||||
|
export {
|
||||||
|
dnsClient,
|
||||||
|
ipfsClient,
|
||||||
|
swarmClient,
|
||||||
|
peerDiscoveryClient,
|
||||||
|
networkRegistryClient,
|
||||||
|
handshakeClient,
|
||||||
|
ethClient,
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { ContentFilter } from "./types.js";
|
||||||
|
|
||||||
|
export class ContentProcessor {
|
||||||
|
private filters: ContentFilter[] = [];
|
||||||
|
|
||||||
|
registerFilter(filter: ContentFilter) {
|
||||||
|
this.filters.push(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async process(response: Response, mimeType: string): Promise<Response> {
|
||||||
|
let processedResponse = response;
|
||||||
|
|
||||||
|
for (const filter of this.filters) {
|
||||||
|
processedResponse = await filter.process(processedResponse, mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedResponse;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { ContentFilter } from "src/types.js";
|
||||||
|
import { getTld } from "@lumeweb/libresolver";
|
||||||
|
import tldEnum from "@lumeweb/tld-enum";
|
||||||
|
import * as cheerio from "cheerio";
|
||||||
|
|
||||||
|
export default class URLRewriteFilter implements ContentFilter {
|
||||||
|
async process(response: Response, mimeType: string): Promise<Response> {
|
||||||
|
if (mimeType !== "text/html") {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = await response.text();
|
||||||
|
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
["a", "link", "script", "img"].forEach((tag) => {
|
||||||
|
$.root()
|
||||||
|
.find(tag)
|
||||||
|
.each((index, element) => {
|
||||||
|
let attrName = ["a", "link"].includes(tag) ? "href" : "src";
|
||||||
|
let urlValue = $(element).attr(attrName);
|
||||||
|
|
||||||
|
if (urlValue) {
|
||||||
|
if (!isICANN(urlValue)) {
|
||||||
|
if (urlValue.startsWith("/")) {
|
||||||
|
$(element).attr(attrName, `/browse${urlValue}`);
|
||||||
|
} else if (urlValue.startsWith("http")) {
|
||||||
|
$(element).attr(attrName, `/browse/${urlValue}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response($.html(), {
|
||||||
|
headers: response.headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isICANN(url: string) {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
const domain = parsedUrl.hostname;
|
||||||
|
return tldEnum.list.includes(getTld(domain));
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
import * as kernel from "@lumeweb/libkernel/kernel";
|
||||||
|
import {
|
||||||
|
dnsClient,
|
||||||
|
ethClient,
|
||||||
|
handshakeClient,
|
||||||
|
ipfsClient,
|
||||||
|
networkRegistryClient,
|
||||||
|
peerDiscoveryClient,
|
||||||
|
swarmClient,
|
||||||
|
} from "./clients.js";
|
||||||
|
import { ed25519 } from "@lumeweb/libkernel";
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () =>
|
||||||
|
document.getElementById("go-button")?.addEventListener("click", () => {
|
||||||
|
let input = (
|
||||||
|
document.getElementById("address-bar") as HTMLInputElement
|
||||||
|
).value.trim();
|
||||||
|
|
||||||
|
// If the input doesn't contain a protocol, assume it's http
|
||||||
|
if (!input.match(/^https?:\/\//)) {
|
||||||
|
input = `http://${input}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to parse it as a URL
|
||||||
|
const url = new URL(input);
|
||||||
|
|
||||||
|
// Update the iframe's src attribute
|
||||||
|
const iframe = document.getElementById(
|
||||||
|
"web-content",
|
||||||
|
) as HTMLIFrameElement;
|
||||||
|
iframe.src = `/browse/${url.hostname}${url.pathname}${url.search}${url.hash}`;
|
||||||
|
} catch (e) {
|
||||||
|
// Handle invalid URLs here, if needed
|
||||||
|
console.error("Invalid URL:", e);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let BOOT_FUNCTIONS: (() => Promise<any>)[] = [];
|
||||||
|
|
||||||
|
async function boot() {
|
||||||
|
await kernel.init();
|
||||||
|
await kernel.login(ed25519.utils.randomPrivateKey());
|
||||||
|
|
||||||
|
const reg = await navigator.serviceWorker.register("/sw.js");
|
||||||
|
await reg.update();
|
||||||
|
|
||||||
|
await kernel.serviceWorkerReady();
|
||||||
|
|
||||||
|
BOOT_FUNCTIONS.push(
|
||||||
|
async () =>
|
||||||
|
await swarmClient.addRelay(
|
||||||
|
"2d7ae1517caf4aae4de73c6d6f400765d2dd00b69d65277a29151437ef1c7d1d",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// IRC
|
||||||
|
BOOT_FUNCTIONS.push(
|
||||||
|
async () =>
|
||||||
|
await peerDiscoveryClient.register(
|
||||||
|
"zdiN5eJ3RfHpZHTYorGxBt1GCsrGJYV9GprwVWkj8snGsjWSrptFm8BtQX",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
BOOT_FUNCTIONS.push(
|
||||||
|
async () => await networkRegistryClient.registerType("content"),
|
||||||
|
);
|
||||||
|
BOOT_FUNCTIONS.push(
|
||||||
|
async () => await networkRegistryClient.registerType("blockchain"),
|
||||||
|
);
|
||||||
|
BOOT_FUNCTIONS.push(async () => await handshakeClient.register());
|
||||||
|
BOOT_FUNCTIONS.push(async () => await ethClient.register());
|
||||||
|
BOOT_FUNCTIONS.push(async () => await ipfsClient.register());
|
||||||
|
|
||||||
|
const resolvers = [
|
||||||
|
"zdiJdDdBJWAdYFTcRa9So5TQQ9f1pYMiMy4dqYcKp9imomQtR11LJUyJyV", // CID
|
||||||
|
"zdiKvnZYNjDqXaM8uF3pGEs7Tt6jqGc7t7M4eqbvJwpkTnrZymncfUW9Cj", // ENS
|
||||||
|
"zrjEH3iojPLr7986o7iCn9THBmJmHiuDWmS1G6oT8DnfuFM", // HNS
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const resolver of resolvers) {
|
||||||
|
BOOT_FUNCTIONS.push(async () => dnsClient.registerResolver(resolver));
|
||||||
|
}
|
||||||
|
|
||||||
|
await bootup();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
ethClient.ready(),
|
||||||
|
handshakeClient.ready(),
|
||||||
|
ipfsClient.ready(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
document.getElementById("booting")!.style.display = "none";
|
||||||
|
(document.getElementById("address-bar") as HTMLInputElement).disabled = false;
|
||||||
|
(document.getElementById("go-button") as HTMLButtonElement).disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootup() {
|
||||||
|
for (const entry of Object.entries(BOOT_FUNCTIONS)) {
|
||||||
|
await entry[1]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", boot);
|
|
@ -0,0 +1,123 @@
|
||||||
|
import exchangeCommunicationKeys from "./messages/exchangeCommunicationKeys.js";
|
||||||
|
import {
|
||||||
|
deleteQuery,
|
||||||
|
getAuthStatus,
|
||||||
|
getAuthStatusKnown,
|
||||||
|
getLoggedInDefer,
|
||||||
|
getQueries,
|
||||||
|
getQuery,
|
||||||
|
resetLoggedInDefer,
|
||||||
|
setAuthStatus,
|
||||||
|
setAuthStatusKnown,
|
||||||
|
getAuthStatusDefer,
|
||||||
|
} from "./vars.js";
|
||||||
|
const kernelMessageHandlers = {
|
||||||
|
exchangeCommunicationKeys,
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function handleIncomingMessage(event: MessageEvent) {
|
||||||
|
if (event.source === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.source === window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = event.data?.data;
|
||||||
|
if (event.data.method === "log") {
|
||||||
|
if (data?.isErr === false) {
|
||||||
|
console.log(data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.method === "kernelAuthStatus") {
|
||||||
|
setAuthStatus(data);
|
||||||
|
if (!getAuthStatusKnown()) {
|
||||||
|
getAuthStatusDefer().resolve();
|
||||||
|
setAuthStatusKnown(true);
|
||||||
|
console.log("bootloader is now initialized");
|
||||||
|
if (!getAuthStatus().loginComplete) {
|
||||||
|
console.log("user is not logged in: waiting until login is confirmed");
|
||||||
|
} else {
|
||||||
|
getLoggedInDefer().resolve();
|
||||||
|
}
|
||||||
|
if (getAuthStatus().logoutComplete) {
|
||||||
|
resetLoggedInDefer();
|
||||||
|
setAuthStatusKnown(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("nonce" in event.data)) {
|
||||||
|
(event.source as WindowProxy).postMessage(
|
||||||
|
{
|
||||||
|
nonce: "N/A",
|
||||||
|
method: "response",
|
||||||
|
err: "message sent to kernel with no nonce",
|
||||||
|
},
|
||||||
|
event.origin,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("method" in event.data)) {
|
||||||
|
(event.source as WindowProxy).postMessage(
|
||||||
|
{
|
||||||
|
nonce: event.data.nonce,
|
||||||
|
method: "response",
|
||||||
|
err: "message sent to kernel with no method",
|
||||||
|
},
|
||||||
|
event.origin,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.data.method in kernelMessageHandlers) {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await kernelMessageHandlers[event.data.method](
|
||||||
|
event.data.data,
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
response = { err: (e as Error).message };
|
||||||
|
}
|
||||||
|
|
||||||
|
(event.source as WindowProxy).postMessage(
|
||||||
|
{
|
||||||
|
nonce: event.data.nonce,
|
||||||
|
data: response,
|
||||||
|
},
|
||||||
|
event.origin,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(event.data.nonce in getQueries())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let receiveResult = getQuery(event.data.nonce);
|
||||||
|
if (event.data.method === "response") {
|
||||||
|
deleteQuery(event.data.nonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveResult?.(event.data);
|
||||||
|
|
||||||
|
if (["moduleCall", "response"].includes(event.data.method)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(event.source as WindowProxy).postMessage(
|
||||||
|
{
|
||||||
|
nonce: event.data.nonce,
|
||||||
|
method: "response",
|
||||||
|
err:
|
||||||
|
"unrecognized method (user may need to log in): " + event.data.method,
|
||||||
|
},
|
||||||
|
event.origin,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { bytesToHex, hexToBytes } from "@lumeweb/libweb";
|
||||||
|
import {
|
||||||
|
getCommunicationPubKey,
|
||||||
|
setFrontendCommunicationPubkey,
|
||||||
|
} from "../vars.js";
|
||||||
|
|
||||||
|
export default function (data: any) {
|
||||||
|
setFrontendCommunicationPubkey(hexToBytes(data));
|
||||||
|
|
||||||
|
return bytesToHex(getCommunicationPubKey());
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
const extToMimes = new Map(
|
||||||
|
Object.entries({
|
||||||
|
html: "text/html",
|
||||||
|
xhtml: "application/xhtml+xml",
|
||||||
|
xml: "application/xml",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Object.freeze(extToMimes);
|
||||||
|
|
||||||
|
export default extToMimes;
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { ContentProcessor } from "./contentProcessor.js";
|
||||||
|
import { ContentProvider } from "./types.js";
|
||||||
|
import { DNSRecord, DNSResult } from "@lumeweb/libresolver";
|
||||||
|
import { URL } from "url";
|
||||||
|
|
||||||
|
export class ProviderManager {
|
||||||
|
private providers: ContentProvider[] = [];
|
||||||
|
|
||||||
|
private _processor = new ContentProcessor();
|
||||||
|
|
||||||
|
get processor(): ContentProcessor {
|
||||||
|
return this._processor;
|
||||||
|
}
|
||||||
|
|
||||||
|
register(provider: ContentProvider) {
|
||||||
|
this.providers.push(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(dnsResult: DNSResult, path: string): Promise<Response> {
|
||||||
|
for (const record of dnsResult.records) {
|
||||||
|
for (const provider of this.providers) {
|
||||||
|
if (provider.supports(record.value)) {
|
||||||
|
const content = await provider.fetchContent(record.value, path);
|
||||||
|
|
||||||
|
if (content.headers.get("Content-Type")) {
|
||||||
|
return this._processor.process(
|
||||||
|
content,
|
||||||
|
content.headers.get("Content-Type")!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("No suitable provider found.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { ContentProvider } from "src/types.js";
|
||||||
|
import { ipfsPath, ipnsPath, path as checkPath } from "is-ipfs";
|
||||||
|
import { createClient } from "@lumeweb/kernel-ipfs-client";
|
||||||
|
import { CID } from "multiformats/cid";
|
||||||
|
import type { UnixFSStats } from "@helia/unixfs";
|
||||||
|
import * as nodePath from "path";
|
||||||
|
import { fileTypeFromBuffer } from "file-type";
|
||||||
|
import extToMimes from "../mimes.js";
|
||||||
|
import { URL } from "url";
|
||||||
|
|
||||||
|
export default class IPFSProvider implements ContentProvider {
|
||||||
|
private _client = createClient();
|
||||||
|
async fetchContent(
|
||||||
|
uri: string,
|
||||||
|
path: string,
|
||||||
|
query?: string,
|
||||||
|
): Promise<Response> {
|
||||||
|
let cid = translatePath(uri);
|
||||||
|
let stat: UnixFSStats | null = null;
|
||||||
|
let urlPath = path;
|
||||||
|
const parsedPath = nodePath.parse(urlPath);
|
||||||
|
let err;
|
||||||
|
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 && stat?.type === "directory") {
|
||||||
|
if (!parsedPath.base.length || !parsedPath.ext.length) {
|
||||||
|
let found = false;
|
||||||
|
for (const indexFile of ["index.html", "index.htm"]) {
|
||||||
|
try {
|
||||||
|
const subPath = nodePath.join(urlPath, indexFile);
|
||||||
|
await this._client.stat(cid, {
|
||||||
|
path: subPath,
|
||||||
|
});
|
||||||
|
urlPath = subPath;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
err = "404";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await this._client.stat(cid, {
|
||||||
|
path: urlPath,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
err = "404";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bufferRead = 0;
|
||||||
|
const fileTypeBufferLength = 4100;
|
||||||
|
const mimeBuffer: Uint8Array[] = [];
|
||||||
|
let reader = await this._client.cat(cid, { path: urlPath });
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mime;
|
||||||
|
|
||||||
|
if (bufferRead >= fileTypeBufferLength) {
|
||||||
|
const totalLength = mimeBuffer.reduce((acc, val) => acc + val.length, 0);
|
||||||
|
const concatenated = new Uint8Array(totalLength);
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of mimeBuffer) {
|
||||||
|
concatenated.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
mime = await fileTypeFromBuffer(concatenated);
|
||||||
|
|
||||||
|
if (!mime) {
|
||||||
|
const ext = nodePath.parse(urlPath).ext.replace(".", "");
|
||||||
|
if (extToMimes.has(ext)) {
|
||||||
|
mime = extToMimes.get(ext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader = await this._client.cat(cid, { path: urlPath });
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
for await (const chunk of reader.iterable()) {
|
||||||
|
controller.enqueue(chunk);
|
||||||
|
}
|
||||||
|
controller.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(stream, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": mime ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
supports(uri: string): boolean {
|
||||||
|
return checkPath(translatePath(uri));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function translatePath(uri: string) {
|
||||||
|
return uri.replace(/:\/\//, "/").replace(/^/, "/");
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { createClient as createDnsClient } from "@lumeweb/kernel-dns-client";
|
||||||
|
import { ProviderManager } from "./providerManager.js";
|
||||||
|
import IPFSProvider from "./providers/ipfs.js";
|
||||||
|
import URLRewriteFilter from "./filters/urlRewrite.js";
|
||||||
|
|
||||||
|
const dnsClient = createDnsClient();
|
||||||
|
|
||||||
|
const providerManager = new ProviderManager();
|
||||||
|
providerManager.register(new IPFSProvider());
|
||||||
|
providerManager.processor.registerFilter(new URLRewriteFilter());
|
||||||
|
|
||||||
|
globalThis.postMessage = async (...args) => {
|
||||||
|
// @ts-ignore
|
||||||
|
let ret = await clients.matchAll({ includeUncontrolled: true });
|
||||||
|
ret.forEach((item) => item.postMessage(...args));
|
||||||
|
|
||||||
|
if (!ret.length) {
|
||||||
|
const cb = (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
postMessage(...args);
|
||||||
|
self.removeEventListener("activate", cb);
|
||||||
|
};
|
||||||
|
self.addEventListener("activate", cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
// @ts-ignore
|
||||||
|
event.waitUntil(
|
||||||
|
(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
await clients.claim();
|
||||||
|
// @ts-ignore
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
addEventListener("fetch", (event: any) => {
|
||||||
|
event.respondWith(
|
||||||
|
(async () => {
|
||||||
|
const req = event.request;
|
||||||
|
const url = new URL(req.url);
|
||||||
|
|
||||||
|
if (
|
||||||
|
["/index.html", "/index.js", "/"].includes(url.pathname) ||
|
||||||
|
!url.pathname.startsWith("/browse/")
|
||||||
|
) {
|
||||||
|
return fetch(event.request).then((response: any) => {
|
||||||
|
response.redirectToFinalURL = true;
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let realUrl = url.pathname.replace(/^\/browse\//, "").replace(/\/$/, "");
|
||||||
|
|
||||||
|
if (!realUrl.match(/^https?:\/\//)) {
|
||||||
|
realUrl = `http://${realUrl}`;
|
||||||
|
}
|
||||||
|
// Use your existing communication framework to resolve DNS.
|
||||||
|
const dnsResult = await dnsClient.resolve(new URL(realUrl).hostname);
|
||||||
|
|
||||||
|
if (!dnsResult.error && dnsResult.records.length > 0) {
|
||||||
|
return providerManager.fetch(dnsResult, new URL(realUrl).pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Sorry, that is not a valid web3 website.");
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
});
|
|
@ -0,0 +1,12 @@
|
||||||
|
export interface ContentProvider {
|
||||||
|
supports: (uri: string) => boolean;
|
||||||
|
fetchContent: (
|
||||||
|
uri: string,
|
||||||
|
path: string,
|
||||||
|
query?: string,
|
||||||
|
) => Promise<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentFilter {
|
||||||
|
process: (response: Response, mineType: string) => Promise<Response>;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import {
|
||||||
|
getKernelLoaded,
|
||||||
|
getLoginComplete,
|
||||||
|
getLogoutComplete,
|
||||||
|
} from "./vars.js";
|
||||||
|
import { objAsString } from "@lumeweb/libkernel";
|
||||||
|
|
||||||
|
export function sendAuthUpdate() {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
method: "kernelAuthStatus",
|
||||||
|
data: {
|
||||||
|
loginComplete: getLoginComplete(),
|
||||||
|
kernelLoaded: getKernelLoaded(),
|
||||||
|
logoutComplete: getLogoutComplete(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bootloaderWLog(isErr: boolean, ...inputs: any) {
|
||||||
|
// Build the message, each item gets its own line. We do this because items
|
||||||
|
// are often full objects.
|
||||||
|
let message = "[lumeweb-kernel-bootloader]";
|
||||||
|
for (let i = 0; i < inputs.length; i++) {
|
||||||
|
message += "\n";
|
||||||
|
message += objAsString(inputs[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the log by sending it to the parent.
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
method: "log",
|
||||||
|
data: {
|
||||||
|
isErr,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function log(...inputs: any) {
|
||||||
|
bootloaderWLog(false, ...inputs);
|
||||||
|
}
|
||||||
|
export function logErr(...inputs: any) {
|
||||||
|
bootloaderWLog(true, ...inputs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reloadKernel() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { x25519 } from "@noble/curves/ed25519";
|
||||||
|
import { defer } from "@lumeweb/libkernel/module";
|
||||||
|
import { KernelAuthStatus } from "@lumeweb/libkernel";
|
||||||
|
|
||||||
|
let queriesNonce = 1;
|
||||||
|
let queries: any = {};
|
||||||
|
let authStatus: KernelAuthStatus;
|
||||||
|
let authStatusKnown = false;
|
||||||
|
let authStatusDefer = defer();
|
||||||
|
let loggedInDefer = defer();
|
||||||
|
|
||||||
|
const store = new Map<string, any>(
|
||||||
|
Object.entries({
|
||||||
|
loginComplete: false,
|
||||||
|
logoutComplete: false,
|
||||||
|
kernelLoaded: "not yet",
|
||||||
|
communicationKey: null,
|
||||||
|
frontendCommunicationPubKey: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export function setLoginComplete(status: boolean) {
|
||||||
|
store.set("loginComplete", status);
|
||||||
|
}
|
||||||
|
export function getLoginComplete(): boolean {
|
||||||
|
return store.get("loginComplete");
|
||||||
|
}
|
||||||
|
export function setLogoutComplete(status: boolean) {
|
||||||
|
store.set("logoutComplete", status);
|
||||||
|
}
|
||||||
|
export function getLogoutComplete(): boolean {
|
||||||
|
return store.get("logoutComplete");
|
||||||
|
}
|
||||||
|
export function setKernelLoaded(status: string) {
|
||||||
|
store.set("kernelLoaded", status);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getKernelLoaded(): string {
|
||||||
|
return store.get("kernelLoaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommunicationKey(): Uint8Array {
|
||||||
|
if (!store.get("communicationKey")) {
|
||||||
|
store.set("communicationKey", x25519.utils.randomPrivateKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
return store.get("communicationKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCommunicationPubKey() {
|
||||||
|
return x25519.getPublicKey(getCommunicationKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFrontendCommunicationPubkey(): Uint8Array {
|
||||||
|
return store.get("frontendCommunicationPubKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setFrontendCommunicationPubkey(key: Uint8Array) {
|
||||||
|
store.set("frontendCommunicationPubKey", key);
|
||||||
|
}
|
||||||
|
export function getAuthStatusDefer() {
|
||||||
|
return authStatusDefer;
|
||||||
|
}
|
||||||
|
export function getQueriesNonce(): number {
|
||||||
|
return queriesNonce;
|
||||||
|
}
|
||||||
|
export function addQuery(nonce: any, func: Function) {
|
||||||
|
queries[nonce] = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function increaseQueriesNonce() {
|
||||||
|
queriesNonce++;
|
||||||
|
}
|
||||||
|
export function setAuthStatus(status: KernelAuthStatus) {
|
||||||
|
authStatus = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthStatusKnown() {
|
||||||
|
return authStatusKnown;
|
||||||
|
}
|
||||||
|
export function setAuthStatusKnown(status: boolean) {
|
||||||
|
authStatusKnown = status;
|
||||||
|
}
|
||||||
|
export function getAuthStatus(): KernelAuthStatus {
|
||||||
|
return authStatus;
|
||||||
|
}
|
||||||
|
export function getLoggedInDefer() {
|
||||||
|
return loggedInDefer;
|
||||||
|
}
|
||||||
|
export function resetLoggedInDefer() {
|
||||||
|
loggedInDefer = defer();
|
||||||
|
}
|
||||||
|
export function getQueries() {
|
||||||
|
return queries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQuery(nonce: any) {
|
||||||
|
return queries[nonce];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteQuery(nonce: any) {
|
||||||
|
delete queries[nonce];
|
||||||
|
}
|
Loading…
Reference in New Issue