electron app refactor: wrap electron config in extendable ElectronReplayApp
This commit is contained in:
@@ -72,6 +72,7 @@
|
|||||||
"dist/*.js",
|
"dist/*.js",
|
||||||
"*.js",
|
"*.js",
|
||||||
"*.html",
|
"*.html",
|
||||||
|
"webmanifest.json",
|
||||||
{
|
{
|
||||||
"from": "_site/assets",
|
"from": "_site/assets",
|
||||||
"to": "assets"
|
"to": "assets"
|
||||||
|
|||||||
+7
-380
@@ -1,384 +1,11 @@
|
|||||||
"use strict";
|
import { ElectronReplayApp } from './electron-replay-app';
|
||||||
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import { Headers } from 'node-fetch';
|
|
||||||
|
|
||||||
import {app, session, BrowserWindow, ipcMain, shell } from 'electron';
|
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
import { ArchiveResponse, Rewriter } from '@webrecorder/wabac/src/rewrite';
|
|
||||||
|
|
||||||
import { PassThrough } from 'stream';
|
// ============================================================================
|
||||||
|
const replayApp = new ElectronReplayApp({
|
||||||
|
staticPath: path.join(__dirname, "../"),
|
||||||
|
profileName: "replaywebpage"
|
||||||
|
});
|
||||||
|
|
||||||
import mime from 'mime-types';
|
replayApp.init();
|
||||||
import { url } from 'inspector';
|
|
||||||
|
|
||||||
global.Headers = Headers;
|
|
||||||
|
|
||||||
const STATIC_PREFIX = "http://localhost:5471/";
|
|
||||||
|
|
||||||
const REPLAY_PREFIX = STATIC_PREFIX + "wabac/";
|
|
||||||
|
|
||||||
const URL_RX = /([^\/]+)\/([\d]+)(?:\w\w_)?\/(.*)$/;
|
|
||||||
|
|
||||||
const appPath = app.getAppPath();
|
|
||||||
|
|
||||||
const projPath = path.join(appPath, "../");
|
|
||||||
|
|
||||||
const staticContentPath = path.join(__dirname, "../");
|
|
||||||
|
|
||||||
let pluginPath = "";
|
|
||||||
|
|
||||||
var proxyColl;
|
|
||||||
var proxyTS;
|
|
||||||
|
|
||||||
var mainWindow;
|
|
||||||
|
|
||||||
let openNextFile = null;
|
|
||||||
|
|
||||||
|
|
||||||
function main() {
|
|
||||||
switch (process.platform) {
|
|
||||||
case 'win32':
|
|
||||||
pluginPath = path.join(projPath, "plugins-win", `pepflashplayer-x86${process.arch === 'x64' ? '_64' : ''}.dll`);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'darwin':
|
|
||||||
pluginPath = path.join(projPath, "plugins-mac", "PepperFlashPlayer.plugin");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'linux':
|
|
||||||
pluginPath = path.join(projPath, "plugins-mac", "libpepflashplayer.so");
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log('platform unsupported: ' + process.platform);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
console.log('app path', appPath);
|
|
||||||
console.log('dir name', __dirname);
|
|
||||||
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);
|
|
||||||
|
|
||||||
app.on('will-finish-launching', function() {
|
|
||||||
app.on('open-file', function(event, filePath) {
|
|
||||||
openNextFile = filePath;
|
|
||||||
if (mainWindow) {
|
|
||||||
createWindow(process.argv)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Single instance lock
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
|
||||||
|
|
||||||
if (!gotTheLock) {
|
|
||||||
console.log("App already running, opening new window in first instance and quitting");
|
|
||||||
app.quit();
|
|
||||||
} else {
|
|
||||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
|
||||||
// Just create a new window in case of second instance request
|
|
||||||
createWindow(commandLine);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on('web-contents-created', (event, contents) => {
|
|
||||||
contents.on('new-window', async (event, navigationUrl) => {
|
|
||||||
|
|
||||||
// load docs in native browser for now
|
|
||||||
if (navigationUrl === STATIC_PREFIX + "docs") {
|
|
||||||
event.preventDefault();
|
|
||||||
await shell.openExternal("https://replayweb.page/docs/");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
mainWindow = createWindow(process.argv);
|
|
||||||
|
|
||||||
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) {
|
|
||||||
mainWindow = 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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function bufferToStream(data) {
|
|
||||||
const rv = new PassThrough();
|
|
||||||
rv.push(data);
|
|
||||||
rv.push(null);
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doIntercept(request, callback) {
|
|
||||||
console.log(`${request.method} ${request.url} from ${request.referrer}`);
|
|
||||||
|
|
||||||
// if local server
|
|
||||||
if (request.url.startsWith(STATIC_PREFIX)) {
|
|
||||||
//if replay prefix
|
|
||||||
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];
|
|
||||||
|
|
||||||
request.url = m[3];
|
|
||||||
return await resolveArchiveResponse(request, callback);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// try serve static file from app dir
|
|
||||||
let filename = request.url.slice(STATIC_PREFIX.length).split("?", 1)[0];
|
|
||||||
|
|
||||||
if (filename === "") {
|
|
||||||
filename = "index.html";
|
|
||||||
} else if (filename === "docs") {
|
|
||||||
filename = "docs/index.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
let ext = path.extname(filename);
|
|
||||||
if (!ext) {
|
|
||||||
ext = ".html";
|
|
||||||
filename += ext;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mimeType = mime.contentType(ext);
|
|
||||||
|
|
||||||
if (mimeType) {
|
|
||||||
const fullPath = path.join(staticContentPath, filename);
|
|
||||||
|
|
||||||
console.log("fullPath: " + fullPath);
|
|
||||||
|
|
||||||
const data = fs.createReadStream(fullPath);
|
|
||||||
|
|
||||||
return callback({statusCode: 200, headers: {"content-type": mimeType}, data});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return notFound(request.url, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.url.startsWith(__APP_FILE_SERVE_PREFIX__)) {
|
|
||||||
const parsedUrl = new URL(request.url);
|
|
||||||
const filename = parsedUrl.searchParams.get("filename");
|
|
||||||
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 = request.method === "HEAD" ? null : fs.createReadStream(filename, {start, end});
|
|
||||||
|
|
||||||
callback({statusCode, headers, data});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// possible 'live leak' attempt, just return not found
|
|
||||||
if (request.referrer && request.referrer.startsWith(REPLAY_PREFIX)) {
|
|
||||||
return notFound(request.url, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
await proxyLive(request, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function proxyLive(request, callback) {
|
|
||||||
let headers = request.headers;
|
|
||||||
|
|
||||||
const method = request.method;
|
|
||||||
const response = await fetch(request.url, {method, headers});
|
|
||||||
const data = method === "HEAD" ? null : response.body;
|
|
||||||
const statusCode = response.status;
|
|
||||||
|
|
||||||
headers = Object.fromEntries(response.headers.entries());
|
|
||||||
callback({statusCode, headers, data});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 resolveArchiveResponse(request, callback) {
|
|
||||||
const channel = `req:${new Date().getTime()}:${request.url}`;
|
|
||||||
|
|
||||||
ipcMain.once(channel, 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
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.webContents.send("getresponse", request, proxyColl, proxyTS, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
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(argv, secondWindow) {
|
|
||||||
const sesh = session.defaultSession;
|
|
||||||
|
|
||||||
if (!secondWindow) {
|
|
||||||
sesh.protocol.interceptStreamProtocol("http", doIntercept);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceString = getOpenUrl(argv);
|
|
||||||
|
|
||||||
// Create the browser window.
|
|
||||||
const theWindow = new BrowserWindow({
|
|
||||||
width: 1024,
|
|
||||||
height: 768,
|
|
||||||
isMaximized: true,
|
|
||||||
show: false,
|
|
||||||
webPreferences: {
|
|
||||||
plugins: true,
|
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
|
||||||
nativeWindowOpen: true,
|
|
||||||
contextIsolation: true,
|
|
||||||
enableRemoteModule: false
|
|
||||||
}
|
|
||||||
}).once('ready-to-show', () => {
|
|
||||||
theWindow.show();
|
|
||||||
theWindow.maximize();
|
|
||||||
});
|
|
||||||
|
|
||||||
theWindow.loadURL(STATIC_PREFIX + "index.html" + sourceString);
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
theWindow.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
|
|
||||||
return theWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOpenUrl(argv) {
|
|
||||||
argv = require('minimist')(argv.slice(process.defaultApp ? 2 : 1));
|
|
||||||
|
|
||||||
const filename = openNextFile || argv.filename || argv.f || (argv._.length && argv._[0]);
|
|
||||||
|
|
||||||
let sourceString = "";
|
|
||||||
|
|
||||||
if (filename) {
|
|
||||||
const sourceParams = new URLSearchParams();
|
|
||||||
sourceParams.set("source", "file://" + filename);
|
|
||||||
sourceString = "?" + sourceParams.toString();
|
|
||||||
|
|
||||||
const urlParams = new URLSearchParams();
|
|
||||||
|
|
||||||
const openUrl = argv.url;
|
|
||||||
|
|
||||||
const openTS = argv.ts || argv.timestamp;
|
|
||||||
|
|
||||||
if (openUrl) {
|
|
||||||
urlParams.set("url", openUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (openTS) {
|
|
||||||
urlParams.set("ts", openTS);
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceString += "#" + urlParams.toString();
|
|
||||||
|
|
||||||
console.log(`Opening Source: ${sourceString}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sourceString;
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
|
|||||||
@@ -0,0 +1,399 @@
|
|||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { Headers } from 'node-fetch';
|
||||||
|
|
||||||
|
import {app, session, BrowserWindow, ipcMain, screen, shell } from 'electron';
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import { ArchiveResponse, Rewriter } from '@webrecorder/wabac/src/rewrite';
|
||||||
|
|
||||||
|
import { PassThrough } from 'stream';
|
||||||
|
|
||||||
|
import mime from 'mime-types';
|
||||||
|
|
||||||
|
global.Headers = Headers;
|
||||||
|
global.fetch = fetch;
|
||||||
|
|
||||||
|
const STATIC_PREFIX = "http://localhost:5471/";
|
||||||
|
|
||||||
|
const REPLAY_PREFIX = STATIC_PREFIX + "wabac/";
|
||||||
|
|
||||||
|
const URL_RX = /([^\/]+)\/([\d]+)(?:\w\w_)?\/(.*)$/;
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
class ElectronReplayApp
|
||||||
|
{
|
||||||
|
constructor({staticPath="./", profileName=""} = {}) {
|
||||||
|
this.pluginPath = "";
|
||||||
|
|
||||||
|
this.appPath = app.getAppPath();
|
||||||
|
|
||||||
|
this.projPath = path.join(this.appPath, "../");
|
||||||
|
|
||||||
|
this.staticContentPath = staticPath;
|
||||||
|
|
||||||
|
this.profileName = profileName;
|
||||||
|
|
||||||
|
this.proxyColl = null;
|
||||||
|
this.proxyTS = null;
|
||||||
|
|
||||||
|
this.mainWindow = null;
|
||||||
|
|
||||||
|
this.openNextFile = null;
|
||||||
|
|
||||||
|
this.screenSize = {width: 1024, height: 768};
|
||||||
|
}
|
||||||
|
|
||||||
|
get mainWindowWebPreferences() {
|
||||||
|
return {
|
||||||
|
plugins: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
|
nativeWindowOpen: true,
|
||||||
|
contextIsolation: true,
|
||||||
|
enableRemoteModule: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Single instance check
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
|
console.log("App already running, opening new window in first instance and quitting");
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||||
|
// Just create a new window in case of second instance request
|
||||||
|
this.createMainWindow(commandLine);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'win32':
|
||||||
|
this.pluginPath = path.join(this.projPath, "plugins-win", `pepflashplayer-x86${process.arch === 'x64' ? '_64' : ''}.dll`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'darwin':
|
||||||
|
this.pluginPath = path.join(this.projPath, "plugins-mac", "PepperFlashPlayer.plugin");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'linux':
|
||||||
|
this.pluginPath = path.join(this.projPath, "plugins-mac", "libpepflashplayer.so");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('platform unsupported: ' + process.platform);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.commandLine.appendSwitch('ppapi-flash-path', this.pluginPath);
|
||||||
|
|
||||||
|
console.log('app path', this.appPath);
|
||||||
|
console.log('dir name', this.__dirname);
|
||||||
|
console.log('proj path', this.projPath);
|
||||||
|
console.log('plugin path', this.pluginPath);
|
||||||
|
|
||||||
|
console.log('app data', app.getPath('appData'));
|
||||||
|
console.log('user data', app.getPath('userData'));
|
||||||
|
|
||||||
|
if (this.profileName) {
|
||||||
|
app.setPath('userData', path.join(app.getPath('appData'), this.profileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on('will-finish-launching', () => {
|
||||||
|
app.on('open-file', (event, filePath) => {
|
||||||
|
this.openNextFile = filePath;
|
||||||
|
if (this.mainWindow) {
|
||||||
|
this.createMainWindow(process.argv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
this.mainWindow = this.createMainWindow(process.argv);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.whenReady().then(() => this.onAppReady());
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onAppReady() {
|
||||||
|
this.screenSize = screen.getPrimaryDisplay().workAreaSize;
|
||||||
|
|
||||||
|
app.on('web-contents-created', (event, contents) => {
|
||||||
|
contents.on('new-window', async (event, navigationUrl) => {
|
||||||
|
|
||||||
|
// load docs in native browser for now
|
||||||
|
if (navigationUrl === STATIC_PREFIX + "docs") {
|
||||||
|
event.preventDefault();
|
||||||
|
await shell.openExternal("https://replayweb.page/docs/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const sesh = session.defaultSession;
|
||||||
|
|
||||||
|
sesh.protocol.interceptStreamProtocol("http", (request, callback) => this.doIntercept(request, callback));
|
||||||
|
|
||||||
|
this.mainWindow = this.createMainWindow(process.argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
_bufferToStream(data) {
|
||||||
|
const rv = new PassThrough();
|
||||||
|
rv.push(data);
|
||||||
|
rv.push(null);
|
||||||
|
return rv;
|
||||||
|
}
|
||||||
|
|
||||||
|
async doIntercept(request, callback) {
|
||||||
|
console.log(`${request.method} ${request.url} from ${request.referrer}`);
|
||||||
|
|
||||||
|
// if local server
|
||||||
|
if (request.url.startsWith(STATIC_PREFIX)) {
|
||||||
|
//if replay prefix
|
||||||
|
if (request.url.startsWith(REPLAY_PREFIX)) {
|
||||||
|
const m = request.url.slice(REPLAY_PREFIX.length).match(URL_RX);
|
||||||
|
if (m) {
|
||||||
|
this.proxyColl = m[1];
|
||||||
|
this.proxyTS = m[2];
|
||||||
|
|
||||||
|
request.url = m[3];
|
||||||
|
return await this.resolveArchiveResponse(request, callback);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// try serve static file from app dir
|
||||||
|
let filename = request.url.slice(STATIC_PREFIX.length).split("?", 1)[0];
|
||||||
|
filename = filename.split("#", 1)[0];
|
||||||
|
|
||||||
|
if (filename === "") {
|
||||||
|
filename = "index.html";
|
||||||
|
} else if (filename === "docs") {
|
||||||
|
filename = "docs/index.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
let ext = path.extname(filename);
|
||||||
|
if (!ext) {
|
||||||
|
ext = ".html";
|
||||||
|
filename += ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mimeType = mime.contentType(ext);
|
||||||
|
|
||||||
|
if (mimeType) {
|
||||||
|
const fullPath = path.join(this.staticContentPath, filename);
|
||||||
|
|
||||||
|
console.log("fullPath: " + fullPath);
|
||||||
|
|
||||||
|
const data = fs.createReadStream(fullPath);
|
||||||
|
|
||||||
|
return callback({statusCode: 200, headers: {"content-type": mimeType}, data});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.notFound(request.url, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.url.startsWith(__APP_FILE_SERVE_PREFIX__)) {
|
||||||
|
const parsedUrl = new URL(request.url);
|
||||||
|
const filename = parsedUrl.searchParams.get("filename");
|
||||||
|
console.log("file serve:", filename);
|
||||||
|
|
||||||
|
const stat = await fs.promises.lstat(filename);
|
||||||
|
|
||||||
|
if (!stat.isFile()) {
|
||||||
|
return this.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} = this.parseRange(reqHeaders, headers, size);
|
||||||
|
|
||||||
|
const data = request.method === "HEAD" ? null : fs.createReadStream(filename, {start, end});
|
||||||
|
|
||||||
|
callback({statusCode, headers, data});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// possible 'live leak' attempt, just return not found
|
||||||
|
if (request.referrer && request.referrer.startsWith(REPLAY_PREFIX)) {
|
||||||
|
return this.notFound(request.url, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.proxyLive(request, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
async proxyLive(request, callback) {
|
||||||
|
let headers = request.headers;
|
||||||
|
|
||||||
|
const method = request.method;
|
||||||
|
const response = await fetch(request.url, {method, headers});
|
||||||
|
const data = method === "HEAD" ? null : response.body;
|
||||||
|
const statusCode = response.status;
|
||||||
|
|
||||||
|
headers = Object.fromEntries(response.headers.entries());
|
||||||
|
callback({statusCode, headers, data});
|
||||||
|
}
|
||||||
|
|
||||||
|
notFound(url, callback) {
|
||||||
|
console.log("not found: " + url);
|
||||||
|
const data = this._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 resolveArchiveResponse(request, callback) {
|
||||||
|
const channel = `req:${new Date().getTime()}:${request.url}`;
|
||||||
|
|
||||||
|
ipcMain.once(channel, 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
|
||||||
|
});
|
||||||
|
|
||||||
|
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} = this.parseRange(request.headers, headers, data.length);
|
||||||
|
if (start !== undefined) {
|
||||||
|
data = data.slice(start, end);
|
||||||
|
}
|
||||||
|
status = statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = this._bufferToStream(data);
|
||||||
|
|
||||||
|
callback({statusCode: status, headers, data});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.mainWindow) {
|
||||||
|
this.mainWindow.webContents.send("getresponse", request, this.proxyColl, this.proxyTS, channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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};
|
||||||
|
}
|
||||||
|
|
||||||
|
createMainWindow(argv, startUrl = "index.html") {
|
||||||
|
const sourceString = this.getOpenUrl(argv);
|
||||||
|
|
||||||
|
// Create the browser window.
|
||||||
|
const theWindow = new BrowserWindow({
|
||||||
|
width: this.screenSize.width,
|
||||||
|
height: this.screenSize.height,
|
||||||
|
isMaximized: true,
|
||||||
|
show: false,
|
||||||
|
webPreferences: this.mainWindowWebPreferences,
|
||||||
|
}).once('ready-to-show', () => {
|
||||||
|
theWindow.show();
|
||||||
|
theWindow.maximize();
|
||||||
|
});
|
||||||
|
|
||||||
|
theWindow.loadURL(STATIC_PREFIX + startUrl + sourceString);
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
theWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
return theWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOpenUrl(argv) {
|
||||||
|
argv = require('minimist')(argv.slice(process.defaultApp ? 2 : 1));
|
||||||
|
|
||||||
|
const filename = this.openNextFile || argv.filename || argv.f || (argv._.length && argv._[0]);
|
||||||
|
this.openNextFile = null;
|
||||||
|
|
||||||
|
let sourceString = "";
|
||||||
|
|
||||||
|
if (filename) {
|
||||||
|
const sourceParams = new URLSearchParams();
|
||||||
|
sourceParams.set("source", "file://" + filename);
|
||||||
|
sourceString = "?" + sourceParams.toString();
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
|
||||||
|
const openUrl = argv.url;
|
||||||
|
|
||||||
|
const openTS = argv.ts || argv.timestamp;
|
||||||
|
|
||||||
|
if (openUrl) {
|
||||||
|
urlParams.set("url", openUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openTS) {
|
||||||
|
urlParams.set("ts", openTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceString += "#" + urlParams.toString();
|
||||||
|
|
||||||
|
console.log(`Opening Source: ${sourceString}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ElectronReplayApp, STATIC_PREFIX };
|
||||||
Reference in New Issue
Block a user