Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/blob/errors/StorageErrorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,43 @@ export default class StorageErrorFactory {
);
}

public static getCrc64Mismatch(
contextID: string = DefaultID,
userSpecifiedCrc64: string,
serverCalculatedCrc64: string
): StorageError {
return new StorageError(
400,
"Crc64Mismatch",
"The CRC64 value specified in the request did not match with the CRC64 value calculated by the server.",
contextID,
{
UserSpecifiedCrc64: userSpecifiedCrc64,
ServerCalculatedCrc64: serverCalculatedCrc64
}
);
}

public static getBothCrc64AndMd5HeaderPresent(
contextID: string = DefaultID
): StorageError {
return new StorageError(
400,
"BothCrc64AndMd5HeaderPresent",
"Both x-ms-content-crc64 header and Content-MD5 header are present.",
contextID
);
}

public static getInvalidMd5(contextID: string = DefaultID): StorageError {
return new StorageError(
400,
"InvalidMd5",
"The MD5 value specified in the request is invalid. The MD5 value must be 128 bits and Base64-encoded.",
contextID
);
}

public static getInvalidPageRange(contextID: string): StorageError {
return new StorageError(
416,
Expand Down
44 changes: 18 additions & 26 deletions src/blob/handlers/AppendBlobHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { convertRawHeadersToMetadata } from "../../common/utils/utils";
import { getMD5FromStream, newEtag } from "../../common/utils/utils";
import {
convertRawHeadersToMetadata,
newEtag
} from "../../common/utils/utils";
import BlobStorageContext from "../context/BlobStorageContext";
import NotImplementedError from "../errors/NotImplementedError";
import StorageErrorFactory from "../errors/StorageErrorFactory";
Expand All @@ -13,7 +15,7 @@ import {
MAX_APPEND_BLOB_BLOCK_COUNT,
MAX_APPEND_BLOB_BLOCK_SIZE
} from "../utils/constants";
import { getTagsFromString } from "../utils/utils";
import { computeAndValidateTransactionalChecksums, getTagsFromString } from "../utils/utils";
import BaseHandler from "./BaseHandler";

export default class AppendBlobHandler extends BaseHandler
Expand Down Expand Up @@ -149,38 +151,28 @@ export default class AppendBlobHandler extends BaseHandler
);
}

// MD5
// MD5 and/or CRC64 transactional integrity validation
const contentMD5 = blobCtx.request!.getHeader(HeaderConstants.CONTENT_MD5);
const contentCRC64 = options.transactionalContentCrc64;
let contentMD5Buffer;
let contentMD5String;

if (contentMD5 !== undefined) {
contentMD5Buffer =
typeof contentMD5 === "string"
? Buffer.from(contentMD5, "base64")
: contentMD5;
contentMD5String =
typeof contentMD5 === "string"
? contentMD5
: contentMD5Buffer.toString("base64");
}

const stream = await this.extentStore.readExtent(
extent,
blobCtx.contextId
// Per the Append Block REST contract, the service always computes a CRC64
// of the appended block and returns it in x-ms-content-crc64.
const stream = await this.extentStore.readExtent(extent, blobCtx.contextId);
const { crc64: calculatedCRC64 } =
await computeAndValidateTransactionalChecksums(
stream,
{ md5: contentMD5, crc64: contentCRC64 },
context.contextId,
{ crc64: true }
);
const calculatedContentMD5Buffer = await getMD5FromStream(stream);
const calculatedContentMD5String = Buffer.from(
calculatedContentMD5Buffer
).toString("base64");

if (contentMD5String !== calculatedContentMD5String) {
throw StorageErrorFactory.getMd5Mismatch(
context.contextId,
contentMD5String,
calculatedContentMD5String
);
}
}

const originOffset = blob.properties.contentLength;

Expand All @@ -206,7 +198,7 @@ export default class AppendBlobHandler extends BaseHandler
eTag: properties.etag,
lastModified: properties.lastModified,
contentMD5: contentMD5Buffer,
xMsContentCrc64: undefined,
xMsContentCrc64: calculatedCRC64,
clientRequestId: options.requestId,
version: BLOB_API_VERSION,
date,
Expand Down
3 changes: 3 additions & 0 deletions src/blob/handlers/BlobHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,9 @@ export default class BlobHandler extends BaseHandler implements IBlobHandler {
date: context.startTime,
copyId: res.copyId,
copyStatus,
// Per the Copy Blob From URL REST contract, echo the source's Content-MD5
// back to the client when it was supplied in x-ms-source-content-md5.
contentMD5: options.sourceContentMD5,
clientRequestId: options.requestId
};

