export function createFirebaseEndpoints()

in src/emulator/storage/apis/firebase.ts [54:628]


export function createFirebaseEndpoints(emulator: StorageEmulator): Router {
  // eslint-disable-next-line new-cap
  const firebaseStorageAPI = Router();
  const { storageLayer } = emulator;

  if (process.env.STORAGE_EMULATOR_DEBUG) {
    firebaseStorageAPI.use((req, res, next) => {
      console.log("--------------INCOMING 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: any[]) => {
        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((req, res, next) => {
    if (!emulator.rules) {
      EmulatorLogger.forEmulator(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();
  });

  // Automatically create a bucket for any route which uses a bucket
  firebaseStorageAPI.use(/.*\/b\/(.+?)\/.*/, (req, res, next) => {
    storageLayer.createBucket(req.params[0]);
    next();
  });

  firebaseStorageAPI.get("/b/:bucketId/o/:objectId", async (req, res) => {
    const decodedObjectId = decodeURIComponent(req.params.objectId);
    const operationPath = ["b", req.params.bucketId, "o", decodedObjectId].join("/");
    const md = storageLayer.getMetadata(req.params.bucketId, decodedObjectId);

    const rulesFiles: {
      before?: RulesResourceMetadata;
    } = {};

    if (md) {
      rulesFiles.before = md.asRulesResource();
    }

    // Query values are used for GETs from Web SDKs
    const isPermittedViaHeader = await isPermitted({
      ruleset: emulator.rules,
      method: RulesetOperationMethod.GET,
      path: operationPath,
      file: rulesFiles,
      authorization: req.header("authorization"),
    });

    // Token headers are used for GETs from Mobile SDKs
    const isPermittedViaToken =
      req.query.token && md && md.downloadTokens.includes(req.query.token.toString());

    const isRequestPermitted: boolean = isPermittedViaHeader || !!isPermittedViaToken;

    if (!isRequestPermitted) {
      res.sendStatus(403);
      return;
    }

    if (!md) {
      res.sendStatus(404);
      return;
    }

    let isGZipped = false;
    if (md.contentEncoding == "gzip") {
      isGZipped = true;
    }

    if (req.query.alt == "media") {
      let data = storageLayer.getBytes(req.params.bucketId, req.params.objectId);
      if (!data) {
        res.sendStatus(404);
        return;
      }

      if (isGZipped) {
        data = gunzipSync(data);
      }

      res.setHeader("Accept-Ranges", "bytes");
      res.setHeader("Content-Type", md.contentType);
      setObjectHeaders(res, md, { "Content-Encoding": isGZipped ? "identity" : undefined });

      const byteRange = [...(req.header("range") || "").split("bytes="), "", ""];

      const [rangeStart, rangeEnd] = byteRange[1].split("-");

      if (rangeStart) {
        const range = {
          start: parseInt(rangeStart),
          end: rangeEnd ? parseInt(rangeEnd) : data.byteLength,
        };
        res.setHeader("Content-Range", `bytes ${range.start}-${range.end - 1}/${data.byteLength}`);
        res.status(206).end(data.slice(range.start, range.end));
      } else {
        res.end(data);
      }

      return;
    }

    if (!md.downloadTokens.length) {
      md.addDownloadToken();
    }

    res.json(new OutgoingFirebaseMetadata(md));
  });

  const handleMetadataUpdate = async (req: Request, res: Response) => {
    const md = storageLayer.getMetadata(req.params.bucketId, req.params.objectId);

    if (!md) {
      res.sendStatus(404);
      return;
    }

    const decodedObjectId = decodeURIComponent(req.params.objectId);
    const operationPath = ["b", req.params.bucketId, "o", decodedObjectId].join("/");

    if (
      !(await isPermitted({
        ruleset: emulator.rules,
        method: RulesetOperationMethod.UPDATE,
        path: operationPath,
        authorization: req.header("authorization"),
        file: {
          before: md.asRulesResource(),
          after: md.asRulesResource(req.body), // TODO
        },
      }))
    ) {
      return res.status(403).json({
        error: {
          code: 403,
          message: `Permission denied. No WRITE permission.`,
        },
      });
    }

    md.update(req.body);

    setObjectHeaders(res, md);
    const outgoingMetadata = new OutgoingFirebaseMetadata(md);
    res.json(outgoingMetadata);
    return;
  };

  // list object handler
  firebaseStorageAPI.get("/b/:bucketId/o", async (req, res) => {
    let maxRes = undefined;
    if (req.query.maxResults) {
      maxRes = +req.query.maxResults.toString();
    }
    const delimiter = req.query.delimiter ? req.query.delimiter.toString() : "/";
    const pageToken = req.query.pageToken ? req.query.pageToken.toString() : undefined;
    const prefix = req.query.prefix ? req.query.prefix.toString() : "";

    const operationPath = ["b", req.params.bucketId, "o", prefix].join("/");

    if (
      !(await isPermitted({
        ruleset: emulator.rules,
        method: RulesetOperationMethod.LIST,
        path: operationPath,
        file: {},
        authorization: req.header("authorization"),
      }))
    ) {
      return res.status(403).json({
        error: {
          code: 403,
          message: `Permission denied. No LIST permission.`,
        },
      });
    }

    res.json(
      storageLayer.listItemsAndPrefixes(req.params.bucketId, prefix, delimiter, pageToken, maxRes)
    );
  });

  const handleUpload = async (req: Request, res: Response) => {
    if (req.query.create_token || req.query.delete_token) {
      const decodedObjectId = decodeURIComponent(req.params.objectId);
      const operationPath = ["b", req.params.bucketId, "o", decodedObjectId].join("/");

      const mdBefore = storageLayer.getMetadata(req.params.bucketId, req.params.objectId);

      if (
        !(await isPermitted({
          ruleset: emulator.rules,
          method: RulesetOperationMethod.UPDATE,
          path: operationPath,
          authorization: req.header("authorization"),
          file: {
            before: mdBefore?.asRulesResource(),
            // TODO: before and after w/ metadata change
          },
        }))
      ) {
        return res.status(403).json({
          error: {
            code: 403,
            message: `Permission denied. No WRITE permission.`,
          },
        });
      }

      if (!mdBefore) {
        return res.status(404).json({
          error: {
            code: 404,
            message: `Request object can not be found`,
          },
        });
      }

      const createTokenParam = req.query["create_token"];
      const deleteTokenParam = req.query["delete_token"];
      let md: StoredFileMetadata | undefined;

      if (createTokenParam) {
        if (createTokenParam != "true") {
          res.sendStatus(400);
          return;
        }
        md = storageLayer.addDownloadToken(req.params.bucketId, req.params.objectId);
      } else if (deleteTokenParam) {
        md = storageLayer.deleteDownloadToken(
          req.params.bucketId,
          req.params.objectId,
          deleteTokenParam.toString()
        );
      }

      if (!md) {
        res.sendStatus(404);
        return;
      }

      setObjectHeaders(res, md);
      return res.json(new OutgoingFirebaseMetadata(md));
    }

    if (!req.query.name) {
      res.sendStatus(400);
      return;
    }

    const name = req.query.name.toString();
    const uploadType = req.header("x-goog-upload-protocol");

    if (uploadType == "multipart") {
      const contentType = req.header("content-type");
      if (!contentType || !contentType.startsWith("multipart/related")) {
        res.sendStatus(400);
        return;
      }

      const boundary = `--${contentType.split("boundary=")[1]}`;
      const bodyString = req.body.toString();
      const bodyStringParts = bodyString.split(boundary).filter((v: string) => v);

      const metadataString = bodyStringParts[0].split("\r\n")[3];
      const blobParts = bodyStringParts[1].split("\r\n");
      const blobContentTypeString = blobParts[1];
      if (!blobContentTypeString || !blobContentTypeString.startsWith("Content-Type: ")) {
        res.sendStatus(400);
        return;
      }
      const blobContentType = blobContentTypeString.slice("Content-Type: ".length);
      const bodyBuffer = req.body as Buffer;

      const metadataSegment = `${boundary}${bodyString.split(boundary)[1]}`;
      const dataSegment = `${boundary}${bodyString.split(boundary).slice(2)[0]}`;
      const dataSegmentHeader = (dataSegment.match(/.+Content-Type:.+?\r\n\r\n/s) || [])[0];

      if (!dataSegmentHeader) {
        res.sendStatus(400);
        return;
      }

      const bufferOffset = metadataSegment.length + dataSegmentHeader.length;

      const blobBytes = Buffer.from(bodyBuffer.slice(bufferOffset, -`\r\n${boundary}--`.length));
      const md = storageLayer.oneShotUpload(
        req.params.bucketId,
        name,
        blobContentType,
        JSON.parse(metadataString),
        Buffer.from(blobBytes)
      );

      if (!md) {
        res.sendStatus(400);
        return;
      }

      const operationPath = ["b", req.params.bucketId, "o", name].join("/");

      if (
        !(await isPermitted({
          ruleset: emulator.rules,
          // TODO: This will be either create or update
          method: RulesetOperationMethod.CREATE,
          path: operationPath,
          authorization: req.header("authorization"),
          file: {
            after: md?.asRulesResource(),
          },
        }))
      ) {
        storageLayer.deleteFile(md?.bucket, md?.name);
        return res.status(403).json({
          error: {
            code: 403,
            message: `Permission denied. No WRITE permission.`,
          },
        });
      }

      if (md.downloadTokens.length == 0) {
        md.addDownloadToken();
      }

      res.json(new OutgoingFirebaseMetadata(md));
      return;
    } else {
      const operationPath = ["b", req.params.bucketId, "o", name].join("/");
      const uploadCommand = req.header("x-goog-upload-command");
      if (!uploadCommand) {
        res.sendStatus(400);
        return;
      }

      if (uploadCommand == "start") {
        let objectContentType =
          req.header("x-goog-upload-header-content-type") ||
          req.header("x-goog-upload-content-type");
        if (!objectContentType) {
          const mimeTypeFromName = mime.getType(name);
          if (!mimeTypeFromName) {
            objectContentType = "application/octet-stream";
          } else {
            objectContentType = mimeTypeFromName;
          }
        }

        const upload = storageLayer.startUpload(
          req.params.bucketId,
          name,
          objectContentType,
          req.body
        );

        storageLayer.uploadBytes(upload.uploadId, Buffer.alloc(0));

        const emulatorInfo = EmulatorRegistry.getInfo(Emulators.STORAGE);

        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-goog-upload-url",
          `http://${req.hostname}:${emulatorInfo?.port}/v0/b/${req.params.bucketId}/o?name=${req.query.name}&upload_id=${upload.uploadId}&upload_protocol=resumable`
        );
        res.header("x-gupload-uploadid", upload.uploadId);

        res.status(200).send();
        return;
      }

      if (!req.query.upload_id) {
        res.sendStatus(400);
        return;
      }

      const uploadId = req.query.upload_id.toString();
      if (uploadCommand == "query") {
        const upload = storageLayer.queryUpload(uploadId);
        if (!upload) {
          res.sendStatus(400);
          return;
        }

        res.header("X-Goog-Upload-Size-Received", upload.currentBytesUploaded.toString());
        res.sendStatus(200);
        return;
      }

      if (uploadCommand == "cancel") {
        const upload = storageLayer.cancelUpload(uploadId);
        if (!upload) {
          res.sendStatus(400);
          return;
        }
        res.sendStatus(200);
        return;
      }

      let upload;
      if (uploadCommand.includes("upload")) {
        if (!(req.body instanceof Buffer)) {
          const bufs: Buffer[] = [];
          req.on("data", (data) => {
            bufs.push(data);
          });

          await new Promise<void>((resolve) => {
            req.on("end", () => {
              req.body = Buffer.concat(bufs);
              resolve();
            });
          });
        }

        upload = storageLayer.uploadBytes(uploadId, req.body);

        if (!upload) {
          res.sendStatus(400);
          return;
        }

        res.header("x-goog-upload-status", "active");
        res.header("x-gupload-uploadid", upload.uploadId);
      }

      if (uploadCommand.includes("finalize")) {
        const finalizedUpload = storageLayer.finalizeUpload(uploadId);
        if (!finalizedUpload) {
          res.sendStatus(400);
          return;
        }
        upload = finalizedUpload.upload;

        res.header("x-goog-upload-status", "final");

        // For resumable uploads, we check auth on finalization in case of byte-dependant rules
        if (
          !(await isPermitted({
            ruleset: emulator.rules,
            // TODO This will be either create or update
            method: RulesetOperationMethod.CREATE,
            path: operationPath,
            authorization: req.header("authorization"),
            file: {
              after: storageLayer.getMetadata(req.params.bucketId, name)?.asRulesResource(),
            },
          }))
        ) {
          storageLayer.deleteFile(upload.bucketId, name);
          return res.status(403).json({
            error: {
              code: 403,
              message: `Permission denied. No WRITE permission.`,
            },
          });
        }

        const md = finalizedUpload.file.metadata;
        if (md.downloadTokens.length == 0) {
          md.addDownloadToken();
        }

        res.json(new OutgoingFirebaseMetadata(finalizedUpload.file.metadata));
      } else if (!upload) {
        res.sendStatus(400);
        return;
      } else {
        res.sendStatus(200);
      }
    }
  };

  // update metata handler
  firebaseStorageAPI.patch("/b/:bucketId/o/:objectId", handleMetadataUpdate);
  firebaseStorageAPI.put("/b/:bucketId/o/:objectId?", async (req, res) => {
    switch (req.header("x-http-method-override")?.toLowerCase()) {
      case "patch":
        return handleMetadataUpdate(req, res);
      default:
        return handleUpload(req, res);
    }
  });
  firebaseStorageAPI.post("/b/:bucketId/o/:objectId?", handleUpload);

  firebaseStorageAPI.delete("/b/:bucketId/o/:objectId", async (req, res) => {
    const decodedObjectId = decodeURIComponent(req.params.objectId);
    const operationPath = ["b", req.params.bucketId, "o", decodedObjectId].join("/");

    if (
      !(await isPermitted({
        ruleset: emulator.rules,
        method: RulesetOperationMethod.DELETE,
        path: operationPath,
        authorization: req.header("authorization"),
        file: {
          // TODO load before metadata
        },
      }))
    ) {
      return res.status(403).json({
        error: {
          code: 403,
          message: `Permission denied. No WRITE permission.`,
        },
      });
    }

    const md = storageLayer.getMetadata(req.params.bucketId, decodedObjectId);

    if (!md) {
      res.sendStatus(404);
      return;
    }

    storageLayer.deleteFile(req.params.bucketId, req.params.objectId);
    res.sendStatus(200);
  });

  firebaseStorageAPI.get("/", (req, res) => {
    res.json({ emulator: "storage" });
  });

  return firebaseStorageAPI;
}