1079 lines
44 KiB
C#
1079 lines
44 KiB
C#
// <copyright file="HttpWebRequestActivitySource.netfx.cs" company="OpenTelemetry Authors">
|
|
// Copyright The OpenTelemetry Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// </copyright>
|
|
#if NETFRAMEWORK
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Net;
|
|
using System.Reflection;
|
|
using System.Reflection.Emit;
|
|
using System.Runtime.CompilerServices;
|
|
using OpenTelemetry.Context.Propagation;
|
|
using OpenTelemetry.Trace;
|
|
|
|
namespace OpenTelemetry.Instrumentation.Http.Implementation
|
|
{
|
|
/// <summary>
|
|
/// Hooks into the <see cref="HttpWebRequest"/> class reflectively and writes diagnostic events as requests are processed.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Inspired from the System.Diagnostics.DiagnosticSource.HttpHandlerDiagnosticListener class which has some bugs and feature gaps.
|
|
/// See https://github.com/dotnet/runtime/pull/33732 for details.
|
|
/// </remarks>
|
|
internal static class HttpWebRequestActivitySource
|
|
{
|
|
public const string ActivitySourceName = "OpenTelemetry.HttpWebRequest";
|
|
public const string ActivityName = ActivitySourceName + ".HttpRequestOut";
|
|
|
|
internal static readonly Func<HttpWebRequest, string, IEnumerable<string>> HttpWebRequestHeaderValuesGetter = (request, name) => request.Headers.GetValues(name);
|
|
internal static readonly Action<HttpWebRequest, string, string> HttpWebRequestHeaderValuesSetter = (request, name, value) => request.Headers.Add(name, value);
|
|
|
|
internal static HttpWebRequestInstrumentationOptions Options = new HttpWebRequestInstrumentationOptions();
|
|
|
|
private static readonly Version Version = typeof(HttpWebRequestActivitySource).Assembly.GetName().Version;
|
|
private static readonly ActivitySource WebRequestActivitySource = new ActivitySource(ActivitySourceName, Version.ToString());
|
|
|
|
// Fields for reflection
|
|
private static FieldInfo connectionGroupListField;
|
|
private static Type connectionGroupType;
|
|
private static FieldInfo connectionListField;
|
|
private static Type connectionType;
|
|
private static FieldInfo writeListField;
|
|
private static Func<object, IAsyncResult> writeAResultAccessor;
|
|
private static Func<object, IAsyncResult> readAResultAccessor;
|
|
|
|
// LazyAsyncResult & ContextAwareResult
|
|
private static Func<object, AsyncCallback> asyncCallbackAccessor;
|
|
private static Action<object, AsyncCallback> asyncCallbackModifier;
|
|
private static Func<object, object> asyncStateAccessor;
|
|
private static Action<object, object> asyncStateModifier;
|
|
private static Func<object, bool> endCalledAccessor;
|
|
private static Func<object, object> resultAccessor;
|
|
private static Func<object, bool> isContextAwareResultChecker;
|
|
|
|
// HttpWebResponse
|
|
private static Func<object[], HttpWebResponse> httpWebResponseCtor;
|
|
private static Func<HttpWebResponse, Uri> uriAccessor;
|
|
private static Func<HttpWebResponse, object> verbAccessor;
|
|
private static Func<HttpWebResponse, string> mediaTypeAccessor;
|
|
private static Func<HttpWebResponse, bool> usesProxySemanticsAccessor;
|
|
private static Func<HttpWebResponse, object> coreResponseDataAccessor;
|
|
private static Func<HttpWebResponse, bool> isWebSocketResponseAccessor;
|
|
private static Func<HttpWebResponse, string> connectionGroupNameAccessor;
|
|
|
|
static HttpWebRequestActivitySource()
|
|
{
|
|
try
|
|
{
|
|
PrepareReflectionObjects();
|
|
PerformInjection();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// If anything went wrong, just no-op. Write an event so at least we can find out.
|
|
HttpInstrumentationEventSource.Log.ExceptionInitializingInstrumentation(typeof(HttpWebRequestActivitySource).FullName, ex);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void AddRequestTagsAndInstrumentRequest(HttpWebRequest request, Activity activity)
|
|
{
|
|
activity.DisplayName = HttpTagHelper.GetOperationNameForHttpMethod(request.Method);
|
|
|
|
if (activity.IsAllDataRequested)
|
|
{
|
|
activity.SetTag(SemanticConventions.AttributeHttpMethod, request.Method);
|
|
activity.SetTag(SemanticConventions.AttributeHttpHost, HttpTagHelper.GetHostTagValueFromRequestUri(request.RequestUri));
|
|
activity.SetTag(SemanticConventions.AttributeHttpUrl, HttpTagHelper.GetUriTagValueFromRequestUri(request.RequestUri));
|
|
if (Options.SetHttpFlavor)
|
|
{
|
|
activity.SetTag(SemanticConventions.AttributeHttpFlavor, HttpTagHelper.GetFlavorTagValueFromProtocolVersion(request.ProtocolVersion));
|
|
}
|
|
|
|
try
|
|
{
|
|
Options.Enrich?.Invoke(activity, "OnStartActivity", request);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
HttpInstrumentationEventSource.Log.EnrichmentException(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void AddResponseTags(HttpWebResponse response, Activity activity)
|
|
{
|
|
if (activity.IsAllDataRequested)
|
|
{
|
|
activity.SetTag(SemanticConventions.AttributeHttpStatusCode, (int)response.StatusCode);
|
|
|
|
activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode((int)response.StatusCode));
|
|
|
|
try
|
|
{
|
|
Options.Enrich?.Invoke(activity, "OnStopActivity", response);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
HttpInstrumentationEventSource.Log.EnrichmentException(ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void AddExceptionTags(Exception exception, Activity activity)
|
|
{
|
|
if (!activity.IsAllDataRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Status status;
|
|
if (exception is WebException wexc)
|
|
{
|
|
if (wexc.Response is HttpWebResponse response)
|
|
{
|
|
activity.SetTag(SemanticConventions.AttributeHttpStatusCode, (int)response.StatusCode);
|
|
|
|
status = SpanHelper.ResolveSpanStatusForHttpStatusCode((int)response.StatusCode);
|
|
}
|
|
else
|
|
{
|
|
switch (wexc.Status)
|
|
{
|
|
case WebExceptionStatus.Timeout:
|
|
case WebExceptionStatus.RequestCanceled:
|
|
status = Status.Error;
|
|
break;
|
|
case WebExceptionStatus.SendFailure:
|
|
case WebExceptionStatus.ConnectFailure:
|
|
case WebExceptionStatus.SecureChannelFailure:
|
|
case WebExceptionStatus.TrustFailure:
|
|
case WebExceptionStatus.ServerProtocolViolation:
|
|
case WebExceptionStatus.MessageLengthLimitExceeded:
|
|
status = Status.Error.WithDescription(exception.Message);
|
|
break;
|
|
default:
|
|
status = Status.Error.WithDescription(exception.Message);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
status = Status.Error.WithDescription(exception.Message);
|
|
}
|
|
|
|
activity.SetStatus(status);
|
|
if (Options.RecordException)
|
|
{
|
|
activity.RecordException(exception);
|
|
}
|
|
|
|
try
|
|
{
|
|
Options.Enrich?.Invoke(activity, "OnException", exception);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
HttpInstrumentationEventSource.Log.EnrichmentException(ex);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static void InstrumentRequest(HttpWebRequest request, ActivityContext activityContext)
|
|
=> Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(activityContext, Baggage.Current), request, HttpWebRequestHeaderValuesSetter);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private static bool IsRequestInstrumented(HttpWebRequest request)
|
|
=> Propagators.DefaultTextMapPropagator.Extract(default, request, HttpWebRequestHeaderValuesGetter) != default;
|
|
|
|
private static void ProcessRequest(HttpWebRequest request)
|
|
{
|
|
if (!WebRequestActivitySource.HasListeners() || !Options.EventFilter(request))
|
|
{
|
|
// No subscribers to the ActivitySource or User provider Filter is
|
|
// filtering this request.
|
|
// Propagation must still be done in such cases, to allow
|
|
// downstream services to continue from parent context, if any.
|
|
// Eg: Parent could be the Asp.Net activity.
|
|
InstrumentRequest(request, Activity.Current?.Context ?? default);
|
|
return;
|
|
}
|
|
|
|
if (IsRequestInstrumented(request))
|
|
{
|
|
// This request was instrumented by previous
|
|
// ProcessRequest, such is the case with redirect responses where the same request is sent again.
|
|
return;
|
|
}
|
|
|
|
var activity = WebRequestActivitySource.StartActivity(ActivityName, ActivityKind.Client);
|
|
var activityContext = Activity.Current?.Context ?? default;
|
|
|
|
// Propagation must still be done in all cases, to allow
|
|
// downstream services to continue from parent context, if any.
|
|
// Eg: Parent could be the Asp.Net activity.
|
|
InstrumentRequest(request, activityContext);
|
|
if (activity == null)
|
|
{
|
|
// There is a listener but it decided not to sample the current request.
|
|
return;
|
|
}
|
|
|
|
IAsyncResult asyncContext = writeAResultAccessor(request);
|
|
if (asyncContext != null)
|
|
{
|
|
// Flow here is for [Begin]GetRequestStream[Async].
|
|
|
|
AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext));
|
|
asyncCallbackModifier(asyncContext, callback.AsyncCallback);
|
|
}
|
|
else
|
|
{
|
|
// Flow here is for [Begin]GetResponse[Async] without a prior call to [Begin]GetRequestStream[Async].
|
|
|
|
asyncContext = readAResultAccessor(request);
|
|
AsyncCallbackWrapper callback = new AsyncCallbackWrapper(request, activity, asyncCallbackAccessor(asyncContext));
|
|
asyncCallbackModifier(asyncContext, callback.AsyncCallback);
|
|
}
|
|
|
|
AddRequestTagsAndInstrumentRequest(request, activity);
|
|
}
|
|
|
|
private static void HookOrProcessResult(HttpWebRequest request)
|
|
{
|
|
IAsyncResult writeAsyncContext = writeAResultAccessor(request);
|
|
if (writeAsyncContext == null || !(asyncCallbackAccessor(writeAsyncContext)?.Target is AsyncCallbackWrapper writeAsyncContextCallback))
|
|
{
|
|
// If we already hooked into the read result during ProcessRequest or we hooked up after the fact already we don't need to do anything here.
|
|
return;
|
|
}
|
|
|
|
// If we got here it means the user called [Begin]GetRequestStream[Async] and we have to hook the read result after the fact.
|
|
|
|
IAsyncResult readAsyncContext = readAResultAccessor(request);
|
|
if (readAsyncContext == null)
|
|
{
|
|
// We're still trying to establish the connection (no read has started).
|
|
return;
|
|
}
|
|
|
|
// Clear our saved callback so we know not to process again.
|
|
asyncCallbackModifier(writeAsyncContext, null);
|
|
|
|
if (endCalledAccessor.Invoke(readAsyncContext) || readAsyncContext.CompletedSynchronously)
|
|
{
|
|
// We need to process the result directly because the read callback has already fired. Force a copy because response has likely already been disposed.
|
|
ProcessResult(readAsyncContext, null, writeAsyncContextCallback.Activity, resultAccessor(readAsyncContext), true);
|
|
return;
|
|
}
|
|
|
|
// Hook into the result callback if it hasn't already fired.
|
|
AsyncCallbackWrapper callback = new AsyncCallbackWrapper(writeAsyncContextCallback.Request, writeAsyncContextCallback.Activity, asyncCallbackAccessor(readAsyncContext));
|
|
asyncCallbackModifier(readAsyncContext, callback.AsyncCallback);
|
|
}
|
|
|
|
private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncCallback, Activity activity, object result, bool forceResponseCopy)
|
|
{
|
|
// We could be executing on a different thread now so restore the activity if needed.
|
|
if (Activity.Current != activity)
|
|
{
|
|
Activity.Current = activity;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (result is Exception ex)
|
|
{
|
|
AddExceptionTags(ex, activity);
|
|
}
|
|
else
|
|
{
|
|
HttpWebResponse response = (HttpWebResponse)result;
|
|
|
|
if (forceResponseCopy || (asyncCallback == null && isContextAwareResultChecker(asyncResult)))
|
|
{
|
|
// For async calls (where asyncResult is ContextAwareResult)...
|
|
// If no callback was set assume the user is manually calling BeginGetResponse & EndGetResponse
|
|
// in which case they could dispose the HttpWebResponse before our listeners have a chance to work with it.
|
|
// Disposed HttpWebResponse throws when accessing properties, so let's make a copy of the data to ensure that doesn't happen.
|
|
|
|
HttpWebResponse responseCopy = httpWebResponseCtor(
|
|
new object[]
|
|
{
|
|
uriAccessor(response), verbAccessor(response), coreResponseDataAccessor(response), mediaTypeAccessor(response),
|
|
usesProxySemanticsAccessor(response), DecompressionMethods.None,
|
|
isWebSocketResponseAccessor(response), connectionGroupNameAccessor(response),
|
|
});
|
|
|
|
AddResponseTags(responseCopy, activity);
|
|
}
|
|
else
|
|
{
|
|
AddResponseTags(response, activity);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
HttpInstrumentationEventSource.Log.FailedProcessResult(ex);
|
|
}
|
|
|
|
activity.Stop();
|
|
}
|
|
|
|
private static void PrepareReflectionObjects()
|
|
{
|
|
// At any point, if the operation failed, it should just throw. The caller should catch all exceptions and swallow.
|
|
|
|
Type servicePointType = typeof(ServicePoint);
|
|
Assembly systemNetHttpAssembly = servicePointType.Assembly;
|
|
connectionGroupListField = servicePointType.GetField("m_ConnectionGroupList", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
connectionGroupType = systemNetHttpAssembly?.GetType("System.Net.ConnectionGroup");
|
|
connectionListField = connectionGroupType?.GetField("m_ConnectionList", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
connectionType = systemNetHttpAssembly?.GetType("System.Net.Connection");
|
|
writeListField = connectionType?.GetField("m_WriteList", BindingFlags.Instance | BindingFlags.NonPublic);
|
|
|
|
writeAResultAccessor = CreateFieldGetter<IAsyncResult>(typeof(HttpWebRequest), "_WriteAResult", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
readAResultAccessor = CreateFieldGetter<IAsyncResult>(typeof(HttpWebRequest), "_ReadAResult", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
// Double checking to make sure we have all the pieces initialized
|
|
if (connectionGroupListField == null ||
|
|
connectionGroupType == null ||
|
|
connectionListField == null ||
|
|
connectionType == null ||
|
|
writeListField == null ||
|
|
writeAResultAccessor == null ||
|
|
readAResultAccessor == null ||
|
|
!PrepareAsyncResultReflectionObjects(systemNetHttpAssembly) ||
|
|
!PrepareHttpWebResponseReflectionObjects(systemNetHttpAssembly))
|
|
{
|
|
// If anything went wrong here, just return false. There is nothing we can do.
|
|
throw new InvalidOperationException("Unable to initialize all required reflection objects");
|
|
}
|
|
}
|
|
|
|
private static bool PrepareAsyncResultReflectionObjects(Assembly systemNetHttpAssembly)
|
|
{
|
|
Type lazyAsyncResultType = systemNetHttpAssembly?.GetType("System.Net.LazyAsyncResult");
|
|
if (lazyAsyncResultType != null)
|
|
{
|
|
asyncCallbackAccessor = CreateFieldGetter<AsyncCallback>(lazyAsyncResultType, "m_AsyncCallback", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
asyncCallbackModifier = CreateFieldSetter<AsyncCallback>(lazyAsyncResultType, "m_AsyncCallback", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
asyncStateAccessor = CreateFieldGetter<object>(lazyAsyncResultType, "m_AsyncState", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
asyncStateModifier = CreateFieldSetter<object>(lazyAsyncResultType, "m_AsyncState", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
endCalledAccessor = CreateFieldGetter<bool>(lazyAsyncResultType, "m_EndCalled", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
resultAccessor = CreateFieldGetter<object>(lazyAsyncResultType, "m_Result", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
}
|
|
|
|
Type contextAwareResultType = systemNetHttpAssembly?.GetType("System.Net.ContextAwareResult");
|
|
if (contextAwareResultType != null)
|
|
{
|
|
isContextAwareResultChecker = CreateTypeChecker(contextAwareResultType);
|
|
}
|
|
|
|
return asyncCallbackAccessor != null
|
|
&& asyncCallbackModifier != null
|
|
&& asyncStateAccessor != null
|
|
&& asyncStateModifier != null
|
|
&& endCalledAccessor != null
|
|
&& resultAccessor != null
|
|
&& isContextAwareResultChecker != null;
|
|
}
|
|
|
|
private static bool PrepareHttpWebResponseReflectionObjects(Assembly systemNetHttpAssembly)
|
|
{
|
|
Type knownHttpVerbType = systemNetHttpAssembly?.GetType("System.Net.KnownHttpVerb");
|
|
Type coreResponseData = systemNetHttpAssembly?.GetType("System.Net.CoreResponseData");
|
|
|
|
if (knownHttpVerbType != null && coreResponseData != null)
|
|
{
|
|
var constructorParameterTypes = new Type[]
|
|
{
|
|
typeof(Uri), knownHttpVerbType, coreResponseData, typeof(string),
|
|
typeof(bool), typeof(DecompressionMethods),
|
|
typeof(bool), typeof(string),
|
|
};
|
|
|
|
ConstructorInfo ctor = typeof(HttpWebResponse).GetConstructor(
|
|
BindingFlags.NonPublic | BindingFlags.Instance,
|
|
null,
|
|
constructorParameterTypes,
|
|
null);
|
|
|
|
if (ctor != null)
|
|
{
|
|
httpWebResponseCtor = CreateTypeInstance<HttpWebResponse>(ctor);
|
|
}
|
|
}
|
|
|
|
uriAccessor = CreateFieldGetter<HttpWebResponse, Uri>("m_Uri", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
verbAccessor = CreateFieldGetter<HttpWebResponse, object>("m_Verb", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
mediaTypeAccessor = CreateFieldGetter<HttpWebResponse, string>("m_MediaType", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
usesProxySemanticsAccessor = CreateFieldGetter<HttpWebResponse, bool>("m_UsesProxySemantics", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
coreResponseDataAccessor = CreateFieldGetter<HttpWebResponse, object>("m_CoreResponseData", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
isWebSocketResponseAccessor = CreateFieldGetter<HttpWebResponse, bool>("m_IsWebSocketResponse", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
connectionGroupNameAccessor = CreateFieldGetter<HttpWebResponse, string>("m_ConnectionGroupName", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
return httpWebResponseCtor != null
|
|
&& uriAccessor != null
|
|
&& verbAccessor != null
|
|
&& mediaTypeAccessor != null
|
|
&& usesProxySemanticsAccessor != null
|
|
&& coreResponseDataAccessor != null
|
|
&& isWebSocketResponseAccessor != null
|
|
&& connectionGroupNameAccessor != null;
|
|
}
|
|
|
|
private static void PerformInjection()
|
|
{
|
|
FieldInfo servicePointTableField = typeof(ServicePointManager).GetField("s_ServicePointTable", BindingFlags.Static | BindingFlags.NonPublic);
|
|
if (servicePointTableField == null)
|
|
{
|
|
// If anything went wrong here, just return false. There is nothing we can do.
|
|
throw new InvalidOperationException("Unable to access the ServicePointTable field");
|
|
}
|
|
|
|
Hashtable originalTable = servicePointTableField.GetValue(null) as Hashtable;
|
|
ServicePointHashtable newTable = new ServicePointHashtable(originalTable ?? new Hashtable());
|
|
|
|
foreach (DictionaryEntry existingServicePoint in originalTable)
|
|
{
|
|
HookServicePoint(existingServicePoint.Value);
|
|
}
|
|
|
|
servicePointTableField.SetValue(null, newTable);
|
|
}
|
|
|
|
private static void HookServicePoint(object value)
|
|
{
|
|
if (value is WeakReference weakRef
|
|
&& weakRef.IsAlive
|
|
&& weakRef.Target is ServicePoint servicePoint)
|
|
{
|
|
// Replace the ConnectionGroup hashtable inside this ServicePoint object,
|
|
// which allows us to intercept each new ConnectionGroup object added under
|
|
// this ServicePoint.
|
|
Hashtable originalTable = connectionGroupListField.GetValue(servicePoint) as Hashtable;
|
|
ConnectionGroupHashtable newTable = new ConnectionGroupHashtable(originalTable ?? new Hashtable());
|
|
|
|
foreach (DictionaryEntry existingConnectionGroup in originalTable)
|
|
{
|
|
HookConnectionGroup(existingConnectionGroup.Value);
|
|
}
|
|
|
|
connectionGroupListField.SetValue(servicePoint, newTable);
|
|
}
|
|
}
|
|
|
|
private static void HookConnectionGroup(object value)
|
|
{
|
|
if (connectionGroupType.IsInstanceOfType(value))
|
|
{
|
|
// Replace the Connection arraylist inside this ConnectionGroup object,
|
|
// which allows us to intercept each new Connection object added under
|
|
// this ConnectionGroup.
|
|
ArrayList originalArrayList = connectionListField.GetValue(value) as ArrayList;
|
|
ConnectionArrayList newArrayList = new ConnectionArrayList(originalArrayList ?? new ArrayList());
|
|
|
|
foreach (object connection in originalArrayList)
|
|
{
|
|
HookConnection(connection);
|
|
}
|
|
|
|
connectionListField.SetValue(value, newArrayList);
|
|
}
|
|
}
|
|
|
|
private static void HookConnection(object value)
|
|
{
|
|
if (connectionType.IsInstanceOfType(value))
|
|
{
|
|
// Replace the HttpWebRequest arraylist inside this Connection object,
|
|
// which allows us to intercept each new HttpWebRequest object added under
|
|
// this Connection.
|
|
ArrayList originalArrayList = writeListField.GetValue(value) as ArrayList;
|
|
HttpWebRequestArrayList newArrayList = new HttpWebRequestArrayList(originalArrayList ?? new ArrayList());
|
|
|
|
writeListField.SetValue(value, newArrayList);
|
|
}
|
|
}
|
|
|
|
private static Func<TClass, TField> CreateFieldGetter<TClass, TField>(string fieldName, BindingFlags flags)
|
|
where TClass : class
|
|
{
|
|
FieldInfo field = typeof(TClass).GetField(fieldName, flags);
|
|
if (field != null)
|
|
{
|
|
string methodName = field.ReflectedType.FullName + ".get_" + field.Name;
|
|
DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new[] { typeof(TClass) }, true);
|
|
ILGenerator generator = getterMethod.GetILGenerator();
|
|
generator.Emit(OpCodes.Ldarg_0);
|
|
generator.Emit(OpCodes.Ldfld, field);
|
|
generator.Emit(OpCodes.Ret);
|
|
return (Func<TClass, TField>)getterMethod.CreateDelegate(typeof(Func<TClass, TField>));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates getter for a field defined in private or internal type
|
|
/// repesented with classType variable.
|
|
/// </summary>
|
|
private static Func<object, TField> CreateFieldGetter<TField>(Type classType, string fieldName, BindingFlags flags)
|
|
{
|
|
FieldInfo field = classType.GetField(fieldName, flags);
|
|
if (field != null)
|
|
{
|
|
string methodName = classType.FullName + ".get_" + field.Name;
|
|
DynamicMethod getterMethod = new DynamicMethod(methodName, typeof(TField), new[] { typeof(object) }, true);
|
|
ILGenerator generator = getterMethod.GetILGenerator();
|
|
generator.Emit(OpCodes.Ldarg_0);
|
|
generator.Emit(OpCodes.Castclass, classType);
|
|
generator.Emit(OpCodes.Ldfld, field);
|
|
generator.Emit(OpCodes.Ret);
|
|
|
|
return (Func<object, TField>)getterMethod.CreateDelegate(typeof(Func<object, TField>));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates setter for a field defined in private or internal type
|
|
/// repesented with classType variable.
|
|
/// </summary>
|
|
private static Action<object, TField> CreateFieldSetter<TField>(Type classType, string fieldName, BindingFlags flags)
|
|
{
|
|
FieldInfo field = classType.GetField(fieldName, flags);
|
|
if (field != null)
|
|
{
|
|
string methodName = classType.FullName + ".set_" + field.Name;
|
|
DynamicMethod setterMethod = new DynamicMethod(methodName, null, new[] { typeof(object), typeof(TField) }, true);
|
|
ILGenerator generator = setterMethod.GetILGenerator();
|
|
generator.Emit(OpCodes.Ldarg_0);
|
|
generator.Emit(OpCodes.Castclass, classType);
|
|
generator.Emit(OpCodes.Ldarg_1);
|
|
generator.Emit(OpCodes.Stfld, field);
|
|
generator.Emit(OpCodes.Ret);
|
|
|
|
return (Action<object, TField>)setterMethod.CreateDelegate(typeof(Action<object, TField>));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an "is" method for the private or internal type.
|
|
/// </summary>
|
|
private static Func<object, bool> CreateTypeChecker(Type classType)
|
|
{
|
|
string methodName = classType.FullName + ".typeCheck";
|
|
DynamicMethod setterMethod = new DynamicMethod(methodName, typeof(bool), new[] { typeof(object) }, true);
|
|
ILGenerator generator = setterMethod.GetILGenerator();
|
|
generator.Emit(OpCodes.Ldarg_0);
|
|
generator.Emit(OpCodes.Isinst, classType);
|
|
generator.Emit(OpCodes.Ldnull);
|
|
generator.Emit(OpCodes.Cgt_Un);
|
|
generator.Emit(OpCodes.Ret);
|
|
|
|
return (Func<object, bool>)setterMethod.CreateDelegate(typeof(Func<object, bool>));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an instance of T using a private or internal ctor.
|
|
/// </summary>
|
|
private static Func<object[], T> CreateTypeInstance<T>(ConstructorInfo constructorInfo)
|
|
{
|
|
Type classType = typeof(T);
|
|
string methodName = classType.FullName + ".ctor";
|
|
DynamicMethod setterMethod = new DynamicMethod(methodName, classType, new Type[] { typeof(object[]) }, true);
|
|
ILGenerator generator = setterMethod.GetILGenerator();
|
|
|
|
ParameterInfo[] ctorParams = constructorInfo.GetParameters();
|
|
for (int i = 0; i < ctorParams.Length; i++)
|
|
{
|
|
generator.Emit(OpCodes.Ldarg_0);
|
|
switch (i)
|
|
{
|
|
case 0: generator.Emit(OpCodes.Ldc_I4_0); break;
|
|
case 1: generator.Emit(OpCodes.Ldc_I4_1); break;
|
|
case 2: generator.Emit(OpCodes.Ldc_I4_2); break;
|
|
case 3: generator.Emit(OpCodes.Ldc_I4_3); break;
|
|
case 4: generator.Emit(OpCodes.Ldc_I4_4); break;
|
|
case 5: generator.Emit(OpCodes.Ldc_I4_5); break;
|
|
case 6: generator.Emit(OpCodes.Ldc_I4_6); break;
|
|
case 7: generator.Emit(OpCodes.Ldc_I4_7); break;
|
|
case 8: generator.Emit(OpCodes.Ldc_I4_8); break;
|
|
default: generator.Emit(OpCodes.Ldc_I4, i); break;
|
|
}
|
|
|
|
generator.Emit(OpCodes.Ldelem_Ref);
|
|
Type paramType = ctorParams[i].ParameterType;
|
|
generator.Emit(paramType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, paramType);
|
|
}
|
|
|
|
generator.Emit(OpCodes.Newobj, constructorInfo);
|
|
generator.Emit(OpCodes.Ret);
|
|
|
|
return (Func<object[], T>)setterMethod.CreateDelegate(typeof(Func<object[], T>));
|
|
}
|
|
|
|
private class HashtableWrapper : Hashtable, IEnumerable
|
|
{
|
|
private readonly Hashtable table;
|
|
|
|
internal HashtableWrapper(Hashtable table)
|
|
: base()
|
|
{
|
|
this.table = table;
|
|
}
|
|
|
|
public override int Count => this.table.Count;
|
|
|
|
public override bool IsReadOnly => this.table.IsReadOnly;
|
|
|
|
public override bool IsFixedSize => this.table.IsFixedSize;
|
|
|
|
public override bool IsSynchronized => this.table.IsSynchronized;
|
|
|
|
public override object SyncRoot => this.table.SyncRoot;
|
|
|
|
public override ICollection Keys => this.table.Keys;
|
|
|
|
public override ICollection Values => this.table.Values;
|
|
|
|
public override object this[object key]
|
|
{
|
|
get => this.table[key];
|
|
set => this.table[key] = value;
|
|
}
|
|
|
|
public override void Add(object key, object value)
|
|
{
|
|
this.table.Add(key, value);
|
|
}
|
|
|
|
public override void Clear()
|
|
{
|
|
this.table.Clear();
|
|
}
|
|
|
|
public override bool Contains(object key)
|
|
{
|
|
return this.table.Contains(key);
|
|
}
|
|
|
|
public override bool ContainsKey(object key)
|
|
{
|
|
return this.table.ContainsKey(key);
|
|
}
|
|
|
|
public override bool ContainsValue(object key)
|
|
{
|
|
return this.table.ContainsValue(key);
|
|
}
|
|
|
|
public override void CopyTo(Array array, int arrayIndex)
|
|
{
|
|
this.table.CopyTo(array, arrayIndex);
|
|
}
|
|
|
|
public override object Clone()
|
|
{
|
|
return new HashtableWrapper((Hashtable)this.table.Clone());
|
|
}
|
|
|
|
IEnumerator IEnumerable.GetEnumerator()
|
|
{
|
|
return this.table.GetEnumerator();
|
|
}
|
|
|
|
public override IDictionaryEnumerator GetEnumerator()
|
|
{
|
|
return this.table.GetEnumerator();
|
|
}
|
|
|
|
public override void Remove(object key)
|
|
{
|
|
this.table.Remove(key);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class used for ServicePointManager.s_ServicePointTable. The goal here is to
|
|
/// intercept each new ServicePoint object being added to ServicePointManager.s_ServicePointTable
|
|
/// and replace its ConnectionGroupList hashtable field.
|
|
/// </summary>
|
|
private sealed class ServicePointHashtable : HashtableWrapper
|
|
{
|
|
public ServicePointHashtable(Hashtable table)
|
|
: base(table)
|
|
{
|
|
}
|
|
|
|
public override object this[object key]
|
|
{
|
|
get => base[key];
|
|
set
|
|
{
|
|
HookServicePoint(value);
|
|
base[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class used for ServicePoint.m_ConnectionGroupList. The goal here is to
|
|
/// intercept each new ConnectionGroup object being added to ServicePoint.m_ConnectionGroupList
|
|
/// and replace its m_ConnectionList arraylist field.
|
|
/// </summary>
|
|
private sealed class ConnectionGroupHashtable : HashtableWrapper
|
|
{
|
|
public ConnectionGroupHashtable(Hashtable table)
|
|
: base(table)
|
|
{
|
|
}
|
|
|
|
public override object this[object key]
|
|
{
|
|
get => base[key];
|
|
set
|
|
{
|
|
HookConnectionGroup(value);
|
|
base[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class used to wrap the array list object. This class itself doesn't actually
|
|
/// have the array elements, but rather access another array list that's given at
|
|
/// construction time.
|
|
/// </summary>
|
|
private class ArrayListWrapper : ArrayList
|
|
{
|
|
private ArrayList list;
|
|
|
|
internal ArrayListWrapper(ArrayList list)
|
|
: base()
|
|
{
|
|
this.list = list;
|
|
}
|
|
|
|
public override int Capacity
|
|
{
|
|
get => this.list.Capacity;
|
|
set => this.list.Capacity = value;
|
|
}
|
|
|
|
public override int Count => this.list.Count;
|
|
|
|
public override bool IsReadOnly => this.list.IsReadOnly;
|
|
|
|
public override bool IsFixedSize => this.list.IsFixedSize;
|
|
|
|
public override bool IsSynchronized => this.list.IsSynchronized;
|
|
|
|
public override object SyncRoot => this.list.SyncRoot;
|
|
|
|
public override object this[int index]
|
|
{
|
|
get => this.list[index];
|
|
set => this.list[index] = value;
|
|
}
|
|
|
|
public override int Add(object value)
|
|
{
|
|
return this.list.Add(value);
|
|
}
|
|
|
|
public override void AddRange(ICollection c)
|
|
{
|
|
this.list.AddRange(c);
|
|
}
|
|
|
|
public override int BinarySearch(object value)
|
|
{
|
|
return this.list.BinarySearch(value);
|
|
}
|
|
|
|
public override int BinarySearch(object value, IComparer comparer)
|
|
{
|
|
return this.list.BinarySearch(value, comparer);
|
|
}
|
|
|
|
public override int BinarySearch(int index, int count, object value, IComparer comparer)
|
|
{
|
|
return this.list.BinarySearch(index, count, value, comparer);
|
|
}
|
|
|
|
public override void Clear()
|
|
{
|
|
this.list.Clear();
|
|
}
|
|
|
|
public override object Clone()
|
|
{
|
|
return new ArrayListWrapper((ArrayList)this.list.Clone());
|
|
}
|
|
|
|
public override bool Contains(object item)
|
|
{
|
|
return this.list.Contains(item);
|
|
}
|
|
|
|
public override void CopyTo(Array array)
|
|
{
|
|
this.list.CopyTo(array);
|
|
}
|
|
|
|
public override void CopyTo(Array array, int index)
|
|
{
|
|
this.list.CopyTo(array, index);
|
|
}
|
|
|
|
public override void CopyTo(int index, Array array, int arrayIndex, int count)
|
|
{
|
|
this.list.CopyTo(index, array, arrayIndex, count);
|
|
}
|
|
|
|
public override IEnumerator GetEnumerator()
|
|
{
|
|
return this.list.GetEnumerator();
|
|
}
|
|
|
|
public override IEnumerator GetEnumerator(int index, int count)
|
|
{
|
|
return this.list.GetEnumerator(index, count);
|
|
}
|
|
|
|
public override int IndexOf(object value)
|
|
{
|
|
return this.list.IndexOf(value);
|
|
}
|
|
|
|
public override int IndexOf(object value, int startIndex)
|
|
{
|
|
return this.list.IndexOf(value, startIndex);
|
|
}
|
|
|
|
public override int IndexOf(object value, int startIndex, int count)
|
|
{
|
|
return this.list.IndexOf(value, startIndex, count);
|
|
}
|
|
|
|
public override void Insert(int index, object value)
|
|
{
|
|
this.list.Insert(index, value);
|
|
}
|
|
|
|
public override void InsertRange(int index, ICollection c)
|
|
{
|
|
this.list.InsertRange(index, c);
|
|
}
|
|
|
|
public override int LastIndexOf(object value)
|
|
{
|
|
return this.list.LastIndexOf(value);
|
|
}
|
|
|
|
public override int LastIndexOf(object value, int startIndex)
|
|
{
|
|
return this.list.LastIndexOf(value, startIndex);
|
|
}
|
|
|
|
public override int LastIndexOf(object value, int startIndex, int count)
|
|
{
|
|
return this.list.LastIndexOf(value, startIndex, count);
|
|
}
|
|
|
|
public override void Remove(object value)
|
|
{
|
|
this.list.Remove(value);
|
|
}
|
|
|
|
public override void RemoveAt(int index)
|
|
{
|
|
this.list.RemoveAt(index);
|
|
}
|
|
|
|
public override void RemoveRange(int index, int count)
|
|
{
|
|
this.list.RemoveRange(index, count);
|
|
}
|
|
|
|
public override void Reverse(int index, int count)
|
|
{
|
|
this.list.Reverse(index, count);
|
|
}
|
|
|
|
public override void SetRange(int index, ICollection c)
|
|
{
|
|
this.list.SetRange(index, c);
|
|
}
|
|
|
|
public override ArrayList GetRange(int index, int count)
|
|
{
|
|
return this.list.GetRange(index, count);
|
|
}
|
|
|
|
public override void Sort()
|
|
{
|
|
this.list.Sort();
|
|
}
|
|
|
|
public override void Sort(IComparer comparer)
|
|
{
|
|
this.list.Sort(comparer);
|
|
}
|
|
|
|
public override void Sort(int index, int count, IComparer comparer)
|
|
{
|
|
this.list.Sort(index, count, comparer);
|
|
}
|
|
|
|
public override object[] ToArray()
|
|
{
|
|
return this.list.ToArray();
|
|
}
|
|
|
|
public override Array ToArray(Type type)
|
|
{
|
|
return this.list.ToArray(type);
|
|
}
|
|
|
|
public override void TrimToSize()
|
|
{
|
|
this.list.TrimToSize();
|
|
}
|
|
|
|
public ArrayList Swap()
|
|
{
|
|
ArrayList old = this.list;
|
|
this.list = new ArrayList(old.Capacity);
|
|
return old;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class used for ConnectionGroup.m_ConnectionList. The goal here is to
|
|
/// intercept each new Connection object being added to ConnectionGroup.m_ConnectionList
|
|
/// and replace its m_WriteList arraylist field.
|
|
/// </summary>
|
|
private sealed class ConnectionArrayList : ArrayListWrapper
|
|
{
|
|
public ConnectionArrayList(ArrayList list)
|
|
: base(list)
|
|
{
|
|
}
|
|
|
|
public override int Add(object value)
|
|
{
|
|
HookConnection(value);
|
|
return base.Add(value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper class used for Connection.m_WriteList. The goal here is to
|
|
/// intercept all new HttpWebRequest objects being added to Connection.m_WriteList
|
|
/// and notify the listener about the HttpWebRequest that's about to send a request.
|
|
/// It also intercepts all HttpWebRequest objects that are about to get removed from
|
|
/// Connection.m_WriteList as they have completed the request.
|
|
/// </summary>
|
|
private sealed class HttpWebRequestArrayList : ArrayListWrapper
|
|
{
|
|
public HttpWebRequestArrayList(ArrayList list)
|
|
: base(list)
|
|
{
|
|
}
|
|
|
|
public override int Add(object value)
|
|
{
|
|
// Add before firing events so if some user code cancels/aborts the request it will be found in the outstanding list.
|
|
int index = base.Add(value);
|
|
|
|
if (value is HttpWebRequest request)
|
|
{
|
|
ProcessRequest(request);
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
public override void RemoveAt(int index)
|
|
{
|
|
object request = this[index];
|
|
|
|
base.RemoveAt(index);
|
|
|
|
if (request is HttpWebRequest webRequest)
|
|
{
|
|
HookOrProcessResult(webRequest);
|
|
}
|
|
}
|
|
|
|
public override void Clear()
|
|
{
|
|
ArrayList oldList = this.Swap();
|
|
for (int i = 0; i < oldList.Count; i++)
|
|
{
|
|
if (oldList[i] is HttpWebRequest request)
|
|
{
|
|
HookOrProcessResult(request);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A closure object so our state is available when our callback executes.
|
|
/// </summary>
|
|
private sealed class AsyncCallbackWrapper
|
|
{
|
|
public AsyncCallbackWrapper(HttpWebRequest request, Activity activity, AsyncCallback originalCallback)
|
|
{
|
|
this.Request = request;
|
|
this.Activity = activity;
|
|
this.OriginalCallback = originalCallback;
|
|
}
|
|
|
|
public HttpWebRequest Request { get; }
|
|
|
|
public Activity Activity { get; }
|
|
|
|
public AsyncCallback OriginalCallback { get; }
|
|
|
|
public void AsyncCallback(IAsyncResult asyncResult)
|
|
{
|
|
object result = resultAccessor(asyncResult);
|
|
if (result is Exception || result is HttpWebResponse)
|
|
{
|
|
ProcessResult(asyncResult, this.OriginalCallback, this.Activity, result, false);
|
|
}
|
|
|
|
this.OriginalCallback?.Invoke(asyncResult);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|