*Skeleton starting point for new extension
This commit is contained in:
commit
250b09e5be
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"prefer-const": "off",
|
||||
"no-var": "off"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
package-lock.json
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Hammer Technologies LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,5 @@
|
|||
# Lume Web Extension
|
||||
|
||||
This is the WIP 0.3.0 extension for Lume Web. It uses the skynet kernel and uses most code from https://github.com/SkynetLabs/skynet-kernel/tree/beta/extension
|
||||
|
||||
If you have questions, from into the discord linked at [https://web3extension.com](https://web3extension.com)
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"description": "Lume Web is your decentralized gateway into the web3 internet, the web owned and controlled by its users",
|
||||
"manifest_version": 2,
|
||||
"name": "Lume Web",
|
||||
"version": "0.3.0",
|
||||
"homepage_url": "https://lumeweb.com",
|
||||
"icons": {
|
||||
"48": "icon.png",
|
||||
"96": "icon@2x.png"
|
||||
},
|
||||
"permissions": ["proxy", "webRequest", "webRequestBlocking", "<all_urls>"],
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://kernel.skynet/"],
|
||||
"js": ["bootloader.js"],
|
||||
"run_at": "document_end",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["bridge.js"],
|
||||
"run_at": "document_end",
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": ["icon@2x.png", "auth.html"]
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import esbuild from "esbuild";
|
||||
|
||||
esbuild.buildSync({
|
||||
entryPoints: ["src/main/background.ts"],
|
||||
outfile: "dist/background.js",
|
||||
format: "iife",
|
||||
bundle: true,
|
||||
legalComments: "external",
|
||||
// minify: true
|
||||
define: {
|
||||
global: "self",
|
||||
},
|
||||
});
|
||||
|
||||
esbuild.buildSync({
|
||||
entryPoints: ["src/main/bootloader.ts"],
|
||||
outfile: "dist/bootloader.js",
|
||||
format: "esm",
|
||||
bundle: true,
|
||||
legalComments: "external",
|
||||
// minify: true
|
||||
define: {
|
||||
global: "self",
|
||||
},
|
||||
});
|
||||
esbuild.buildSync({
|
||||
entryPoints: ["src/main/bridge.ts"],
|
||||
outfile: "dist/bridge.js",
|
||||
format: "esm",
|
||||
bundle: true,
|
||||
legalComments: "external",
|
||||
// minify: true
|
||||
define: {
|
||||
global: "self",
|
||||
},
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "@lumeweb/extension",
|
||||
"version": "0.3.0",
|
||||
"description": "Lume Web is your decentralized gateway into the web3 internet, the web owned and controlled by its users",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"deps": "npm install libskynet@latest",
|
||||
"prettier": "prettier --write .",
|
||||
"eslint": "eslint src --fix",
|
||||
"lint": "npm run deps && npm run prettier && npm run eslint",
|
||||
"compile": "node build.js",
|
||||
"build": "npm run compile && cpy \"assets/*\" dist"
|
||||
},
|
||||
"author": "David Vorick",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@lumeweb/webextension-polyfill": "https://github.com/LumeWeb/webextension-polyfill.git",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"@types/read": "^0.0.29",
|
||||
"@types/webextension-polyfill": "^0.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"cpy-cli": "^4.1.0",
|
||||
"esbuild": "^0.14.51",
|
||||
"eslint": "^8.13.0",
|
||||
"prettier": "^2.6.2",
|
||||
"rollup": "^2.75.6",
|
||||
"typescript": "^4.6.3",
|
||||
"webextension-polyfill": "^0.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@lumeweb/tld-enum": "https://github.com/LumeWeb/list-of-top-level-domains.git",
|
||||
"libkernel": "^0.1.38",
|
||||
"libskynet": "^0.0.62"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import {
|
||||
BlockingResponse,
|
||||
OnBeforeRequestDetailsType,
|
||||
OnHeadersReceivedDetailsType,
|
||||
OnRequestDetailsType,
|
||||
} from "../types";
|
||||
import WebEngine from "../webEngine.js";
|
||||
|
||||
export default abstract class BaseProvider {
|
||||
private engine: WebEngine;
|
||||
constructor(engine: WebEngine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
async shouldHandleRequest(
|
||||
details: OnBeforeRequestDetailsType
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async handleRequest(
|
||||
details: OnBeforeRequestDetailsType
|
||||
): Promise<BlockingResponse | boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async handleProxy(details: OnRequestDetailsType): Promise<any | boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async handleHeaders(
|
||||
details: OnHeadersReceivedDetailsType
|
||||
): Promise<BlockingResponse | boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import BaseProvider from "./baseProvider.js";
|
||||
import {
|
||||
BlockingResponse,
|
||||
OnBeforeRequestDetailsType,
|
||||
OnHeadersReceivedDetailsType,
|
||||
OnRequestDetailsType,
|
||||
} from "../types.js";
|
||||
import browser from "@lumeweb/webextension-polyfill";
|
||||
import { RequestOverrideResponse } from "libskynet";
|
||||
import { queryKernel } from "../main/background.js";
|
||||
import { requestProxies } from "../util.js";
|
||||
|
||||
export default class InternalProvider extends BaseProvider {
|
||||
async shouldHandleRequest(
|
||||
details: OnBeforeRequestDetailsType
|
||||
): Promise<boolean> {
|
||||
return [
|
||||
"http://kernel.skynet/",
|
||||
"http://kernel.skynet/auth.html",
|
||||
"http://kernel.skynet/favicon.ico",
|
||||
].includes(details.url);
|
||||
}
|
||||
|
||||
async handleRequest(
|
||||
details: OnBeforeRequestDetailsType
|
||||
): Promise<BlockingResponse | boolean> {
|
||||
// For the kernel, we swallow the entire page. The 'bootloader' content
|
||||
// script will everything that we need.
|
||||
if (details.url === "http://kernel.skynet/") {
|
||||
// Get the filter and swallow any response from the server.
|
||||
let filter = browser.webRequest.filterResponseData(details.requestId);
|
||||
filter.onstart = () => {
|
||||
filter.close();
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
// For the favicon, we make a request to a content script that has access
|
||||
// to the favicon.
|
||||
if (details.url === "http://kernel.skynet/favicon.ico") {
|
||||
// Send a message to the kernel requesting an override for the
|
||||
// favicon.ico. The kernel is itself loading this favicon from the
|
||||
// browser, I just wasn't certain how to get binary objects directly to
|
||||
// the background page, so we fetch it via a content script instead.
|
||||
let faviconPromise = queryKernel({
|
||||
method: "requestOverride",
|
||||
data: {
|
||||
url: details.url,
|
||||
method: details.method,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the filter and swallow any response from the server. Setting
|
||||
// 'onData' to a blank function will swallow all data from the server.
|
||||
let filter = browser.webRequest.filterResponseData(details.requestId);
|
||||
filter.ondata = () => {
|
||||
// By setting 'ondata' to the emtpy function, we effectively ensure
|
||||
// that none of the data will be processed.
|
||||
};
|
||||
filter.onstop = () => {
|
||||
faviconPromise.then((result: RequestOverrideResponse) => {
|
||||
filter.write(result.body as Uint8Array);
|
||||
filter.close();
|
||||
});
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
// For the favicon, we make a request to a content script that has access
|
||||
// to the favicon.
|
||||
if (details.url === "http://kernel.skynet/auth.html") {
|
||||
// Send a message to the kernel requesting an override for the auth
|
||||
// page. The kernel is itself loading the auth page from the browser, I
|
||||
// just wasn't certain how to get binary objects directly to the
|
||||
// background page, so we fetch it via a content script instead.
|
||||
let authPagePromise = queryKernel({
|
||||
method: "requestOverride",
|
||||
data: {
|
||||
url: details.url,
|
||||
method: details.method,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the filter and swallow any response from the server. Setting
|
||||
// 'onData' to a blank function will swallow all data from the server.
|
||||
let filter = browser.webRequest.filterResponseData(details.requestId);
|
||||
filter.ondata = () => {
|
||||
// By setting 'ondata' to the emtpy function, we effectively ensure
|
||||
// that none of the data will be processed.
|
||||
};
|
||||
filter.onstop = () => {
|
||||
authPagePromise.then((result: RequestOverrideResponse) => {
|
||||
filter.write(result.body as Uint8Array);
|
||||
filter.close();
|
||||
});
|
||||
};
|
||||
return {};
|
||||
}
|
||||
|
||||
// Otherwise do nothing.
|
||||
return false;
|
||||
}
|
||||
|
||||
async handleHeaders(
|
||||
details: OnHeadersReceivedDetailsType
|
||||
): Promise<OnRequestDetailsType | boolean> {
|
||||
if (
|
||||
details.url === "http://kernel.skynet/" ||
|
||||
details.url === "http://kernel.skynet/auth.html"
|
||||
) {
|
||||
let headers = [
|
||||
{
|
||||
name: "content-type",
|
||||
value: "text/html; charset=utf8",
|
||||
},
|
||||
];
|
||||
return { responseHeaders: headers } as unknown as OnRequestDetailsType;
|
||||
}
|
||||
|
||||
// For the favicon, replace the headers with png headers.
|
||||
if (details.url === "http://kernel.skynet/favicon.ico") {
|
||||
let headers = [
|
||||
{
|
||||
name: "content-type",
|
||||
value: "image/png",
|
||||
},
|
||||
];
|
||||
return { responseHeaders: headers } as unknown as OnRequestDetailsType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async handleProxy(details: OnRequestDetailsType): Promise<any> {
|
||||
const hostname = new URL(details.url).hostname;
|
||||
if (hostname === "kernel.skynet") {
|
||||
return requestProxies;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
import type { DataFn, KernelAuthStatus } from "libskynet";
|
||||
|
||||
declare var browser: any; // tsc
|
||||
let queriesNonce = 1;
|
||||
let queries: any = {};
|
||||
let portsNonce = 0;
|
||||
let openPorts = {} as any;
|
||||
let timer = 20000;
|
||||
|
||||
function logLargeObjects() {
|
||||
let queriesLen = Object.keys(queries).length;
|
||||
let portsLen = Object.keys(openPorts).length;
|
||||
if (queriesLen > 500) {
|
||||
console.error("queries appears to be leaking:", queriesLen);
|
||||
}
|
||||
if (portsLen > 50) {
|
||||
console.error("ports appears to be leaking:", portsLen);
|
||||
}
|
||||
timer *= 1.25;
|
||||
setTimeout(logLargeObjects, timer);
|
||||
}
|
||||
setTimeout(logLargeObjects, timer);
|
||||
|
||||
let authStatus: KernelAuthStatus;
|
||||
let authStatusKnown = false;
|
||||
let authStatusResolve: DataFn;
|
||||
let blockForBootloader = new Promise((resolve) => {
|
||||
authStatusResolve = resolve;
|
||||
});
|
||||
|
||||
export function queryKernel(query: any): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
let receiveResponse = function (data: any) {
|
||||
resolve(data.data);
|
||||
};
|
||||
|
||||
blockForBootloader.then(() => {
|
||||
let nonce = queriesNonce;
|
||||
queriesNonce += 1;
|
||||
query.nonce = nonce;
|
||||
queries[nonce] = receiveResponse;
|
||||
if (kernelFrame.contentWindow !== null) {
|
||||
kernelFrame.contentWindow.postMessage(query, "http://kernel.skynet");
|
||||
} else {
|
||||
console.error(
|
||||
"kernelFrame.contentWindow was null, cannot send message!"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function handleKernelMessage(event: MessageEvent) {
|
||||
if (event.origin !== "http://kernel.skynet") {
|
||||
return;
|
||||
}
|
||||
let data = event.data.data;
|
||||
|
||||
if (event.data.method === "log") {
|
||||
if (data.isErr === false) {
|
||||
console.log(data.message);
|
||||
} else {
|
||||
console.error(data.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.method === "KernelAuthStatus") {
|
||||
authStatus = data;
|
||||
if (authStatusKnown === false) {
|
||||
authStatusResolve();
|
||||
authStatusKnown = true;
|
||||
console.log("bootloader is now initialized");
|
||||
if (authStatus.loginComplete !== true) {
|
||||
console.log("user is not logged in: waiting until login is confirmed");
|
||||
}
|
||||
}
|
||||
|
||||
for (let [, port] of Object.entries(openPorts)) {
|
||||
try {
|
||||
(port as any).postMessage(event.data);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (data.logoutComplete === true) {
|
||||
console.log("received logout signal, clearing all ports");
|
||||
|
||||
for (let [, port] of Object.entries(openPorts)) {
|
||||
try {
|
||||
(port as any).disconnect();
|
||||
} catch {}
|
||||
}
|
||||
openPorts = {};
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(event.data.nonce in queries)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let receiveResult = queries[event.data.nonce];
|
||||
if (event.data.method === "response") {
|
||||
delete queries[event.data.nonce];
|
||||
}
|
||||
|
||||
receiveResult(event.data);
|
||||
}
|
||||
window.addEventListener("message", handleKernelMessage);
|
||||
|
||||
function handleBridgeMessage(
|
||||
port: any,
|
||||
portNonce: number,
|
||||
data: any,
|
||||
domain: string
|
||||
) {
|
||||
if (!("nonce" in data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.method !== "queryUpdate") {
|
||||
queries[data.nonce] = (response: any) => {
|
||||
if (portNonce in openPorts) {
|
||||
port.postMessage(response);
|
||||
}
|
||||
};
|
||||
data["domain"] = domain;
|
||||
}
|
||||
kernelFrame.contentWindow!.postMessage(data, "http://kernel.skynet");
|
||||
}
|
||||
function bridgeListener(port: any) {
|
||||
let portNonce = portsNonce;
|
||||
portsNonce++;
|
||||
openPorts[portNonce] = port;
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
delete openPorts[portNonce];
|
||||
});
|
||||
|
||||
let domain = new URL(port.sender.url).hostname;
|
||||
|
||||
port.onMessage.addListener(function (data: any) {
|
||||
handleBridgeMessage(port, portNonce, data, domain);
|
||||
});
|
||||
|
||||
blockForBootloader.then(() => {
|
||||
port.postMessage({
|
||||
method: "KernelAuthStatus",
|
||||
data: authStatus,
|
||||
});
|
||||
});
|
||||
}
|
||||
browser.runtime.onConnect.addListener(bridgeListener);
|
||||
|
||||
import WebEngine from "../webEngine.js";
|
||||
import InternalProvider from "../contentProviders/internalProvider.js";
|
||||
|
||||
const engine = new WebEngine();
|
||||
engine.registerContentProvider(new InternalProvider(engine));
|
||||
|
||||
// @ts-ignore
|
||||
let kernelFrame: HTMLIFrameElement = document.createElement("iframe");
|
||||
kernelFrame.src = "http://kernel.skynet";
|
||||
document.body.appendChild(kernelFrame);
|
|
@ -0,0 +1,477 @@
|
|||
import {
|
||||
addContextToErr,
|
||||
b64ToBuf,
|
||||
bufToHex,
|
||||
bufToStr,
|
||||
computeRegistrySignature,
|
||||
defaultPortalList,
|
||||
deriveChildSeed,
|
||||
deriveRegistryEntryID,
|
||||
downloadSkylink,
|
||||
Ed25519Keypair,
|
||||
entryIDToSkylink,
|
||||
Err,
|
||||
hexToBuf,
|
||||
progressiveFetch,
|
||||
progressiveFetchResult,
|
||||
taggedRegistryEntryKeys,
|
||||
objAsString,
|
||||
verifyRegistryWriteResponse,
|
||||
} from "libskynet";
|
||||
|
||||
var browser: any; // tsc
|
||||
|
||||
const defaultKernelResolverLink =
|
||||
"AQBFjXpEBwbMwkBwYg0gdkeAM-yy9vlajfLtZSee9f-MDg";
|
||||
|
||||
document.title = "kernel.skynet";
|
||||
let header = document.createElement("h1");
|
||||
header.textContent =
|
||||
"Something went wrong! You should not be visiting this page, this page should only be accessed via an invisible iframe.";
|
||||
document.body.appendChild(header);
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
function log(...inputs: any) {
|
||||
bootloaderWLog(false, ...inputs);
|
||||
}
|
||||
function logErr(...inputs: any) {
|
||||
bootloaderWLog(true, ...inputs);
|
||||
}
|
||||
|
||||
var handleIncomingMessage = function (event: MessageEvent) {
|
||||
if (event.source === null) {
|
||||
return;
|
||||
}
|
||||
if (event.source === window) {
|
||||
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 === "requestOverride") {
|
||||
handleSkynetKernelRequestOverride(event);
|
||||
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;
|
||||
};
|
||||
window.addEventListener("message", (event: MessageEvent) => {
|
||||
handleIncomingMessage(event);
|
||||
});
|
||||
|
||||
let kernelFavicon: Uint8Array;
|
||||
let blockForFavicon: Promise<void> = new Promise((resolve) => {
|
||||
try {
|
||||
let faviconURL = browser.runtime.getURL("icon@2x.png");
|
||||
fetch(faviconURL).then((response) => {
|
||||
response.arrayBuffer().then((faviconData) => {
|
||||
kernelFavicon = new Uint8Array(faviconData);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
kernelFavicon = new Uint8Array(0);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
let kernelAuthPage: Uint8Array;
|
||||
let blockForAuthPage: Promise<void> = new Promise((resolve) => {
|
||||
try {
|
||||
let authURL = browser.runtime.getURL("auth.html");
|
||||
fetch(authURL).then((response) => {
|
||||
response.arrayBuffer().then((authData) => {
|
||||
kernelAuthPage = new Uint8Array(authData);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} catch (err: any) {
|
||||
kernelAuthPage = new TextEncoder().encode(
|
||||
addContextToErr(err, "unable to load the kernel auth page")
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
function handleSkynetKernelRequestOverride(event: MessageEvent) {
|
||||
if (event.source === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.origin.startsWith("moz")) {
|
||||
return;
|
||||
}
|
||||
if (event.data.data.method !== "GET") {
|
||||
return;
|
||||
}
|
||||
|
||||
let respondOverride = function (headers: any, body: Uint8Array) {
|
||||
(event.source as WindowProxy).postMessage(
|
||||
{
|
||||
nonce: event.data.nonce,
|
||||
method: "response",
|
||||
err: null,
|
||||
data: {
|
||||
override: true,
|
||||
headers,
|
||||
body,
|
||||
},
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
};
|
||||
|
||||
if (event.data.data.url === "http://kernel.skynet/favicon.ico") {
|
||||
blockForFavicon.then(() => {
|
||||
let headers = [
|
||||
{
|
||||
name: "content-type",
|
||||
value: "image/png",
|
||||
},
|
||||
];
|
||||
respondOverride(headers, kernelFavicon);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.data.url === "http://kernel.skynet/auth.html") {
|
||||
blockForAuthPage.then(() => {
|
||||
let headers = [
|
||||
{
|
||||
name: "content-type",
|
||||
value: "text/html; charset=utf8",
|
||||
},
|
||||
];
|
||||
respondOverride(headers, kernelAuthPage);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
(event.source as WindowProxy).postMessage(
|
||||
{
|
||||
nonce: event.data.nonce,
|
||||
method: "response",
|
||||
err: null,
|
||||
data: {
|
||||
override: false,
|
||||
},
|
||||
},
|
||||
event.origin
|
||||
);
|
||||
}
|
||||
|
||||
var handleStorage = function (event: StorageEvent) {
|
||||
if (event.key !== null && event.key !== "v1-seed") {
|
||||
return;
|
||||
}
|
||||
if (logoutComplete === true) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === null && loginComplete === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "v1-seed" && loginComplete === false) {
|
||||
let userSeedString = window.localStorage.getItem("v1-seed");
|
||||
if (userSeedString === null) {
|
||||
sendAuthUpdate();
|
||||
return;
|
||||
}
|
||||
let [decodedSeed, errHTB] = hexToBuf(userSeedString);
|
||||
if (errHTB !== null) {
|
||||
logErr(addContextToErr(errHTB, "seed could not be decoded from hex"));
|
||||
sendAuthUpdate();
|
||||
return;
|
||||
}
|
||||
userSeed = decodedSeed;
|
||||
|
||||
log("user is now logged in, attempting to load kernel");
|
||||
loginComplete = true;
|
||||
loadKernel();
|
||||
sendAuthUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
logoutComplete = true;
|
||||
sendAuthUpdate();
|
||||
log("attempting to do a page reload");
|
||||
window.location.reload();
|
||||
};
|
||||
window.addEventListener("storage", (event) => handleStorage(event));
|
||||
|
||||
function downloadKernel(
|
||||
kernelSkylink: string
|
||||
): Promise<[kernelCode: string, err: Err]> {
|
||||
return new Promise((resolve) => {
|
||||
downloadSkylink(kernelSkylink).then(([fileData, err]) => {
|
||||
if (err === "404") {
|
||||
resolve(["", err]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err !== null) {
|
||||
resolve([
|
||||
"",
|
||||
addContextToErr(err, "unable to download the default kernel"),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
let [kernelCode, errBBTS] = bufToStr(fileData);
|
||||
if (errBBTS !== null) {
|
||||
resolve([
|
||||
"",
|
||||
addContextToErr(err, "unable to decode the default kernel"),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
resolve([kernelCode, null]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function downloadDefaultKernel(): Promise<[kernelCode: string, err: Err]> {
|
||||
return downloadKernel(defaultKernelResolverLink);
|
||||
}
|
||||
|
||||
function setUserKernelAsDefault(keypair: Ed25519Keypair, dataKey: Uint8Array) {
|
||||
log(
|
||||
"user kernel not found, setting user kernel to " + defaultKernelResolverLink
|
||||
);
|
||||
|
||||
let [defaultKernelSkylink, err64] = b64ToBuf(defaultKernelResolverLink);
|
||||
if (err64 !== null) {
|
||||
log("unable to convert default kernel link to a Uint8Array");
|
||||
return;
|
||||
}
|
||||
|
||||
let [sig, errCRS] = computeRegistrySignature(
|
||||
keypair.secretKey,
|
||||
dataKey,
|
||||
defaultKernelSkylink,
|
||||
0n
|
||||
);
|
||||
if (errCRS !== null) {
|
||||
log(
|
||||
addContextToErr(
|
||||
errCRS,
|
||||
"unable to compute registry signature to set user kernel"
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let dataKeyHex = bufToHex(dataKey);
|
||||
let endpoint = "/skynet/registry";
|
||||
let postBody = {
|
||||
publickey: {
|
||||
algorithm: "ed25519",
|
||||
key: Array.from(keypair.publicKey),
|
||||
},
|
||||
datakey: dataKeyHex,
|
||||
revision: 0,
|
||||
data: Array.from(defaultKernelSkylink),
|
||||
signature: Array.from(sig),
|
||||
};
|
||||
let fetchOpts = {
|
||||
method: "post",
|
||||
body: JSON.stringify(postBody),
|
||||
};
|
||||
|
||||
progressiveFetch(
|
||||
endpoint,
|
||||
fetchOpts,
|
||||
defaultPortalList,
|
||||
verifyRegistryWriteResponse
|
||||
).then((result: progressiveFetchResult) => {
|
||||
if (result.success !== true) {
|
||||
log("unable to update the user kernel registry entry\n", result.logs);
|
||||
return;
|
||||
}
|
||||
log(
|
||||
"successfully updated the user kernel registry entry to the default kernel"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function downloadUserKernel(): Promise<[kernelCode: string, err: Err]> {
|
||||
return new Promise((resolve) => {
|
||||
let kernelEntrySeed = deriveChildSeed(userSeed, "userPreferredKernel2");
|
||||
|
||||
let [keypair, dataKey, errTREK] = taggedRegistryEntryKeys(
|
||||
kernelEntrySeed,
|
||||
"user kernel"
|
||||
);
|
||||
if (errTREK !== null) {
|
||||
resolve([
|
||||
"",
|
||||
addContextToErr(errTREK, "unable to create user kernel registry keys"),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
let [entryID, errREID] = deriveRegistryEntryID(keypair.publicKey, dataKey);
|
||||
if (errREID !== null) {
|
||||
resolve([
|
||||
"",
|
||||
addContextToErr(errREID, "unable to derive registry entry id"),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
let userKernelSkylink = entryIDToSkylink(entryID);
|
||||
|
||||
downloadKernel(userKernelSkylink).then(([kernelCode, err]) => {
|
||||
if (err === "404") {
|
||||
downloadDefaultKernel().then(([defaultCode, errDefault]) => {
|
||||
if (errDefault === null) {
|
||||
setUserKernelAsDefault(keypair, dataKey);
|
||||
}
|
||||
resolve([defaultCode, errDefault]);
|
||||
return;
|
||||
});
|
||||
return;
|
||||
}
|
||||
log("found user kernel, using: " + userKernelSkylink);
|
||||
|
||||
resolve([kernelCode, err]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadKernel() {
|
||||
downloadUserKernel().then(([kernelCode, err]) => {
|
||||
if (err !== null) {
|
||||
let extErr = addContextToErr(err, "unable to download kernel");
|
||||
kernelLoaded = extErr;
|
||||
logErr(extErr);
|
||||
sendAuthUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
window.eval(kernelCode);
|
||||
kernelLoaded = "success";
|
||||
sendAuthUpdate();
|
||||
log("kernel successfully loaded");
|
||||
return;
|
||||
} catch (err: any) {
|
||||
let extErr = addContextToErr(err, "unable to eval kernel");
|
||||
kernelLoaded = extErr;
|
||||
logErr(extErr);
|
||||
logErr(err.toString());
|
||||
console.error(extErr);
|
||||
console.error(err);
|
||||
sendAuthUpdate();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let loginComplete = false;
|
||||
let logoutComplete = false;
|
||||
let kernelLoaded = "not yet";
|
||||
function sendAuthUpdate() {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
method: "kernelAuthStatus",
|
||||
data: {
|
||||
loginComplete: loginComplete,
|
||||
kernelLoaded: kernelLoaded,
|
||||
logoutComplete: logoutComplete,
|
||||
},
|
||||
},
|
||||
"*"
|
||||
);
|
||||
}
|
||||
sendAuthUpdate();
|
||||
|
||||
let userSeed: Uint8Array;
|
||||
function checkForLoadKernel() {
|
||||
let userSeedString = window.localStorage.getItem("v1-seed");
|
||||
if (userSeedString === null) {
|
||||
sendAuthUpdate();
|
||||
return;
|
||||
}
|
||||
let [decodedSeed, errHTB] = hexToBuf(userSeedString);
|
||||
if (errHTB !== null) {
|
||||
logErr(addContextToErr(errHTB, "seed could not be decoded from hex"));
|
||||
sendAuthUpdate();
|
||||
return;
|
||||
}
|
||||
userSeed = decodedSeed;
|
||||
|
||||
log("user is already logged in, attempting to load kernel");
|
||||
loginComplete = true;
|
||||
sendAuthUpdate();
|
||||
loadKernel();
|
||||
}
|
||||
|
||||
let accessFailedStr =
|
||||
"unable to get access to localStorage, user may need to reduce their privacy settings";
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(document, "requestStorageAccess") &&
|
||||
window.origin === "https://skt.us"
|
||||
) {
|
||||
document
|
||||
.requestStorageAccess()
|
||||
.then(() => {
|
||||
checkForLoadKernel();
|
||||
})
|
||||
.catch((err) => {
|
||||
log(addContextToErr(err, accessFailedStr));
|
||||
sendAuthUpdate();
|
||||
});
|
||||
} else {
|
||||
checkForLoadKernel();
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
export {};
|
||||
|
||||
// Need to declare the browser variable so typescript doesn't complain. We
|
||||
// declare it as 'any' because typescript doesn't know type 'Browser'.
|
||||
declare var browser: any;
|
||||
|
||||
// This is the same as the dataFn declared in libskynet, but since it is the
|
||||
// only import I decided to re-implement it here.
|
||||
type dataFn = (data?: any) => void;
|
||||
|
||||
// Create the object that will track the current auth status of the kernel. We
|
||||
// need a promise because the client may connect to the bridge after all of the
|
||||
// auth messages have been processed, there's no guarantee that the client will
|
||||
// catch the initial messages.
|
||||
//
|
||||
// blockForAuthStatus is a promise that will resolve when the auth status is
|
||||
// initially known. 'authStatus' is the object that contains the latest auth
|
||||
// information from
|
||||
let authStatus: any; // matches the data field of the kernelAuthStatus message
|
||||
let authStatusKnown = false;
|
||||
let authStatusResolve: dataFn;
|
||||
let blockForAuthStatus: Promise<void> = new Promise((resolve) => {
|
||||
authStatusResolve = resolve;
|
||||
});
|
||||
|
||||
// Create the handler for messages from the background page. The background
|
||||
// will be exclusively relaying messages from the bridge to the kernel.
|
||||
function handleBackgroundMessage(data: any) {
|
||||
// If this is the first auth status message from the kernel, resolve the
|
||||
// auth promise.
|
||||
if (data.method === "kernelAuthStatus") {
|
||||
authStatus = data.data;
|
||||
if (authStatusKnown === false) {
|
||||
authStatusKnown = true;
|
||||
authStatusResolve();
|
||||
}
|
||||
}
|
||||
|
||||
// Pass the message through to the main page.
|
||||
window.postMessage(data);
|
||||
}
|
||||
|
||||
// Connect to the background page.
|
||||
let port = browser.runtime.connect();
|
||||
port.onMessage.addListener(handleBackgroundMessage);
|
||||
|
||||
// handleVersion will send a response providing the version of the bridge. When
|
||||
// the bridge version is queried, the bridge assumes that a new consumer has
|
||||
// appeared which will need the auth status of the kernel repeated.
|
||||
function handleVersion(data: any) {
|
||||
// Send a message indicating that the bridge is alive.
|
||||
window.postMessage({
|
||||
nonce: data.nonce,
|
||||
method: "response",
|
||||
err: null,
|
||||
data: {
|
||||
version: "v0.2.0",
|
||||
},
|
||||
});
|
||||
|
||||
// Wait until the kernel auth status is known, then send a message with
|
||||
// the kernel auth status.
|
||||
blockForAuthStatus.then(() => {
|
||||
window.postMessage({
|
||||
method: "kernelAuthStatus",
|
||||
data: authStatus,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// handleKernelQuery handles messages sent by the page that are intended to
|
||||
// eventually reach the kernel.
|
||||
function handleKernelQuery(data: any) {
|
||||
// Check for a kernel query. We already checked that a nonce exists.
|
||||
if (!("data" in data)) {
|
||||
window.postMessage({
|
||||
nonce: data.nonce,
|
||||
method: "response",
|
||||
err: "missing data from newKernelQuery message: " + JSON.stringify(data),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass the message along to the kernel. The caller is responsible for
|
||||
// ensuring the nonce is unique.
|
||||
port.postMessage(data.data);
|
||||
}
|
||||
|
||||
// handleQueryUpdate will forward an update to a query to the kernel.
|
||||
function handleQueryUpdate(data: any) {
|
||||
// Send the update to the kernel.
|
||||
port.postMessage(data);
|
||||
}
|
||||
|
||||
// This is the listener for the content script, it will receive messages from
|
||||
// the page script that it can forward to the kernel.
|
||||
function handleMessage(event: MessageEvent) {
|
||||
// Throughout this function, errors are typically not logged because we
|
||||
// will be receiving all messages sent to the window, including messages
|
||||
// that have nothing to do with the Skynet kernel protocol. If there is an
|
||||
// error, we generally assume that the message has an unrelated purpose and
|
||||
// doesn't need to be logged.
|
||||
|
||||
// Authenticate the message as a message from the kernel.
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
// Check that a nonce and method were both provided.
|
||||
if (!("nonce" in event.data) || !("method" in event.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Switch on the method.
|
||||
if (event.data.method === "kernelBridgeVersion") {
|
||||
handleVersion(event.data);
|
||||
return;
|
||||
}
|
||||
if (event.data.method === "newKernelQuery") {
|
||||
handleKernelQuery(event.data);
|
||||
return;
|
||||
}
|
||||
if (event.data.method === "queryUpdate") {
|
||||
handleQueryUpdate(event.data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything else just gets ignored.
|
||||
}
|
||||
window.addEventListener("message", handleMessage);
|
|
@ -0,0 +1,16 @@
|
|||
import { WebRequest, Proxy } from "webextension-polyfill";
|
||||
import OnHeadersReceivedDetailsType = WebRequest.OnHeadersReceivedDetailsType;
|
||||
import OnBeforeRequestDetailsType = WebRequest.OnBeforeRequestDetailsType;
|
||||
import OnCompletedDetailsType = WebRequest.OnCompletedDetailsType;
|
||||
import OnErrorOccurredDetailsType = WebRequest.OnErrorOccurredDetailsType;
|
||||
import BlockingResponse = WebRequest.BlockingResponse;
|
||||
import OnRequestDetailsType = Proxy.OnRequestDetailsType;
|
||||
|
||||
export {
|
||||
OnHeadersReceivedDetailsType,
|
||||
OnBeforeRequestDetailsType,
|
||||
OnCompletedDetailsType,
|
||||
OnErrorOccurredDetailsType,
|
||||
BlockingResponse,
|
||||
OnRequestDetailsType,
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
export function isIp(ip: string) {
|
||||
return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
|
||||
ip
|
||||
);
|
||||
}
|
||||
|
||||
export function isDomain(domain: string) {
|
||||
return /(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]/.test(
|
||||
domain
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeDomain(domain: string): string {
|
||||
return domain.replace(/^\.+|\.+$/g, "").replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
export const requestProxies = [
|
||||
{ type: "http", host: "localhost", port: 25252 },
|
||||
{ type: "http", host: "web3portal.com", port: 80 },
|
||||
{ type: "http", host: "siasky.net", port: 80 },
|
||||
{ type: "http", host: "skynetfree.net", port: 80 },
|
||||
{ type: "http", host: "skynetpro.net", port: 80 },
|
||||
];
|
|
@ -0,0 +1,128 @@
|
|||
import browser from "@lumeweb/webextension-polyfill";
|
||||
|
||||
import BaseProvider from "./contentProviders/baseProvider.js";
|
||||
import {
|
||||
BlockingResponse,
|
||||
OnBeforeRequestDetailsType,
|
||||
OnCompletedDetailsType,
|
||||
OnErrorOccurredDetailsType,
|
||||
OnHeadersReceivedDetailsType,
|
||||
OnRequestDetailsType,
|
||||
} from "./types";
|
||||
|
||||
export default class WebEngine {
|
||||
private contentProviders: BaseProvider[] = [];
|
||||
private requests: Map<string, OnBeforeRequestDetailsType> = new Map();
|
||||
|
||||
constructor() {
|
||||
browser.webRequest.onHeadersReceived.addListener(
|
||||
this.headerHandler.bind(this),
|
||||
{ urls: ["<all_urls>"] },
|
||||
["blocking", "responseHeaders"]
|
||||
);
|
||||
browser.proxy.onRequest.addListener(this.proxyHandler.bind(this), {
|
||||
urls: ["<all_urls>"],
|
||||
});
|
||||
browser.webRequest.onBeforeRequest.addListener(
|
||||
this.requestHandler.bind(this),
|
||||
{ urls: ["<all_urls>"] },
|
||||
["blocking"]
|
||||
);
|
||||
browser.webRequest.onCompleted.addListener(
|
||||
this.onCompletedHandler.bind(this),
|
||||
{
|
||||
urls: ["<all_urls>"],
|
||||
}
|
||||
);
|
||||
browser.webRequest.onErrorOccurred.addListener(
|
||||
this.onErrorHandler.bind(this),
|
||||
{
|
||||
urls: ["<all_urls>"],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async headerHandler(
|
||||
details: OnHeadersReceivedDetailsType
|
||||
): Promise<BlockingResponse> {
|
||||
const def = { responseHeaders: details.responseHeaders };
|
||||
if (!this.requests.has(details.requestId)) {
|
||||
return def;
|
||||
}
|
||||
|
||||
for (const provider of this.contentProviders) {
|
||||
const response = await provider.handleHeaders(details);
|
||||
if (response !== false) {
|
||||
return response as BlockingResponse;
|
||||
}
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
private async proxyHandler(details: OnRequestDetailsType): Promise<any> {
|
||||
let handle = false;
|
||||
for (const provider of this.contentProviders) {
|
||||
if (await provider.shouldHandleRequest(details)) {
|
||||
handle = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!handle) {
|
||||
return {};
|
||||
}
|
||||
|
||||
this.requests.set(details.requestId, details);
|
||||
|
||||
for (const provider of this.contentProviders) {
|
||||
const response = await provider.handleProxy(details);
|
||||
if (response !== false) {
|
||||
return response as any;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private async requestHandler(
|
||||
details: OnBeforeRequestDetailsType
|
||||
): Promise<BlockingResponse> {
|
||||
if (!this.requests.has(details.requestId)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const provider of this.contentProviders) {
|
||||
const response = await provider.handleRequest(details);
|
||||
if (response !== false) {
|
||||
return response as BlockingResponse;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private async onCompletedHandler(
|
||||
details: OnCompletedDetailsType
|
||||
): Promise<void> {
|
||||
if (this.requests.has(details.requestId)) {
|
||||
this.requests.delete(details.requestId);
|
||||
}
|
||||
}
|
||||
|
||||
private async onErrorHandler(
|
||||
details: OnErrorOccurredDetailsType
|
||||
): Promise<void> {
|
||||
if (this.requests.has(details.requestId)) {
|
||||
this.requests.delete(details.requestId);
|
||||
}
|
||||
}
|
||||
|
||||
public registerContentProvider(provider: BaseProvider) {
|
||||
if (this.contentProviders.includes(provider)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.contentProviders.push(provider);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"declaration": true,
|
||||
"moduleResolution": "node",
|
||||
"outDir": "./build",
|
||||
"strict": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/__tests__/*"]
|
||||
}
|
Reference in New Issue