UNPKG

@solid/community-server

Version:

Community Solid Server: an open and modular implementation of the Solid specifications

192 lines 8.81 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StaticAssetHandler = exports.StaticAssetEntry = void 0; const node_fs_1 = require("node:fs"); const escape_string_regexp_1 = __importDefault(require("escape-string-regexp")); const mime = __importStar(require("mime-types")); const global_logger_factory_1 = require("global-logger-factory"); const ContentTypes_1 = require("../../util/ContentTypes"); const InternalServerError_1 = require("../../util/errors/InternalServerError"); const NotFoundHttpError_1 = require("../../util/errors/NotFoundHttpError"); const NotImplementedHttpError_1 = require("../../util/errors/NotImplementedHttpError"); const PathUtil_1 = require("../../util/PathUtil"); const StreamUtil_1 = require("../../util/StreamUtil"); const HttpHandler_1 = require("../HttpHandler"); /** * Used to link file paths with relative URLs. * By using a separate class instead of a key/value map it is easier to replace values in Components.js. */ class StaticAssetEntry { relativeUrl; filePath; constructor(relativeUrl, filePath) { this.relativeUrl = relativeUrl; this.filePath = filePath; } } exports.StaticAssetEntry = StaticAssetEntry; /** * Handler that serves static resources on specific paths. * Relative file paths are assumed to be relative to the current working directory. * Relative file paths can be preceded by `@css:`, e.g. `@css:foo/bar`, * in case they need to be relative to the module root. * File paths ending in a slash assume the target is a folder and map all of its contents. */ class StaticAssetHandler extends HttpHandler_1.HttpHandler { mappings; pathMatcher; expires; logger = (0, global_logger_factory_1.getLoggerFor)(this); /** * Creates a handler for the provided static resources. * * @param assets - A list of {@link StaticAssetEntry}. * @param baseUrl - The base URL of the server. * @param options - Specific options. * @param options.expires - Cache expiration time in seconds. */ constructor(assets, baseUrl, options = {}) { super(); this.mappings = {}; const rootPath = (0, PathUtil_1.ensureTrailingSlash)(new URL(baseUrl).pathname); for (const { relativeUrl, filePath } of assets) { this.mappings[(0, PathUtil_1.trimLeadingSlashes)(relativeUrl)] = (0, PathUtil_1.resolveAssetPath)(filePath); } this.pathMatcher = this.createPathMatcher(rootPath); this.expires = Number.isInteger(options.expires) ? Math.max(0, options.expires) : 0; } /** * Creates a regular expression that matches the URL paths. */ createPathMatcher(rootPath) { // Sort longest paths first to ensure the longest match has priority const paths = Object.keys(this.mappings) .sort((pathA, pathB) => pathB.length - pathA.length); // Collect regular expressions for files and folders separately. // The arrays need initial values to prevent matching everything, as they will if these are empty. const files = ['.^']; const folders = ['.^']; for (const path of paths) { const filePath = this.mappings[path]; if (filePath.endsWith('/') && !path.endsWith('/')) { throw new InternalServerError_1.InternalServerError(`Server is misconfigured: StaticAssetHandler can not ` + `have a file path ending on a slash if the URL does not, but received ${path} and ${filePath}`); } (filePath.endsWith('/') ? folders : files).push((0, escape_string_regexp_1.default)(path)); } // Either match an exact document or a file within a folder (stripping the query string) return new RegExp(`^${rootPath}(?:(${files.join('|')})|(${folders.join('|')})([^?]+))(?:\\?.*)?$`, 'u'); } /** * Obtains the file path corresponding to the asset URL */ getFilePath({ url }) { // Verify if the URL matches any of the paths const match = this.pathMatcher.exec(url ?? ''); if (!match || match[0].includes('/..')) { throw new NotImplementedHttpError_1.NotImplementedHttpError(`No static resource configured at ${url}`); } // The mapping is either a known document, or a file within a folder const [, document, folder, file] = match; return typeof document === 'string' ? this.mappings[document] : (0, PathUtil_1.joinFilePath)(this.mappings[folder], decodeURIComponent(file)); } async canHandle({ request }) { if (request.method !== 'GET' && request.method !== 'HEAD') { throw new NotImplementedHttpError_1.NotImplementedHttpError('Only GET and HEAD requests are supported'); } this.getFilePath(request); } async handle({ request, response }) { // Determine the asset to serve const filePath = this.getFilePath(request); this.logger.debug(`Serving ${request.url} via static asset ${filePath}`); // Resolve when asset loading succeeds const asset = (0, node_fs_1.createReadStream)(filePath); return new Promise((resolve, reject) => { // Write a 200 response when the asset becomes readable asset.once('readable', () => { const contentType = mime.lookup(filePath) || ContentTypes_1.APPLICATION_OCTET_STREAM; response.writeHead(200, { // eslint-disable-next-line ts/naming-convention 'content-type': contentType, ...this.getCacheHeaders(), }); // With HEAD, only write the headers if (request.method === 'HEAD') { response.end(); asset.destroy(); // With GET, pipe the entire response } else { (0, StreamUtil_1.pipeSafely)(asset, response); } resolve(); }); // Pass the error when something goes wrong asset.once('error', (error) => { const { code } = error; // When the file if not found or a folder, signal a 404 if (code === 'ENOENT' || code === 'EISDIR') { this.logger.debug(`Static asset ${filePath} not found`); reject(new NotFoundHttpError_1.NotFoundHttpError(`Cannot find ${request.url}`)); // In other cases, we might already have started writing, so just hang up } else { this.logger.warn(`Error reading asset ${filePath}: ${error.message}`); response.end(); asset.destroy(); resolve(); } }); }); } getCacheHeaders() { return this.expires <= 0 ? {} : { // eslint-disable-next-line ts/naming-convention 'cache-control': `max-age=${this.expires}`, expires: new Date(Date.now() + this.expires * 1000).toUTCString(), }; } } exports.StaticAssetHandler = StaticAssetHandler; //# sourceMappingURL=StaticAssetHandler.js.map