Expand Down
122 changes: 63 additions & 59 deletions src/blob/handlers/BlockBlobHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { convertRawHeadersToMetadata } from "../../common/utils/utils";
import {
getMD5FromStream,
convertRawHeadersToMetadata,
getMD5FromString,
newEtag
} from "../../common/utils/utils";
Expand All @@ -14,7 +13,11 @@ import { parseXML } from "../generated/utils/xml";
import { BlobModel, BlockModel } from "../persistence/IBlobMetadataStore";
import { BLOB_API_VERSION } from "../utils/constants";
import BaseHandler from "./BaseHandler";
import { getTagsFromString } from "../utils/utils";
import {
computeAndValidateTransactionalChecksums,
getTagsFromString,
isValidMd5Header
} from "../utils/utils";

/**
* BlobHandler handles Azure Storage BlockBlob related requests.
Expand Down Expand Up @@ -45,11 +48,33 @@ export default class BlockBlobHandler
options.blobHTTPHeaders.blobContentType ||
context.request!.getHeader("content-type") ||
"application/octet-stream";
const contentMD5 = context.request!.getHeader("content-md5")
|| context.request!.getHeader("x-ms-blob-content-md5")
? options.blobHTTPHeaders.blobContentMD5 ||
context.request!.getHeader("content-md5")
: undefined;

// x-ms-blob-content-md5 is a blob property header (stored as the blob's
// contentMD5 metadata). Real Azure rejects malformed values with
// InvalidHeaderValue (HTTP 400). Validate format here - the transactional
// helper validates Content-MD5 separately with InvalidMd5.
const blobContentMD5Header = context.request!.getHeader("x-ms-blob-content-md5");
if (
typeof blobContentMD5Header === "string" &&
!isValidMd5Header(blobContentMD5Header)
) {
throw StorageErrorFactory.getInvalidHeaderValue(context.contextId!, {
HeaderName: "x-ms-blob-content-md5",
HeaderValue: blobContentMD5Header
});
}

// Per the Put Blob REST contract, x-ms-blob-content-md5 takes precedence
// over Content-MD5 for transit integrity verification on BlockBlob.
// Verified live. Prefer the SDK-parsed blobContentMD5 option; fall back
// to the raw x-ms-blob-content-md5 header (for clients that inject it
// directly without going through the SDK option); finally fall back to
// Content-MD5.
const contentMD5 =
options.blobHTTPHeaders.blobContentMD5
?? blobContentMD5Header
?? context.request!.getHeader("content-md5");
const contentCRC64 = options.transactionalContentCrc64;

await this.metadataStore.checkContainerExist(
context,
Expand All @@ -68,32 +93,19 @@ export default class BlockBlobHandler
);
}

// Calculate MD5 for validation
// MD5 is always needed (persisted as the blob's contentMD5 property);
// CRC64 is computed in the same pass only when the client supplied one.
const stream = await this.extentStore.readExtent(
persistency,
context.contextId
);
const calculatedContentMD5 = await getMD5FromStream(stream);
if (contentMD5 !== undefined) {
if (typeof contentMD5 === "string") {
const calculatedContentMD5String = Buffer.from(
calculatedContentMD5
).toString("base64");
if (contentMD5 !== calculatedContentMD5String) {
throw StorageErrorFactory.getInvalidOperation(
context.contextId!,
"Provided contentMD5 doesn't match."
);
}
} else {
if (!Buffer.from(contentMD5).equals(calculatedContentMD5)) {
throw StorageErrorFactory.getInvalidOperation(
context.contextId!,
"Provided contentMD5 doesn't match."
);
}
}
}
const { md5: calculatedContentMD5 } =
await computeAndValidateTransactionalChecksums(
stream,
{ md5: contentMD5, crc64: contentCRC64 },
context.contextId,
{ md5: true }
);

const blob: BlobModel = {
deleted: false,
Expand Down Expand Up @@ -179,14 +191,15 @@ export default class BlockBlobHandler
const blobName = blobCtx.blob!;
const date = blobCtx.startTime!;

// stageBlock operation doesn't have blobHTTPHeaders
// stageBlock operation doesn't accept blob property headers per the
// Put Block REST contract: only Content-MD5 and x-ms-content-crc64 are
// honored. Verified live: real Azure silently ignores x-ms-blob-content-md5
// here (even malformed values), so don't use it as a fallback source.
// https://learn.microsoft.com/en-us/rest/api/storageservices/put-block
// options.blobHTTPHeaders = options.blobHTTPHeaders || {};
const contentMD5 = context.request!.getHeader("content-md5")
|| context.request!.getHeader("x-ms-blob-content-md5")
? options.transactionalContentMD5 ||
context.request!.getHeader("content-md5")
: undefined;
const contentMD5 =
options.transactionalContentMD5 ||
context.request!.getHeader("content-md5");
const contentCRC64 = options.transactionalContentCrc64;

this.validateBlockId(blockId, blobCtx);

Expand All @@ -208,32 +221,22 @@ export default class BlockBlobHandler
);
}

// Calculate MD5 for validation
// Per the Put Block REST contract, the service computes a CRC64 of the
// staged block and echoes it back in x-ms-content-crc64 unless the client
// supplied a Content-MD5 (Azure rejects supplying both). Compute CRC64
// whenever no MD5 was supplied, regardless of whether the client supplied
// a CRC64 themselves.
const stream = await this.extentStore.readExtent(
persistency,
context.contextId
);
const calculatedContentMD5 = await getMD5FromStream(stream);
if (contentMD5 !== undefined) {
if (typeof contentMD5 === "string") {
const calculatedContentMD5String = Buffer.from(
calculatedContentMD5
).toString("base64");
if (contentMD5 !== calculatedContentMD5String) {
throw StorageErrorFactory.getInvalidOperation(
context.contextId!,
"Provided contentMD5 doesn't match."
);
}
} else {
if (!Buffer.from(contentMD5).equals(calculatedContentMD5)) {
throw StorageErrorFactory.getInvalidOperation(
context.contextId!,
"Provided contentMD5 doesn't match."
);
}
}
}
const { crc64: calculatedCRC64 } =
await computeAndValidateTransactionalChecksums(
stream,
{ md5: contentMD5, crc64: contentCRC64 },
context.contextId,
{ crc64: contentMD5 === undefined }
);

const block: BlockModel = {
accountName,
Expand All @@ -255,6 +258,7 @@ export default class BlockBlobHandler
const response: Models.BlockBlobStageBlockResponse = {
statusCode: 201,
contentMD5: undefined, // TODO: Block content MD5
xMsContentCrc64: calculatedCRC64,
requestId: blobCtx.contextId,
version: BLOB_API_VERSION,
date,
Expand Down
28 changes: 25 additions & 3 deletions src/blob/handlers/PageBlobHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import BlobWriteLeaseValidator from "../lease/BlobWriteLeaseValidator";
import IBlobMetadataStore, {
BlobModel
} from "../persistence/IBlobMetadataStore";
import { BLOB_API_VERSION } from "../utils/constants";
import { deserializePageBlobRangeHeader, getTagsFromString } from "../utils/utils";
import { BLOB_API_VERSION, HeaderConstants } from "../utils/constants";
import {
computeAndValidateTransactionalChecksums,
deserializePageBlobRangeHeader,
getTagsFromString
} from "../utils/utils";
import BaseHandler from "./BaseHandler";
import IPageBlobRangesManager from "./IPageBlobRangesManager";

Expand Down Expand Up @@ -236,6 +240,19 @@ export default class PageBlobHandler extends BaseHandler
);
}

// Transactional integrity validation. Real Azure always returns a
// server-computed x-ms-content-crc64 on Put Page; force CRC64 always.
const contentMD5 = blobCtx.request!.getHeader(HeaderConstants.CONTENT_MD5);
const contentCRC64 = options.transactionalContentCrc64;
const stream = await this.extentStore.readExtent(persistency, blobCtx.contextId);
const { crc64: calculatedCRC64 } =
await computeAndValidateTransactionalChecksums(
stream,
{ md5: contentMD5, crc64: contentCRC64 },
context.contextId,
{ crc64: true }
);

const res = await this.metadataStore.uploadPages(
context,
blob,
Expand All @@ -251,7 +268,12 @@ export default class PageBlobHandler extends BaseHandler
statusCode: 201,
eTag: res.etag,
lastModified: date,
contentMD5: undefined, // TODO
contentMD5: contentMD5 === undefined
? undefined
: typeof contentMD5 === "string"
? Buffer.from(contentMD5, "base64")
: contentMD5,
xMsContentCrc64: calculatedCRC64,
blobSequenceNumber: res.blobSequenceNumber,
requestId: blobCtx.contextId,
version: BLOB_API_VERSION,
Expand Down
1 change: 0 additions & 1 deletion src/blob/middlewares/StrictModelMiddlewareFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export const UnsupportedHeadersBlocker: StrictModelRequestValidator = async (
logger: ILogger
): Promise<void> => {
const UnsupportedHeaderKeys = [
HeaderConstants.X_MS_CONTENT_CRC64,
HeaderConstants.X_MS_RANGE_GET_CONTENT_CRC64,
HeaderConstants.X_MS_ENCRYPTION_KEY,
HeaderConstants.X_MS_ENCRYPTION_KEY_SHA256,
Expand Down
Loading