UNPKG

r2-streamer-js

Version:

Readium 2 'streamer' for NodeJS (TypeScript)

482 lines 22.7 kB
"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