internal ListenerOperation CreateProxy(Listener listener) { var proxy = new ListenerOperation(listener, Operation, Handler, Uri); proxy.TargetOperation = this; parentOperation = proxy; return(proxy); }
internal void ReleaseInstrumentation(ListenerOperation operation) { lock (this) { Debug($"{ME} RELEASE CONTEXT: {operation.ME}"); if (currentOperation != operation) { throw new InvalidOperationException(); } currentOperation = null; mainLoopEvent.Set(); } }
public ListenerContext ReuseConnection() { disposed = true; var oldConnection = Interlocked.Exchange(ref connection, null); if (oldConnection == null) { throw new InvalidOperationException(); } var newContext = new ListenerContext(Listener, oldConnection, true) { State = Listener.UsingInstrumentation ? ConnectionState.WaitingForContext : ConnectionState.WaitingForRequest }; assignedOperation = null; currentOperation = null; State = ConnectionState.Closed; return(newContext); }
public ListenerOperation RegisterOperation(TestContext ctx, HttpOperation operation, Handler handler, string path) { lock (this) { if (TargetListener != null) { var targetOperation = TargetListener.RegisterOperation(ctx, operation, handler, path); registry.Add(targetOperation.Uri.LocalPath, targetOperation); return(targetOperation.CreateProxy(this)); } if (path == null) { var id = Interlocked.Increment(ref nextRequestID); path = $"/id/{operation.ID}/{handler.GetType ().Name}/"; } var me = $"{nameof (RegisterOperation)}({handler.Value})"; Debug($"{me} {path}"); var uri = new Uri(Server.TargetUri, path); var listenerOperation = new ListenerOperation(this, operation, handler, uri); registry.Add(path, listenerOperation); return(listenerOperation); } }
internal async Task <ListenerContext> FindContext(TestContext ctx, ListenerOperation operation, bool reusing) { var me = $"{ME}({operation.Operation.ME}:{reusing}) FIND CONTEXT"; var instrumentation = operation; while (true) { ListenerOperation oldInstrumentation; lock (this) { oldInstrumentation = Interlocked.CompareExchange(ref currentOperation, instrumentation, null); if (oldInstrumentation == null) { break; } ctx.LogDebug(2, $"{me} - WAITING FOR OPERATION {oldInstrumentation.Operation.ME}"); } await oldInstrumentation.Wait().ConfigureAwait(false); } mainLoopEvent.Set(); return(await instrumentation.WaitForContext().ConfigureAwait(false)); }
internal void AssignContext(ListenerOperation operation) { if (!Listener.UsingInstrumentation) { throw new InvalidOperationException(); } var oldOperation = Interlocked.CompareExchange(ref assignedOperation, operation, null); if (oldOperation != null) { if (!operation.Operation.HasAnyFlags(HttpOperationFlags.DelayedListenerContext) || oldOperation != operation) { throw new InvalidOperationException(); } } if (State != ConnectionState.Idle && State != ConnectionState.WaitingForContext) { if (!operation.Operation.HasAnyFlags(HttpOperationFlags.DelayedListenerContext) || State != ConnectionState.Listening) { throw new InvalidOperationException(); } } State = ConnectionState.Listening; }
internal void UnregisterOperation(ListenerOperation redirect) { lock (this) { registry.Remove(redirect.Uri.LocalPath); } }
public async Task <Response> RunWithContext(TestContext ctx, ListenerOperation operation, Request request, ClientFunc clientFunc, CancellationToken cancellationToken) { var me = $"{ME}({operation.Operation.ME}) RUN WITH CONTEXT"; ListenerContext context = null; ListenerContext targetContext = null; var reusing = !operation.Operation.HasAnyFlags(HttpOperationFlags.DontReuseConnection); var delayedContext = operation.Operation.HasAnyFlags(HttpOperationFlags.DelayedListenerContext); /* * When using HttpOperationFlags.DelayedListenerContext, then we call the `clientFunc` before we * actually start listening. */ if (UsingInstrumentation && !delayedContext) { context = await FindContext(ctx, operation, reusing); reusing = context.ReusingConnection; ctx.LogDebug(2, $"{me} - CREATE CONTEXT: {reusing} {context.ME}"); await context.ServerStartTask.ConfigureAwait(false); ctx.LogDebug(2, $"{me} - CREATE CONTEXT #1: {reusing} {context.ME}"); } if (TargetListener?.UsingInstrumentation ?? false) { targetContext = await TargetListener.FindContext(ctx, operation.TargetOperation, false); ctx.LogDebug(2, $"{me} - CREATE TARGET CONTEXT: {reusing} {targetContext.ME}"); try { await targetContext.ServerStartTask.ConfigureAwait(false); } catch { context?.Dispose(); throw; } } var clientTask = clientFunc(ctx, request, cancellationToken); var serverInitTask = operation.ServerInitTask; var serverFinishedTask = operation.ServerFinishedTask; ExceptionDispatchInfo throwMe = null; bool initDone = false, serverDone = false, clientDone = false; while (!initDone || !serverDone || !clientDone) { ctx.LogDebug(2, $"{me} LOOP: init={initDone} server={serverDone} client={clientDone}"); if (clientDone) { if (operation.Operation.HasAnyFlags( HttpOperationFlags.AbortAfterClientExits, HttpOperationFlags.ServerAbortsHandshake, HttpOperationFlags.ClientAbortsHandshake)) { ctx.LogDebug(2, $"{me} - ABORTING"); break; } if (!initDone) { ctx.LogDebug(2, $"{me} - ERROR: {clientTask.Result}"); throwMe = ExceptionDispatchInfo.Capture(new ConnectionException( $"{me} client exited before server accepted connection.")); break; } } var tasks = new List <Task> (); if (!initDone) { tasks.Add(serverInitTask); } if (!serverDone) { tasks.Add(serverFinishedTask); } if (!clientDone) { tasks.Add(clientTask); } var finished = await Task.WhenAny(tasks).ConfigureAwait(false); string which; if (finished == serverInitTask) { which = "init"; initDone = true; } else if (finished == serverFinishedTask) { which = "server"; serverDone = true; } else if (finished == clientTask) { which = "client"; clientDone = true; } else { throwMe = ExceptionDispatchInfo.Capture(new InvalidOperationException()); break; } ctx.LogDebug(2, $"{me} #4: {which} exited - {finished.Status}"); if (finished.Status == TaskStatus.Faulted || finished.Status == TaskStatus.Canceled) { if (operation.Operation.HasAnyFlags(HttpOperationFlags.ExpectServerException) && (finished == serverFinishedTask || finished == serverInitTask)) { ctx.LogDebug(2, $"{me} EXPECTED EXCEPTION {finished.Exception.GetType ()}"); } else if (finished.Status == TaskStatus.Canceled) { ctx.LogDebug(2, $"{me} CANCELED"); throwMe = ExceptionDispatchInfo.Capture(new OperationCanceledException()); break; } else { ctx.LogDebug(2, $"{me} FAILED: {finished.Exception.Message}"); throwMe = ExceptionDispatchInfo.Capture(finished.Exception); break; } } } if (throwMe != null) { ctx.LogDebug(2, $"{me} THROWING {throwMe.SourceException.Message}"); lock (this) { operation.OnError(throwMe.SourceException); if (context != null) { context.Dispose(); } if (targetContext != null) { targetContext.Dispose(); } mainLoopEvent.Set(); } throwMe.Throw(); } return(clientTask.Result); }
void RunScheduler() { do { Cleanup(); if (!UsingInstrumentation) { CreateConnections(); } } while (!StartTasks()); void Cleanup() { var iter = connections.First; while (iter != null) { var node = iter; var context = node.Value; iter = iter.Next; if (context.State == ConnectionState.Closed) { connections.Remove(node); context.Dispose(); } else if (context.State == ConnectionState.ReuseConnection) { connections.Remove(node); var newContext = context.ReuseConnection(); connections.AddLast(newContext); } } } void CreateConnections() { while (connections.Count < requestParallelConnections) { Debug($"RUN SCHEDULER: {connections.Count}"); var connection = Backend.CreateConnection(); connections.AddLast(new ListenerContext(this, connection, false)); Debug($"RUN SCHEDULER #1: {connection.ME}"); } } bool StartTasks() { var listening = false; ListenerContext listeningContext = null; ListenerContext redirectContext = null; ListenerContext idleContext = null; var me = $"RUN SCHEDULER - TASKS"; Debug($"{me}"); var idx = 0; var iter = connections.First; while (iter != null) { var node = iter; var context = node.Value; iter = iter.Next; Debug($"{me} ({++idx}/{connections.Count}): {context.State} {context.CurrentTask != null} {context.ME}"); if (!listening && context.State == ConnectionState.Listening) { listening = true; } if (false && UsingInstrumentation && listening && context.State == ConnectionState.Listening) { continue; } if (context.CurrentTask != null) { continue; } switch (context.State) { case ConnectionState.Idle: case ConnectionState.WaitingForContext: if (!UsingInstrumentation) { throw new InvalidOperationException(); } if (idleContext == null && listeningContext == null) { idleContext = context; } continue; case ConnectionState.NeedContextForRedirect: case ConnectionState.CannotReuseConnection: if (redirectContext == null) { redirectContext = context; } continue; case ConnectionState.Listening: if (idleContext == null && listeningContext == null) { listeningContext = context; } break; } var task = context.MainLoopListenerTask(TestContext, cts.Token); listenerTasks.AddLast(task); if (false && context.State == ConnectionState.Listening && !listening) { listeningContext = context; listening = true; } } Debug($"{me} DONE: instrumentation={currentOperation?.ID} listening={listening} " + $"redirect={redirectContext != null} idleContext={idleContext != null} " + $"listeningContext={listeningContext != null} " + $"assigned={currentOperation?.AssignedContext != null}"); var availableContext = idleContext ?? listeningContext; if (currentOperation != null) { if (currentOperation.AssignedContext != null) { return(true); } var operation = currentOperation.Operation; var forceNewCnc = operation.HasAnyFlags(HttpOperationFlags.ForceNewConnection); if (listening && !forceNewCnc) { return(true); } if (availableContext == null || forceNewCnc) { var connection = Backend.CreateConnection(); availableContext = new ListenerContext(this, connection, false); connections.AddLast(availableContext); } Debug($"{me} ASSIGN CONTEXT: {availableContext.ME} {currentOperation.ME}"); currentOperation.AssignContext(availableContext); return(false); } if (redirectContext == null) { return(true); } if (availableContext == null) { var connection = Backend.CreateConnection(); availableContext = new ListenerContext(this, connection, false); connections.AddLast(availableContext); } currentOperation = redirectContext.Redirect(availableContext); currentOperation.AssignContext(availableContext); Debug($"{me} DONE #1: {availableContext.ME} {currentOperation?.ME}"); return(false); } }
async void MainLoop() { while (!closed) { Debug($"MAIN LOOP"); var taskList = new List <Task> (); var contextList = new List <ListenerTask> (); lock (this) { Debug($"MAIN LOOP - SCHEDULER: {connections.Count}"); RunScheduler(); taskList.Add(mainLoopEvent.WaitAsync()); PopulateTaskList(contextList, taskList); Debug($"MAIN LOOP - SCHEDULER DONE: {connections.Count} {taskList.Count}"); } var finished = await Task.WhenAny(taskList).ConfigureAwait(false); Debug($"MAIN LOOP TASK: {finished.Status} {finished == taskList[0]} {taskList[0].Status}"); ListenerOperation operation = null; ListenerContext context; bool success; lock (this) { if (closed) { break; } if (finished == taskList[0]) { mainLoopEvent.Reset(); continue; } int idx = -1; for (int i = 0; i < contextList.Count; i++) { if (finished == taskList[i + 1]) { idx = i; break; } } var task = contextList[idx]; context = task.Context; listenerTasks.Remove(task); var me = $"MAIN LOOP TASK #1 ({idx}, {context.State})"; Debug($"{me}"); if (context.State == ConnectionState.Listening && currentOperation?.AssignedContext == context) { operation = Interlocked.Exchange(ref currentOperation, null); Debug($"{me} GOT OPERATION {operation.ID}"); } try { success = context.MainLoopListenerTaskDone(TestContext, cts.Token); } catch (Exception ex) { Debug($"{me} EX: {ex.Message}"); connections.Remove(context); context.Dispose(); success = false; } Debug($"{me} OP={operation?.ID}"); } if (operation != null) { operation.Finish(); } } Debug($"MAIN LOOP COMPLETE"); lock (this) { var iter = connections.First; while (iter != null) { var node = iter.Value; iter = iter.Next; node.Dispose(); connections.Remove(node); } cts.Dispose(); exited = true; } Debug($"MAIN LOOP COMPLETE #1"); finishedEvent.SetResult(null); void PopulateTaskList(List <ListenerTask> contextList, List <Task> taskList) { var iter = listenerTasks.First; while (iter != null) { var node = iter; iter = iter.Next; contextList.Add(node.Value); taskList.Add(node.Value.Task); } } }
internal void RegisterProxyAuth(ListenerOperation redirect) { redirect.parentOperation = this; }
public ListenerTask MainLoopListenerTask(TestContext ctx, CancellationToken cancellationToken) { var me = $"{Listener.ME}({Connection.ME}) TASK"; HttpOperation clientOperation = null; lock (Listener) { if (currentListenerTask != null) { throw new InvalidOperationException(); } if (Listener.UsingInstrumentation) { if (assignedOperation == null) { throw new InvalidOperationException(); } clientOperation = assignedOperation.Operation; } } ctx.LogDebug(5, $"{me} {State}"); currentListenerTask = StartListenerTask(); currentListenerTask.Start(); return(currentListenerTask); ListenerTask StartListenerTask() { switch (State) { case ConnectionState.Listening: return(ListenerTask.Create(this, State, Start, Accepted)); case ConnectionState.InitializeConnection: return(ListenerTask.Create(this, State, InitializeConnection, Initialized)); case ConnectionState.WaitingForRequest: return(ListenerTask.Create(this, State, ReadRequestHeader, GotRequest)); case ConnectionState.HasRequest: return(ListenerTask.Create(this, State, HandleRequest, RequestComplete)); case ConnectionState.RequestComplete: return(ListenerTask.Create(this, State, WriteResponse, ResponseWritten)); case ConnectionState.InitializeProxyConnection: return(ListenerTask.Create(this, State, InitProxyConnection, InitProxyConnectionDone)); case ConnectionState.ConnectToTarget: return(ListenerTask.Create(this, State, ConnectToTarget, ProxyConnectionEstablished)); case ConnectionState.HandleProxyConnection: return(ListenerTask.Create(this, State, HandleProxyConnection, ProxyConnectionFinished)); case ConnectionState.RunTunnel: return(ListenerTask.Create(this, State, ProxyConnect, ProxyConnectDone)); case ConnectionState.WaitForClose: return(ListenerTask.Create(this, State, WaitForClose, WaitForCloseDone)); default: throw ctx.AssertFail(State); } } Task Start() { return(Accept(ctx, cancellationToken)); } ConnectionState Accepted() { return(ConnectionState.InitializeConnection); } Task <(bool complete, bool success)> InitializeConnection() { return(Initialize(ctx, clientOperation, cancellationToken)); } ConnectionState Initialized(bool completed, bool success) { ctx.LogDebug(5, $"{me} INITIALIZED: {completed} {success}"); if (!completed) { return(ConnectionState.CannotReuseConnection); } if (!success) { return(ConnectionState.Closed); } return(ConnectionState.WaitingForRequest); } Task <HttpRequest> ReadRequestHeader() { ctx.LogDebug(5, $"{me} READ REQUEST HEADER"); return(Connection.ReadRequestHeader(ctx, cancellationToken)); } ConnectionState GotRequest(HttpRequest request) { currentRequest = request; if (request.Method == "CONNECT") { Server.BumpRequestCount(); return(ConnectionState.RunTunnel); } var operation = Listener.GetOperation(this, request); if (operation == null) { ctx.LogDebug(5, $"{me} INVALID REQUEST: {request.Path}"); return(ConnectionState.Closed); } currentOperation = operation; ctx.LogDebug(5, $"{me} GOT REQUEST"); if (Listener.TargetListener == null) { return(ConnectionState.HasRequest); } return(ConnectionState.InitializeProxyConnection); } Task <HttpResponse> HandleRequest() { return(currentOperation.HandleRequest(ctx, this, Connection, currentRequest, cancellationToken)); } async Task InitProxyConnection() { await currentRequest.ReadHeaders(ctx, cancellationToken).ConfigureAwait(false); await currentRequest.Read(ctx, cancellationToken); } ConnectionState InitProxyConnectionDone() { var request = currentRequest; var operation = currentOperation; var remoteAddress = connection.RemoteEndPoint.Address; request.AddHeader("X-Forwarded-For", remoteAddress); var authManager = ((BuiltinProxyServer)Listener.Server).AuthenticationManager; if (authManager != null) { AuthenticationState state; var response = authManager.HandleAuthentication(ctx, connection, request, out state); if (response != null) { Listener.TargetListener.UnregisterOperation(operation); response.Redirect = Listener.RegisterOperation(ctx, operation.Operation, operation.Handler, request.Path); operation.RegisterProxyAuth(response.Redirect); return(RequestComplete(response)); } // HACK: Mono rewrites chunked requests into non-chunked. request.AddHeader("X-Mono-Redirected", "true"); } return(ConnectionState.ConnectToTarget); } async Task ConnectToTarget() { ctx.LogDebug(5, $"{me} CONNECT TO TARGET"); targetConnection = new SocketConnection(Listener.TargetListener.Server); var targetEndPoint = new DnsEndPoint(currentOperation.Uri.Host, currentOperation.Uri.Port); ctx.LogDebug(5, $"{me} CONNECT TO TARGET #1: {targetEndPoint}"); cancellationToken.ThrowIfCancellationRequested(); await targetConnection.ConnectAsync(ctx, targetEndPoint, cancellationToken); ctx.LogDebug(5, $"{me} CONNECT TO TARGET #2"); cancellationToken.ThrowIfCancellationRequested(); await targetConnection.Initialize(ctx, currentOperation.Operation, cancellationToken); ctx.LogDebug(5, $"{me} CONNECT TO TARGET #3"); } ConnectionState ProxyConnectionEstablished() { return(ConnectionState.HandleProxyConnection); } async Task <HttpResponse> HandleProxyConnection() { var copyResponseTask = CopyResponse(); cancellationToken.ThrowIfCancellationRequested(); await targetConnection.WriteRequest(ctx, currentRequest, cancellationToken); ctx.LogDebug(5, $"{me} HANDLE PROXY CONNECTION"); cancellationToken.ThrowIfCancellationRequested(); var response = await copyResponseTask.ConfigureAwait(false); ctx.LogDebug(5, $"{me} HANDLE PROXY CONNECTION #1"); return(response); } ConnectionState ProxyConnectionFinished(HttpResponse response) { currentResponse = response; targetConnection.Dispose(); targetConnection = null; return(ConnectionState.RequestComplete); } async Task <HttpResponse> CopyResponse() { cancellationToken.ThrowIfCancellationRequested(); var response = await targetConnection.ReadResponse(ctx, cancellationToken).ConfigureAwait(false); response.SetHeader("Connection", "close"); response.SetHeader("Proxy-Connection", "close"); return(response); } async Task ProxyConnect() { await CreateTunnel(ctx, ((ProxyConnection)connection).Stream, currentRequest, cancellationToken); } ConnectionState ProxyConnectDone() { return(ConnectionState.Closed); } ConnectionState RequestComplete(HttpResponse response) { ctx.LogDebug(5, $"{me}: {response} {response.Redirect?.ME}"); currentResponse = response; var keepAlive = (response.KeepAlive ?? false) && !response.CloseConnection; if (response.Redirect != null && Listener.UsingInstrumentation && (!keepAlive || Operation.Operation.HasAnyFlags(HttpOperationFlags.RedirectOnNewConnection))) { return(ConnectionState.NeedContextForRedirect); } return(ConnectionState.RequestComplete); } async Task <bool> WriteResponse() { var response = Interlocked.Exchange(ref currentResponse, null); var redirect = Interlocked.Exchange(ref redirectContext, null); redirectRequested = response.Redirect; var keepAlive = (response.KeepAlive ?? false) && !response.CloseConnection; if (redirect != null) { ctx.LogDebug(5, $"{me} REDIRECT ON NEW CONTEXT: {redirect.ME}!"); await redirect.ServerStartTask.ConfigureAwait(false); ctx.LogDebug(5, $"{me} REDIRECT ON NEW CONTEXT #1: {redirect.ME}!"); keepAlive = false; } if (!Operation.Operation.HasAnyFlags(HttpOperationFlags.DontWriteResponse)) { await connection.WriteResponse(ctx, response, cancellationToken).ConfigureAwait(false); } return(keepAlive); } ConnectionState ResponseWritten(bool keepAlive) { var request = Interlocked.Exchange(ref currentRequest, null); var operation = Interlocked.Exchange(ref currentOperation, null); var redirect = Interlocked.Exchange(ref redirectRequested, null); if (redirect != null && (operation?.Operation.HasAnyFlags(HttpOperationFlags.RedirectOnNewConnection) ?? false)) { return(ConnectionState.WaitForClose); } if (!keepAlive) { return(ConnectionState.Closed); } if (redirect == null) { return(ConnectionState.ReuseConnection); } if (operation?.Operation.HasAnyFlags(HttpOperationFlags.ServerAbortsRedirection) ?? false) { connection.Dispose(); } return(ConnectionState.WaitingForRequest); } async Task <bool> WaitForClose() { var result = await Connection.WaitForRequest(2500, cancellationToken).ConfigureAwait(false); ctx.LogDebug(5, $"{me} WAIT FOR CLOSE #1: {result}"); return(ctx.Expect(result, Is.False, "expected client to close the connection")); } ConnectionState WaitForCloseDone(bool result) { return(result ? ConnectionState.Closed : ConnectionState.CannotReuseConnection); } }