Skip to content

Commit

Permalink
feat: Support custom hostname and port in signers.
Browse files Browse the repository at this point in the history
  • Loading branch information
amanda-tarafa committed Mar 6, 2024
1 parent 1b62ee8 commit adaa7c4
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class V4SignerConformanceTest
{
public static TheoryData<SigningV4Test> V4SigningTestData { get; } = StorageConformanceTestData.TestData.GetTheoryData(f =>
// We skip test data with features that we don't support.
f.SigningV4Tests.Where(data => data.Hostname == "" && data.EmulatorHostname == "" && data.ClientEndpoint == "" && data.UniverseDomain == ""));
f.SigningV4Tests.Where(data => data.EmulatorHostname == "" && data.ClientEndpoint == "" && data.UniverseDomain == ""));
public static TheoryData<PostPolicyV4Test> V4PostPolicyTestData { get; } = StorageConformanceTestData.TestData.GetTheoryData(f => f.PostPolicyV4Tests);

private static readonly Dictionary<string, HttpMethod> s_methods = new Dictionary<string, HttpMethod>
Expand All @@ -55,7 +55,17 @@ public void SigningTest(SigningV4Test test)
var options = Options
.FromDuration(TimeSpan.FromSeconds(test.Expiration))
.WithSigningVersion(SigningVersion.V4)
.WithScheme(test.Scheme);
.WithScheme(test.Scheme == "" ? null : test.Scheme);

// SigningV4Test.Hostname can include a port but our UrlSigner.Options have individual options
// for scheme, hostname and port so we have to split the hostname here.
// Note that there's SigningV4Test.Scheme so we mapped that to our options already.
if (test.Hostname != "")
{
var hostAndPort = test.Hostname.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
options = options.WithHost(hostAndPort[0]);
options = hostAndPort.Length == 2 ? options.WithPort(int.Parse(hostAndPort[1])) : options;
}

switch (test.UrlStyle)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Google Inc. All Rights Reserved.
// Copyright 2020 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -176,6 +176,70 @@ public void WithScheme_InvalidThrows()

Assert.Throws<ArgumentException>(() => options.WithScheme("ftp"));
}

[Fact]
public void Host_Defaults()
{
var options = Options.FromDuration(TimeSpan.FromMinutes(1));

Assert.Equal("storage.googleapis.com", options.Host);
}

[Fact]
public void WithHost()
{
var options = Options.FromDuration(TimeSpan.FromMinutes(1));

var newHost = options.WithHost("another.custom.host");

Assert.NotSame(options, newHost);
Assert.Equal("another.custom.host", newHost.Host);
}

[Fact]
public void WithHost_Null()
{
var options = Options.FromDuration(TimeSpan.FromMinutes(1));

options = options.WithHost("another.custom.host");

var newOptions = options.WithHost(null);

Assert.NotSame(options, newOptions);
Assert.Equal("storage.googleapis.com", newOptions.Host);
}

[Fact]
public void Port_Defaults()
{
var options = Options.FromDuration(TimeSpan.FromMinutes(1));

Assert.Null(options.Port);
}

[Fact]
public void WithPort()
{
var options = Options.FromDuration(TimeSpan.FromMinutes(1));

var newPort = options.WithPort(443);

Assert.NotSame(options, newPort);
Assert.Equal(443, newPort.Port);
}

