UNPKG

azurite

Version:

An open source Azure Storage API compatible server

307 lines 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const msRest = tslib_1.__importStar(require("@azure/ms-rest-js")); const glob_to_regexp_1 = tslib_1.__importDefault(require("glob-to-regexp")); const BlobStorageContext_1 = tslib_1.__importDefault(require("../context/BlobStorageContext")); const StorageErrorFactory_1 = tslib_1.__importDefault(require("../errors/StorageErrorFactory")); const Mappers = tslib_1.__importStar(require("../generated/artifacts/mappers")); const specifications_1 = tslib_1.__importDefault(require("../generated/artifacts/specifications")); const MiddlewareError_1 = tslib_1.__importDefault(require("../generated/errors/MiddlewareError")); const constants_1 = require("../utils/constants"); class PreflightMiddlewareFactory { constructor(logger) { this.logger = logger; } createOptionsHandlerMiddleware(metadataStore) { return (err, req, res, next) => { if (req.method.toUpperCase() === constants_1.MethodConstants.OPTIONS) { const context = new BlobStorageContext_1.default(res.locals, constants_1.DEFAULT_CONTEXT_PATH); const requestId = context.contextId; const account = context.account; this.logger.info(`PreflightMiddlewareFactory.createOptionsHandlerMiddleware(): OPTIONS request.`, requestId); const origin = req.header(constants_1.HeaderConstants.ORIGIN); if (origin === undefined || typeof origin !== "string") { return next(StorageErrorFactory_1.default.getInvalidCorsHeaderValue(requestId, { MessageDetails: `Invalid required CORS header Origin ${JSON.stringify(origin)}` })); } const requestMethod = req.header(constants_1.HeaderConstants.ACCESS_CONTROL_REQUEST_METHOD); if (requestMethod === undefined || typeof requestMethod !== "string") { return next(StorageErrorFactory_1.default.getInvalidCorsHeaderValue(requestId, { MessageDetails: `Invalid required CORS header Access-Control-Request-Method ${JSON.stringify(requestMethod)}` })); } const requestHeaders = req.headers[constants_1.HeaderConstants.ACCESS_CONTROL_REQUEST_HEADERS]; metadataStore .getServiceProperties(context, account) .then((properties) => { if (properties === undefined || properties.cors === undefined) { return next(StorageErrorFactory_1.default.corsPreflightFailure(requestId, { MessageDetails: "No CORS rules matches this request" })); } const corsSet = properties.cors; for (const cors of corsSet) { if (!this.checkOrigin(origin, cors.allowedOrigins) || !this.checkMethod(requestMethod, cors.allowedMethods)) { continue; } if (requestHeaders !== undefined && !this.checkHeaders(requestHeaders, cors.allowedHeaders || "")) { continue; } res.setHeader(constants_1.HeaderConstants.ACCESS_CONTROL_ALLOW_ORIGIN, origin); res.setHeader(constants_1.HeaderConstants.ACCESS_CONTROL_ALLOW_METHODS, requestMethod); if (requestHeaders !== undefined) { res.setHeader(constants_1.HeaderConstants.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders); } res.setHeader(constants_1.HeaderConstants.ACCESS_CONTROL_MAX_AGE, cors.maxAgeInSeconds); res.setHeader(constants_1.HeaderConstants.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); return next(); } return next(StorageErrorFactory_1.default.corsPreflightFailure(requestId, { MessageDetails: "No CORS rules matches this request" })); }) .catch(next); } else { next(err); } }; } createCorsRequestMiddleware(metadataStore, blockErrorRequest = false) { const internalMethod = (err, req, res, next) => { if (req.method.toUpperCase() === constants_1.MethodConstants.OPTIONS) { return next(err); } const context = new BlobStorageContext_1.default(res.locals, constants_1.DEFAULT_CONTEXT_PATH); const account = context.account; const origin = req.headers[constants_1.HeaderConstants.ORIGIN]; if (origin === undefined) { return next(err); } const method = req.method; if (method === undefined || typeof method !== "string") { return next(err); } metadataStore .getServiceProperties(context, account) .then((properties) => { if (properties === undefined || properties.cors === undefined) { return next(err); } const corsSet = properties.cors; const resHeaders = this.getResponseHeaders(res, err instanceof MiddlewareError_1.default ? err : undefined); // Here we will match CORS settings in order and select first matched CORS for (const cors of corsSet) { if (this.checkOrigin(origin, cors.allowedOrigins) && this.checkMethod(method, cors.allowedMethods)) { const exposedHeaders = this.getExposedHeaders(resHeaders, cors.exposedHeaders || ""); res.setHeader(constants_1.HeaderConstants.ACCESS_CONTROL_EXPOSE_HEADERS, exposedHeaders); res.setHeader(constants_1.HeaderConstants.ACCESS_CONTROL_ALLOW_ORIGIN, cors.allowedOrigins === "*" ? "*" : origin // origin is not undefined as checked in checkOrigin() ); if (cors.allowedOrigins !== "*") { res.setHeader(constants_1.HeaderConstants.VARY, "Origin"); res.setHeader(constants_1.HeaderConstants.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); } return next(err); } } if (corsSet.length > 0) { res.setHeader(constants_1.HeaderConstants.VARY, "Origin"); } return next(err); }) .catch(next); }; if (blockErrorRequest) { return internalMethod; } else { return (req, res, next) => { internalMethod(undefined, req, res, next); }; } } checkOrigin(origin, allowedOrigin) { if (allowedOrigin === "*") { return true; } if (origin === undefined) { return false; } const allowedOriginArray = allowedOrigin.split(","); for (const corsOrigin of allowedOriginArray) { if (corsOrigin.includes("*")) { return (0, glob_to_regexp_1.default)(corsOrigin.trim().toLowerCase()).test(origin.trim().toLowerCase()); } if (origin.trim().toLowerCase() === corsOrigin.trim().toLowerCase()) { return true; } } return false; } checkMethod(method, allowedMethod) { const allowedMethodArray = allowedMethod.split(","); for (const corsMethod of allowedMethodArray) { if (method.trim().toLowerCase() === corsMethod.trim().toLowerCase()) { return true; } } return false; } checkHeaders(headers, allowedHeaders) { const headersArray = headers.split(","); const allowedHeadersArray = allowedHeaders.split(","); for (const header of headersArray) { let flag = false; const trimmedHeader = header.trim().toLowerCase(); for (const allowedHeader of allowedHeadersArray) { // TODO: Should remove the wrapping blank when set CORS through set properties for service. const trimmedAllowedHeader = allowedHeader.trim().toLowerCase(); if (trimmedHeader === trimmedAllowedHeader || (trimmedAllowedHeader[trimmedAllowedHeader.length - 1] === "*" && trimmedHeader.startsWith(trimmedAllowedHeader.substr(0, trimmedAllowedHeader.length - 1)))) { flag = true; break; } } if (flag === false) { return false; } } return true; } getResponseHeaders(res, err) { const responseHeaderSet = []; const context = new BlobStorageContext_1.default(res.locals, constants_1.DEFAULT_CONTEXT_PATH); const handlerResponse = context.handlerResponses; if (handlerResponse && context.operation) { const statusCodeInResponse = handlerResponse.statusCode; const spec = specifications_1.default[context.operation]; const responseSpec = spec.responses[statusCodeInResponse]; if (!responseSpec) { throw new TypeError(`Request specification doesn't include provided response status code`); } // Serialize headers const headerSerializer = new msRest.Serializer(Mappers); const headersMapper = responseSpec.headersMapper; if (headersMapper && headersMapper.type.name === "Composite") { const mappersForAllHeaders = headersMapper.type.modelProperties || {}; // Handle headerMapper one by one for (const key in mappersForAllHeaders) { if (mappersForAllHeaders.hasOwnProperty(key)) { const headerMapper = mappersForAllHeaders[key]; const headerName = headerMapper.serializedName; const headerValueOriginal = handlerResponse[key]; const headerValueSerialized = headerSerializer.serialize(headerMapper, headerValueOriginal); // Handle collection of headers starting with same prefix, such as x-ms-meta prefix const headerCollectionPrefix = headerMapper.headerCollectionPrefix; if (headerCollectionPrefix !== undefined && headerValueOriginal !== undefined) { for (const collectionHeaderPartialName in headerValueSerialized) { if (headerValueSerialized.hasOwnProperty(collectionHeaderPartialName)) { const collectionHeaderValueSerialized = headerValueSerialized[collectionHeaderPartialName]; const collectionHeaderName = `${headerCollectionPrefix}${collectionHeaderPartialName}`; if (collectionHeaderName && collectionHeaderValueSerialized !== undefined) { responseHeaderSet.push(collectionHeaderName); } } } } else { if (headerName && headerValueSerialized !== undefined) { responseHeaderSet.push(headerName); } } } } } if (spec.isXML && responseSpec.bodyMapper && responseSpec.bodyMapper.type.name !== "Stream") { responseHeaderSet.push("content-type"); responseHeaderSet.push("content-length"); } else if (handlerResponse.body && responseSpec.bodyMapper && responseSpec.bodyMapper.type.name === "Stream") { responseHeaderSet.push("content-length"); } } const headers = res.getHeaders(); for (const header in headers) { if (typeof header === "string") { responseHeaderSet.push(header); } } if (err) { for (const key in err.headers) { if (err.headers.hasOwnProperty(key)) { responseHeaderSet.push(key); } } } // TODO: Should extract the header by some policy. // or apply a referred list indicates the related headers. responseHeaderSet.push("Date"); responseHeaderSet.push("Connection"); responseHeaderSet.push("Transfer-Encoding"); return responseHeaderSet; } getExposedHeaders(responseHeaders, exposedHeaders) { const exposedHeaderRules = exposedHeaders.split(","); const prefixRules = []; const simpleHeaders = []; for (let i = 0; i < exposedHeaderRules.length; i++) { exposedHeaderRules[i] = exposedHeaderRules[i].trim(); if (exposedHeaderRules[i].endsWith("*")) { prefixRules.push(exposedHeaderRules[i] .substr(0, exposedHeaderRules[i].length - 1) .toLowerCase()); } else { simpleHeaders.push(exposedHeaderRules[i]); } } const resExposedHeaders = []; for (const header of responseHeaders) { let isMatch = false; for (const rule of prefixRules) { if (header.toLowerCase().startsWith(rule)) { isMatch = true; break; } } if (!isMatch) { for (const simpleHeader of simpleHeaders) { if (header.toLowerCase() === simpleHeader.toLowerCase()) { isMatch = true; break; } } } if (isMatch) { resExposedHeaders.push(header); } } for (const simpleHeader of simpleHeaders) { let isMatch = false; for (const header of resExposedHeaders) { if (simpleHeader.toLowerCase() === header.toLowerCase()) { isMatch = true; break; } } if (!isMatch) { resExposedHeaders.push(simpleHeader); } } return resExposedHeaders.join(","); } } exports.default = PreflightMiddlewareFactory; //# sourceMappingURL=PreflightMiddlewareFactory.js.map