r2-streamer-js
Version:
Readium 2 'streamer' for NodeJS (TypeScript)
482 lines • 22.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Server = exports.MAX_PREFETCH_LINKS = void 0;
const tslib_1 = require("tslib");
const child_process = require("child_process");
const debug_ = require("debug");
const express = require("express");
const fs = require("fs");
const http = require("http");
const https = require("https");
const path = require("path");
const tmp_1 = require("tmp");
const serializable_1 = require("r2-lcp-js/dist/es7-es2016/src/serializable");
const opds2_1 = require("r2-opds-js/dist/es7-es2016/src/opds/opds2/opds2");
const publication_1 = require("r2-shared-js/dist/es7-es2016/src/models/publication");
const publication_parser_1 = require("r2-shared-js/dist/es7-es2016/src/parser/publication-parser");
const UrlUtils_1 = require("r2-utils-js/dist/es7-es2016/src/_utils/http/UrlUtils");
const zipFactory_1 = require("r2-utils-js/dist/es7-es2016/src/_utils/zip/zipFactory");
const self_signed_1 = require("../utils/self-signed");
const server_assets_1 = require("./server-assets");
const server_lcp_lsd_show_1 = require("./server-lcp-lsd-show");
const server_manifestjson_1 = require("./server-manifestjson");
const server_mediaoverlays_1 = require("./server-mediaoverlays");
const server_opds_browse_v1_1 = require("./server-opds-browse-v1");
const server_opds_browse_v2_1 = require("./server-opds-browse-v2");
const server_opds_convert_v1_to_v2_1 = require("./server-opds-convert-v1-to-v2");
const server_opds_local_feed_1 = require("./server-opds-local-feed");
const server_pub_1 = require("./server-pub");
const server_root_1 = require("./server-root");
const server_secure_1 = require("./server-secure");
const server_url_1 = require("./server-url");
const server_version_1 = require("./server-version");
const debug = debug_("r2:streamer#http/server");
const isValidHexPassphraseHashSha256 = (str) => {
if (str.length !== 64) {
return false;
}
let isHex = true;
for (let i = 0; i < str.length; i += 2) {
const hexByte = str.substr(i, 2).toLowerCase();
if (!/^[0-9a-f][0-9a-f]$/.test(hexByte)) {
isHex = false;
break;
}
const parsedInt = parseInt(hexByte, 16);
if (isNaN(parsedInt)) {
isHex = false;
break;
}
}
return isHex;
};
exports.MAX_PREFETCH_LINKS = 10;
class Server {
constructor(options) {
this.lcpBeginToken = "*-";
this.lcpEndToken = "-*";
this.disableReaders = !!(options === null || options === void 0 ? void 0 : options.disableReaders);
this.disableDecryption = !!(options === null || options === void 0 ? void 0 : options.disableDecryption);
this.disableRemotePubUrl = !!(options === null || options === void 0 ? void 0 : options.disableRemotePubUrl);
this.disableOPDS = !!(options === null || options === void 0 ? void 0 : options.disableOPDS);
this.enableSignedExpiry = !!(options === null || options === void 0 ? void 0 : options.enableSignedExpiry);
this.maxPrefetchLinks = (options === null || options === void 0 ? void 0 : options.maxPrefetchLinks) ? options.maxPrefetchLinks : exports.MAX_PREFETCH_LINKS;
this.publications = [];
this.pathPublicationMap = {};
this.publicationsOPDSfeed = undefined;
this.publicationsOPDSfeedNeedsUpdate = true;
this.creatingPublicationsOPDS = false;
this.opdsJsonFilePath = (0, tmp_1.tmpNameSync)({ prefix: "readium2-OPDS2-", postfix: ".json" });
this.expressApp = express();
(0, server_secure_1.serverSecure)(this, this.expressApp);
const staticOptions = {
etag: false,
};
if (!this.disableReaders) {
this.expressApp.use("/readerNYPL", express.static("misc/readers/reader-NYPL", staticOptions));
this.expressApp.use("/readerHADRIEN", express.static("misc/readers/reader-HADRIEN", staticOptions));
}
(0, server_root_1.serverRoot)(this, this.expressApp);
(0, server_version_1.serverVersion)(this, this.expressApp);
if (!this.disableRemotePubUrl) {
(0, server_url_1.serverRemotePub)(this, this.expressApp);
(0, server_lcp_lsd_show_1.serverLCPLSD_show)(this, this.expressApp);
}
if (!this.disableOPDS) {
(0, server_opds_browse_v1_1.serverOPDS_browse_v1)(this, this.expressApp);
(0, server_opds_browse_v2_1.serverOPDS_browse_v2)(this, this.expressApp);
(0, server_opds_local_feed_1.serverOPDS_local_feed)(this, this.expressApp);
(0, server_opds_convert_v1_to_v2_1.serverOPDS_convert_v1_to_v2)(this, this.expressApp);
}
const routerPathBase64 = (0, server_pub_1.serverPub)(this, this.expressApp);
(0, server_manifestjson_1.serverManifestJson)(this, routerPathBase64);
(0, server_mediaoverlays_1.serverMediaOverlays)(this, routerPathBase64);
(0, server_assets_1.serverAssets)(this, routerPathBase64);
}
preventRobots() {
this.expressApp.get("/robots.txt", (_req, res) => {
const robotsTxt = `User-agent: *
Disallow: /
`;
res.header("Content-Type", "text/plain");
res.status(200).send(robotsTxt);
});
}
expressUse(pathf, func) {
this.expressApp.use(pathf, func);
}
expressGet(paths, func) {
this.expressApp.get(paths, func);
}
isStarted() {
return (typeof this.serverInfo() !== "undefined") &&
(typeof this.httpServer !== "undefined") ||
(typeof this.httpsServer !== "undefined");
}
isSecured() {
return (typeof this.serverInfo() !== "undefined") &&
(typeof this.httpsServer !== "undefined");
}
getSecureHTTPHeader(url) {
return (0, server_secure_1.serverSecureHTTPHeader)(this, url);
}
start(port, secure) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
if (this.isStarted()) {
return Promise.resolve(this.serverInfo());
}
let envPort = 0;
try {
envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 0;
}
catch (err) {
debug(err);
envPort = 0;
}
const p = port || envPort || 3000;
debug(`PORT: ${port} || ${envPort} || 3000 => ${p}`);
if (secure) {
this.httpServer = undefined;
return new Promise((resolve, reject) => tslib_1.__awaiter(this, void 0, void 0, function* () {
let certData;
try {
certData = yield (0, self_signed_1.generateSelfSignedData)();
}
catch (err) {
debug(err);
reject("err");
return;
}
this.httpsServer = https.createServer({ key: certData.private, cert: certData.cert }, this.expressApp).listen(p, () => {
this.serverData = Object.assign(Object.assign({}, certData), { urlHost: "127.0.0.1", urlPort: p, urlScheme: "https" });
resolve(this.serverData);
});
}));
}
else {
this.httpsServer = undefined;
return new Promise((resolve, _reject) => {
this.httpServer = http.createServer(this.expressApp).listen(p, () => {
this.serverData = {
urlHost: "127.0.0.1",
urlPort: p,
urlScheme: "http",
};
resolve(this.serverData);
});
});
}
});
}
stop() {
if (this.isStarted()) {
if (this.httpServer) {
this.httpServer.close();
this.httpServer = undefined;
}
if (this.httpsServer) {
this.httpsServer.close();
this.httpsServer = undefined;
}
this.serverData = undefined;
this.uncachePublications();
}
}
serverInfo() {
return this.serverData;
}
serverUrl() {
if (!this.isStarted()) {
return undefined;
}
const info = this.serverInfo();
if (!info) {
return undefined;
}
if (info.urlPort === 443 || info.urlPort === 80) {
return `${info.urlScheme}://${info.urlHost}`;
}
return `${info.urlScheme}://${info.urlHost}:${info.urlPort}`;
}
setResponseCacheHeaders(res, enableCaching) {
if (enableCaching) {
res.setHeader("Cache-Control", "public,max-age=86400");
}
else {
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");
}
}
setResponseCORS(res) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Ranges, Content-Range, Range, Link, Transfer-Encoding, X-Requested-With, Authorization, Accept, Origin, User-Agent, DNT, Cache-Control, Keep-Alive, If-Modified-Since");
res.setHeader("Access-Control-Expose-Headers", "Content-Type, Content-Length, Accept-Ranges, Content-Range, Range, Link, Transfer-Encoding, X-Requested-With, Authorization, Accept, Origin, User-Agent, DNT, Cache-Control, Keep-Alive, If-Modified-Since");
}
addPublications(pubs) {
pubs.forEach((pub) => {
if (this.publications.indexOf(pub) < 0) {
this.publicationsOPDSfeedNeedsUpdate = true;
this.publications.push(pub);
}
});
return pubs.map((pub) => {
const pubid = (0, UrlUtils_1.encodeURIComponent_RFC3986)(Buffer.from(pub).toString("base64"));
return `/pub/${pubid}/manifest.json`;
});
}
removePublications(pubs) {
pubs.forEach((pub) => {
this.uncachePublication(pub);
const i = this.publications.indexOf(pub);
if (i >= 0) {
this.publicationsOPDSfeedNeedsUpdate = true;
this.publications.splice(i, 1);
}
});
return pubs.map((pub) => {
const pubid = (0, UrlUtils_1.encodeURIComponent_RFC3986)(Buffer.from(pub).toString("base64"));
return `/pub/${pubid}/manifest.json`;
});
}
getPublications() {
return this.publications;
}
loadOrGetCachedPublication(filePath) {
return tslib_1.__awaiter(this, void 0, void 0, function* () {
let publication = this.cachedPublication(filePath);
if (!publication) {
if (filePath.endsWith("_manifest.json")) {
try {
const zip = yield (0, zipFactory_1.zipLoadPromise)(filePath.replace(/_manifest\.json$/, ""));
const publicationJsonStr = fs.readFileSync(filePath, { encoding: "utf8" });
const publicationJsonObj = global.JSON.parse(publicationJsonStr);
publication = (0, serializable_1.TaJsonDeserialize)(publicationJsonObj, publication_1.Publication);
publication.AddToInternal("filename", path.basename(filePath));
publication.AddToInternal("type", "daisy");
publication.AddToInternal("zip", zip);
}
catch (err) {
debug(err);
return Promise.reject(err);
}
}
else {
try {
publication = yield (0, publication_parser_1.PublicationParsePromise)(filePath);
}
catch (err) {
debug(err);
return Promise.reject(err);
}
}
if (!publication) {
return Promise.reject("!PUBLICATION??");
}
if (!publication.LCP && !this.disableDecryption) {
try {
const contentKeys = [];
const contentKeyPath = path.join(path.dirname(filePath), path.basename(filePath) + ".contentkey");
if (fs.existsSync(contentKeyPath)) {
let contentKey = fs.readFileSync(contentKeyPath, { encoding: "utf8" });
if (contentKey) {
contentKey = contentKey.trim();
if (isValidHexPassphraseHashSha256(contentKey)) {
contentKeys.push(contentKey);
}
}
}
const lcpContentKeysPath = path.join(process.cwd(), "LCP", ".contentkeys");
if (fs.existsSync(lcpContentKeysPath)) {
let contentKeysData = fs.readFileSync(lcpContentKeysPath, { encoding: "utf8" });
if (contentKeysData) {
contentKeysData = contentKeysData.trim();
const contentKeysMap = contentKeysData.split("\n").map((contentKeyLine) => {
contentKeyLine = contentKeyLine.trim();
if (!contentKeyLine) {
return null;
}
const keyValuePair = contentKeyLine.split("_::_");
if (keyValuePair[0] && keyValuePair[1]) {
return [keyValuePair[0].trim(), keyValuePair[1].trim()];
}
return null;
}).filter((item) => !!item);
for (const keyValuePair of contentKeysMap) {
const key = keyValuePair[0];
const value = keyValuePair[1];
if (key === path.relative(process.cwd(), filePath)) {
if (isValidHexPassphraseHashSha256(value)) {
contentKeys.push(value);
}
}
}
}
}
const contentKeysUnique = Array.from(new Set(contentKeys));
debug("SUCCESS contentKeys:");
debug(filePath);
debug(path.relative(process.cwd(), filePath));
debug(contentKeys.length);
debug(contentKeysUnique.length);
if (contentKeysUnique.length) {
publication["AES256CBCContentKey"] = Buffer.from(contentKeysUnique[0], "hex");
}
}
catch (err) {
debug(err);
const errMsg = "FAIL AES256CBCContentKey: " + err;
debug(errMsg);
}
}
if (publication.LCP && !this.disableDecryption) {
try {
const userKeys = [];
const lcpUserKeyPath = path.join(path.dirname(filePath), path.basename(filePath) + ".userkey");
if (fs.existsSync(lcpUserKeyPath)) {
let userKey = fs.readFileSync(lcpUserKeyPath, { encoding: "utf8" });
if (userKey) {
userKey = userKey.trim();
if (isValidHexPassphraseHashSha256(userKey)) {
userKeys.push(userKey);
}
}
}
const lcpUserKeysPath = path.join(process.cwd(), "LCP", ".userkeys");
if (fs.existsSync(lcpUserKeysPath)) {
let userKeysData = fs.readFileSync(lcpUserKeysPath, { encoding: "utf8" });
if (userKeysData) {
userKeysData = userKeysData.trim();
const userKeysMap = userKeysData.split("\n").map((userKeyLine) => {
userKeyLine = userKeyLine.trim();
if (!userKeyLine) {
return null;
}
const keyValuePair = userKeyLine.split("_::_");
if (keyValuePair[0] && keyValuePair[1]) {
return [keyValuePair[0].trim(), keyValuePair[1].trim()];
}
return null;
}).filter((item) => !!item);
for (const keyValuePair of userKeysMap) {
const key = keyValuePair[0];
const value = keyValuePair[1];
if (key === publication.LCP.Provider || key === publication.LCP.ID) {
if (isValidHexPassphraseHashSha256(value)) {
userKeys.push(value);
}
}
}
}
}
const userKeysUnique = Array.from(new Set(userKeys));
try {
yield publication.LCP.tryUserKeys(userKeysUnique);
debug("SUCCESS publication.LCP.tryUserKeys():");
debug(filePath);
debug(publication.LCP.Provider);
debug(publication.LCP.ID);
debug(userKeys.length);
debug(userKeysUnique.length);
if (publication.LCP.ContentKey) {
debug(publication.LCP.ContentKey.toString("hex"));
}
}
catch (err) {
publication.LCP.ContentKey = undefined;
debug(err);
const errMsg = "FAIL publication.LCP.tryUserKeys(): " + err;
debug(errMsg);
}
}
catch (err) {
publication.LCP.ContentKey = undefined;
debug(err);
const errMsg = "FAIL before publication.LCP.tryUserKeys(): " + err;
debug(errMsg);
}
}
this.cachePublication(filePath, publication);
}
return publication;
});
}
isPublicationCached(filePath) {
return typeof this.cachedPublication(filePath) !== "undefined";
}
cachedPublication(filePath) {
return this.pathPublicationMap[filePath];
}
cachePublication(filePath, pub) {
if (!this.isPublicationCached(filePath)) {
this.pathPublicationMap[filePath] = pub;
}
}
uncachePublication(filePath) {
if (this.isPublicationCached(filePath)) {
const pub = this.cachedPublication(filePath);
if (pub) {
try {
pub.freeDestroy();
}
catch (ex) {
debug(ex);
}
}
this.pathPublicationMap[filePath] = undefined;
delete this.pathPublicationMap[filePath];
}
}
uncachePublications() {
Object.keys(this.pathPublicationMap).forEach((filePath) => {
this.uncachePublication(filePath);
});
}
publicationsOPDS() {
if (this.publicationsOPDSfeedNeedsUpdate) {
this.publicationsOPDSfeed = undefined;
if (fs.existsSync(this.opdsJsonFilePath)) {
fs.unlinkSync(this.opdsJsonFilePath);
}
}
if (this.publicationsOPDSfeed) {
return this.publicationsOPDSfeed;
}
debug(`OPDS2.json => ${this.opdsJsonFilePath}`);
if (!fs.existsSync(this.opdsJsonFilePath)) {
if (!this.creatingPublicationsOPDS) {
this.creatingPublicationsOPDS = true;
this.publicationsOPDSfeedNeedsUpdate = false;
const jsFile = path.join(__dirname, "opds2-create-cli.js");
const args = [jsFile, this.opdsJsonFilePath];
this.publications.forEach((pub) => {
const filePathBase64 = (0, UrlUtils_1.encodeURIComponent_RFC3986)(Buffer.from(pub).toString("base64"));
args.push(filePathBase64);
});
debug(`SPAWN OPDS2-create: ${args[0]}`);
const child = child_process.spawn("node", args, {
cwd: process.cwd(),
env: process.env,
});
child.stdout.on("data", (data) => {
debug(data.toString());
});
child.stderr.on("data", (data) => {
debug(data.toString());
});
}
return undefined;
}
this.creatingPublicationsOPDS = false;
const jsonStr = fs.readFileSync(this.opdsJsonFilePath, { encoding: "utf8" });
if (!jsonStr) {
return undefined;
}
const json = global.JSON.parse(jsonStr);
this.publicationsOPDSfeed = (0, serializable_1.TaJsonDeserialize)(json, opds2_1.OPDSFeed);
return this.publicationsOPDSfeed;
}
}
exports.Server = Server;
//# sourceMappingURL=server.js.map