예제 #1
0
                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}");
                    }
                }
예제 #2
0
        /// <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]);
        }
예제 #3
0
        /// <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;
 }
예제 #5
0
        /// <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;
                        }
                    }
                }