Skip to content

Commit

Permalink
feat: support proxy (box/box-codegen#577) (#274)
Browse files Browse the repository at this point in the history
  • Loading branch information
box-sdk-build authored Oct 16, 2024
1 parent dad2f52 commit dea1937
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .codegen.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "engineHash": "9ff9f04", "specHash": "6b64d06", "version": "1.2.0" }
{ "engineHash": "f5d1f42", "specHash": "f0c2ce4", "version": "1.2.0" }
10 changes: 10 additions & 0 deletions Box.Sdk.Gen/Client/BoxClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,5 +261,15 @@ public BoxClient WithCustomBaseUrls(BaseUrls baseUrls) {
return new BoxClient(auth: this.Auth, networkSession: this.NetworkSession.WithCustomBaseUrls(baseUrls: baseUrls));
}

/// <summary>
/// Create a new client with a custom proxy that will be used for every API call
/// </summary>
/// <param name="config">
///
/// </param>
public BoxClient WithProxy(ProxyConfig config) {
return new BoxClient(auth: this.Auth, networkSession: this.NetworkSession.WithProxy(config: config));
}

}
}
10 changes: 6 additions & 4 deletions Box.Sdk.Gen/Internal/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@ internal class Result<T>
public T? Value { get; }
public bool IsSuccess { get; }
public Exception? Exception { get; }
public bool IsRetryable { get; }

private Result(bool isSuccess, T? value = default, Exception? exception = default)
private Result(bool isSuccess, T? value = default, Exception? exception = default, bool isRetryable = true)
{
IsSuccess = isSuccess;
Value = value;
Exception = exception;
IsRetryable = isRetryable;
}

public static Result<T> Ok(T value)
{
return new Result<T>(true, value);
}