[Fact]
public void WithPort_Null()
{
var options = Options.FromDuration(TimeSpan.FromMinutes(1));

options = options.WithPort(443);

var newOptions = options.WithPort(null);

Assert.NotSame(options, newOptions);
Assert.Null(newOptions.Port);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Google LLC
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -25,6 +25,8 @@ public sealed partial class UrlSigner
/// </summary>
public sealed class Options
{
private const string DefaultStorageHost = "storage.googleapis.com";

/// <summary>
/// The length of time for which the signed URL should remain usable,
/// counting from the moment the signed URL is created.
Expand Down Expand Up @@ -67,6 +69,23 @@ public sealed class Options
/// </summary>
public string Scheme { get; }

/// <summary>
/// The host to use for generating the signed URL.
/// This will never be null. If null is specified in <see cref="WithHost(string)"/>
/// then <see cref="DefaultStorageHost"/> will be used.
/// Will be ignored if <see cref="UrlStyle"/> is set to <see cref="UrlStyle.BucketBoundHostname"/>.
/// </summary>
public string Host { get; }

/// <summary>
/// The port for the signed URL. The port is not included on the signature itself, only on the
/// resulting signed URL. Defaults to null.
/// Will be ignored if <see cref="UrlStyle"/> is set to <see cref="UrlStyle.BucketBoundHostname"/>.
/// </summary>
public int? Port { get; }

internal string HostAndPort => Port is null ? Host : $"{Host}:{Port}";

/// <summary>
/// A bucket bound host to use for generating the signed URL.
/// If <see cref="UrlStyle"/> is <see cref="UrlStyle.BucketBoundHostname"/> this won't be null.
Expand All @@ -82,7 +101,7 @@ public sealed class Options
public string BucketBoundHostname { get; }

private Options(
TimeSpan? duration, DateTimeOffset? expiration, SigningVersion version, UrlStyle? urlStyle, string scheme, string bucketBoundHostname)
TimeSpan? duration, DateTimeOffset? expiration, SigningVersion version, UrlStyle? urlStyle, string scheme, string host, int? port, string bucketBoundHostname)
{
GaxPreconditions.CheckArgument(
duration.HasValue != expiration.HasValue,
Expand Down Expand Up @@ -113,6 +132,8 @@ public sealed class Options
SigningVersion = version;
UrlStyle = urlStyle ?? UrlStyle.PathStyle;
Scheme = scheme ?? "https";
Host = host ?? DefaultStorageHost;
Port = port;
BucketBoundHostname = bucketBoundHostname;
}

Expand All @@ -122,15 +143,15 @@ public sealed class Options
/// <param name="duration">The duration to create these options with.</param>
/// <returns>A new options set.</returns>
public static Options FromDuration(TimeSpan duration) =>
new Options(duration, null, SigningVersion.Default, null, null, null);
new Options(duration, null, SigningVersion.Default, null, null, null, null, null);

/// <summary>
/// Creates a new <see cref="UrlSigner.Options"/> from the given expiration.
/// </summary>
/// <param name="expiration">The expiration to create these options with.</param>
/// <returns>A new options set.</returns>
public static Options FromExpiration(DateTimeOffset expiration) =>
new Options(null, expiration, SigningVersion.Default, null, null, null);
new Options(null, expiration, SigningVersion.Default, null, null, null, null, null);

/// <summary>
/// If this set of options was duration based, this method will return a new set
Expand All @@ -149,40 +170,57 @@ public sealed class Options
/// <param name="duration">The new duration.</param>
/// <returns>A new set of options with the given duration.</returns>
public Options WithDuration(TimeSpan duration) =>
new Options(duration, null, SigningVersion, UrlStyle, Scheme, BucketBoundHostname);
new Options(duration, null, SigningVersion, UrlStyle, Scheme, Host, Port, BucketBoundHostname);

/// <summary>
/// Returns a new set of options with the same values as this one but expiration based.
/// </summary>
/// <param name="expiration">The new expiration.</param>
/// <returns>A new set of options with the given expiration.</returns>
public Options WithExpiration(DateTimeOffset expiration) =>
new Options(null, expiration, SigningVersion, UrlStyle, Scheme, BucketBoundHostname);
new Options(null, expiration, SigningVersion, UrlStyle, Scheme, Host, Port, BucketBoundHostname);

/// <summary>
/// Returns a new set of options with the same values as this one except for the signing version.
/// </summary>
/// <param name="version">The new signing version.</param>
/// <returns>A set of options with the given signing version.</returns>
public Options WithSigningVersion(SigningVersion version) =>
new Options(Duration, Expiration, GaxPreconditions.CheckEnumValue(version, nameof(version)), UrlStyle, Scheme, BucketBoundHostname);
new Options(Duration, Expiration, GaxPreconditions.CheckEnumValue(version, nameof(version)), UrlStyle, Scheme, Host, Port, BucketBoundHostname);

/// <summary>
/// Returns a new set of options with the same values as this one except for the
/// <see cref="UrlStyle"/> value.
/// <see cref="UrlStyle"/> value and <see cref="BucketBoundHostname"/> set to null.
/// </summary>
/// <remarks> Use <see cref="WithBucketBoundHostname(string)"/> to set <see cref="UrlStyle.BucketBoundHostname"/>.</remarks>
/// <param name="urlStyle">The new url style.</param>
/// <returns>A new set ofoptions with the given url style.</returns>
public Options WithUrlStyle(UrlStyle urlStyle) =>
new Options(Duration, Expiration, SigningVersion, urlStyle, Scheme, null);
new Options(Duration, Expiration, SigningVersion, urlStyle, Scheme, Host, Port, null);

/// <summary>
/// Returns a new set of options with the same values as this one except for the scheme.
/// </summary>
/// <param name="scheme">The new scheme. May be null in which case https will be used.</param>
/// <returns>A new set of options with the given scheme.</returns>
public Options WithScheme(string scheme) =>
new Options(Duration, Expiration, SigningVersion, UrlStyle, scheme, BucketBoundHostname);
new Options(Duration, Expiration, SigningVersion, UrlStyle, scheme, Host, Port, BucketBoundHostname);

/// <summary>
/// Returns a new set of options with the same values as this one except for the host.
/// </summary>
/// <param name="host">The new host. May be null in which case <see cref="DefaultStorageHost"/> will be used.</param>
/// <returns>A new set of options with the given host.</returns>
public Options WithHost(string host) =>
new Options(Duration, Expiration, SigningVersion, UrlStyle, Scheme, host, Port, BucketBoundHostname);

/// <summary>
/// Returns a new set of options with the same values as this one except for the port.
/// </summary>
/// <param name="port">The new port. May be null.</param>
/// <returns>A new set of options with the given host.</returns>
public Options WithPort(int? port) =>
new Options(Duration, Expiration, SigningVersion, UrlStyle, Scheme, Host, port, BucketBoundHostname);

/// <summary>
/// Returns a new set of options with the same values as this one except for bucket bound domain
Expand All @@ -192,7 +230,7 @@ public sealed class Options
/// <returns>A new set of options with the given bucket bound domain and the url style set to
/// <see cref="UrlStyle.BucketBoundHostname"/>.</returns>
public Options WithBucketBoundHostname(string bucketBoundHostname) =>
new Options(Duration, Expiration, SigningVersion, UrlStyle.BucketBoundHostname, Scheme, bucketBoundHostname);
new Options(Duration, Expiration, SigningVersion, UrlStyle.BucketBoundHostname, Scheme, Host, Port, bucketBoundHostname);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ internal SigningState(RequestTemplate template, Options options, IBlobSigner blo

(_host, _urlResourcePath) = options.UrlStyle switch
{
UrlStyle.PathStyle => (StorageHost, $"/{template.Bucket}"),
UrlStyle.VirtualHostedStyle => ($"{template.Bucket}.{StorageHost}", string.Empty),
UrlStyle.PathStyle => (options.HostAndPort, $"/{template.Bucket}"),
UrlStyle.VirtualHostedStyle => ($"{template.Bucket}.{options.HostAndPort}", string.Empty),
_ => throw new ArgumentOutOfRangeException(
nameof(options.UrlStyle),
$"When using {nameof(SigningVersion.V2)} only {nameof(UrlStyle.PathStyle)} or {nameof(UrlStyle.VirtualHostedStyle)} can be specified.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,17 @@ public async Task<SignedPostPolicy> SignAsync(PostPolicy postPolicy, Options opt
private readonly string _canonicalQueryString;
internal readonly byte[] _blobToSign;
private readonly string _scheme;
private readonly string _host;
private readonly string _hostForSignature;
private readonly string _hostForUrl;


internal UrlSigningState(RequestTemplate template, Options options, IBlobSigner blobSigner, BlobSignerParameters signerParameters)
{
(_host, _resourcePath) = options.UrlStyle switch
(_hostForSignature, _hostForUrl, _resourcePath) = options.UrlStyle switch
{
UrlStyle.PathStyle => (StorageHost, $"/{template.Bucket}"),
UrlStyle.VirtualHostedStyle => ($"{template.Bucket}.{StorageHost}", string.Empty),
UrlStyle.BucketBoundHostname => (options.BucketBoundHostname, string.Empty),
UrlStyle.PathStyle => (options.Host, options.HostAndPort, $"/{template.Bucket}"),
UrlStyle.VirtualHostedStyle => ($"{template.Bucket}.{options.Host}", $"{template.Bucket}.{options.HostAndPort}", string.Empty),
UrlStyle.BucketBoundHostname => (options.BucketBoundHostname, options.BucketBoundHostname, string.Empty),
_ => throw new ArgumentOutOfRangeException(nameof(options.UrlStyle))
};

Expand All @@ -126,7 +128,7 @@ internal UrlSigningState(RequestTemplate template, Options options, IBlobSigner
string credentialScope = $"{datestamp}/{signerParameters.Region}/{signerParameters.Service}/{signerParameters.RequestType}";

var headers = new SortedDictionary<string, string>(StringComparer.Ordinal);
headers.AddHeader("host", _host);
headers.AddHeader("host", _hostForSignature);
var effectiveRequestMethod = template.HttpMethod;
if (effectiveRequestMethod == ResumableHttpMethod)
{
Expand Down Expand Up @@ -177,7 +179,7 @@ internal UrlSigningState(RequestTemplate template, Options options, IBlobSigner
}

internal string GetResult(string signature) =>
$"{_scheme}://{_host}{_resourcePath}?{_canonicalQueryString}&X-Goog-Signature={WebUtility.UrlEncode(signature)}";
$"{_scheme}://{_hostForUrl}{_resourcePath}?{_canonicalQueryString}&X-Goog-Signature={WebUtility.UrlEncode(signature)}";
}

/// <summary>
Expand All @@ -196,10 +198,10 @@ internal PostPolicySigningState(PostPolicy policy, Options options, IBlobSigner
{
string uri = options.UrlStyle switch
{
UrlStyle.PathStyle => policy.Bucket == null ? StorageHost : $"{StorageHost}/{policy.Bucket}",
UrlStyle.PathStyle => policy.Bucket == null ? options.HostAndPort : $"{options.HostAndPort}/{policy.Bucket}",
UrlStyle.VirtualHostedStyle => policy.Bucket == null ?
throw new ArgumentNullException(nameof(PostPolicy.Bucket), $"When using {UrlStyle.VirtualHostedStyle} a bucket condition must be set in the policy.") :
$"{policy.Bucket}.{StorageHost}",
$"{policy.Bucket}.{options.HostAndPort}",
UrlStyle.BucketBoundHostname => options.BucketBoundHostname,
_ => throw new ArgumentOutOfRangeException(nameof(options.UrlStyle))
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ namespace Google.Cloud.Storage.V1
/// </remarks>
public sealed partial class UrlSigner
{
private const string StorageHost = "storage.googleapis.com";
private static readonly ISigner s_v2Signer = new V2Signer();
private static readonly ISigner s_v4Signer = new V4Signer();

Expand Down

0 comments on commit adaa7c4

Please sign in to comment.