add electron build!

- use webpack to build electron-main and electron-preload
- intercept 'https' protocol to run on 'https://replayweb.page' in app
- serve local files from 'https://files.replayweb.page'
- support flash in with service worker replay: catch any fallthrough urls and load via intercept with render process callback
- add icons for electron build
This commit is contained in:
Ilya Kreymer
2020-05-19 22:49:08 -07:00
parent abd5060d20
commit 1a2f2c00a2
13 changed files with 1481 additions and 47 deletions
+294
View File
@@ -0,0 +1,294 @@
"use strict";
const {app, session, BrowserWindow, protocol, ipcMain} = require('electron');
const path = require('path');
const fs = require('fs');
const qs = require('querystring');
const { ArchiveResponse, Rewriter } = require('wabac/src/rewrite');
const { Headers } = require('node-fetch');
global.Headers = Headers;
const { PassThrough } = require('stream');
const STATIC_PREFIX = `https://replayweb.page/`;
const REPLAY_PREFIX = STATIC_PREFIX + "wabac/";
const STATIC_PATHS = {
'index.html': 'text/html',
'dist/frontend.js': 'application/javascript',
'dist/sw.js': 'application/javascript',
'sw.js': 'application/javascript'
};
const URL_RX = /([^\/]+)\/([\d]+)(?:\w\w_)?\/(.*)$/;
const appPath = app.getAppPath();
const projPath = path.join(appPath, "../");
const pluginPath = path.join(projPath, "plugins", "PepperFlashPlayer.plugin");
console.log('app path', appPath);
console.log('proj path', projPath);
console.log('plugin path', pluginPath);
console.log('app data', app.getPath('appData'));
app.setPath('userData', path.join(app.getPath('appData'), 'replaywebpage'));
console.log('user data', app.getPath('userData'));
app.commandLine.appendSwitch('ppapi-flash-path', pluginPath);
var proxyColl;
var proxyTS;
var mainContents;
function bufferToStream(data) {
const rv = new PassThrough();
rv.push(data);
rv.push(null);
return rv;
}
async function doIntercept(request, callback) {
console.log("request", request.url);
if (request.url.startsWith(STATIC_PREFIX)) {
let filename = request.url.slice(STATIC_PREFIX.length).split("?", 1)[0];
if (filename === "") {
filename = "index.html";
}
const type = STATIC_PATHS[filename];
if (type) {
const fullPath = path.join(__dirname, "../", filename);
console.log("fullPath: " + fullPath);
const data = fs.createReadStream(fullPath);
callback({statusCode: 200, headers: {"content-type": type}, data});
return;
} else if (request.url.startsWith(REPLAY_PREFIX)) {
const m = request.url.slice(REPLAY_PREFIX.length).match(URL_RX);
if (m) {
proxyColl = m[1];
proxyTS = m[2];
//const data = bufferToStream(`<html><head><script>window.location.href = "${m[3]}";</script></head></html>`);
//const statusCode = 200;
//const headers = {"location": m[3], "cache-control": "no-cache"};
//callback({statusCode, headers, data});
request.url = m[3];
await resolveResponse(request, callback);
return;
}
}
return notFound(request.url, callback);
}
if (request.url.startsWith(__APP_FILE_SERVE_PREFIX__)) {
const filename = qs.unescape(request.url.slice(__APP_FILE_SERVE_PREFIX__.length));
console.log("file serve:", filename);
const stat = await fs.promises.lstat(filename);
if (!stat.isFile()) {
return notFound(filename, callback);
}
const size = stat.size;
const headers = {"Content-Length": "" + size, "Content-Type": "application/octet-stream"};
const reqHeaders = new Headers(request.headers);
const {statusCode, start, end} = parseRange(reqHeaders, headers, size);
const data = fs.createReadStream(filename, {start, end});
callback({statusCode, headers, data});
}
await resolveResponse(request, callback);
}
function notFound(url, callback) {
console.log("not found: " + url);
const data = bufferToStream(`Sorry, the url <b>${url}</b> could not be found in this archive.`);
callback({statusCode: 404, headers: {"Content-Type": 'text/html; charset="utf-8"'}, data});
}
async function resolveResponse(request, callback) {
//mainContents.once("ipc-message", async (event, channel, status, headers, payload) => {
ipcMain.once("req:" + request.url, async (event, status, headers, payload) => {
const url = request.url;
if (status === 404 && !payload) {
return notFound(url, callback);
} else {
console.log("got response for: " + url);
}
headers = new Headers(headers);
const date = new Date();
let response = new ArchiveResponse({payload, headers, status, date, url});
const rewriter = new Rewriter({
baseUrl: url,
prefix: "",
urlRewrite: false,
contentRewrite: true,
decode: true,
useBaseRules: true
});
//const timestamp = proxyTS;
request.headers = new Headers(request.headers);
try {
response = await rewriter.rewrite(response, request);
headers = Object.fromEntries(response.headers.entries());
let data = await response.getBuffer();
if (status === 206 || status === 200) {
const {statusCode, start, end} = parseRange(request.headers, headers, data.length);
if (start !== undefined) {
data = data.slice(start, end);
}
status = statusCode;
}
data = bufferToStream(data);
callback({statusCode: status, headers, data});
} catch (e) {
console.warn(e);
}
});
mainContents.send("getresponse", request, proxyColl, proxyTS);
}
function parseRange(reqHeaders, headers, size) {
let statusCode = 200;
const range = reqHeaders.get("range");
if (!range) {
return {statusCode};
}
const m = range.match(/bytes=([\d]+)-([\d]*)/);
if (!m) {
return {statusCode};
}
const start = Number(m[1]);
const end = m[2] ? Number(m[2]) : size - 1;
statusCode = 206;
if (headers) {
headers["Content-Range"] = `bytes ${start}-${end}/${size}`;
}
return {statusCode, start, end};
}
function createWindow () {
//createServer();
const sesh = session.defaultSession;
protocol.registerStreamProtocol("filex", async (request, callback) => {
const filename = qs.unescape(request.url.slice("filex://-".length));
console.log("filex", filename);
if (filename.endsWith("?redir")) {
const newLoc = request.url.replace("?redir", "");
console.log("redir to: " + newLoc);
callback({statusCode: 304, headers: {"Location": newLoc}, data: bufferToStream(NOT_FOUND)});
return;
}
});
sesh.protocol.interceptStreamProtocol("http", doIntercept);
sesh.protocol.interceptStreamProtocol("https", doIntercept);
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1024,
height: 768,
isMaximized: true,
show: false,
webPreferences: {
plugins: true,
preload: path.join(__dirname, 'preload.js'),
}
}).once('ready-to-show', () => {
//const sesh = session.fromPartition("persist:replay");
//const filter = {"urls": ['*://*/*']};
//sesh.webRequest.onBeforeRequest(filter, (request, callback) => {
// console.log("got onBeforeRequest");
// callback({cancel: false});
//});
//sesh.protocol.interceptStreamProtocol("http", doIntercept);
//sesh.protocol.interceptStreamProtocol("https", doIntercept);
mainWindow.show();
mainWindow.maximize();
});
mainWindow.loadURL(STATIC_PREFIX + "index.html");
mainWindow.webContents.openDevTools();
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
createWindow();
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
//if (process.platform !== 'darwin')
app.quit();
});
app.on('web-contents-created', (event, contents) => {
if (contents.getType() === "webview") {
contents.openDevTools();
} else if (contents.getType() === "window") {
mainContents = contents;
}
});
+46
View File
@@ -0,0 +1,46 @@
"use strict";
import { CollectionLoader } from 'wabac/src/loaders';
const { ipcRenderer } = require('electron');
const dbs = {};
const loader = new CollectionLoader();
async function getDB(name) {
if (!dbs[name]) {
dbs[name] = await loader.loadColl(name);
console.log(dbs[name]);
}
return dbs[name].store;
}
async function getResponse(event, request, coll, ts) {
const db = await getDB(coll);
await db.initing;
const req = {request, url: request.url, timestamp: ts};
const result = await db.getResource(req, "", {request});
const channel = "req:" + request.url;
if (!result) {
ipcRenderer.send(channel, 404);
return;
}
const headers = Object.fromEntries(result.headers.entries());
const buffer = await result.getBuffer();
ipcRenderer.send(channel, result.status, headers, buffer);
}
ipcRenderer.on('getresponse', getResponse);
+6 -6
View File
@@ -268,7 +268,7 @@ class AppMain extends LitElement
onStartLoad(event) {
// just redirect right away?
if (!event.detail.blobUrl) {
if (!event.detail.isFile) {
this.pageParams.set("source", event.detail.sourceUrl);
window.location.search = this.pageParams.toString();
return;
@@ -388,7 +388,7 @@ You can select a file to upload from the main page by clicking the \'Choose File
source = {
sourceUrl: this.loadInfo.sourceUrl,
loadUrl: this.loadInfo.blobUrl,
loadUrl: this.loadInfo.loadUrl,
name: this.loadInfo.name
}
break;
@@ -537,7 +537,7 @@ class WrIndex extends LitElement
}
async loadColls() {
const resp = await fetch("/wabac/api/index");
const resp = await fetch("./wabac/api/index");
try {
const json = await resp.json();
this.colls = json.colls;
@@ -574,7 +574,7 @@ class WrIndex extends LitElement
}
this.file = event.currentTarget.files[0];
this.fileDisplayName = "file://" + this.file.name;
this.fileDisplayName = "file://" + (this.file.path || this.file.name);
this.requestUpdate();
}
@@ -584,7 +584,8 @@ class WrIndex extends LitElement
const detail = {sourceUrl: this.fileDisplayName};
if (this.file) {
detail.blobUrl = URL.createObjectURL(this.file);
detail.isFile = true;
detail.loadUrl = this.file.path ? __APP_FILE_SERVE_PREFIX__ + this.file.path : URL.createObjectURL(this.file);
detail.name = this.fileDisplayName;
}
@@ -2082,7 +2083,6 @@ function registerSW(url) {
}
async function main() {
//const swPromise = initSW(__SW_PATH__ + "?replayPrefix=wabac&stats=true");
const swPromise = registerSW(__SW_PATH__);
await swPromise;
customElements.define("app-main", AppMain);