public static Result<T> Fail(Exception ex)
public static Result<T> Fail(Exception ex, bool isRetryable = true)
{
return new Result<T>(false, default(T), ex);
return new Result<T>(false, default(T), ex, isRetryable);
}
}
}
}
89 changes: 85 additions & 4 deletions Box.Sdk.Gen/Networking/Fetch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -25,6 +26,8 @@ static class HttpClientAdapter
{
static IHttpClientFactory _clientFactory;

private static ProxyClient? _proxyClient;

static HttpClientAdapter()
{
var serviceCollection = new ServiceCollection();
Expand All @@ -46,9 +49,10 @@ static HttpClientAdapter()
/// <returns>A http/s Response as a FetchResponse.</returns>
internal static async Task<FetchResponse> FetchAsync(FetchOptions options)
{
var client = _clientFactory.CreateClient();

var networkSession = options.NetworkSession ?? new NetworkSession();

var client = GetOrCreateHttpClient(networkSession);

var attempt = 1;
var cancellationToken = options.CancellationToken ?? default(System.Threading.CancellationToken);

Expand Down Expand Up @@ -103,6 +107,10 @@ internal static async Task<FetchResponse> FetchAsync(FetchOptions options)

await System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(retryTimeout)).ConfigureAwait(false);
}
else if (statusCode == 407)
{
throw new BoxSdkException($"Proxy authorization required. Check provided credentials in proxy configuration.", DateTimeOffset.UtcNow);
}
else
{
seekableStream?.Dispose();
Expand All @@ -113,7 +121,12 @@ internal static async Task<FetchResponse> FetchAsync(FetchOptions options)
}
else
{
if (attempt >= networkSession.RetryAttempts)
if (!result.IsRetryable)
{
seekableStream?.Dispose();
throw new BoxSdkException($"Request was not retried. Inner exception: {result.Exception?.ToString()}", DateTimeOffset.UtcNow);
}
else if (attempt >= networkSession.RetryAttempts)
{
seekableStream?.Dispose();
throw new BoxSdkException($"Network error. Max retry attempts excedeed. {result.Exception?.ToString()}", DateTimeOffset.UtcNow);
Expand All @@ -134,6 +147,47 @@ internal static async Task<FetchResponse> FetchAsync(FetchOptions options)

}

private static HttpClient GetOrCreateHttpClient(NetworkSession networkSession)
{
if (networkSession.proxyConfig == null)
{
return _clientFactory.CreateClient();
}
else
{
//To handle proxy clients with different configurations better, we could use ConcurrentDictionary instead
var proxyKey = GenerateProxyKey(networkSession.proxyConfig);
if (_proxyClient == null || _proxyClient.ProxyKey != proxyKey)
{
var newClient = new ProxyClient(proxyKey, networkSession.proxyConfig);
_proxyClient = newClient;
return newClient.HttpClient;
}
return _proxyClient.HttpClient!;
}
}

private static HttpClient CreateProxyClient(ProxyConfig proxyConfig)
{
var handler = new HttpClientHandler
{
Proxy = new WebProxy(proxyConfig.Url)
{
Credentials = new NetworkCredential(proxyConfig.Username, proxyConfig.Password, proxyConfig.Domain)
},
UseProxy = true,
PreAuthenticate = true,
UseDefaultCredentials = false
};

return new HttpClient(handler, disposeHandler: true);
}

private static string GenerateProxyKey(ProxyConfig proxyConfig)
{
return $"{proxyConfig.Url}_{proxyConfig.Domain}_{proxyConfig.Username}_{proxyConfig.Password}";
}

private static async Task<Exception> BuildApiException(HttpRequestMessage request, HttpResponseMessage? response, FetchOptions options,
int statusCode, System.Threading.CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -230,6 +284,21 @@ private static async Task<Result<HttpResponseMessage>> ExecuteRequest(HttpClient
var response = await client.SendAsync(httpRequestMessage, completionOption, cancellationToken).ConfigureAwait(false);
return Result<HttpResponseMessage>.Ok(response);
}
catch (HttpRequestException ex)
{
string pattern = @"status code\s*'(\d+)'";
Match match = Regex.Match(ex.Message, pattern);
if (match.Success)
{
string statusCode = match.Groups[1].Value;

if (statusCode == "407")
{
return Result<HttpResponseMessage>.Fail(ex, false);
}
}
return Result<HttpResponseMessage>.Fail(ex);
}
catch (Exception ex)
{
return Result<HttpResponseMessage>.Fail(ex);
Expand Down Expand Up @@ -305,5 +374,17 @@ private static async Task<MemoryStream> ToMemoryStreamAsync(Stream inputStream)
memoryStream.Position = 0;
return memoryStream;
}

private class ProxyClient
{
public string ProxyKey { get; }
public HttpClient HttpClient { get; }

public ProxyClient(string proxyKey, ProxyConfig proxyConfig)
{
ProxyKey = proxyKey;
HttpClient = CreateProxyClient(proxyConfig);
}
}
}
}
}
27 changes: 23 additions & 4 deletions Box.Sdk.Gen/Networking/NetworkSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public class NetworkSession
/// </summary>
public IRetryStrategy RetryStrategy { get; init; } = new ExponentialBackoffRetryStrategy();

/// <summary>
/// Proxy configuration
/// </summary>
public ProxyConfig? proxyConfig { get; init; }

