/// <summary> /// Logs a message to all configured loggers /// </summary> /// <returns>The awaitable task.</returns> /// <param name="context">The request context.</param> /// <param name="ex">Exception data, if any.</param> /// <param name="start">The request start time.</param> /// <param name="duration">The request duration.</param> private static Task LogMessageAsync(RunnerControl controller, HttpContext context, Exception ex, DateTime start, TimeSpan duration) { var config = controller.Config; if (config.Loggers == null) { return(Task.FromResult(true)); } CopyLogData?.Invoke(context); var count = config.Loggers.Count; if (count == 0) { return(Task.FromResult(true)); } else if (count == 1) { return(config.Loggers[0].LogRequest(context, ex, start, duration)); } else { return(Task.WhenAll(config.Loggers.Select(x => x.LogRequest(context, ex, start, duration)))); } }
/// <summary> /// Listens to incoming connections and calls the spawner method for each new connection /// </summary> /// <returns>Awaitable task.</returns> /// <param name="addr">The address to listen to.</param> /// <param name="usessl">A flag indicating if the socket listens for SSL requests</param> /// <param name="stoptoken">The stoptoken.</param> /// <param name="config">The server configuration</param> /// <param name="spawner">The method handling the new connection.</param> private static async Task ListenToSocketInternalAsync(IPEndPoint addr, bool usessl, CancellationToken stoptoken, ServerConfig config, Action <TcpClient, EndPoint, string, RunnerControl> spawner) { var rc = new RunnerControl(stoptoken, usessl, config); var listener = new TcpListener(addr); listener.Start(config.SocketBacklog); var taskid = SetLoggingSocketHandlerID(); while (!stoptoken.IsCancellationRequested) { // Wait if there are too many active config.DebugLogHandler?.Invoke("Waiting for throttle", taskid, null); await rc.ThrottleTask; config.DebugLogHandler?.Invoke("Waiting for socket", taskid, null); var ls = listener.AcceptTcpClientAsync(); if (await Task.WhenAny(rc.StopTask, ls) == ls) { config.DebugLogHandler?.Invoke("Re-waiting for socket", taskid, null); var client = await ls; var newtaskid = SetLoggingTaskHandlerID(); try { int wt, cpt; ThreadPool.GetAvailableThreads(out wt, out cpt); config.DebugLogHandler?.Invoke(string.Format("Threadpool says {0}, {1}", wt, cpt), taskid, newtaskid); config.DebugLogHandler?.Invoke(string.Format("Spawning runner with id: {0}", newtaskid), taskid, newtaskid); // Read the endpoint here to avoid crashes when invoking the spawner var ep = client.Client.RemoteEndPoint; ThreadPool.QueueUserWorkItem(x => spawner(client, ep, newtaskid, rc)); } catch (Exception ex) { config.DebugLogHandler?.Invoke("Failed to listen to socket", taskid, ex); } } } config.DebugLogHandler?.Invoke("Stopping", taskid, null); listener.Stop(); rc.Stop(taskid); config.DebugLogHandler?.Invoke("Socket stopped, waiting for workers ...", taskid, null); await rc.FinishedTask; config.DebugLogHandler?.Invoke("Stopped", taskid, null); }
/// <summary> /// Setup this instance /// </summary> /// <param name="usessl">If set to <c>true</c> use ssl.</param> /// <param name="config">The configuration.</param> protected void Setup(bool usessl, ServerConfig config) { if (StopToken != null) { throw new Exception("Cannot call setup more than once"); } if (config == null) { throw new ArgumentNullException(nameof(config)); } StopToken = new CancellationTokenSource(); Controller = new RunnerControl(StopToken.Token, usessl, config); }
/// <summary> /// Listens to incoming connections and calls the spawner method for each new connection /// </summary> /// <returns>Awaitable task.</returns> /// <param name="acceptAsync">Method that returns an accepted socket.</param> /// <param name="usessl">A flag indicating if the socket listens for SSL requests</param> /// <param name="stoptoken">The stoptoken.</param> /// <param name="config">The server configuration</param> /// <param name="spawner">The method handling the new connection.</param> private static async Task ListenToSocketInternalAsync(Func <CancellationToken, Task <KeyValuePair <long, EndPoint> > > acceptAsync, bool usessl, CancellationToken stoptoken, ServerConfig config, Action <long, EndPoint, string, RunnerControl> spawner) { if (acceptAsync == null) { throw new ArgumentNullException(nameof(acceptAsync)); } var rc = new RunnerControl(stoptoken, usessl, config); var taskid = SetLoggingSocketHandlerID(); while (!stoptoken.IsCancellationRequested) { // Wait if there are too many active config.DebugLogHandler?.Invoke("Waiting for throttle", taskid, null); await rc.ThrottleTask; config.DebugLogHandler?.Invoke("Waiting for socket", taskid, null); var ls = acceptAsync(stoptoken); if (await Task.WhenAny(rc.StopTask, ls) == ls) { config.DebugLogHandler?.Invoke("Re-waiting for socket", taskid, null); var client = await ls; var newtaskid = SetLoggingTaskHandlerID(); try { int wt, cpt; ThreadPool.GetAvailableThreads(out wt, out cpt); config.DebugLogHandler?.Invoke(string.Format("Threadpool says {0}, {1}", wt, cpt), taskid, newtaskid); config.DebugLogHandler?.Invoke(string.Format("Spawning runner with id: {0}", newtaskid), taskid, newtaskid); ThreadPool.QueueUserWorkItem(x => spawner(client.Key, client.Value, newtaskid, rc)); } catch (Exception ex) { config.DebugLogHandler?.Invoke("Failed to listen to socket", taskid, ex); } } } config.DebugLogHandler?.Invoke("Stopping", taskid, null); rc.Stop(taskid); config.DebugLogHandler?.Invoke("Socket stopped, waiting for workers ...", taskid, null); await rc.FinishedTask; config.DebugLogHandler?.Invoke("Stopped", taskid, null); }
/// <summary> /// Dispatcher method for handling a request /// </summary> /// <param name="stream">The underlying stream.</param> /// <param name="endpoint">The remote endpoint.</param> /// <param name="logtaskid">The task id for logging and tracing</param> /// <param name="clientcert">The client certificate if any.</param> /// <param name="controller">The runner controller.</param> /// <param name="sslProtocol">The SSL protocol being used</param> /// <param name="isConnected">A method for checking if the socket is connected</param> private static async Task Runner(Stream stream, EndPoint endpoint, string logtaskid, X509Certificate clientcert, SslProtocols sslProtocol, RunnerControl controller, Func <bool> isConnected) { var config = controller.Config; var storage = config.Storage; var requests = config.KeepAliveMaxRequests; bool keepingalive = false; HttpContext context = null; HttpRequest cur = null; HttpResponse resp = null; DateTime started = new DateTime(); using (var bs = new BufferedStreamReader(stream)) { try { config.DebugLogHandler?.Invoke("Running task", logtaskid, endpoint); if (!controller.RegisterActive(logtaskid)) { return; } do { var reqid = SetLoggingRequestID(); bs.ResetReadLength(config.MaxPostSize); started = DateTime.Now; context = new HttpContext( cur = new HttpRequest(endpoint, logtaskid, reqid, clientcert, sslProtocol, isConnected), resp = new HttpResponse(stream, config), storage ); // Setup up the callback for allowing handlers to report errors context.LogHandlerDelegate = (ex) => LogMessageAsync(controller, context, ex, started, DateTime.Now - started); var timeoutcontroltask = new TaskCompletionSource <bool>(); var idletime = TimeSpan.FromSeconds(config.RequestHeaderReadTimeoutSeconds); // Set up timeout for processing cur.SetProcessingTimeout(TimeSpan.FromSeconds(config.MaxProcessingTimeSeconds)); config.DebugLogHandler?.Invoke("Parsing headers", logtaskid, endpoint); try { var ct = new CancellationTokenSource(); ct.CancelAfter(TimeSpan.FromSeconds(keepingalive ? config.KeepAliveTimeoutSeconds : config.RequestIdleTimeoutSeconds)); using (ct.Token.Register(() => timeoutcontroltask.TrySetCanceled(), useSynchronizationContext: false)) await cur.Parse(bs, config, idletime, timeoutcontroltask.Task, controller.StopTask); } catch (EmptyStreamClosedException) { // Client has closed the connection break; } catch (HttpException hex) { // Errors during header parsing are unlikely to // keep the connection in a consistent state resp.StatusCode = hex.StatusCode; resp.StatusMessage = hex.StatusMessage; await resp.FlushHeadersAsync(); throw; } catch (Exception ex) { config.DebugLogHandler?.Invoke($"Failed while reading header: {ex}", logtaskid, cur); throw; } string keepalive; cur.Headers.TryGetValue("Connection", out keepalive); if (("keep-alive".Equals(keepalive, StringComparison.OrdinalIgnoreCase) || keepingalive) && requests > 1) { resp.KeepAlive = true; if (!keepingalive) { resp.AddHeader("Keep-Alive", string.Format("timeout={0}, max={1}", config.KeepAliveTimeoutSeconds, config.KeepAliveMaxRequests)); } } else { resp.KeepAlive = false; } if (config.Loggers != null) { var count = config.Loggers.Count; if (count == 1) { var sl = config.Loggers[0] as IStartLogger; if (sl != null) { await sl.LogRequestStarted(cur); } } else if (count != 0) { await Task.WhenAll(config.Loggers.Where(x => x is IStartLogger).Cast <IStartLogger>().Select(x => x.LogRequestStarted(cur))); } } config.DebugLogHandler?.Invoke("Running handler", logtaskid, cur); try { // Trigger the streams to stop reading/writing data when the timeout happens using (cur.TimeoutCancellationToken.Register(() => timeoutcontroltask.TrySetCanceled(), useSynchronizationContext: false)) { if (cur.TimeoutCancellationToken.IsCancellationRequested) { if (timeoutcontroltask.Task.Status == TaskStatus.Canceled) { throw new TimeoutException(); } else { timeoutcontroltask.TrySetCanceled(); throw new OperationCanceledException(); } } // Process the request do { cur.ClearHandlerStack(); var target = resp.ClearInternalRedirect(); if (target != null) { cur.Path = target; } if (!await config.Router.Process(context)) { throw new HttpException(Ceen.HttpStatusCode.NotFound); } }while (resp.IsRedirectingInternally); } } catch (HttpException hex) { // Try to set the status code to 500 if (resp.HasSentHeaders) { throw; } resp.StatusCode = hex.StatusCode; resp.StatusMessage = hex.StatusMessage; } config.DebugLogHandler?.Invoke("Flushing response", logtaskid, cur); // If the handler has not flushed, we do it await resp.FlushAndSetLengthAsync(); // Check if keep-alive is possible keepingalive = resp.KeepAlive && resp.HasWrittenCorrectLength; requests--; await LogMessageAsync(controller, context, null, started, DateTime.Now - started); } while (keepingalive); } catch (Exception ex) { // If possible, report a 500 error to the client if (resp != null) { try { if (!resp.HasSentHeaders) { resp.StatusCode = Ceen.HttpStatusCode.InternalServerError; resp.StatusMessage = HttpStatusMessages.DefaultMessage(Ceen.HttpStatusCode.InternalServerError); } } catch (Exception nex) { config.DebugLogHandler?.Invoke($"Failed to send headers: {nex}", logtaskid, cur); } try { await resp.FlushAsErrorAsync(); } catch (Exception nex) { config.DebugLogHandler?.Invoke($"Failed to FlushAsErrors: {nex}", logtaskid, cur); } } try { stream.Close(); } catch (Exception nex) { config.DebugLogHandler?.Invoke($"Failed to close stream: {nex}", logtaskid, cur); } try { await LogMessageAsync(controller, context, ex, started, DateTime.Now - started); } catch (Exception nex) { config.DebugLogHandler?.Invoke($"Failed to log request: {nex}", logtaskid, cur); } config.DebugLogHandler?.Invoke("Failed handler", logtaskid, cur); } finally { controller.RegisterStopped(logtaskid); config.DebugLogHandler?.Invoke("Terminating handler", logtaskid, cur); } } }
/// <summary> /// Handler method for connections /// </summary> /// <param name="stream">The stream.</param> /// <param name="remoteEndPoint">The remote endpoint.</param> /// <param name="logtaskid">The task id for logging and tracing</param> /// <param name="controller">The runner controller.</param> private static async Task RunClient(Stream stream, EndPoint remoteEndPoint, string logtaskid, RunnerControl controller, Func <bool> isConnected) { var config = controller.Config; var storage = config.Storage; using (stream) using (var ssl = controller.m_useSSL ? new SslStream(stream, false) : null) { config.DebugLogHandler?.Invoke(string.Format("Running {0}", controller.m_useSSL ? "SSL" : "plain"), logtaskid, remoteEndPoint); // Slightly higher value here to avoid races with the other timeout mechanisms stream.ReadTimeout = stream.WriteTimeout = (controller.Config.RequestIdleTimeoutSeconds + 1) * 1000; X509Certificate clientcert = null; // For SSL only: negotiate the connection if (ssl != null) { config.DebugLogHandler?.Invoke("Authenticate SSL", logtaskid, remoteEndPoint); try { await ssl.AuthenticateAsServerAsync(config.SSLCertificate, config.SSLRequireClientCert, config.SSLEnabledProtocols, config.SSLCheckCertificateRevocation); } catch (Exception aex) { config.DebugLogHandler?.Invoke("Failed setting up SSL", logtaskid, remoteEndPoint); // Log a message indicating that we failed setting up SSL await LogMessageAsync(controller, new HttpContext(new HttpRequest(remoteEndPoint, logtaskid, logtaskid, null, SslProtocols.None, () => false), null, storage), aex, DateTime.Now, new TimeSpan()); return; } config.DebugLogHandler?.Invoke("Run SSL", logtaskid, remoteEndPoint); clientcert = ssl.RemoteCertificate; } await Runner(ssl == null?(Stream)stream : ssl, remoteEndPoint, logtaskid, clientcert, ssl == null?SslProtocols.None : ssl.SslProtocol, controller, isConnected); config.DebugLogHandler?.Invoke("Done running", logtaskid, remoteEndPoint); } }
/// <summary> /// Handler method for connections /// </summary> /// <param name="client">The new connection.</param> /// <param name="remoteEndPoint">The remote endpoint.</param> /// <param name="logtaskid">The task id for logging and tracing</param> /// <param name="controller">The runner controller.</param> private static async void RunClient(TcpClient client, EndPoint remoteEndPoint, string logtaskid, RunnerControl controller) { using (client) await RunClient(client.GetStream(), remoteEndPoint, logtaskid, controller, () => client.Connected); }
/// <summary> /// Runs a client, using a socket handle from DuplicateAndClose /// </summary> /// <param name="socketinfo">The socket handle.</param> /// <param name="remoteEndPoint">The remote endpoint.</param> /// <param name="logtaskid">The log task ID.</param> /// <param name="controller">The controller instance</param> private static void RunClient(SocketInformation socketinfo, EndPoint remoteEndPoint, string logtaskid, RunnerControl controller) { var client = new TcpClient() { Client = new Socket(socketinfo) }; RunClient(client, remoteEndPoint, logtaskid, controller); }