internal SigningState( string bucket, string objectName, DateTimeOffset expiration, HttpMethod requestMethod, Dictionary <string, IEnumerable <string> > requestHeaders, Dictionary <string, IEnumerable <string> > contentHeaders, IBlobSigner blobSigner) { StorageClientImpl.ValidateBucketName(bucket); bool isResumableUpload = false; if (requestMethod == null) { requestMethod = HttpMethod.Get; } else if (requestMethod == ResumableHttpMethod) { isResumableUpload = true; requestMethod = HttpMethod.Post; } string expiryUnixSeconds = ((int)(expiration - UnixEpoch).TotalSeconds).ToString(CultureInfo.InvariantCulture); resourcePath = $"/{bucket}"; if (objectName != null) { resourcePath += $"/{Uri.EscapeDataString(objectName)}"; } var extensionHeaders = GetExtensionHeaders(requestHeaders, contentHeaders); if (isResumableUpload) { extensionHeaders["x-goog-resumable"] = new StringBuilder("start"); } var contentMD5 = GetFirstHeaderValue(contentHeaders, "Content-MD5"); var contentType = GetFirstHeaderValue(contentHeaders, "Content-Type"); var signatureLines = new List <string> { requestMethod.ToString(), contentMD5, contentType, expiryUnixSeconds }; signatureLines.AddRange(extensionHeaders.Select( header => $"{header.Key}:{string.Join(", ", header.Value)}")); signatureLines.Add(resourcePath); blobToSign = Encoding.UTF8.GetBytes(string.Join("\n", signatureLines)); queryParameters = new List <string> { $"GoogleAccessId={blobSigner.Id}" }; if (expiryUnixSeconds != null) { queryParameters.Add($"Expires={expiryUnixSeconds}"); } }
/// <summary> /// Removes a label from a bucket, if it previously existed. It is not an error to /// attempt to remove a label that doesn't already exist. /// </summary> /// <remarks> /// This method is implemented by creating a single-element dictionary which is passed to /// <see cref="ModifyBucketLabels(string, IDictionary{string, string}, ModifyBucketLabelsOptions)"/>. /// </remarks> /// <param name="bucket">The name of the bucket. Must not be null.</param> /// <param name="labelName">The name of the label to remove. Must not be null.</param> /// <param name="options">The options for the operation. May be null, in which case defaults will be supplied.</param> /// <returns>The previous value of the label, or null if the label did not previously exist.</returns> public virtual string RemoveBucketLabel(string bucket, string labelName, ModifyBucketLabelsOptions options = null) { StorageClientImpl.ValidateBucketName(bucket); GaxPreconditions.CheckNotNull(labelName, nameof(labelName)); var newLabels = new Dictionary <string, string> { [labelName] = null }; var oldLabels = ModifyBucketLabels(bucket, newLabels, options); return(oldLabels[labelName]); }
/// <summary> /// Removes a label from a bucket, if it previously existed, asynchronously. It is not an error to /// attempt to remove a label that doesn't already exist. /// </summary> /// <remarks> /// This method is implemented by creating a single-element dictionary which is passed to /// <see cref="ModifyBucketLabelsAsync(string, IDictionary{string, string}, ModifyBucketLabelsOptions, CancellationToken)"/>. /// </remarks> /// <param name="bucket">The name of the bucket. Must not be null.</param> /// <param name="labelName">The name of the label to remove. Must not be null.</param> /// <param name="options">The options for the operation. May be null, in which case defaults will be supplied.</param> /// <param name="cancellationToken">The token to monitor for cancellation requests.</param> /// <returns>The previous value of the label, or null if the label did not previously exist.</returns> public virtual async Task <string> RemoveBucketLabelAsync(string bucket, string labelName, ModifyBucketLabelsOptions options = null, CancellationToken cancellationToken = default) { StorageClientImpl.ValidateBucketName(bucket); GaxPreconditions.CheckNotNull(labelName, nameof(labelName)); var newLabels = new Dictionary <string, string> { [labelName] = null }; var oldLabels = await ModifyBucketLabelsAsync(bucket, newLabels, options, cancellationToken).ConfigureAwait(false); return(oldLabels[labelName]); }
private RequestTemplate( string bucket, string objectName, HttpMethod httpMethod, IReadOnlyDictionary <string, IReadOnlyCollection <string> > requestHeaders, IReadOnlyDictionary <string, IReadOnlyCollection <string> > contentHeaders, IReadOnlyDictionary <string, IReadOnlyCollection <string> > queryParameters) { Bucket = StorageClientImpl.ValidateBucketName(bucket); ObjectName = objectName; HttpMethod = httpMethod ?? HttpMethod.Get; RequestHeaders = requestHeaders ?? s_empty; ContentHeaders = contentHeaders ?? s_empty; QueryParameters = queryParameters ?? s_empty; }
/// <summary> /// Creates a signed URL which can be used to provide limited access to specific buckets and objects to anyone /// in possession of the URL, regardless of whether they have a Google account. /// </summary> /// <remarks> /// <para> /// When either of the headers collections are specified, there are certain headers which will be included in the /// signed URL's signature, and therefore must be included in requests made with the created URL. These are the /// Content-MD5 and Content-Type content headers as well as any content or request header with a name starting /// with "x-goog-". /// </para> /// <para> /// If the headers collections are null or empty, no headers are included in the signed URL's signature, so any /// requests made with the created URL must not contain Content-MD5, Content-Type, or any header starting with "x-goog-". /// </para> /// <para> /// Note that if the entity is encrypted with customer-supplied encryption keys (see /// https://cloud.google.com/storage/docs/encryption for more information), the <b>x-goog-encryption-algorithm</b>, /// <b>x-goog-encryption-key</b>, and <b>x-goog-encryption-key-sha256</b> headers will be required when making the /// request. However, only the x-goog-encryption-algorithm header is included in the signature for the signed URL. /// So the <paramref name="requestHeaders"/> specified only need to have the x-goog-encryption-algorithm header. /// The other headers can be included, but will be ignored. /// </para> /// <para> /// Note that when GET is specified as the <paramref name="requestMethod"/> (or it is null, in which case GET is /// used), both GET and HEAD requests can be made with the created signed URL. /// </para> /// <para> /// See https://cloud.google.com/storage/docs/access-control/signed-urls for more information on signed URLs. /// </para> /// </remarks> /// <param name="bucket">The name of the bucket. Must not be null.</param> /// <param name="objectName">The name of the object within the bucket. May be null, in which case the signed URL /// will refer to the bucket instead of an object.</param> /// <param name="expiration">The point in time after which the signed URL will be invalid. May be null, in which /// case the signed URL never expires.</param> /// <param name="requestMethod">The HTTP request method for which the signed URL is allowed to be used. May be null, /// in which case GET will be used.</param> /// <param name="requestHeaders">The headers which will be included with the request. May be null.</param> /// <param name="contentHeaders">The headers for the content which will be included with the request. /// May be null.</param> /// <returns> /// The signed URL which can be used to provide access to a bucket or object for a limited amount of time. /// </returns> public string Sign( string bucket, string objectName, DateTimeOffset?expiration, HttpMethod requestMethod = null, Dictionary <string, IEnumerable <string> > requestHeaders = null, Dictionary <string, IEnumerable <string> > contentHeaders = null) { StorageClientImpl.ValidateBucketName(bucket); var expiryUnixSeconds = ((int?)((expiration - UnixEpoch)?.TotalSeconds))?.ToString(CultureInfo.InvariantCulture); var resourcePath = $"/{bucket}"; if (objectName != null) { resourcePath += $"/{Uri.EscapeDataString(objectName)}"; } var extensionHeaders = GetExtensionHeaders(requestHeaders, contentHeaders); var contentMD5 = GetFirstHeaderValue(contentHeaders, "Content-MD5"); var contentType = GetFirstHeaderValue(contentHeaders, "Content-Type"); var signatureLines = new List <string> { (requestMethod ?? HttpMethod.Get).ToString(), contentMD5, contentType, expiryUnixSeconds }; signatureLines.AddRange(extensionHeaders.Select( header => $"{header.Key}:{string.Join(", ", header.Value)}")); signatureLines.Add(resourcePath); var signature = _credentials.CreateSignature(Encoding.UTF8.GetBytes(string.Join("\n", signatureLines))); var queryParameters = new List <string> { $"GoogleAccessId={_credentials.Id}" }; if (expiryUnixSeconds != null) { queryParameters.Add($"Expires={expiryUnixSeconds}"); } queryParameters.Add($"Signature={WebUtility.UrlEncode(signature)}"); return($"{StorageHost}{resourcePath}?{string.Join("&", queryParameters)}"); }
internal SigningState( string bucket, string objectName, DateTimeOffset expiration, HttpMethod requestMethod, Dictionary <string, IEnumerable <string> > requestHeaders, Dictionary <string, IEnumerable <string> > contentHeaders, IBlobSigner blobSigner, IClock clock) { StorageClientImpl.ValidateBucketName(bucket); var now = clock.GetCurrentDateTimeUtc(); var timestamp = now.ToString("yyyyMMdd'T'HHmmss'Z'", CultureInfo.InvariantCulture); var datestamp = now.ToString("yyyyMMdd", CultureInfo.InvariantCulture); // TODO: Validate against maximum expiry duration int expirySeconds = (int)(expiration - now).TotalSeconds; string expiryText = expirySeconds.ToString(CultureInfo.InvariantCulture); string credentialScope = $"{datestamp}/{DefaultRegion}/{ScopeSuffix}"; var headers = new SortedDictionary <string, string>(StringComparer.Ordinal); headers["host"] = HostHeaderValue; AddHeaders(headers, requestHeaders); AddHeaders(headers, contentHeaders); var canonicalHeaders = string.Join("", headers.Select(pair => $"{pair.Key}:{pair.Value}\n")); var signedHeaders = string.Join(";", headers.Keys.Select(k => k.ToLowerInvariant())); var queryParameters = new SortedDictionary <string, string>(StringComparer.Ordinal) { { "X-Goog-Algorithm", Algorithm }, { "X-Goog-Credential", $"{blobSigner.Id}/{credentialScope}" }, { "X-Goog-Date", timestamp }, { "X-Goog-Expires", expirySeconds.ToString(CultureInfo.InvariantCulture) }, { "X-Goog-SignedHeaders", signedHeaders } }; if (requestMethod == null) { requestMethod = HttpMethod.Get; } else if (requestMethod == ResumableHttpMethod) { requestMethod = HttpMethod.Post; queryParameters["X-Goog-Resumable"] = "Start"; } _canonicalQueryString = string.Join("&", queryParameters.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); _resourcePath = $"/{bucket}"; if (objectName != null) { _resourcePath += $"/{Uri.EscapeDataString(objectName)}"; } var canonicalRequest = $"{requestMethod}\n{_resourcePath}\n{_canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\nUNSIGNED-PAYLOAD"; string hashHex; using (var sha256 = SHA256.Create()) { hashHex = FormatHex(sha256.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest))); } _blobToSign = Encoding.UTF8.GetBytes($"{Algorithm}\n{timestamp}\n{credentialScope}\n{hashHex}"); void AddHeaders(SortedDictionary <string, string> canonicalized, IDictionary <string, IEnumerable <string> > headersToAdd) { if (headersToAdd == null) { return; } foreach (var pair in headersToAdd) { if (pair.Value == null) { continue; } var headerName = pair.Key.ToLowerInvariant(); if (headerName == EncryptionKey.KeyHeader || headerName == EncryptionKey.KeyHashHeader || headerName == EncryptionKey.AlgorithmHeader) { continue; } var value = string.Join(", ", pair.Value.Select(PrepareHeaderValue)).Trim(); if (canonicalized.TryGetValue(headerName, out var existingValue)) { value = $"{existingValue}, {value}"; } canonicalized[headerName] = value; } } }
internal SigningState( string bucket, string objectName, DateTimeOffset expiration, HttpMethod requestMethod, Dictionary <string, IEnumerable <string> > requestHeaders, Dictionary <string, IEnumerable <string> > contentHeaders, IBlobSigner blobSigner, IClock clock) { StorageClientImpl.ValidateBucketName(bucket); bool isResumableUpload = false; if (requestMethod == null) { requestMethod = HttpMethod.Get; } else if (requestMethod == ResumableHttpMethod) { isResumableUpload = true; requestMethod = HttpMethod.Post; } var now = clock.GetCurrentDateTimeUtc(); var timestamp = now.ToString("yyyyMMdd'T'HHmmss'Z'", CultureInfo.InvariantCulture); var datestamp = now.ToString("yyyyMMdd", CultureInfo.InvariantCulture); // TODO: Validate again maximum expirary duration int expirySeconds = (int)(expiration - now).TotalSeconds; string expiryText = expirySeconds.ToString(CultureInfo.InvariantCulture); string clientEmail = blobSigner.Id; string credentialScope = $"{datestamp}/auto/gcs/goog4_request"; string credential = WebUtility.UrlEncode($"{blobSigner.Id}/{credentialScope}"); // FIXME: Use requestHeaders and contentHeaders var headers = new SortedDictionary <string, string>(); headers["host"] = "storage.googleapis.com"; var canonicalHeaderBuilder = new StringBuilder(); foreach (var pair in headers) { canonicalHeaderBuilder.Append($"{pair.Key}:{pair.Value}\n"); } var canonicalHeaders = canonicalHeaderBuilder.ToString().ToLowerInvariant(); var signedHeaders = string.Join(";", headers.Keys.Select(k => k.ToLowerInvariant())); queryParameters = new List <string> { "X-Goog-Algorithm=GOOG4-RSA-SHA256", $"X-Goog-Credential={credential}", $"X-Goog-Date={timestamp}", $"X-Goog-Expires={expirySeconds}", $"X-Goog-SignedHeaders={signedHeaders}" }; if (isResumableUpload) { queryParameters.Insert(4, "X-Goog-Resumable=Start"); } var canonicalQueryString = string.Join("&", queryParameters); resourcePath = $"/{bucket}"; if (objectName != null) { resourcePath += $"/{Uri.EscapeDataString(objectName)}"; } var canonicalRequest = $"{requestMethod}\n{resourcePath}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\nUNSIGNED-PAYLOAD"; string hashHex; using (var sha256 = SHA256.Create()) { hashHex = FormatHex(sha256.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest))); } blobToSign = Encoding.UTF8.GetBytes($"GOOG4-RSA-SHA256\n{timestamp}\n{credentialScope}\n{hashHex}"); }
internal SigningState( string bucket, string objectName, DateTimeOffset expiration, HttpMethod requestMethod, Dictionary <string, IEnumerable <string> > requestHeaders, Dictionary <string, IEnumerable <string> > contentHeaders, IBlobSigner blobSigner, IClock clock) { StorageClientImpl.ValidateBucketName(bucket); var now = clock.GetCurrentDateTimeUtc(); var timestamp = now.ToString("yyyyMMdd'T'HHmmss'Z'", CultureInfo.InvariantCulture); var datestamp = now.ToString("yyyyMMdd", CultureInfo.InvariantCulture); int expirySeconds = (int)(expiration - now).TotalSeconds; if (expirySeconds <= 0) { throw new ArgumentOutOfRangeException(nameof(expiration), "Expiration must be at least 1 second"); } if (expirySeconds > MaxExpirySecondsInclusive) { throw new ArgumentOutOfRangeException(nameof(expiration), "Expiration must not be greater than 7 days."); } string expiryText = expirySeconds.ToString(CultureInfo.InvariantCulture); string credentialScope = $"{datestamp}/{DefaultRegion}/{ScopeSuffix}"; var headers = new SortedDictionary <string, string>(StringComparer.Ordinal); headers["host"] = HostHeaderValue; AddHeaders(headers, requestHeaders); AddHeaders(headers, contentHeaders); var canonicalHeaders = string.Join("", headers.Select(pair => $"{pair.Key}:{pair.Value}\n")); var signedHeaders = string.Join(";", headers.Keys.Select(k => k.ToLowerInvariant())); var queryParameters = new SortedDictionary <string, string>(StringComparer.Ordinal) { { "X-Goog-Algorithm", Algorithm }, { "X-Goog-Credential", $"{blobSigner.Id}/{credentialScope}" }, { "X-Goog-Date", timestamp }, { "X-Goog-Expires", expirySeconds.ToString(CultureInfo.InvariantCulture) }, { "X-Goog-SignedHeaders", signedHeaders } }; if (requestMethod == null) { requestMethod = HttpMethod.Get; } else if (requestMethod == ResumableHttpMethod) { requestMethod = HttpMethod.Post; queryParameters["X-Goog-Resumable"] = "Start"; } _canonicalQueryString = string.Join("&", queryParameters.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); _resourcePath = $"/{bucket}"; if (!string.IsNullOrEmpty(objectName)) { // EscapeDataString escapes slashes, which we *don't* want to escape here. The simplest option is to // split the path into segments by slashes, escape each segment, then join the escaped segments together again. var segments = objectName.Split('/'); var escaped = string.Join("/", segments.Select(Uri.EscapeDataString)); _resourcePath = _resourcePath + "/" + escaped; } var canonicalRequest = $"{requestMethod}\n{_resourcePath}\n{_canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\nUNSIGNED-PAYLOAD"; string hashHex; using (var sha256 = SHA256.Create()) { hashHex = FormatHex(sha256.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest))); } _blobToSign = Encoding.UTF8.GetBytes($"{Algorithm}\n{timestamp}\n{credentialScope}\n{hashHex}"); void AddHeaders(SortedDictionary <string, string> canonicalized, IDictionary <string, IEnumerable <string> > headersToAdd) { if (headersToAdd == null) { return; } foreach (var pair in headersToAdd) { if (pair.Value == null) { continue; } var headerName = pair.Key.ToLowerInvariant(); // Note: the comma-space separating here is because this is what HttpClient does. // Google Cloud Storage itself will just use commas if it receives multiple values for the same header name, // but HttpClient coalesces the values itself. This approach means that if the same request is made from .NET // with the signed URL, it will succeed - but it does mean that the signed URL won't be valid when used from // another platform that sends actual multiple values. var value = string.Join(", ", pair.Value.Select(PrepareHeaderValue)).Trim(); if (canonicalized.TryGetValue(headerName, out var existingValue)) { value = $"{existingValue}, {value}"; } canonicalized[headerName] = value; } } }