r2-navigator-js
Version:
Readium 2 'navigator' for NodeJS (TypeScript)
560 lines • 20.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.secureSessions = secureSessions;
exports.initSessions = initSessions;
exports.clearSession = clearSession;
exports.getWebViewSession = getWebViewSession;
exports.clearWebviewSession = clearWebviewSession;
exports.clearDefaultSession = clearDefaultSession;
exports.clearSessions = clearSessions;
const tslib_1 = require("tslib");
const debug_ = require("debug");
const electron_1 = require("electron");
const request = require("request");
const transformer_1 = require("r2-shared-js/dist/es7-es2016/src/transform/transformer");
const transformer_html_1 = require("r2-shared-js/dist/es7-es2016/src/transform/transformer-html");
const dom_1 = require("../common/dom");
const sessions_1 = require("../common/sessions");
const url_params_1 = require("../renderer/common/url-params");
const debug = debug_("r2:navigator#electron/main/sessions");
const USE_STREAM_PROTOCOL_INSTEAD_OF_HTTP = true;
function promiseAllSettled(promises) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const promises_ = promises.map((promise) => tslib_1.__awaiter(this, void 0, void 0, function* () {
return promise
.then((value) => {
return {
status: "fulfilled",
value,
};
})
.catch((reason) => {
return {
reason,
status: "rejected",
};
});
}));
return Promise.all(promises_);
});
}
let _server;
function secureSessions(server) {
_server = server;
const filter = { urls: ["*://*/*"] };
const onHeadersReceivedCB = (details, callback) => {
if (!details.url) {
callback({});
return;
}
const serverUrl = server.serverUrl();
if ((serverUrl && details.url.startsWith(serverUrl)) ||
details.url.startsWith(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL + "://")) {
callback({
cancel: false,
responseHeaders: Object.assign(Object.assign({}, details.responseHeaders), { "Content-Security-Policy": [`default-src 'self' 'unsafe-inline' 'unsafe-eval' data: http: https: ${sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL}: ${serverUrl}`] }),
});
}
else {
callback({
cancel: false,
responseHeaders: Object.assign({}, details.responseHeaders),
});
}
};
const onBeforeSendHeadersCB = (details, callback) => {
if (!details.url) {
callback({});
return;
}
const serverUrl = server.serverUrl();
if (server.isSecured() &&
((serverUrl && details.url.startsWith(serverUrl)) ||
details.url.startsWith(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL + "://"))) {
const header = server.getSecureHTTPHeader(details.url);
if (header) {
details.requestHeaders[header.name] = header.value;
}
callback({
cancel: false,
requestHeaders: Object.assign({}, details.requestHeaders),
});
}
else {
callback({
cancel: false,
requestHeaders: Object.assign({}, details.requestHeaders),
});
}
};
const setCertificateVerifyProcCB = (req, callback) => {
if (server.isSecured()) {
const info = server.serverInfo();
if (info) {
if (req.hostname === info.urlHost) {
callback(0);
return;
}
}
}
callback(-3);
};
if (electron_1.session.defaultSession) {
electron_1.session.defaultSession.webRequest.onHeadersReceived(filter, onHeadersReceivedCB);
electron_1.session.defaultSession.webRequest.onBeforeSendHeaders(filter, onBeforeSendHeadersCB);
electron_1.session.defaultSession.setCertificateVerifyProc(setCertificateVerifyProcCB);
}
const webViewSession = getWebViewSession();
if (webViewSession) {
webViewSession.webRequest.onHeadersReceived(filter, onHeadersReceivedCB);
webViewSession.webRequest.onBeforeSendHeaders(filter, onBeforeSendHeadersCB);
webViewSession.setCertificateVerifyProc(setCertificateVerifyProcCB);
}
electron_1.app.on("certificate-error", (event, _webContents, url, _error, _certificate, callback) => {
if (server.isSecured()) {
const info = server.serverInfo();
if (info) {
if (url.indexOf(info.urlScheme + "://" + info.urlHost) === 0) {
event.preventDefault();
callback(true);
return;
}
}
}
callback(false);
});
}
let _customUrlProtocolSchemeHandlerWasCalled = false;
const streamProtocolHandler = (req, callback) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
_customUrlProtocolSchemeHandlerWasCalled = true;
const url = (0, sessions_1.convertCustomSchemeToHttpUrl)(req.url);
const u = new URL(url);
let ref = u.origin;
if (req.referrer && req.referrer.trim()) {
ref = req.referrer;
}
const failure = (err) => {
debug(err);
callback({});
};
const success = (response) => {
const headers = {};
Object.keys(response.headers).forEach((header) => {
const val = response.headers[header];
if (val) {
headers[header] = val;
}
});
if (!headers.referer) {
headers.referer = ref;
}
if (response.statusCode && (response.statusCode < 200 || response.statusCode >= 300)) {
failure("HTTP CODE " + response.statusCode);
return;
}
response
.on("error", function h() {
debug("RESPONSE ERROR " + url);
});
const obj = {
data: response,
headers,
statusCode: response.statusCode,
};
callback(obj);
};
const reqHeaders = req.headers;
if (_server) {
const serverUrl = _server.serverUrl();
if (_server.isSecured() &&
((serverUrl && url.startsWith(serverUrl)) ||
url.startsWith(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL + "://"))) {
const header = _server.getSecureHTTPHeader(url);
if (header) {
reqHeaders[header.name] = header.value;
}
}
}
request.get({
headers: reqHeaders,
method: "GET",
rejectUnauthorized: false,
uri: url,
})
.on("response", (response) => {
success(response);
})
.on("error", (err) => {
failure(err);
});
});
const httpProtocolHandler = (req, callback) => {
_customUrlProtocolSchemeHandlerWasCalled = true;
const url = (0, sessions_1.convertCustomSchemeToHttpUrl)(req.url);
callback({
method: req.method,
session: getWebViewSession(),
url,
});
};
const transformerAudioVideo = (_publication, link, url, htmlStr, _sessionInfo) => {
if (!_customUrlProtocolSchemeHandlerWasCalled) {
return htmlStr;
}
if (!url) {
return htmlStr;
}
if (htmlStr.indexOf("<audio") < 0 && htmlStr.indexOf("<video") < 0) {
return htmlStr;
}
const iHtmlStart = htmlStr.indexOf("<html");
if (iHtmlStart < 0) {
return htmlStr;
}
const iBodyStart = htmlStr.indexOf("<body");
if (iBodyStart < 0) {
return htmlStr;
}
const parseableChunk = htmlStr.substr(iHtmlStart);
const htmlStrToParse = `xml version="1.0" encoding="utf-8"${parseableChunk}`;
let mediaType = "application/xhtml+xml";
if (link && link.TypeLink) {
mediaType = link.TypeLink;
}
const documantFromXmlDom = (0, dom_1.parseDOM)(htmlStrToParse, mediaType);
let urlHttp = url;
if (urlHttp.startsWith(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL + "://")) {
urlHttp = (0, sessions_1.convertCustomSchemeToHttpUrl)(urlHttp);
}
const url_ = new URL(urlHttp);
url_.search = "";
url_.hash = "";
const urlStr = url_.toString();
const patchElementSrc = (el) => {
const src = el.getAttribute("src");
if (!src || src[0] === "/" ||
/^https?:\/\//.test(src) || /^data:\/\//.test(src)) {
return;
}
let src_ = src;
if (src_.startsWith("./")) {
src_ = src_.substr(2);
}
src_ = `${urlStr}/../${src_}`;
debug(`VIDEO/AUDIO SRC PATCH: ${src} ==> ${src_}`);
el.setAttribute("src", src_);
};
const processTree = (el) => {
let elName = el.nodeName.toLowerCase();
if (elName === "audio" || elName === "video") {
patchElementSrc(el);
if (!el.childNodes) {
return;
}
for (let i = 0; i < el.childNodes.length; i++) {
const childNode = el.childNodes[i];
if (childNode.nodeType === 1) {
elName = childNode.nodeName.toLowerCase();
if (elName === "source") {
patchElementSrc(childNode);
}
}
}
}
else {
if (!el.childNodes) {
return;
}
for (let i = 0; i < el.childNodes.length; i++) {
const childNode = el.childNodes[i];
if (childNode.nodeType === 1) {
processTree(childNode);
}
}
}
};
processTree(documantFromXmlDom.body);
const serialized = (0, dom_1.serializeDOM)(documantFromXmlDom);
const prefix = htmlStr.substr(0, iHtmlStart);
const iHtmlStart_ = serialized.indexOf("<html");
if (iHtmlStart_ < 0) {
return htmlStr;
}
const remaining = serialized.substr(iHtmlStart_);
const newStr = `${prefix}${remaining}`;
return newStr;
};
const transformerHttpBaseIframes = (_publication, link, url, htmlStr, _sessionInfo) => {
if (!_customUrlProtocolSchemeHandlerWasCalled) {
return htmlStr;
}
if (!url) {
return htmlStr;
}
if (htmlStr.indexOf("<iframe") < 0) {
return htmlStr;
}
const iHtmlStart = htmlStr.indexOf("<html");
if (iHtmlStart < 0) {
return htmlStr;
}
const iBodyStart = htmlStr.indexOf("<body");
if (iBodyStart < 0) {
return htmlStr;
}
const parseableChunk = htmlStr.substr(iHtmlStart);
const htmlStrToParse = `xml version="1.0" encoding="utf-8"${parseableChunk}`;
let mediaType = "application/xhtml+xml";
if (link && link.TypeLink) {
mediaType = link.TypeLink;
}
const documantFromXmlDom = (0, dom_1.parseDOM)(htmlStrToParse, mediaType);
let urlHttp = url;
if (!urlHttp.startsWith(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL + "://")) {
urlHttp = (0, sessions_1.convertHttpUrlToCustomScheme)(urlHttp);
}
const url_ = new URL(urlHttp);
const r2CSS = url_.searchParams.get(url_params_1.URL_PARAM_CSS);
const r2isMO = url_.searchParams.get(url_params_1.URL_PARAM_EPUBMEDIAOVERLAYS);
const r2ERS = url_.searchParams.get(url_params_1.URL_PARAM_EPUBREADINGSYSTEM);
const r2DEBUG = url_.searchParams.get(url_params_1.URL_PARAM_DEBUG_VISUALS);
const r2A11YSUPPORTENABLED = url_.searchParams.get(url_params_1.URL_PARAM_A11Y_SUPPORT_ENABLED);
const r2CLIPBOARDINTERCEPT = url_.searchParams.get(url_params_1.URL_PARAM_CLIPBOARD_INTERCEPT);
const r2SESSIONINFO = url_.searchParams.get(url_params_1.URL_PARAM_SESSION_INFO);
const r2WEBVIEWSLOT = url_.searchParams.get(url_params_1.URL_PARAM_WEBVIEW_SLOT);
const r2SECONDWEBVIEW = url_.searchParams.get(url_params_1.URL_PARAM_SECOND_WEBVIEW);
url_.search = "";
url_.hash = "";
const urlStr = url_.toString();
const patchElementSrc = (el) => {
const src = el.getAttribute("src");
if (!src || src[0] === "/" ||
/^https?:\/\//.test(src) || /^data:\/\//.test(src)) {
return;
}
let src_ = src;
if (src_.startsWith("./")) {
src_ = src_.substr(2);
}
src_ = `${urlStr}/../${src_}`;
const iframeUrl = new URL(src_);
if (r2A11YSUPPORTENABLED) {
iframeUrl.searchParams.append(url_params_1.URL_PARAM_A11Y_SUPPORT_ENABLED, r2A11YSUPPORTENABLED);
}
if (r2CLIPBOARDINTERCEPT) {
iframeUrl.searchParams.append(url_params_1.URL_PARAM_CLIPBOARD_INTERCEPT, r2CLIPBOARDINTERCEPT);
}
if (r2SESSIONINFO) {
iframeUrl.searchParams.append(url_params_1.URL_PARAM_SESSION_INFO, r2SESSIONINFO);
}
if (r2DEBUG) {
iframeUrl.searchParams.append(url_params_1.URL_PARAM_DEBUG_VISUALS, r2DEBUG);
}
if (r2ERS) {
iframeUrl.searchParams.append(url_params_1.URL_PARAM_EPUBREADINGSYSTEM, r2ERS);
}
if (r2CSS) {
iframeUrl.searchParams.append(url_params_1.URL_PARAM_CSS, r2CSS);
}
if (r2isMO) {
iframeUrl.searchParams.append(url_params_1.URL_PARAM_EPUBMEDIAOVERLAYS, r2isMO);
}
if (r2WEBVIEWSLOT) {
iframeUrl.searchParams.append(url_params_1.URL_PARAM_WEBVIEW_SLOT, r2WEBVIEWSLOT);
}
if (r2SECONDWEBVIEW) {
iframeUrl.searchParams.append(url_params_1.URL_PARAM_SECOND_WEBVIEW, r2SECONDWEBVIEW);
}
iframeUrl.searchParams.append(url_params_1.URL_PARAM_IS_IFRAME, "1");
src_ = iframeUrl.toString();
debug(`IFRAME SRC PATCH: ${src} ==> ${src_}`);
el.setAttribute("src", src_);
};
const processTree = (el) => {
const elName = el.nodeName.toLowerCase();
if (elName === "iframe") {
patchElementSrc(el);
}
else {
if (!el.childNodes) {
return;
}
for (let i = 0; i < el.childNodes.length; i++) {
const childNode = el.childNodes[i];
if (childNode.nodeType === 1) {
processTree(childNode);
}
}
}
};
processTree(documantFromXmlDom.body);
const serialized = (0, dom_1.serializeDOM)(documantFromXmlDom);
const prefix = htmlStr.substr(0, iHtmlStart);
const iHtmlStart_ = serialized.indexOf("<html");
if (iHtmlStart_ < 0) {
return htmlStr;
}
const remaining = serialized.substr(iHtmlStart_);
const newStr = `${prefix}${remaining}`;
return newStr;
};
const transformerHttpBase = (publication, link, url, htmlStr, sessionInfo) => {
if (!_customUrlProtocolSchemeHandlerWasCalled) {
return htmlStr;
}
if (!url) {
return htmlStr;
}
const iHead = htmlStr.indexOf("</head>");
if (iHead < 0) {
return htmlStr;
}
let urlHttp = url;
if (urlHttp.startsWith(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL + "://")) {
urlHttp = (0, sessions_1.convertCustomSchemeToHttpUrl)(urlHttp);
}
const url_ = new URL(urlHttp);
url_.search = "";
url_.hash = "";
const urlStr = url_.toString();
const baseStr = `
<base href="${urlStr}" />
`;
let newStr = htmlStr.substr(0, iHead) + baseStr + htmlStr.substr(iHead);
newStr = newStr.replace(/<(audio|video)/g, "<$1 data-r2-crossorigin=\"true\" crossorigin=\"anonymous\" ");
newStr = transformerHttpBaseIframes(publication, link, url, newStr, sessionInfo);
return newStr;
};
const INJECT_HTTP_BASE = true;
function initSessions() {
electron_1.app.commandLine.appendSwitch("autoplay-policy", "no-user-gesture-required");
if (INJECT_HTTP_BASE) {
transformer_1.Transformers.instance().add(new transformer_html_1.TransformerHTML(transformerHttpBase));
}
else {
transformer_1.Transformers.instance().add(new transformer_html_1.TransformerHTML(transformerAudioVideo));
}
if (electron_1.protocol.registerStandardSchemes) {
electron_1.protocol.registerStandardSchemes([sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL], { secure: true });
}
else {
electron_1.protocol.registerSchemesAsPrivileged([{
privileges: {
allowServiceWorkers: false,
bypassCSP: false,
corsEnabled: true,
secure: true,
standard: true,
stream: true,
supportFetchAPI: true,
},
scheme: sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL,
}]);
}
electron_1.app.on("ready", () => tslib_1.__awaiter(this, void 0, void 0, function* () {
debug("app ready");
try {
yield clearSessions();
}
catch (err) {
debug(err);
}
if (electron_1.session.defaultSession) {
if (USE_STREAM_PROTOCOL_INSTEAD_OF_HTTP) {
electron_1.session.defaultSession.protocol.registerStreamProtocol(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL, streamProtocolHandler);
}
else {
electron_1.session.defaultSession.protocol.registerHttpProtocol(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL, httpProtocolHandler);
}
}
const webViewSession = getWebViewSession();
if (webViewSession) {
if (USE_STREAM_PROTOCOL_INSTEAD_OF_HTTP) {
webViewSession.protocol.registerStreamProtocol(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL, streamProtocolHandler);
}
else {
webViewSession.protocol.registerHttpProtocol(sessions_1.READIUM2_ELECTRON_HTTP_PROTOCOL, httpProtocolHandler);
}
webViewSession.setPermissionRequestHandler((wc, permission, callback) => {
debug("setPermissionRequestHandler webViewSession");
debug(wc.getURL());
debug(permission);
callback(false);
});
webViewSession.setPermissionCheckHandler((wc, permission, origin) => {
debug("setPermissionCheckHandler webViewSession");
debug(wc === null || wc === void 0 ? void 0 : wc.getURL());
debug(permission);
debug(origin);
return false;
});
}
}));
}
function clearSession(sess, str) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const prom1 = sess.clearCache();
const prom2 = sess.clearStorageData({
origin: "*",
quotas: [
"temporary",
],
storages: [
"serviceworkers",
"cachestorage",
],
});
try {
const results = yield promiseAllSettled([prom1, prom2]);
for (const result of results) {
debug(`SESSION CACHE + STORAGE DATA CLEARED - ${str} => ${result.status}`);
}
}
catch (err) {
debug(err);
}
return Promise.resolve();
});
}
function getWebViewSession() {
return electron_1.session.fromPartition(sessions_1.R2_SESSION_WEBVIEW, { cache: true });
}
function clearWebviewSession() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
const sess = getWebViewSession();
if (sess) {
try {
yield clearSession(sess, "[" + sessions_1.R2_SESSION_WEBVIEW + "]");
}
catch (err) {
debug(err);
}
}
return Promise.resolve();
});
}
function clearDefaultSession() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
if (electron_1.session.defaultSession) {
try {
yield clearSession(electron_1.session.defaultSession, "[default]");
}
catch (err) {
debug(err);
}
}
return Promise.resolve();
});
}
function clearSessions() {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
try {
yield promiseAllSettled([clearDefaultSession(), clearWebviewSession()]);
}
catch (err) {
debug(err);
}
return Promise.resolve();
});
}
//# sourceMappingURL=sessions.js.map