UNPKG

firebase-tools

Version:
501 lines (500 loc) 20.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createFirebaseEndpoints = void 0; const emulatorLogger_1 = require("../../emulatorLogger"); const types_1 = require("../../types"); const uuid = require("uuid"); const metadata_1 = require("../metadata"); const express_1 = require("express"); const shared_1 = require("./shared"); const registry_1 = require("../../registry"); const multipart_1 = require("../multipart"); const errors_1 = require("../errors"); const upload_1 = require("../upload"); const request_1 = require("../../shared/request"); function createFirebaseEndpoints(emulator) { const firebaseStorageAPI = (0, express_1.Router)(); const { storageLayer, uploadService } = emulator; if (process.env.STORAGE_EMULATOR_DEBUG) { firebaseStorageAPI.use((req, res, next) => { console.log("--------------INCOMING FIREBASE REQUEST--------------"); console.log(`${req.method.toUpperCase()} ${req.path}`); console.log("-- query:"); console.log(JSON.stringify(req.query, undefined, 2)); console.log("-- headers:"); console.log(JSON.stringify(req.headers, undefined, 2)); console.log("-- body:"); if (req.body instanceof Buffer) { console.log(`Buffer of ${req.body.length}`); } else if (req.body) { console.log(req.body); } else { console.log("Empty body (could be stream)"); } const resJson = res.json.bind(res); res.json = (...args) => { console.log("-- response:"); args.forEach((data) => console.log(JSON.stringify(data, undefined, 2))); return resJson.call(res, ...args); }; const resSendStatus = res.sendStatus.bind(res); res.sendStatus = (status) => { console.log("-- response status:"); console.log(status); return resSendStatus.call(res, status); }; const resStatus = res.status.bind(res); res.status = (status) => { console.log("-- response status:"); console.log(status); return resStatus.call(res, status); }; next(); }); } firebaseStorageAPI.use(/.*\/b\/(.+?)\/.*/, (req, res, next) => { const bucketId = req.params[0]; storageLayer.createBucket(bucketId); if (!emulator.rulesManager.getRuleset(bucketId)) { emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.STORAGE).log("WARN", "Permission denied because no Storage ruleset is currently loaded, check your rules for syntax errors."); return res.status(403).json({ error: { code: 403, message: "Permission denied. Storage Emulator has no loaded ruleset.", }, }); } next(); }); firebaseStorageAPI.get("/b/:bucketId/o/:objectId", async (req, res) => { var _a; let metadata; let data; try { ({ metadata, data } = await storageLayer.getObject({ bucketId: req.params.bucketId, decodedObjectId: decodeURIComponent(req.params.objectId), authorization: req.header("authorization"), downloadToken: (_a = req.query.token) === null || _a === void 0 ? void 0 : _a.toString(), })); } catch (err) { if (err instanceof errors_1.NotFoundError) { return res.sendStatus(404); } else if (err instanceof errors_1.ForbiddenError) { return res.status(403).json({ error: { code: 403, message: `Permission denied. No READ permission.`, }, }); } throw err; } if (metadata.downloadTokens.length === 0) { metadata.addDownloadToken(true); } if (req.query.alt === "media") { return (0, shared_1.sendFileBytes)(metadata, data, req, res); } return res.json(new metadata_1.OutgoingFirebaseMetadata(metadata)); }); firebaseStorageAPI.get("/b/:bucketId/o", async (req, res) => { var _a, _b, _c, _d; const maxResults = (_a = req.query.maxResults) === null || _a === void 0 ? void 0 : _a.toString(); let listResponse; let prefix = ""; if (req.query.prefix) { prefix = req.query.prefix.toString(); if (prefix.charAt(prefix.length - 1) !== "/") { return res.status(400).json({ error: { code: 400, message: "The prefix parameter is required to be empty or ends with a single / character.", }, }); } } try { listResponse = await storageLayer.listObjects({ bucketId: req.params.bucketId, prefix: prefix, delimiter: req.query.delimiter ? req.query.delimiter.toString() : "", pageToken: (_b = req.query.pageToken) === null || _b === void 0 ? void 0 : _b.toString(), maxResults: maxResults ? +maxResults : undefined, authorization: req.header("authorization"), }); } catch (err) { if (err instanceof errors_1.ForbiddenError) { return res.status(403).json({ error: { code: 403, message: `Permission denied. No LIST permission.`, }, }); } throw err; } return res.status(200).json({ nextPageToken: listResponse.nextPageToken, prefixes: ((_c = listResponse.prefixes) !== null && _c !== void 0 ? _c : []).filter(isValidPrefix), items: ((_d = listResponse.items) !== null && _d !== void 0 ? _d : []) .filter((item) => isValidNonEncodedPathString(item.name)) .map((item) => { return { name: item.name, bucket: item.bucket }; }), }); }); const handleUpload = async (req, res) => { var _a, _b; const bucketId = req.params.bucketId; const objectId = req.params.objectId ? decodeURIComponent(req.params.objectId) : ((_a = req.query.name) === null || _a === void 0 ? void 0 : _a.toString()) || null; const uploadType = (_b = req.header("x-goog-upload-protocol")) === null || _b === void 0 ? void 0 : _b.toString(); async function finalizeOneShotUpload(upload) { var _a, _b, _c; if (!((_b = (_a = upload.metadata) === null || _a === void 0 ? void 0 : _a.metadata) === null || _b === void 0 ? void 0 : _b.firebaseStorageDownloadTokens)) { const customMetadata = Object.assign(Object.assign({}, (((_c = upload.metadata) === null || _c === void 0 ? void 0 : _c.metadata) || {})), { firebaseStorageDownloadTokens: uuid.v4() }); upload.metadata = Object.assign(Object.assign({}, (upload.metadata || {})), { metadata: customMetadata }); } let metadata; try { metadata = await storageLayer.uploadObject(upload); } catch (err) { if (err instanceof errors_1.ForbiddenError) { res.header("x-goog-upload-status", "final"); uploadService.setResponseCode(upload.id, 403); return res.status(403).json({ error: { code: 403, message: "Permission denied. No WRITE permission.", }, }); } throw err; } if (!metadata.contentDisposition) { metadata.contentDisposition = "inline"; } return res.status(200).json(new metadata_1.OutgoingFirebaseMetadata(metadata)); } if (uploadType === "resumable" || req.header("x-goog-upload-command")) { const uploadCommand = req.header("x-goog-upload-command"); if (!uploadCommand) { res.sendStatus(400); return; } if (uploadCommand === "start") { if (!objectId) { res.sendStatus(400); return; } const upload = uploadService.startResumableUpload({ bucketId, objectId, metadata: req.body, authorization: req.header("authorization"), }); res.header("x-goog-upload-chunk-granularity", "10000"); res.header("x-goog-upload-control-url", ""); res.header("x-goog-upload-status", "active"); res.header("x-gupload-uploadid", upload.id); const uploadUrl = registry_1.EmulatorRegistry.url(types_1.Emulators.STORAGE, req); uploadUrl.pathname = `/v0/b/${bucketId}/o`; uploadUrl.searchParams.set("name", objectId); uploadUrl.searchParams.set("upload_id", upload.id); uploadUrl.searchParams.set("upload_protocol", "resumable"); res.header("x-goog-upload-url", uploadUrl.toString()); return res.sendStatus(200); } if (!req.query.upload_id) { return res.sendStatus(400); } const uploadId = req.query.upload_id.toString(); if (uploadCommand === "query") { let upload; try { upload = uploadService.getResumableUpload(uploadId); } catch (err) { if (err instanceof errors_1.NotFoundError) { return res.sendStatus(404); } throw err; } res.header("X-Goog-Upload-Size-Received", upload.size.toString()); res.header("x-goog-upload-status", upload.status); return res.sendStatus(200); } if (uploadCommand === "cancel") { try { uploadService.cancelResumableUpload(uploadId); } catch (err) { if (err instanceof errors_1.NotFoundError) { return res.sendStatus(404); } else if (err instanceof upload_1.NotCancellableError) { return res.sendStatus(400); } throw err; } return res.sendStatus(200); } if (uploadCommand.includes("upload")) { let upload; try { upload = uploadService.continueResumableUpload(uploadId, await (0, request_1.reqBodyToBuffer)(req)); } catch (err) { if (err instanceof errors_1.NotFoundError) { return res.sendStatus(404); } else if (err instanceof upload_1.UploadNotActiveError) { return res.sendStatus(400); } throw err; } if (!uploadCommand.includes("finalize")) { res.header("x-goog-upload-status", "active"); res.header("x-gupload-uploadid", upload.id); return res.sendStatus(200); } } if (uploadCommand.includes("finalize")) { let upload; try { upload = uploadService.finalizeResumableUpload(uploadId); } catch (err) { if (err instanceof errors_1.NotFoundError) { uploadService.setResponseCode(uploadId, 404); return res.sendStatus(404); } else if (err instanceof upload_1.UploadNotActiveError) { uploadService.setResponseCode(uploadId, 400); return res.sendStatus(400); } else if (err instanceof upload_1.UploadPreviouslyFinalizedError) { res.header("x-goog-upload-status", "final"); return res.sendStatus(uploadService.getPreviousResponseCode(uploadId)); } throw err; } res.header("x-goog-upload-status", "final"); return await finalizeOneShotUpload(upload); } } if (!objectId) { res.sendStatus(400); return; } if (uploadType === "multipart") { const contentTypeHeader = req.header("content-type"); if (!contentTypeHeader) { return res.sendStatus(400); } let metadataRaw; let dataRaw; try { ({ metadataRaw, dataRaw } = (0, multipart_1.parseObjectUploadMultipartRequest)(contentTypeHeader, await (0, request_1.reqBodyToBuffer)(req))); } catch (err) { if (err instanceof Error) { return res.status(400).send(err.message); } throw err; } const upload = uploadService.multipartUpload({ bucketId, objectId, metadata: JSON.parse(metadataRaw), dataRaw: dataRaw, authorization: req.header("authorization"), }); return await finalizeOneShotUpload(upload); } const upload = uploadService.mediaUpload({ bucketId: req.params.bucketId, objectId: objectId, dataRaw: await (0, request_1.reqBodyToBuffer)(req), authorization: req.header("authorization"), }); return await finalizeOneShotUpload(upload); }; const handleTokenRequest = (req, res) => { var _a, _b; if (!req.query.create_token && !req.query.delete_token) { return res.sendStatus(400); } const bucketId = req.params.bucketId; const decodedObjectId = decodeURIComponent(req.params.objectId); const authorization = req.header("authorization"); let metadata; if (req.query.create_token) { if (req.query.create_token !== "true") { return res.sendStatus(400); } try { metadata = storageLayer.createDownloadToken({ bucketId, decodedObjectId, authorization, }); } catch (err) { if (err instanceof errors_1.ForbiddenError) { return res.status(403).json({ error: { code: 403, message: `Missing admin credentials.`, }, }); } if (err instanceof errors_1.NotFoundError) { return res.sendStatus(404); } throw err; } } else { try { metadata = storageLayer.deleteDownloadToken({ bucketId, decodedObjectId, token: (_b = (_a = req.query["delete_token"]) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "", authorization, }); } catch (err) { if (err instanceof errors_1.ForbiddenError) { return res.status(403).json({ error: { code: 403, message: `Missing admin credentials.`, }, }); } if (err instanceof errors_1.NotFoundError) { return res.sendStatus(404); } throw err; } } setObjectHeaders(res, metadata); return res.json(new metadata_1.OutgoingFirebaseMetadata(metadata)); }; const handleObjectPostRequest = async (req, res) => { if (req.query.create_token || req.query.delete_token) { return handleTokenRequest(req, res); } return handleUpload(req, res); }; const handleMetadataUpdate = async (req, res) => { let metadata; try { metadata = await storageLayer.updateObjectMetadata({ bucketId: req.params.bucketId, decodedObjectId: decodeURIComponent(req.params.objectId), metadata: req.body, authorization: req.header("authorization"), }); } catch (err) { if (err instanceof errors_1.ForbiddenError) { return res.status(403).json({ error: { code: 403, message: `Permission denied. No WRITE permission.`, }, }); } if (err instanceof errors_1.NotFoundError) { return res.sendStatus(404); } throw err; } setObjectHeaders(res, metadata); return res.json(new metadata_1.OutgoingFirebaseMetadata(metadata)); }; firebaseStorageAPI.patch("/b/:bucketId/o/:objectId", handleMetadataUpdate); firebaseStorageAPI.put("/b/:bucketId/o/:objectId?", async (req, res) => { var _a; switch ((_a = req.header("x-http-method-override")) === null || _a === void 0 ? void 0 : _a.toLowerCase()) { case "patch": return handleMetadataUpdate(req, res); default: return handleObjectPostRequest(req, res); } }); firebaseStorageAPI.post("/b/:bucketId/o/:objectId?", handleObjectPostRequest); firebaseStorageAPI.delete("/b/:bucketId/o/:objectId", async (req, res) => { try { await storageLayer.deleteObject({ bucketId: req.params.bucketId, decodedObjectId: decodeURIComponent(req.params.objectId), authorization: req.header("authorization"), }); } catch (err) { if (err instanceof errors_1.ForbiddenError) { return res.status(403).json({ error: { code: 403, message: `Permission denied. No WRITE permission.`, }, }); } if (err instanceof errors_1.NotFoundError) { return res.sendStatus(404); } throw err; } res.sendStatus(204); }); firebaseStorageAPI.get("/", (req, res) => { res.json({ emulator: "storage" }); }); return firebaseStorageAPI; } exports.createFirebaseEndpoints = createFirebaseEndpoints; function setObjectHeaders(res, metadata) { if (metadata.contentDisposition) { res.setHeader("Content-Disposition", metadata.contentDisposition); } if (metadata.contentEncoding) { res.setHeader("Content-Encoding", metadata.contentEncoding); } if (metadata.cacheControl) { res.setHeader("Cache-Control", metadata.cacheControl); } if (metadata.contentLanguage) { res.setHeader("Content-Language", metadata.contentLanguage); } } function isValidPrefix(prefix) { return isValidNonEncodedPathString(removeAtMostOneTrailingSlash(prefix)); } function isValidNonEncodedPathString(path) { if (path.startsWith("/")) { path = path.substring(1); } if (!path) { return false; } for (const pathSegment of path.split("/")) { if (!pathSegment) { return false; } } return true; } function removeAtMostOneTrailingSlash(path) { return path.replace(/\/$/, ""); }