Skip to content

Commit

Permalink
feat: Use HttpWebRequest instead of HttpClient on .NET Framework (#1853)
Browse files Browse the repository at this point in the history
  • Loading branch information
tippmar-nr committed Aug 18, 2023
1 parent 7da3e59 commit 8d6cf0f
Show file tree
Hide file tree
Showing 37 changed files with 1,792 additions and 481 deletions.
24 changes: 23 additions & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

root = true

file_header_template = Copyright 2020 New Relic, Inc. All rights reserved.\nSPDX-License-Identifier: Apache-2.0

[*]
insert_final_newline = true
indent_style = space
Expand Down Expand Up @@ -62,10 +64,17 @@ dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Use PascalCase for constant fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
end_of_line = crlf
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion

###############################
# C# Code Style Rules #
Expand Down Expand Up @@ -139,6 +148,19 @@ csharp_space_after_dot = false
# Wrapping preferences
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion

# error if file doesn't have the required header
dotnet_diagnostic.SA1633.severity = error

##################################
# Visual Basic Code Style Rules #
Expand Down
17 changes: 17 additions & 0 deletions src/Agent/NewRelic/Agent/Core/DataTransport/Client/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

namespace NewRelic.Agent.Core.DataTransport.Client
{
/// <summary>
/// Constants shared between implementations of IHttpClient
/// </summary>
public static class Constants
{
public const string EmptyResponseBody = "{}";
public const int CompressMinimumByteLength = 20;
public const int ProtocolVersion = 17;
public const string LicenseKeyParameterName = "license_key";

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using NewRelic.Agent.Core.Logging;
using NewRelic.Core;

namespace NewRelic.Agent.Core.DataTransport.Client
{
/// <summary>
/// Simple wrapper for audit logging, shared by implementations of IHttpClient
/// </summary>
public static class DataTransportAuditLogger
{
/// <summary>
/// This represents the direction or flow of data. Used for audit logs.
/// </summary>
public enum AuditLogDirection
{
Sent = 1,
Received = 2
}

/// <summary>
/// This represents the origin or source of data. Used for audit logs.
/// </summary>
public enum AuditLogSource
{
Collector = 1,
Beacon = 2,
InstrumentedApp = 3
}

public const string AuditLogFormat = "Data {0} from the {1} : {2}";
private const string LicenseKeyParameterName = "license_key";

public static void Log(AuditLogDirection direction, AuditLogSource source, string uri)
{
if (AuditLog.IsAuditLogEnabled)
{
var message = string.Format(AuditLogFormat, direction, source,
Strings.ObfuscateLicenseKeyInAuditLog(uri, LicenseKeyParameterName));
AuditLog.Log(message);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using NewRelic.Agent.Core.DataTransport.Client.Interfaces;
using NewRelic.Core.Logging;

namespace NewRelic.Agent.Core.DataTransport.Client
{
/// <summary>
/// Abstract base shared by implementations of IHttpClient
/// </summary>
public abstract class HttpClientBase : IHttpClient
{
protected bool _diagnoseConnectionError = true;
protected static IWebProxy _proxy = null;

protected HttpClientBase(IWebProxy proxy)
{
_proxy = proxy;
}

public abstract Task<IHttpResponse> SendAsync(IHttpRequest request);

public virtual void Dispose()
{
#if !NETFRAMEWORK
if (_lazyHttpClient.IsValueCreated)
_lazyHttpClient.Value.Dispose();
#endif
}


protected void DiagnoseConnectionError(string host)
{
_diagnoseConnectionError = false;
try
{
if (!IPAddress.TryParse(host, out _))
{
Dns.GetHostEntry(host);
}
}
catch (Exception)
{
Log.ErrorFormat("Unable to resolve host name \"{0}\"", host);
}

TestConnection();
}

protected void TestConnection()
{
const string testAddress = "http://www.google.com";
try
{
#if NETFRAMEWORK
using (var wc = new WebClient())
{
wc.Proxy = _proxy;

wc.DownloadString(testAddress);
}
#else
_lazyHttpClient.Value.GetAsync(testAddress).GetAwaiter().GetResult();
#endif
Log.InfoFormat("Connection test to \"{0}\" succeeded", testAddress);
}
catch (Exception)
{
var message = $"Connection test to \"{testAddress}\" failed.";
if (_proxy != null)
{
message += $" Check your proxy settings ({_proxy.GetProxy(new Uri(testAddress))})";
}

Log.Error(message);
}
}

#if !NETFRAMEWORK
// use a single HttpClient for all TestConnection() invocations
private readonly Lazy<HttpClient> _lazyHttpClient = new Lazy<HttpClient>(() => new HttpClient(new HttpClientHandler() { Proxy = _proxy }));
#endif
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#if !NETFRAMEWORK
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using NewRelic.Agent.Configuration;
using NewRelic.Agent.Core.Config;
using NewRelic.Agent.Core.DataTransport.Client.Interfaces;

namespace NewRelic.Agent.Core.DataTransport.Client
{
public class HttpClientWrapper : IHttpClientWrapper
{
private readonly HttpClient _httpClient;
private readonly int _timeoutMilliseconds;

public HttpClientWrapper(HttpClient client, int timeoutMilliseconds)
{
_httpClient = client;
_timeoutMilliseconds = timeoutMilliseconds;
}


public void Dispose()
{
_httpClient.Dispose();
}

public async Task<IHttpResponseMessageWrapper> SendAsync(HttpRequestMessage message)
{
var cts = new CancellationTokenSource(_timeoutMilliseconds);
return new HttpResponseMessageWrapper(await _httpClient.SendAsync(message, cts.Token));
}

public TimeSpan Timeout
{
get
{
return _httpClient.Timeout;
}
set
{
_httpClient.Timeout = value;
}
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#if !NETFRAMEWORK
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using NewRelic.Agent.Core.DataTransport.Client.Interfaces;

namespace NewRelic.Agent.Core.DataTransport.Client
{
/// <summary>
/// HttpContentHeaders wrapper to enable mocking in unit tests
/// </summary>
public class HttpContentHeadersWrapper : IHttpContentHeadersWrapper
{
private readonly HttpContentHeaders _headers;

public HttpContentHeadersWrapper(HttpContentHeaders headers)
{
_headers = headers;
}

public ICollection<string> ContentEncoding => _headers.ContentEncoding.ToList();
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

#if !NETFRAMEWORK
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using NewRelic.Agent.Core.DataTransport.Client.Interfaces;

namespace NewRelic.Agent.Core.DataTransport.Client
{
/// <summary>
/// HttpContent wrapper to enable mocking in unit tests
/// </summary>
public class HttpContentWrapper : IHttpContentWrapper
{
private readonly HttpContent _httpContent;

public HttpContentWrapper(HttpContent httpContent)
{
_httpContent = httpContent;
}

public Task<Stream> ReadAsStreamAsync()
{
return _httpContent.ReadAsStreamAsync();
}

public IHttpContentHeadersWrapper Headers => new HttpContentHeadersWrapper(_httpContent.Headers);
}
}
#endif
54 changes: 54 additions & 0 deletions src/Agent/NewRelic/Agent/Core/DataTransport/Client/HttpRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2020 New Relic, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

using System;
using System.Collections.Generic;
using System.Text;
using NewRelic.Agent.Configuration;
using NewRelic.Agent.Core.DataTransport.Client.Interfaces;

namespace NewRelic.Agent.Core.DataTransport.Client
{
/// <summary>
/// Abstraction of a client request
/// </summary>
public class HttpRequest : IHttpRequest
{
private readonly IConfiguration _configuration;
private Uri _uri;

public HttpRequest(IConfiguration configuration)
{
_configuration = configuration;
Content = new NRHttpContent(_configuration);
}

public IConnectionInfo ConnectionInfo { get; set; }
public string Endpoint { get; set; }
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>();
public Uri Uri => _uri ??= GetUri(Endpoint, ConnectionInfo); // cache the Uri

public IHttpContent Content { get; }
public Guid RequestGuid { get; set; }

private Uri GetUri(string method, IConnectionInfo connectionInfo)
{
var uri = new StringBuilder("/agent_listener/invoke_raw_method?method=")
.Append(method)
.Append($"&{Constants.LicenseKeyParameterName}=")
.Append(_configuration.AgentLicenseKey)
.Append("&marshal_format=json")
.Append("&protocol_version=")
.Append(Constants.ProtocolVersion);

if (_configuration.AgentRunId != null)
{
uri.Append("&run_id=").Append(_configuration.AgentRunId);
}

var uriBuilder = new UriBuilder(connectionInfo.HttpProtocol, connectionInfo.Host, connectionInfo.Port, uri.ToString());

return new Uri(uriBuilder.Uri.ToString().Replace("%3F", "?"));
}
}
}
Loading

0 comments on commit 8d6cf0f

Please sign in to comment.