public NetworkSession(Dictionary<string, string>? additionalHeaders = default, BaseUrls? baseUrls = null)
{
AdditionalHeaders = additionalHeaders ?? new Dictionary<string, string>();
Expand All @@ -41,8 +46,9 @@ public NetworkSession(Dictionary<string, string>? additionalHeaders = default, B
/// <param name="additionalHeaders">
/// Map of headers, which are appended to each API request
/// </param>
public NetworkSession WithAdditionalHeaders(Dictionary<string, string> additionalHeaders) {
return new NetworkSession(DictionaryUtils.MergeDictionaries(this.AdditionalHeaders, additionalHeaders), this.BaseUrls) { RetryAttempts = this.RetryAttempts, RetryStrategy = this.RetryStrategy };
public NetworkSession WithAdditionalHeaders(Dictionary<string, string> additionalHeaders)
{
return new NetworkSession(DictionaryUtils.MergeDictionaries(this.AdditionalHeaders, additionalHeaders), this.BaseUrls) { RetryAttempts = this.RetryAttempts, RetryStrategy = this.RetryStrategy, proxyConfig = this.proxyConfig };

Check warning on line 51 in Box.Sdk.Gen/Networking/NetworkSession.cs

View workflow job for this annotation

GitHub Actions / .NET 6

Argument of type 'Dictionary<string, string>' cannot be used for parameter 'dict1' of type 'Dictionary<string, string?>' in 'Dictionary<string, string?> DictionaryUtils.MergeDictionaries<string, string>(Dictionary<string, string?> dict1, Dictionary<string, string?>? dict2)' due to differences in the nullability of reference types.

Check warning on line 51 in Box.Sdk.Gen/Networking/NetworkSession.cs

View workflow job for this annotation

GitHub Actions / .NET 6

Argument of type 'Dictionary<string, string>' cannot be used for parameter 'dict2' of type 'Dictionary<string, string?>' in 'Dictionary<string, string?> DictionaryUtils.MergeDictionaries<string, string>(Dictionary<string, string?> dict1, Dictionary<string, string?>? dict2)' due to differences in the nullability of reference types.

Check warning on line 51 in Box.Sdk.Gen/Networking/NetworkSession.cs

View workflow job for this annotation

GitHub Actions / .NET 6

Argument of type 'Dictionary<string, string?>' cannot be used for parameter 'additionalHeaders' of type 'Dictionary<string, string>' in 'NetworkSession.NetworkSession(Dictionary<string, string>? additionalHeaders = null, BaseUrls? baseUrls = null)' due to differences in the nullability of reference types.
}

/// <summary>
Expand All @@ -52,8 +58,21 @@ public NetworkSession WithAdditionalHeaders(Dictionary<string, string> additiona
/// <param name="baseUrls">
/// Custom base urls.
/// </param>
public NetworkSession WithCustomBaseUrls(BaseUrls baseUrls) {
return new NetworkSession(this.AdditionalHeaders, baseUrls) { RetryAttempts = this.RetryAttempts, RetryStrategy = this.RetryStrategy };
public NetworkSession WithCustomBaseUrls(BaseUrls baseUrls)
{
return new NetworkSession(this.AdditionalHeaders, baseUrls) { RetryAttempts = this.RetryAttempts, RetryStrategy = this.RetryStrategy, proxyConfig = this.proxyConfig };
}

/// <summary>
/// Generate a fresh network session by duplicating the existing configuration and network parameters,
/// while also including a proxy to be used for each API call.
/// </summary>
/// <param name="proxyConfig">
/// Proxy configuration.
/// </param>
public NetworkSession WithProxy(ProxyConfig config)
{
return new NetworkSession(this.AdditionalHeaders, this.BaseUrls) { RetryAttempts = this.RetryAttempts, RetryStrategy = this.RetryStrategy, proxyConfig = config };
}
}
}
17 changes: 17 additions & 0 deletions Box.Sdk.Gen/Networking/ProxyConfig/ProxyConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Box.Sdk.Gen;

namespace Box.Sdk.Gen {
public class ProxyConfig {
public string Url { get; }

public string? Username { get; init; }

public string? Password { get; init; }

public string? Domain { get; init; }

public ProxyConfig(string url) {
Url = url;
}
}
}
11 changes: 10 additions & 1 deletion docs/Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ divided across resource managers.
- [Suppress notifications](#suppress-notifications)
- [Custom headers](#custom-headers)
- [Custom Base URLs](#custom-base-urls)
- [Use Proxy for API calls](#use-proxy-for-api-calls)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -72,10 +73,18 @@ var newClient = client.WithExtraHeaders(extraHeaders: extraHeaders);
You can also specify the custom base URLs, which will be used for API calls made by client.
Calling the `client.WithCustomBaseUrls()` method creates a new client, leaving the original client unmodified.

```python
```c#
var newClient = client.WithCustomBaseUrls(new BaseUrls(
baseUrl: "https://api.box2.com",
uploadUrl: "https://upload.box.com/api",
oauth2Url: "https://account.box.com/api/oauth2"
));
```

# Use Proxy for API calls

In order to use a proxy for API calls, calling the `client.WithProxy(proxyConfig)` method creates a new client, leaving the original client unmodified, with the username and password being optional.

```c#
var newClient = client.WithProxy(new ProxyConfig("http://proxy.com") { Username = "username", Password = "password", Domain = "example" });
```

0 comments on commit dea1937

Please sign in to comment.