Skip to content
Closed
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
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Blob:
- Fixed service- and container-level Filter Blobs requests failing when the optional `where` query parameter is omitted.
- Fixed blob operations hanging when a client disconnects before the operation queue processes the request. (issue #2575)
- Implement `PutBlobFromUrl` (`Put Blob From URL`), which previously returned 501. The source is fetched over loopback, as `PutBlockFromURL` already does, so that SAS authentication and the `x-ms-source-if-*` conditions are enforced by the existing download path. Standard blob properties are copied from the source unless `x-ms-copy-source-blob-properties` is false, request blob content headers override them either way, request metadata replaces the source's rather than adding to it, and `x-ms-copy-source-tag-option: COPY` reads the source's tags over that same authorized path. An `x-ms-source-content-md5`, `x-ms-blob-content-md5`, `Content-MD5`, or `x-ms-content-crc64` header is checked against the copied content, and the response reports the MD5 and CRC64 of that content. A SAS needs Create or Write to create the blob, Write to overwrite it, and Tag as well when the request sets tags with `x-ms-tags` or copies the source's. As with `CopyBlobFromURL`, only sources on the same Azurite instance are supported.
- Fixed `PutBlobFromUrl` and `PutBlockFromURL` ignoring `x-ms-copy-source-authorization`. The header is now sent as the authorization of the copy source read, including the tags read for `x-ms-copy-source-tag-option: COPY`, so a bearer token authorizes a private source as on Azure. Bearer scheme casing and multiple separator spaces are normalized before the source read. Only the bearer scheme is accepted; any other scheme fails with `InvalidHeaderValue`. (issue #2815)

Queue:

Expand Down
74 changes: 66 additions & 8 deletions src/blob/handlers/BlockBlobHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TLSSocket } from "tls";
import { Readable } from "stream";

import { IExtentChunk } from "../../common/persistence/IExtentStore";
import { BEARER_TOKEN_PREFIX } from "../../common/utils/constants";
import {
convertRawHeadersToMetadata,
getMD5FromString,
Expand Down Expand Up @@ -268,15 +269,23 @@ export default class BlockBlobHandler
containerName
);

const sourceAuthorization = BlockBlobHandler.sourceAuthorizationHeaders(
context,
options.copySourceAuthorization
);

// Put Blob From URL always copies the whole source, so no range rides
// along with the conditions.
const sourceResponse = await this.readCopySource(
context,
"putBlobFromUrl",
copySource,
BlockBlobHandler.sourceConditionHeaders(
options.sourceModifiedAccessConditions
)
{
...BlockBlobHandler.sourceConditionHeaders(
options.sourceModifiedAccessConditions
),
...sourceAuthorization
}
);

// The status was only the response headers arriving; the body can still
Expand Down Expand Up @@ -328,7 +337,11 @@ export default class BlockBlobHandler
// came over, so a source that the caller may read but not tag refuses
// the copy rather than leaking them.
const blobTags = copySourceTags
? await this.readCopySourceTags(context, copySource)
? await this.readCopySourceTags(
context,
copySource,
sourceAuthorization
)
: options.blobTagsString === undefined
? undefined
: getTagsFromString(options.blobTagsString, context.contextId!);
Expand Down Expand Up @@ -595,6 +608,10 @@ export default class BlockBlobHandler
headers,
BlockBlobHandler.sourceConditionHeaders(
options.sourceModifiedAccessConditions
),
BlockBlobHandler.sourceAuthorizationHeaders(
context,
options.copySourceAuthorization
)
);

Expand Down Expand Up @@ -960,6 +977,43 @@ export default class BlockBlobHandler
return headers;
}

/**
* Send the credential a copy request names for its source in
* x-ms-copy-source-authorization as the Authorization header of the
* source read, so that the token authenticator judges it as it would a
* client reading the source itself.
*
* Azure only accepts the Bearer scheme here. Any other scheme is refused
* rather than forwarded, since the source read would otherwise accept a
* shared key signature for it.
*
* @private
* @param {Context} context
* @param {string} [copySourceAuthorization]
* @returns {{ [key: string]: string }}
* @memberof BlockBlobHandler
*/
private static sourceAuthorizationHeaders(
context: Context,
copySourceAuthorization?: string
): { [key: string]: string } {
if (!copySourceAuthorization) {
return {};
}
// The token authenticator expects "Bearer" and one separator space.
const scheme = copySourceAuthorization.split(/\s/, 1)[0];
if (scheme.toLowerCase() !== "bearer") {
throw StorageErrorFactory.getInvalidHeaderValue(context.contextId, {
HeaderName: "x-ms-copy-source-authorization"
});
}
return {
authorization: `${BEARER_TOKEN_PREFIX}${copySourceAuthorization.slice(
scheme.length
).replace(/^ +/, " ")}`
};
}

/**
* Read a copy source with a loopback self-request, so that SAS
* authentication, ranges, and source conditions are answered by the
Expand All @@ -976,7 +1030,8 @@ export default class BlockBlobHandler
* @param {Context} context
* @param {string} operation Handler method name, for log messages
* @param {string} copySource The source URL the request named
* @param {{ [key: string]: string }} headers Conditions, ranges
* @param {{ [key: string]: string }} headers Conditions, ranges, and
* the source authorization
* @param {string} [subresource] A query to append, such as "comp=tags"
* @returns {Promise<AxiosResponse>} A response whose body is a stream
* @memberof BlockBlobHandler
Expand Down Expand Up @@ -1110,23 +1165,26 @@ export default class BlockBlobHandler
* Read the tags of a copy source, for the copy that asks to carry them
* over. Real Azure charges this to the caller as its own Get Blob Tags
* request against the source, and so does this: the authorization the
* source URL carries has to allow reading them.
* source URL or the source authorization header carries has to allow
* reading them.
*
* @private
* @param {Context} context
* @param {string} copySource The source URL the request named
* @param {{ [key: string]: string }} headers The source authorization
* @returns {Promise<Models.BlobTags | undefined>}
* @memberof BlockBlobHandler
*/
private async readCopySourceTags(
context: Context,
copySource: string
copySource: string,
headers: { [key: string]: string }
): Promise<Models.BlobTags | undefined> {
const response = await this.readCopySource(
context,
"putBlobFromUrl",
copySource,
{},
headers,
"comp=tags"
);

Expand Down
Loading