*Skeleton starting point for new extension

This commit is contained in:
Derrick Hammer 2022-07-29 06:37:20 -04:00
commit 250b09e5be
20 changed files with 4616 additions and 0 deletions

21
.eslintrc.json Normal file
View File

@ -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"
}
}

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
package-lock.json

21
LICENSE Normal file
View File

@ -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.

5
README.md Normal file
View File

@ -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)

1742
assets/auth.html Normal file

File diff suppressed because it is too large Load Diff

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/icon@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

30
assets/manifest.json Normal file
View File

@ -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"]
}

36
build.js Normal file
View File

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

36
package.json Normal file
View File

@ -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"
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

164
src/main/background.ts Normal file
View File

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

477
src/main/bootloader.ts Normal file
View File

@ -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();
}

129
src/main/bridge.ts Normal file
View File

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

16
src/types.ts Normal file
View File

@ -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,
};

23
src/util.ts Normal file
View File

@ -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 },
];

128
src/webEngine.ts Normal file
View File

@ -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);
}
}

13
tsconfig.json Normal file
View File

@ -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__/*"]
}

1596
yarn.lock Normal file

File diff suppressed because it is too large Load Diff