azurite
Version:
An open source Azure Storage API compatible server
307 lines • 15.2 kB
JavaScript
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
;