diff --git a/src/Titanium.Web.Proxy/EventArguments/LimitedStream.cs b/src/Titanium.Web.Proxy/EventArguments/LimitedStream.cs index e26059e99..646390a3c 100644 --- a/src/Titanium.Web.Proxy/EventArguments/LimitedStream.cs +++ b/src/Titanium.Web.Proxy/EventArguments/LimitedStream.cs @@ -3,6 +3,7 @@ using System.IO; using System.Threading.Tasks; using Titanium.Web.Proxy.Exceptions; +using Titanium.Web.Proxy.Helpers; using Titanium.Web.Proxy.StreamExtended.BufferPool; using Titanium.Web.Proxy.StreamExtended.Network; @@ -11,16 +12,16 @@ namespace Titanium.Web.Proxy.EventArguments internal class LimitedStream : Stream { private readonly IBufferPool bufferPool; - private readonly CustomBufferedStream baseStream; + private readonly IHttpStreamReader baseReader; private readonly bool isChunked; private long bytesRemaining; private bool readChunkTrail; - internal LimitedStream(CustomBufferedStream baseStream, IBufferPool bufferPool, bool isChunked, + internal LimitedStream(IHttpStreamReader baseStream, IBufferPool bufferPool, bool isChunked, long contentLength) { - this.baseStream = baseStream; + this.baseReader = baseStream; this.bufferPool = bufferPool; this.isChunked = isChunked; bytesRemaining = isChunked @@ -49,12 +50,12 @@ private void getNextChunk() if (readChunkTrail) { // read the chunk trail of the previous chunk - string? s = baseStream.ReadLineAsync().Result; + string? s = baseReader.ReadLineAsync().Result; } readChunkTrail = true; - string? chunkHead = baseStream.ReadLineAsync().Result!; + string? chunkHead = baseReader.ReadLineAsync().Result!; int idx = chunkHead.IndexOf(";", StringComparison.Ordinal); if (idx >= 0) { @@ -73,7 +74,7 @@ private void getNextChunk() bytesRemaining = -1; // chunk trail - var task = baseStream.ReadLineAsync(); + var task = baseReader.ReadLineAsync(); if (!task.IsCompleted) task.AsTask().Wait(); } @@ -119,7 +120,7 @@ public override int Read(byte[] buffer, int offset, int count) } int toRead = (int)Math.Min(count, bytesRemaining); - int res = baseStream.Read(buffer, offset, toRead); + int res = baseReader.Read(buffer, offset, toRead); bytesRemaining -= res; if (res == 0) diff --git a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs index ab872705b..97294ea92 100644 --- a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgs.cs @@ -10,6 +10,7 @@ using Titanium.Web.Proxy.Http.Responses; using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Network; +using Titanium.Web.Proxy.Network.Tcp; using Titanium.Web.Proxy.StreamExtended.Network; namespace Titanium.Web.Proxy.EventArguments @@ -35,8 +36,8 @@ public class SessionEventArgs : SessionEventArgsBase /// /// Constructor to initialize the proxy /// - internal SessionEventArgs(ProxyServer server, ProxyEndPoint endPoint, ProxyClient proxyClient, ConnectRequest? connectRequest, CancellationTokenSource cancellationTokenSource) - : base(server, endPoint, proxyClient, connectRequest, new Request(), cancellationTokenSource) + internal SessionEventArgs(ProxyServer server, ProxyEndPoint endPoint, TcpClientConnection clientConnection, HttpClientStream clientStream, ConnectRequest? connectRequest, CancellationTokenSource cancellationTokenSource) + : base(server, endPoint, clientConnection, clientStream, connectRequest, new Request(), cancellationTokenSource) { } @@ -64,14 +65,9 @@ public bool ReRequest /// public event EventHandler? MultipartRequestPartSent; - private CustomBufferedStream getStreamReader(bool isRequest) + private HttpStream getStream(bool isRequest) { - return isRequest ? ProxyClient.ClientStream : HttpClient.Connection.Stream; - } - - private HttpWriter getStreamWriter(bool isRequest) - { - return isRequest ? (HttpWriter)ProxyClient.ClientStreamWriter : HttpClient.Connection.StreamWriter; + return isRequest ? (HttpStream)ClientStream : HttpClient.Connection.Stream; } /// @@ -197,21 +193,19 @@ private async Task readResponseBodyAsync(CancellationToken cancellationToken) private async Task readBodyAsync(bool isRequest, CancellationToken cancellationToken) { - using (var bodyStream = new MemoryStream()) - { - var writer = new HttpWriter(bodyStream, BufferPool); - - if (isRequest) - { - await CopyRequestBodyAsync(writer, TransformationMode.Uncompress, cancellationToken); - } - else - { - await CopyResponseBodyAsync(writer, TransformationMode.Uncompress, cancellationToken); - } + using var bodyStream = new MemoryStream(); + using var http = new HttpStream(bodyStream, BufferPool); - return bodyStream.ToArray(); + if (isRequest) + { + await CopyRequestBodyAsync(http, TransformationMode.Uncompress, cancellationToken); } + else + { + await CopyResponseBodyAsync(http, TransformationMode.Uncompress, cancellationToken); + } + + return bodyStream.ToArray(); } /// @@ -229,18 +223,16 @@ internal async Task SyphonOutBodyAsync(bool isRequest, CancellationToken cancell return; } - using (var bodyStream = new MemoryStream()) - { - var writer = new HttpWriter(bodyStream, BufferPool); - await copyBodyAsync(isRequest, true, writer, TransformationMode.None, null, cancellationToken); - } + using var bodyStream = new MemoryStream(); + using var http = new HttpStream(bodyStream, BufferPool); + await copyBodyAsync(isRequest, true, http, TransformationMode.None, null, cancellationToken); } /// /// This is called when the request is PUT/POST/PATCH to read the body /// /// - internal async Task CopyRequestBodyAsync(HttpWriter writer, TransformationMode transformation, CancellationToken cancellationToken) + internal async Task CopyRequestBodyAsync(IHttpStreamWriter writer, TransformationMode transformation, CancellationToken cancellationToken) { var request = HttpClient.Request; @@ -249,7 +241,7 @@ internal async Task CopyRequestBodyAsync(HttpWriter writer, TransformationMode t // send the request body bytes to server if (contentLength > 0 && hasMulipartEventSubscribers && request.IsMultipartFormData) { - var reader = getStreamReader(true); + var reader = getStream(true); var boundary = HttpHelper.GetBoundaryFromContentType(request.ContentType); using (var copyStream = new CopyStream(reader, writer, BufferPool)) @@ -279,14 +271,14 @@ internal async Task CopyRequestBodyAsync(HttpWriter writer, TransformationMode t } } - internal async Task CopyResponseBodyAsync(HttpWriter writer, TransformationMode transformation, CancellationToken cancellationToken) + internal async Task CopyResponseBodyAsync(IHttpStreamWriter writer, TransformationMode transformation, CancellationToken cancellationToken) { await copyBodyAsync(false, false, writer, transformation, OnDataReceived, cancellationToken); } - private async Task copyBodyAsync(bool isRequest, bool useOriginalHeaderValues, HttpWriter writer, TransformationMode transformation, Action? onCopy, CancellationToken cancellationToken) + private async Task copyBodyAsync(bool isRequest, bool useOriginalHeaderValues, IHttpStreamWriter writer, TransformationMode transformation, Action? onCopy, CancellationToken cancellationToken) { - var stream = getStreamReader(isRequest); + var stream = getStream(isRequest); var requestResponse = isRequest ? (RequestResponseBase)HttpClient.Request : HttpClient.Response; @@ -313,10 +305,8 @@ private async Task copyBodyAsync(bool isRequest, bool useOriginalHeaderValues, H try { - using (var bufStream = new CustomBufferedStream(s, BufferPool, true)) - { - await writer.CopyBodyAsync(bufStream, false, -1, onCopy, cancellationToken); - } + var http = new HttpStream(s, BufferPool, true); + await writer.CopyBodyAsync(http, false, -1, onCopy, cancellationToken); } finally { diff --git a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs index 84b1e856d..f3bf92309 100644 --- a/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs +++ b/src/Titanium.Web.Proxy/EventArguments/SessionEventArgsBase.cs @@ -26,7 +26,12 @@ public abstract class SessionEventArgsBase : EventArgs, IDisposable internal TcpServerConnection ServerConnection => HttpClient.Connection; - internal TcpClientConnection ClientConnection => ProxyClient.Connection; + /// + /// Holds a reference to client + /// + internal TcpClientConnection ClientConnection { get; } + + internal HttpClientStream ClientStream { get; } protected readonly IBufferPool BufferPool; protected readonly ExceptionHandler ExceptionFunc; @@ -41,7 +46,7 @@ public abstract class SessionEventArgsBase : EventArgs, IDisposable /// Initializes a new instance of the class. /// private protected SessionEventArgsBase(ProxyServer server, ProxyEndPoint endPoint, - ProxyClient proxyClient, ConnectRequest? connectRequest, Request request, CancellationTokenSource cancellationTokenSource) + TcpClientConnection clientConnection, HttpClientStream clientStream, ConnectRequest? connectRequest, Request request, CancellationTokenSource cancellationTokenSource) { BufferPool = server.BufferPool; ExceptionFunc = server.ExceptionFunc; @@ -49,17 +54,13 @@ private protected SessionEventArgsBase(ProxyServer server, ProxyEndPoint endPoin CancellationTokenSource = cancellationTokenSource; - ProxyClient = proxyClient; - HttpClient = new HttpWebClient(connectRequest, request, new Lazy(() => ProxyClient.Connection.GetProcessId(endPoint))); + ClientConnection = clientConnection; + ClientStream = clientStream; + HttpClient = new HttpWebClient(connectRequest, request, new Lazy(() => clientConnection.GetProcessId(endPoint))); LocalEndPoint = endPoint; EnableWinAuth = server.EnableWinAuth && isWindowsAuthenticationSupported; } - /// - /// Holds a reference to client - /// - internal ProxyClient ProxyClient { get; } - /// /// Returns a user data for this request/response session which is /// same as the user data of HttpClient. @@ -93,7 +94,7 @@ public bool EnableWinAuth /// /// Client End Point. /// - public IPEndPoint ClientEndPoint => (IPEndPoint)ProxyClient.Connection.RemoteEndPoint; + public IPEndPoint ClientEndPoint => (IPEndPoint)ClientConnection.RemoteEndPoint; /// /// The web client used to communicate with server for this session. @@ -106,7 +107,7 @@ public bool EnableWinAuth /// /// Are we using a custom upstream HTTP(S) proxy? /// - public ExternalProxy? CustomUpStreamProxyUsed { get; internal set; } + public IExternalProxy? CustomUpStreamProxyUsed { get; internal set; } /// /// Local endpoint via which we make the request. diff --git a/src/Titanium.Web.Proxy/EventArguments/TunnelConnectEventArgs.cs b/src/Titanium.Web.Proxy/EventArguments/TunnelConnectEventArgs.cs index b02ab94d8..494b51754 100644 --- a/src/Titanium.Web.Proxy/EventArguments/TunnelConnectEventArgs.cs +++ b/src/Titanium.Web.Proxy/EventArguments/TunnelConnectEventArgs.cs @@ -1,8 +1,10 @@ using System; using System.Threading; +using Titanium.Web.Proxy.Helpers; using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Models; using Titanium.Web.Proxy.Network; +using Titanium.Web.Proxy.Network.Tcp; using Titanium.Web.Proxy.StreamExtended.Network; namespace Titanium.Web.Proxy.EventArguments @@ -15,8 +17,8 @@ public class TunnelConnectSessionEventArgs : SessionEventArgsBase private bool? isHttpsConnect; internal TunnelConnectSessionEventArgs(ProxyServer server, ProxyEndPoint endPoint, ConnectRequest connectRequest, - ProxyClient proxyClient, CancellationTokenSource cancellationTokenSource) - : base(server, endPoint, proxyClient, connectRequest, connectRequest, cancellationTokenSource) + TcpClientConnection clientConnection, HttpClientStream clientStream, CancellationTokenSource cancellationTokenSource) + : base(server, endPoint, clientConnection, clientStream, connectRequest, connectRequest, cancellationTokenSource) { } diff --git a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs index cf92d860f..c8402c9c7 100644 --- a/src/Titanium.Web.Proxy/ExplicitClientHandler.cs +++ b/src/Titanium.Web.Proxy/ExplicitClientHandler.cs @@ -36,8 +36,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - var clientStream = new CustomBufferedStream(clientConnection.GetStream(), BufferPool); - var clientStreamWriter = new HttpResponseWriter(clientStream, BufferPool); + var clientStream = new HttpClientStream(clientConnection.GetStream(), BufferPool); Task? prefetchConnectionTask = null; bool closeServerConnection = false; @@ -70,7 +69,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect await HeaderParser.ReadHeaders(clientStream, connectRequest.Headers, cancellationToken); connectArgs = new TunnelConnectSessionEventArgs(this, endPoint, connectRequest, - new ProxyClient(clientConnection, clientStream, clientStreamWriter), cancellationTokenSource); + clientConnection, clientStream, cancellationTokenSource); clientStream.DataRead += (o, args) => connectArgs.OnDataSent(args.Buffer, args.Offset, args.Count); clientStream.DataWrite += (o, args) => connectArgs.OnDataReceived(args.Buffer, args.Offset, args.Count); @@ -92,7 +91,7 @@ private async Task handleClient(ExplicitProxyEndPoint endPoint, TcpClientConnect } // send the response - await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, + await clientStream.WriteResponseAsync(connectArgs.HttpClient.Response, cancellationToken: cancellationToken); return; } @@ -102,7 +101,7 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, await endPoint.InvokeBeforeTunnelConnectResponse(this, connectArgs, ExceptionFunc); // send the response - await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, + await clientStream.WriteResponseAsync(connectArgs.HttpClient.Response, cancellationToken: cancellationToken); return; } @@ -115,14 +114,13 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, response.Headers.FixProxyHeaders(); connectArgs.HttpClient.Response = response; - await clientStreamWriter.WriteResponseAsync(response, cancellationToken: cancellationToken); + await clientStream.WriteResponseAsync(response, cancellationToken: cancellationToken); var clientHelloInfo = await SslTools.PeekClientHello(clientStream, BufferPool, cancellationToken); bool isClientHello = clientHelloInfo != null; if (clientHelloInfo != null) { - connectRequest.IsHttps = true; connectRequest.TunnelType = TunnelType.Https; connectRequest.ClientHelloInfo = clientHelloInfo; } @@ -131,6 +129,7 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, if (decryptSsl && clientHelloInfo != null) { + connectRequest.IsHttps = true; // todo: move this line to the previous "if" clientConnection.SslProtocol = clientHelloInfo.SslProtocol; bool http2Supported = false; @@ -216,10 +215,9 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, #endif // HTTPS server created - we can now decrypt the client's traffic - clientStream = new CustomBufferedStream(sslStream, BufferPool); + clientStream = new HttpClientStream(sslStream, BufferPool); clientStream.DataRead += (o, args) => connectArgs.OnDecryptedDataSent(args.Buffer, args.Offset, args.Count); clientStream.DataWrite += (o, args) => connectArgs.OnDecryptedDataReceived(args.Buffer, args.Offset, args.Count); - clientStreamWriter = new HttpResponseWriter(clientStream, BufferPool); } catch (Exception e) { @@ -274,7 +272,7 @@ await clientStreamWriter.WriteResponseAsync(connectArgs.HttpClient.Response, { // clientStream.Available should be at most BufferSize because it is using the same buffer size await clientStream.ReadAsync(data, 0, available, cancellationToken); - await connection.StreamWriter.WriteAsync(data, 0, available, true, cancellationToken); + await connection.Stream.WriteAsync(data, 0, available, true, cancellationToken); } finally { @@ -335,9 +333,9 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, { #if NETSTANDARD2_1 var connectionPreface = new ReadOnlyMemory(Http2Helper.ConnectionPreface); - await connection.StreamWriter.WriteAsync(connectionPreface, cancellationToken); + await connection.Stream.WriteAsync(connectionPreface, cancellationToken); await Http2Helper.SendHttp2(clientStream, connection.Stream, - () => new SessionEventArgs(this, endPoint, new ProxyClient(clientConnection, clientStream, clientStreamWriter), connectArgs?.HttpClient.ConnectRequest, cancellationTokenSource) + () => new SessionEventArgs(this, endPoint, clientConnection, clientStream, connectArgs?.HttpClient.ConnectRequest, cancellationTokenSource) { UserData = connectArgs?.UserData }, @@ -356,8 +354,7 @@ await Http2Helper.SendHttp2(clientStream, connection.Stream, calledRequestHandler = true; // Now create the request - await handleHttpSessionRequest(endPoint, clientConnection, clientStream, clientStreamWriter, - cancellationTokenSource, connectArgs, prefetchConnectionTask); + await handleHttpSessionRequest(endPoint, clientConnection, clientStream, cancellationTokenSource, connectArgs, prefetchConnectionTask); } catch (ProxyException e) { @@ -388,11 +385,6 @@ await handleHttpSessionRequest(endPoint, clientConnection, clientStream, clientS sslStream?.Dispose(); clientStream.Dispose(); - - if (!cancellationTokenSource.IsCancellationRequested) - { - cancellationTokenSource.Cancel(); - } } } } diff --git a/src/Titanium.Web.Proxy/Helpers/HttpResponseWriter.cs b/src/Titanium.Web.Proxy/Helpers/HttpClientStream.cs similarity index 87% rename from src/Titanium.Web.Proxy/Helpers/HttpResponseWriter.cs rename to src/Titanium.Web.Proxy/Helpers/HttpClientStream.cs index bbc9a71f7..09f7e22a8 100644 --- a/src/Titanium.Web.Proxy/Helpers/HttpResponseWriter.cs +++ b/src/Titanium.Web.Proxy/Helpers/HttpClientStream.cs @@ -7,9 +7,9 @@ namespace Titanium.Web.Proxy.Helpers { - internal sealed class HttpResponseWriter : HttpWriter + internal sealed class HttpClientStream : HttpStream { - internal HttpResponseWriter(Stream stream, IBufferPool bufferPool) + internal HttpClientStream(Stream stream, IBufferPool bufferPool) : base(stream, bufferPool) { } diff --git a/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs b/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs index defe76bbc..7b4ba277b 100644 --- a/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs +++ b/src/Titanium.Web.Proxy/Helpers/HttpHelper.cs @@ -170,18 +170,18 @@ internal static string GetWildCardDomainName(string hostname) /// Determines whether is connect method. /// /// 1: when CONNECT, 0: when valid HTTP method, -1: otherwise - internal static Task IsConnectMethod(CustomBufferedStream clientStreamReader, IBufferPool bufferPool, CancellationToken cancellationToken = default) + internal static Task IsConnectMethod(IPeekStream httpReader, IBufferPool bufferPool, CancellationToken cancellationToken = default) { - return startsWith(clientStreamReader, bufferPool, "CONNECT", cancellationToken); + return startsWith(httpReader, bufferPool, "CONNECT", cancellationToken); } /// /// Determines whether is pri method (HTTP/2). /// /// 1: when PRI, 0: when valid HTTP method, -1: otherwise - internal static Task IsPriMethod(CustomBufferedStream clientStreamReader, IBufferPool bufferPool, CancellationToken cancellationToken = default) + internal static Task IsPriMethod(IPeekStream httpReader, IBufferPool bufferPool, CancellationToken cancellationToken = default) { - return startsWith(clientStreamReader, bufferPool, "PRI", cancellationToken); + return startsWith(httpReader, bufferPool, "PRI", cancellationToken); } /// @@ -190,7 +190,7 @@ internal static Task IsPriMethod(CustomBufferedStream clientStreamReader, I /// /// 1: when starts with the given string, 0: when valid HTTP method, -1: otherwise /// - private static async Task startsWith(CustomBufferedStream clientStreamReader, IBufferPool bufferPool, string expectedStart, CancellationToken cancellationToken = default) + private static async Task startsWith(IPeekStream httpReader, IBufferPool bufferPool, string expectedStart, CancellationToken cancellationToken = default) { const int lengthToCheck = 10; if (bufferPool.BufferSize < lengthToCheck) @@ -205,7 +205,7 @@ private static async Task startsWith(CustomBufferedStream clientStreamReade int i = 0; while (i < lengthToCheck) { - int peeked = await clientStreamReader.PeekBytesAsync(buffer, i, i, lengthToCheck - i, cancellationToken); + int peeked = await httpReader.PeekBytesAsync(buffer, i, i, lengthToCheck - i, cancellationToken); if (peeked <= 0) return -1; diff --git a/src/Titanium.Web.Proxy/Helpers/HttpRequestWriter.cs b/src/Titanium.Web.Proxy/Helpers/HttpServerStream.cs similarity index 87% rename from src/Titanium.Web.Proxy/Helpers/HttpRequestWriter.cs rename to src/Titanium.Web.Proxy/Helpers/HttpServerStream.cs index 2800eb5ed..acfd9f43d 100644 --- a/src/Titanium.Web.Proxy/Helpers/HttpRequestWriter.cs +++ b/src/Titanium.Web.Proxy/Helpers/HttpServerStream.cs @@ -6,9 +6,9 @@ namespace Titanium.Web.Proxy.Helpers { - internal sealed class HttpRequestWriter : HttpWriter + internal sealed class HttpServerStream : HttpStream { - internal HttpRequestWriter(Stream stream, IBufferPool bufferPool) + internal HttpServerStream(Stream stream, IBufferPool bufferPool) : base(stream, bufferPool) { } diff --git a/src/Titanium.Web.Proxy/StreamExtended/Network/CustomBufferedStream.cs b/src/Titanium.Web.Proxy/Helpers/HttpStream.cs similarity index 65% rename from src/Titanium.Web.Proxy/StreamExtended/Network/CustomBufferedStream.cs rename to src/Titanium.Web.Proxy/Helpers/HttpStream.cs index 7055202f6..89e9337ff 100644 --- a/src/Titanium.Web.Proxy/StreamExtended/Network/CustomBufferedStream.cs +++ b/src/Titanium.Web.Proxy/Helpers/HttpStream.cs @@ -1,24 +1,23 @@ using System; +using System.Buffers; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using Titanium.Web.Proxy.Helpers; +using Titanium.Web.Proxy.Http; using Titanium.Web.Proxy.Models; +using Titanium.Web.Proxy.Shared; using Titanium.Web.Proxy.StreamExtended.BufferPool; +using Titanium.Web.Proxy.StreamExtended.Network; -namespace Titanium.Web.Proxy.StreamExtended.Network +namespace Titanium.Web.Proxy.Helpers { - /// - /// A custom network stream inherited from stream - /// with an underlying read buffer supporting both read/write - /// of UTF-8 encoded string or raw bytes asynchronously from last read position. - /// - /// - internal class CustomBufferedStream : Stream, IPeekStream, ILineStream + internal class HttpStream : Stream, IHttpStreamWriter, IHttpStreamReader, IPeekStream { + private readonly bool swallowException; private readonly bool leaveOpen; private readonly byte[] streamBuffer; @@ -40,11 +39,11 @@ internal class CustomBufferedStream : Stream, IPeekStream, ILineStream public event EventHandler? DataWrite; - public Stream BaseStream { get; } + private Stream baseStream { get; } public bool IsClosed => closed; - static CustomBufferedStream() + static HttpStream() { // TODO: remove this hack when removing .NET 4.x support try @@ -62,15 +61,22 @@ static CustomBufferedStream() } } + private static readonly byte[] newLine = ProxyConstants.NewLineBytes; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The base stream. /// Bufferpool. /// to leave the stream open after disposing the object; otherwise, . - public CustomBufferedStream(Stream baseStream, IBufferPool bufferPool, bool leaveOpen = false) + internal HttpStream(Stream baseStream, IBufferPool bufferPool, bool leaveOpen = false) { - BaseStream = baseStream; + if (baseStream is NetworkStream) + { + swallowException = true; + } + + this.baseStream = baseStream; this.leaveOpen = leaveOpen; streamBuffer = bufferPool.GetBuffer(); this.bufferPool = bufferPool; @@ -81,7 +87,7 @@ public CustomBufferedStream(Stream baseStream, IBufferPool bufferPool, bool leav /// public override void Flush() { - BaseStream.Flush(); + baseStream.Flush(); } /// @@ -96,7 +102,7 @@ public override long Seek(long offset, SeekOrigin origin) { bufferLength = 0; bufferPos = 0; - return BaseStream.Seek(offset, origin); + return baseStream.Seek(offset, origin); } /// @@ -105,7 +111,7 @@ public override long Seek(long offset, SeekOrigin origin) /// The desired length of the current stream in bytes. public override void SetLength(long value) { - BaseStream.SetLength(value); + baseStream.SetLength(value); } /// @@ -138,14 +144,14 @@ public override int Read(byte[] buffer, int offset, int count) /// /// When overridden in a derived class, writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. /// - /// An array of bytes. This method copies bytes from to the current stream. - /// The zero-based byte offset in at which to begin copying bytes to the current stream. + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. /// The number of bytes to be written to the current stream. [DebuggerStepThrough] public override void Write(byte[] buffer, int offset, int count) { OnDataWrite(buffer, offset, count); - BaseStream.Write(buffer, offset, count); + baseStream.Write(buffer, offset, count); } /// @@ -178,7 +184,7 @@ public override async Task CopyToAsync(Stream destination, int bufferSize, Cance /// public override Task FlushAsync(CancellationToken cancellationToken) { - return BaseStream.FlushAsync(cancellationToken); + return baseStream.FlushAsync(cancellationToken); } /// @@ -378,18 +384,14 @@ public byte ReadByteFromBuffer() /// Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests. /// /// The buffer to write data from. - /// The zero-based byte offset in from which to begin copying bytes to the stream. + /// The zero-based byte offset in buffer from which to begin copying bytes to the stream. /// The maximum number of bytes to write. - /// The token to monitor for cancellation requests. The default value is . - /// - /// A task that represents the asynchronous write operation. - /// + /// The token to monitor for cancellation requests. The default value is . [DebuggerStepThrough] public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { OnDataWrite(buffer, offset, count); - - await BaseStream.WriteAsync(buffer, offset, count, cancellationToken); + await baseStream.WriteAsync(buffer, offset, count, cancellationToken); } /// @@ -403,7 +405,7 @@ public override void WriteByte(byte value) { buffer[0] = value; OnDataWrite(buffer, 0, 1); - BaseStream.Write(buffer, 0, 1); + baseStream.Write(buffer, 0, 1); } finally { @@ -433,7 +435,7 @@ protected override void Dispose(bool disposing) closed = true; if (!leaveOpen) { - BaseStream.Dispose(); + baseStream.Dispose(); } bufferPool.ReturnBuffer(streamBuffer); @@ -443,27 +445,27 @@ protected override void Dispose(bool disposing) /// /// When overridden in a derived class, gets a value indicating whether the current stream supports reading. /// - public override bool CanRead => BaseStream.CanRead; + public override bool CanRead => baseStream.CanRead; /// /// When overridden in a derived class, gets a value indicating whether the current stream supports seeking. /// - public override bool CanSeek => BaseStream.CanSeek; + public override bool CanSeek => baseStream.CanSeek; /// /// When overridden in a derived class, gets a value indicating whether the current stream supports writing. /// - public override bool CanWrite => BaseStream.CanWrite; + public override bool CanWrite => baseStream.CanWrite; /// /// Gets a value that determines whether the current stream can time out. /// - public override bool CanTimeout => BaseStream.CanTimeout; + public override bool CanTimeout => baseStream.CanTimeout; /// /// When overridden in a derived class, gets the length in bytes of the stream. /// - public override long Length => BaseStream.Length; + public override long Length => baseStream.Length; /// /// Gets a value indicating whether data is available. @@ -480,8 +482,8 @@ protected override void Dispose(bool disposing) /// public override long Position { - get => BaseStream.Position; - set => BaseStream.Position = value; + get => baseStream.Position; + set => baseStream.Position = value; } /// @@ -489,8 +491,8 @@ public override long Position /// public override int ReadTimeout { - get => BaseStream.ReadTimeout; - set => BaseStream.ReadTimeout = value; + get => baseStream.ReadTimeout; + set => baseStream.ReadTimeout = value; } /// @@ -498,8 +500,8 @@ public override int ReadTimeout /// public override int WriteTimeout { - get => BaseStream.WriteTimeout; - set => BaseStream.WriteTimeout = value; + get => baseStream.WriteTimeout; + set => baseStream.WriteTimeout = value; } /// @@ -524,7 +526,7 @@ public bool FillBuffer() bool result = false; try { - int readBytes = BaseStream.Read(streamBuffer, bufferLength, streamBuffer.Length - bufferLength); + int readBytes = baseStream.Read(streamBuffer, bufferLength, streamBuffer.Length - bufferLength); result = readBytes > 0; if (result) { @@ -574,7 +576,7 @@ public async ValueTask FillBufferAsync(CancellationToken cancellationToken bool result = false; try { - int readBytes = await BaseStream.ReadAsync(streamBuffer, bufferLength, bytesToRead, cancellationToken); + int readBytes = await baseStream.ReadAsync(streamBuffer, bufferLength, bytesToRead, cancellationToken); result = readBytes > 0; if (result) { @@ -582,6 +584,11 @@ public async ValueTask FillBufferAsync(CancellationToken cancellationToken bufferLength += readBytes; } } + catch + { + if (!swallowException) + throw; + } finally { if (!result) @@ -721,7 +728,7 @@ public override int EndRead(IAsyncResult asyncResult) return ((TaskResult)asyncResult).Result; } - + /// /// Fix the .net bug with SslStream slow WriteAsync /// https://github.com/justcoding121/Titanium-Web-Proxy/issues/495 @@ -756,5 +763,292 @@ public override void EndWrite(IAsyncResult asyncResult) ((TaskResult)asyncResult).GetResult(); } + + /// + /// Writes a line async + /// + /// Optional cancellation token for this async task. + /// + internal Task WriteLineAsync(CancellationToken cancellationToken = default) + { + return WriteAsync(newLine, cancellationToken: cancellationToken); + } + + internal Task WriteAsync(string value, CancellationToken cancellationToken = default) + { + return writeAsyncInternal(value, false, cancellationToken); + } + + private async Task writeAsyncInternal(string value, bool addNewLine, CancellationToken cancellationToken) + { + int newLineChars = addNewLine ? newLine.Length : 0; + int charCount = value.Length; + if (charCount < bufferPool.BufferSize - newLineChars) + { + var buffer = bufferPool.GetBuffer(); + try + { + int idx = encoding.GetBytes(value, 0, charCount, buffer, 0); + if (newLineChars > 0) + { + Buffer.BlockCopy(newLine, 0, buffer, idx, newLineChars); + idx += newLineChars; + } + + await baseStream.WriteAsync(buffer, 0, idx, cancellationToken); + } + finally + { + bufferPool.ReturnBuffer(buffer); + } + } + else + { + var buffer = new byte[charCount + newLineChars + 1]; + int idx = encoding.GetBytes(value, 0, charCount, buffer, 0); + if (newLineChars > 0) + { + Buffer.BlockCopy(newLine, 0, buffer, idx, newLineChars); + idx += newLineChars; + } + + await baseStream.WriteAsync(buffer, 0, idx, cancellationToken); + } + } + + internal Task WriteLineAsync(string value, CancellationToken cancellationToken = default) + { + return writeAsyncInternal(value, true, cancellationToken); + } + + /// + /// Write the headers to client + /// + /// + /// + /// + internal async Task WriteHeadersAsync(HeaderBuilder headerBuilder, CancellationToken cancellationToken = default) + { + var buffer = headerBuilder.GetBuffer(); + await WriteAsync(buffer.Array, buffer.Offset, buffer.Count, true, cancellationToken); + } + + /// + /// Writes the data to the stream. + /// + /// The data. + /// Should we flush after write? + /// The cancellation token. + internal async Task WriteAsync(byte[] data, bool flush = false, CancellationToken cancellationToken = default) + { + await baseStream.WriteAsync(data, 0, data.Length, cancellationToken); + if (flush) + { + await baseStream.FlushAsync(cancellationToken); + } + } + + internal async Task WriteAsync(byte[] data, int offset, int count, bool flush, + CancellationToken cancellationToken = default) + { + await baseStream.WriteAsync(data, offset, count, cancellationToken); + if (flush) + { + await baseStream.FlushAsync(cancellationToken); + } + } + + /// + /// Writes the byte array body to the stream; optionally chunked + /// + /// + /// + /// + /// + internal Task WriteBodyAsync(byte[] data, bool isChunked, CancellationToken cancellationToken) + { + if (isChunked) + { + return writeBodyChunkedAsync(data, cancellationToken); + } + + return WriteAsync(data, cancellationToken: cancellationToken); + } + + /// + /// Copies the specified content length number of bytes to the output stream from the given inputs stream + /// optionally chunked + /// + /// + /// + /// + /// + /// + /// + public Task CopyBodyAsync(IHttpStreamReader streamReader, bool isChunked, long contentLength, + Action? onCopy, CancellationToken cancellationToken) + { + // For chunked request we need to read data as they arrive, until we reach a chunk end symbol + if (isChunked) + { + return copyBodyChunkedAsync(streamReader, onCopy, cancellationToken); + } + + // http 1.0 or the stream reader limits the stream + if (contentLength == -1) + { + contentLength = long.MaxValue; + } + + // If not chunked then its easy just read the amount of bytes mentioned in content length header + return copyBytesFromStream(streamReader, contentLength, onCopy, cancellationToken); + } + + /// + /// Copies the given input bytes to output stream chunked + /// + /// + /// + /// + private async Task writeBodyChunkedAsync(byte[] data, CancellationToken cancellationToken) + { + var chunkHead = Encoding.ASCII.GetBytes(data.Length.ToString("x2")); + + await WriteAsync(chunkHead, cancellationToken: cancellationToken); + await WriteLineAsync(cancellationToken); + await WriteAsync(data, cancellationToken: cancellationToken); + await WriteLineAsync(cancellationToken); + + await WriteLineAsync("0", cancellationToken); + await WriteLineAsync(cancellationToken); + } + + /// + /// Copies the streams chunked + /// + /// + /// + /// + /// + private async Task copyBodyChunkedAsync(IHttpStreamReader reader, Action? onCopy, CancellationToken cancellationToken) + { + while (true) + { + string chunkHead = (await reader.ReadLineAsync(cancellationToken))!; + int idx = chunkHead.IndexOf(";"); + if (idx >= 0) + { + chunkHead = chunkHead.Substring(0, idx); + } + + int chunkSize = int.Parse(chunkHead, NumberStyles.HexNumber); + + await WriteLineAsync(chunkHead, cancellationToken); + + if (chunkSize != 0) + { + await copyBytesFromStream(reader, chunkSize, onCopy, cancellationToken); + } + + await WriteLineAsync(cancellationToken); + + // chunk trail + await reader.ReadLineAsync(cancellationToken); + + if (chunkSize == 0) + { + break; + } + } + } + + /// + /// Copies the specified bytes to the stream from the input stream + /// + /// + /// + /// + /// + /// + private async Task copyBytesFromStream(IHttpStreamReader reader, long count, Action? onCopy, + CancellationToken cancellationToken) + { + var buffer = bufferPool.GetBuffer(); + + try + { + long remainingBytes = count; + + while (remainingBytes > 0) + { + int bytesToRead = buffer.Length; + if (remainingBytes < bytesToRead) + { + bytesToRead = (int)remainingBytes; + } + + int bytesRead = await reader.ReadAsync(buffer, 0, bytesToRead, cancellationToken); + if (bytesRead == 0) + { + break; + } + + remainingBytes -= bytesRead; + + await baseStream.WriteAsync(buffer, 0, bytesRead, cancellationToken); + + onCopy?.Invoke(buffer, 0, bytesRead); + } + } + finally + { + bufferPool.ReturnBuffer(buffer); + } + } + + /// + /// Writes the request/response headers and body. + /// + /// + /// + /// + /// + protected async Task WriteAsync(RequestResponseBase requestResponse, HeaderBuilder headerBuilder, CancellationToken cancellationToken = default) + { + var body = requestResponse.CompressBodyAndUpdateContentLength(); + headerBuilder.WriteHeaders(requestResponse.Headers); + await WriteHeadersAsync(headerBuilder, cancellationToken); + + if (body != null) + { + await WriteBodyAsync(body, requestResponse.IsChunked, cancellationToken); + } + } + +#if NETSTANDARD2_1 + /// + /// Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests. + /// + /// The buffer to write data from. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous write operation. + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + return baseStream.WriteAsync(buffer, cancellationToken); + } +#else + /// + /// Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests. + /// + /// The buffer to write data from. + /// The token to monitor for cancellation requests. The default value is . + /// A task that represents the asynchronous write operation. + public Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + var buf = ArrayPool.Shared.Rent(buffer.Length); + buffer.CopyTo(buf); + return baseStream.WriteAsync(buf, 0, buf.Length, cancellationToken); + } +#endif } } diff --git a/src/Titanium.Web.Proxy/Helpers/HttpWriter.cs b/src/Titanium.Web.Proxy/Helpers/HttpWriter.cs deleted file mode 100644 index 62e9a8a60..000000000 --- a/src/Titanium.Web.Proxy/Helpers/HttpWriter.cs +++ /dev/null @@ -1,346 +0,0 @@ -using System; -using System.Buffers; -using System.Globalization; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Titanium.Web.Proxy.Http; -using Titanium.Web.Proxy.Models; -using Titanium.Web.Proxy.Shared; -using Titanium.Web.Proxy.StreamExtended.BufferPool; -using Titanium.Web.Proxy.StreamExtended.Network; - -namespace Titanium.Web.Proxy.Helpers -{ - internal class HttpWriter : ICustomStreamWriter - { - private readonly Stream stream; - private readonly IBufferPool bufferPool; - - private static readonly byte[] newLine = ProxyConstants.NewLineBytes; - - private static Encoding encoding => HttpHeader.Encoding; - - internal HttpWriter(Stream stream, IBufferPool bufferPool) - { - this.stream = stream; - this.bufferPool = bufferPool; - } - - /// - /// Writes a line async - /// - /// Optional cancellation token for this async task. - /// - internal Task WriteLineAsync(CancellationToken cancellationToken = default) - { - return WriteAsync(newLine, cancellationToken: cancellationToken); - } - - internal Task WriteAsync(string value, CancellationToken cancellationToken = default) - { - return writeAsyncInternal(value, false, cancellationToken); - } - - private async Task writeAsyncInternal(string value, bool addNewLine, CancellationToken cancellationToken) - { - int newLineChars = addNewLine ? newLine.Length : 0; - int charCount = value.Length; - if (charCount < bufferPool.BufferSize - newLineChars) - { - var buffer = bufferPool.GetBuffer(); - try - { - int idx = encoding.GetBytes(value, 0, charCount, buffer, 0); - if (newLineChars > 0) - { - Buffer.BlockCopy(newLine, 0, buffer, idx, newLineChars); - idx += newLineChars; - } - - await stream.WriteAsync(buffer, 0, idx, cancellationToken); - } - finally - { - bufferPool.ReturnBuffer(buffer); - } - } - else - { - var buffer = new byte[charCount + newLineChars + 1]; - int idx = encoding.GetBytes(value, 0, charCount, buffer, 0); - if (newLineChars > 0) - { - Buffer.BlockCopy(newLine, 0, buffer, idx, newLineChars); - idx += newLineChars; - } - - await stream.WriteAsync(buffer, 0, idx, cancellationToken); - } - } - - internal Task WriteLineAsync(string value, CancellationToken cancellationToken = default) - { - return writeAsyncInternal(value, true, cancellationToken); - } - - /// - /// Write the headers to client - /// - /// - /// - /// - internal async Task WriteHeadersAsync(HeaderBuilder headerBuilder, CancellationToken cancellationToken = default) - { - var buffer = headerBuilder.GetBuffer(); - await WriteAsync(buffer.Array, buffer.Offset, buffer.Count, true, cancellationToken); - } - - /// - /// Writes the data to the stream. - /// - /// The data. - /// Should we flush after write? - /// The cancellation token. - internal async Task WriteAsync(byte[] data, bool flush = false, CancellationToken cancellationToken = default) - { - await stream.WriteAsync(data, 0, data.Length, cancellationToken); - if (flush) - { - await stream.FlushAsync(cancellationToken); - } - } - - internal async Task WriteAsync(byte[] data, int offset, int count, bool flush, - CancellationToken cancellationToken = default) - { - await stream.WriteAsync(data, offset, count, cancellationToken); - if (flush) - { - await stream.FlushAsync(cancellationToken); - } - } - - /// - /// Writes the byte array body to the stream; optionally chunked - /// - /// - /// - /// - /// - internal Task WriteBodyAsync(byte[] data, bool isChunked, CancellationToken cancellationToken) - { - if (isChunked) - { - return writeBodyChunkedAsync(data, cancellationToken); - } - - return WriteAsync(data, cancellationToken: cancellationToken); - } - - /// - /// Copies the specified content length number of bytes to the output stream from the given inputs stream - /// optionally chunked - /// - /// - /// - /// - /// - /// - /// - internal Task CopyBodyAsync(CustomBufferedStream streamReader, bool isChunked, long contentLength, - Action? onCopy, CancellationToken cancellationToken) - { - // For chunked request we need to read data as they arrive, until we reach a chunk end symbol - if (isChunked) - { - return copyBodyChunkedAsync(streamReader, onCopy, cancellationToken); - } - - // http 1.0 or the stream reader limits the stream - if (contentLength == -1) - { - contentLength = long.MaxValue; - } - - // If not chunked then its easy just read the amount of bytes mentioned in content length header - return copyBytesFromStream(streamReader, contentLength, onCopy, cancellationToken); - } - - /// - /// Copies the given input bytes to output stream chunked - /// - /// - /// - /// - private async Task writeBodyChunkedAsync(byte[] data, CancellationToken cancellationToken) - { - var chunkHead = Encoding.ASCII.GetBytes(data.Length.ToString("x2")); - - await WriteAsync(chunkHead, cancellationToken: cancellationToken); - await WriteLineAsync(cancellationToken); - await WriteAsync(data, cancellationToken: cancellationToken); - await WriteLineAsync(cancellationToken); - - await WriteLineAsync("0", cancellationToken); - await WriteLineAsync(cancellationToken); - } - - /// - /// Copies the streams chunked - /// - /// - /// - /// - /// - private async Task copyBodyChunkedAsync(CustomBufferedStream reader, Action? onCopy, CancellationToken cancellationToken) - { - while (true) - { - string chunkHead = (await reader.ReadLineAsync(cancellationToken))!; - int idx = chunkHead.IndexOf(";"); - if (idx >= 0) - { - chunkHead = chunkHead.Substring(0, idx); - } - - int chunkSize = int.Parse(chunkHead, NumberStyles.HexNumber); - - await WriteLineAsync(chunkHead, cancellationToken); - - if (chunkSize != 0) - { - await copyBytesFromStream(reader, chunkSize, onCopy, cancellationToken); - } - - await WriteLineAsync(cancellationToken); - - // chunk trail - await reader.ReadLineAsync(cancellationToken); - - if (chunkSize == 0) - { - break; - } - } - } - - /// - /// Copies the specified bytes to the stream from the input stream - /// - /// - /// - /// - /// - /// - private async Task copyBytesFromStream(CustomBufferedStream reader, long count, Action? onCopy, - CancellationToken cancellationToken) - { - var buffer = bufferPool.GetBuffer(); - - try - { - long remainingBytes = count; - - while (remainingBytes > 0) - { - int bytesToRead = buffer.Length; - if (remainingBytes < bytesToRead) - { - bytesToRead = (int)remainingBytes; - } - - int bytesRead = await reader.ReadAsync(buffer, 0, bytesToRead, cancellationToken); - if (bytesRead == 0) - { - break; - } - - remainingBytes -= bytesRead; - - await stream.WriteAsync(buffer, 0, bytesRead, cancellationToken); - - onCopy?.Invoke(buffer, 0, bytesRead); - } - } - finally - { - bufferPool.ReturnBuffer(buffer); - } - } - - /// - /// Writes the request/response headers and body. - /// - /// - /// - /// - /// - protected async Task WriteAsync(RequestResponseBase requestResponse, HeaderBuilder headerBuilder, CancellationToken cancellationToken = default) - { - var body = requestResponse.CompressBodyAndUpdateContentLength(); - headerBuilder.WriteHeaders(requestResponse.Headers); - await WriteHeadersAsync(headerBuilder, cancellationToken); - - if (body != null) - { - await WriteBodyAsync(body, requestResponse.IsChunked, cancellationToken); - } - } - - /// When overridden in a derived class, writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. - /// An array of bytes. This method copies count bytes from buffer to the current stream. - /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. - /// The number of bytes to be written to the current stream. - /// The sum of offset and count is greater than the buffer length. - /// buffer is null. - /// offset or count is negative. - /// An I/O error occured, such as the specified file cannot be found. - /// The stream does not support writing. - /// was called after the stream was closed. - public void Write(byte[] buffer, int offset, int count) - { - stream.Write(buffer, offset, count); - } - - /// - /// Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests. - /// - /// The buffer to write data from. - /// The zero-based byte offset in from which to begin copying bytes to the stream. - /// The maximum number of bytes to write. - /// The token to monitor for cancellation requests. The default value is . - /// A task that represents the asynchronous write operation. - public Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return stream.WriteAsync(buffer, offset, count, cancellationToken); - } - -#if NETSTANDARD2_1 - /// - /// Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests. - /// - /// The buffer to write data from. - /// The token to monitor for cancellation requests. The default value is . - /// A task that represents the asynchronous write operation. - public ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) - { - return stream.WriteAsync(buffer, cancellationToken); - } -#else - /// - /// Asynchronously writes a sequence of bytes to the current stream, advances the current position within this stream by the number of bytes written, and monitors cancellation requests. - /// - /// The buffer to write data from. - /// The token to monitor for cancellation requests. The default value is . - /// A task that represents the asynchronous write operation. - public Task WriteAsync2(ReadOnlyMemory buffer, CancellationToken cancellationToken) - { - var buf = ArrayPool.Shared.Rent(buffer.Length); - buffer.CopyTo(buf); - return stream.WriteAsync(buf, 0, buf.Length, cancellationToken); - } -#endif - } -} diff --git a/src/Titanium.Web.Proxy/Helpers/WinHttp/WinHttpWebProxyFinder.cs b/src/Titanium.Web.Proxy/Helpers/WinHttp/WinHttpWebProxyFinder.cs index 9ccbf31d9..8b8ec3295 100644 --- a/src/Titanium.Web.Proxy/Helpers/WinHttp/WinHttpWebProxyFinder.cs +++ b/src/Titanium.Web.Proxy/Helpers/WinHttp/WinHttpWebProxyFinder.cs @@ -95,7 +95,7 @@ public bool GetAutoProxies(Uri destination, out IList? proxyList) return true; } - public ExternalProxy? GetProxy(Uri destination) + public IExternalProxy? GetProxy(Uri destination) { if (GetAutoProxies(destination, out var proxies)) { diff --git a/src/Titanium.Web.Proxy/Http/HttpWebClient.cs b/src/Titanium.Web.Proxy/Http/HttpWebClient.cs index 1957aa1d9..b793a1525 100644 --- a/src/Titanium.Web.Proxy/Http/HttpWebClient.cs +++ b/src/Titanium.Web.Proxy/Http/HttpWebClient.cs @@ -111,7 +111,7 @@ internal async Task SendRequest(bool enable100ContinueBehaviour, bool isTranspar bool useUpstreamProxy = upstreamProxy != null && Connection.IsHttps == false; - var writer = Connection.StreamWriter; + var serverStream = Connection.Stream; string url; if (useUpstreamProxy || isTransparent) @@ -153,7 +153,7 @@ internal async Task SendRequest(bool enable100ContinueBehaviour, bool isTranspar headerBuilder.WriteLine(); - await writer.WriteHeadersAsync(headerBuilder, cancellationToken); + await serverStream.WriteHeadersAsync(headerBuilder, cancellationToken); if (enable100ContinueBehaviour && Request.ExpectContinue) { diff --git a/src/Titanium.Web.Proxy/Models/ExternalProxy.cs b/src/Titanium.Web.Proxy/Models/ExternalProxy.cs index 4c1deca60..931248233 100644 --- a/src/Titanium.Web.Proxy/Models/ExternalProxy.cs +++ b/src/Titanium.Web.Proxy/Models/ExternalProxy.cs @@ -6,7 +6,7 @@ namespace Titanium.Web.Proxy.Models /// /// An upstream proxy this proxy uses if any. /// - public class ExternalProxy + public class ExternalProxy : IExternalProxy { private static readonly Lazy defaultCredentials = new Lazy(() => CredentialCache.DefaultNetworkCredentials); diff --git a/src/Titanium.Web.Proxy/Models/IExternalProxy.cs b/src/Titanium.Web.Proxy/Models/IExternalProxy.cs new file mode 100644 index 000000000..7eb877859 --- /dev/null +++ b/src/Titanium.Web.Proxy/Models/IExternalProxy.cs @@ -0,0 +1,37 @@ +namespace Titanium.Web.Proxy.Models +{ + public interface IExternalProxy + { + /// + /// Use default windows credentials? + /// + bool UseDefaultCredentials { get; set; } + + /// + /// Bypass this proxy for connections to localhost? + /// + bool BypassLocalhost { get; set; } + + /// + /// Username. + /// + string? UserName { get; set; } + + /// + /// Password. + /// + string? Password { get; set; } + + /// + /// Host name. + /// + string HostName { get; set; } + + /// + /// Port. + /// + int Port { get; set; } + + string ToString(); + } +} diff --git a/src/Titanium.Web.Proxy/Network/DebugCustomBufferedStream.cs b/src/Titanium.Web.Proxy/Network/DebugCustomBufferedStream.cs deleted file mode 100644 index a49894dc9..000000000 --- a/src/Titanium.Web.Proxy/Network/DebugCustomBufferedStream.cs +++ /dev/null @@ -1,74 +0,0 @@ - -using Titanium.Web.Proxy.StreamExtended.BufferPool; -using Titanium.Web.Proxy.StreamExtended.Network; -#if DEBUG -using System; -using System.IO; -using System.Text; -using System.Threading; - -namespace Titanium.Web.Proxy.Network -{ - internal class DebugCustomBufferedStream : CustomBufferedStream - { - private const string basePath = @"."; - - private static int counter; - - private readonly FileStream fileStreamReceived; - - private readonly FileStream fileStreamSent; - - public DebugCustomBufferedStream(Guid connectionId, string type, Stream baseStream, IBufferPool bufferPool, bool leaveOpen = false) - : base(baseStream, bufferPool, leaveOpen) - { - Counter = Interlocked.Increment(ref counter); - fileStreamSent = new FileStream(Path.Combine(basePath, $"{connectionId}_{type}_{Counter}_sent.dat"), FileMode.Create); - fileStreamReceived = new FileStream(Path.Combine(basePath, $"{connectionId}_{type}_{Counter}_received.dat"), FileMode.Create); - } - - public int Counter { get; } - - protected override void OnDataWrite(byte[] buffer, int offset, int count) - { - fileStreamSent.Write(buffer, offset, count); - Flush(); - } - - protected override void OnDataRead(byte[] buffer, int offset, int count) - { - fileStreamReceived.Write(buffer, offset, count); - Flush(); - } - - public void LogException(Exception ex) - { - var data = Encoding.UTF8.GetBytes("EXCEPTION: " + ex); - fileStreamReceived.Write(data, 0, data.Length); - fileStreamReceived.Flush(); - } - public override void Flush() - { - fileStreamSent.Flush(true); - fileStreamReceived.Flush(true); - - if (CanWrite) - { - base.Flush(); - } - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - Flush(); - fileStreamSent.Dispose(); - fileStreamReceived.Dispose(); - } - - base.Dispose(disposing); - } - } -} -#endif diff --git a/src/Titanium.Web.Proxy/Network/ProxyClient.cs b/src/Titanium.Web.Proxy/Network/ProxyClient.cs deleted file mode 100644 index 1969cb1db..000000000 --- a/src/Titanium.Web.Proxy/Network/ProxyClient.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Titanium.Web.Proxy.Helpers; -using Titanium.Web.Proxy.Network.Tcp; -using Titanium.Web.Proxy.StreamExtended.Network; - -namespace Titanium.Web.Proxy.Network -{ - /// - /// This class wraps Tcp connection to client - /// - internal class ProxyClient - { - public ProxyClient(TcpClientConnection connection, CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter) - { - Connection = connection; - ClientStream = clientStream; - ClientStreamWriter = clientStreamWriter; - } - - /// - /// TcpClient connection used to communicate with client - /// - internal TcpClientConnection Connection { get; } - - /// - /// Holds the stream to client - /// - internal CustomBufferedStream ClientStream { get; } - - /// - /// Used to write line by line to client - /// - internal HttpResponseWriter ClientStreamWriter { get; } - } -} diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpClientConnection.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpClientConnection.cs index a8d4ec45c..22d09da03 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpClientConnection.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpClientConnection.cs @@ -86,7 +86,6 @@ public void Dispose() proxyServer.UpdateClientConnectionCount(false); tcpClient.CloseSocket(); }); - } } } diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs index ac11521b1..779950f6f 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpConnectionFactory.cs @@ -49,7 +49,7 @@ internal TcpConnectionFactory(ProxyServer server) internal string GetConnectionCacheKey(string remoteHostName, int remotePort, bool isHttps, List? applicationProtocols, - IPEndPoint? upStreamEndPoint, ExternalProxy? externalProxy) + IPEndPoint? upStreamEndPoint, IExternalProxy? externalProxy) { // http version is ignored since its an application level decision b/w HTTP 1.0/1.1 // also when doing connect request MS Edge browser sends http 1.0 but uses 1.1 after server sends 1.1 its response. @@ -115,7 +115,7 @@ internal async Task GetConnectionCacheKey(ProxyServer server, SessionEve applicationProtocols = new List { applicationProtocol }; } - ExternalProxy? customUpStreamProxy = null; + IExternalProxy? customUpStreamProxy = null; bool isHttps = session.IsHttps; if (server.GetCustomUpStreamProxyFunc != null) @@ -170,7 +170,7 @@ internal Task GetServerConnection(ProxyServer server, Sessi internal async Task GetServerConnection(ProxyServer server, SessionEventArgsBase session, bool isConnect, List? applicationProtocols, bool noCache, CancellationToken cancellationToken) { - ExternalProxy? customUpStreamProxy = null; + IExternalProxy? customUpStreamProxy = null; bool isHttps = session.IsHttps; if (server.GetCustomUpStreamProxyFunc != null) @@ -208,10 +208,10 @@ internal async Task GetServerConnection(ProxyServer server, /// internal async Task GetServerConnection(string remoteHostName, int remotePort, Version httpVersion, bool isHttps, List? applicationProtocols, bool isConnect, - ProxyServer proxyServer, SessionEventArgsBase? session, IPEndPoint? upStreamEndPoint, ExternalProxy? externalProxy, + ProxyServer proxyServer, SessionEventArgsBase? session, IPEndPoint? upStreamEndPoint, IExternalProxy? externalProxy, bool noCache, CancellationToken cancellationToken) { - var sslProtocol = session?.ProxyClient.Connection.SslProtocol ?? SslProtocols.None; + var sslProtocol = session?.ClientConnection.SslProtocol ?? SslProtocols.None; var cacheKey = GetConnectionCacheKey(remoteHostName, remotePort, isHttps, applicationProtocols, upStreamEndPoint, externalProxy); @@ -262,7 +262,7 @@ internal async Task GetServerConnection(string remoteHostNa /// private async Task createServerConnection(string remoteHostName, int remotePort, Version httpVersion, bool isHttps, SslProtocols sslProtocol, List? applicationProtocols, bool isConnect, - ProxyServer proxyServer, SessionEventArgsBase? session, IPEndPoint? upStreamEndPoint, ExternalProxy? externalProxy, string cacheKey, + ProxyServer proxyServer, SessionEventArgsBase? session, IPEndPoint? upStreamEndPoint, IExternalProxy? externalProxy, string cacheKey, CancellationToken cancellationToken) { // deny connection to proxy end points to avoid infinite connection loop. @@ -297,14 +297,14 @@ private async Task createServerConnection(string remoteHost } TcpClient? tcpClient = null; - CustomBufferedStream? stream = null; + HttpServerStream? stream = null; SslApplicationProtocol negotiatedApplicationProtocol = default; bool retry = true; var enabledSslProtocols = sslProtocol; - retry: +retry: try { string hostname = useUpstreamProxy ? externalProxy!.HostName : remoteHostName; @@ -323,6 +323,7 @@ private async Task createServerConnection(string remoteHost Array.Sort(ipAddresses, (x, y) => x.AddressFamily.CompareTo(y.AddressFamily)); + Exception lastException = null; for (int i = 0; i < ipAddresses.Length; i++) { try @@ -352,28 +353,29 @@ private async Task createServerConnection(string remoteHost } catch (Exception e) { - if (i == ipAddresses.Length - 1) - { - throw new Exception($"Could not establish connection to {hostname}", e); - } - // dispose the current TcpClient and try the next address + lastException = e; tcpClient?.Dispose(); + tcpClient = null; } } + if (tcpClient == null) + { + throw new Exception($"Could not establish connection to {hostname}", lastException); + } + if (session != null) { session.TimeLine["Connection Established"] = DateTime.Now; } - await proxyServer.InvokeConnectionCreateEvent(tcpClient!, false); + await proxyServer.InvokeConnectionCreateEvent(tcpClient, false); - stream = new CustomBufferedStream(tcpClient!.GetStream(), proxyServer.BufferPool); + stream = new HttpServerStream(tcpClient.GetStream(), proxyServer.BufferPool); if (useUpstreamProxy && (isConnect || isHttps)) { - var writer = new HttpRequestWriter(stream, proxyServer.BufferPool); string authority = $"{remoteHostName}:{remotePort}"; var connectRequest = new ConnectRequest(authority) { @@ -391,7 +393,7 @@ private async Task createServerConnection(string remoteHost HttpHeader.GetProxyAuthorizationHeader(externalProxy.UserName, externalProxy.Password)); } - await writer.WriteRequestAsync(connectRequest, cancellationToken: cancellationToken); + await stream.WriteRequestAsync(connectRequest, cancellationToken: cancellationToken); string httpStatus = await stream.ReadLineAsync(cancellationToken) ?? throw new ServerConnectionException("Server connection was closed."); @@ -411,7 +413,7 @@ private async Task createServerConnection(string remoteHost { var sslStream = new SslStream(stream, false, proxyServer.ValidateServerCertificate, proxyServer.SelectClientCertificate); - stream = new CustomBufferedStream(sslStream, proxyServer.BufferPool); + stream = new HttpServerStream(sslStream, proxyServer.BufferPool); var options = new SslClientAuthenticationOptions { @@ -430,11 +432,13 @@ private async Task createServerConnection(string remoteHost { session.TimeLine["HTTPS Established"] = DateTime.Now; } - } } catch (IOException ex) when (ex.HResult == unchecked((int)0x80131620) && retry && enabledSslProtocols >= SslProtocols.Tls11) { + stream?.Dispose(); + tcpClient?.Close(); + enabledSslProtocols = SslProtocols.Tls; retry = false; goto retry; diff --git a/src/Titanium.Web.Proxy/Network/Tcp/TcpServerConnection.cs b/src/Titanium.Web.Proxy/Network/Tcp/TcpServerConnection.cs index ef40391bb..6cc0bf431 100644 --- a/src/Titanium.Web.Proxy/Network/Tcp/TcpServerConnection.cs +++ b/src/Titanium.Web.Proxy/Network/Tcp/TcpServerConnection.cs @@ -15,15 +15,14 @@ namespace Titanium.Web.Proxy.Network.Tcp /// internal class TcpServerConnection : IDisposable { - internal TcpServerConnection(ProxyServer proxyServer, TcpClient tcpClient, CustomBufferedStream stream, + internal TcpServerConnection(ProxyServer proxyServer, TcpClient tcpClient, HttpServerStream stream, string hostName, int port, bool isHttps, SslApplicationProtocol negotiatedApplicationProtocol, - Version version, bool useUpstreamProxy, ExternalProxy? upStreamProxy, IPEndPoint? upStreamEndPoint, string cacheKey) + Version version, bool useUpstreamProxy, IExternalProxy? upStreamProxy, IPEndPoint? upStreamEndPoint, string cacheKey) { - this.tcpClient = tcpClient; + TcpClient = tcpClient; LastAccess = DateTime.Now; this.proxyServer = proxyServer; this.proxyServer.UpdateServerConnectionCount(true); - StreamWriter = new HttpRequestWriter(stream, proxyServer.BufferPool); Stream = stream; HostName = hostName; Port = port; @@ -41,7 +40,7 @@ internal TcpServerConnection(ProxyServer proxyServer, TcpClient tcpClient, Custo internal bool IsClosed => Stream.IsClosed; - internal ExternalProxy? UpStreamProxy { get; set; } + internal IExternalProxy? UpStreamProxy { get; set; } internal string HostName { get; set; } @@ -63,22 +62,15 @@ internal TcpServerConnection(ProxyServer proxyServer, TcpClient tcpClient, Custo /// internal Version Version { get; set; } = HttpHeader.VersionUnknown; - private readonly TcpClient tcpClient; - /// /// The TcpClient. /// - internal TcpClient TcpClient => tcpClient; + internal TcpClient TcpClient { get; } /// /// Used to write lines to server /// - internal HttpRequestWriter StreamWriter { get; } - - /// - /// Server stream - /// - internal CustomBufferedStream Stream { get; } + internal HttpServerStream Stream { get; } /// /// Last time this connection was used @@ -107,8 +99,8 @@ public void Dispose() // This way we can push tcp Time_Wait to server side when possible. await Task.Delay(1000); proxyServer.UpdateServerConnectionCount(false); - Stream?.Dispose(); - tcpClient.CloseSocket(); + Stream.Dispose(); + TcpClient.CloseSocket(); }); } diff --git a/src/Titanium.Web.Proxy/ProxyServer.cs b/src/Titanium.Web.Proxy/ProxyServer.cs index 31e3ff34f..0bd684df2 100644 --- a/src/Titanium.Web.Proxy/ProxyServer.cs +++ b/src/Titanium.Web.Proxy/ProxyServer.cs @@ -61,7 +61,7 @@ public partial class ProxyServer : IDisposable /// private WinHttpWebProxyFinder? systemProxyResolver; - + /// /// /// Initializes a new instance of ProxyServer class with provided parameters. @@ -145,7 +145,7 @@ public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerNam /// Defaults to false. /// public bool EnableWinAuth { get; set; } - + /// /// Enable disable HTTP/2 support. /// Warning: HTTP/2 support is very limited @@ -253,12 +253,12 @@ public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerNam /// /// External proxy used for Http requests. /// - public ExternalProxy? UpStreamHttpProxy { get; set; } + public IExternalProxy? UpStreamHttpProxy { get; set; } /// /// External proxy used for Https requests. /// - public ExternalProxy? UpStreamHttpsProxy { get; set; } + public IExternalProxy? UpStreamHttpsProxy { get; set; } /// /// Local adapter/NIC endpoint where proxy makes request via. @@ -275,7 +275,7 @@ public ProxyServer(string? rootCertificateName, string? rootCertificateIssuerNam /// A callback to provide authentication credentials for up stream proxy this proxy is using for HTTP(S) requests. /// User should return the ExternalProxy object with valid credentials. /// - public Func>? GetCustomUpStreamProxyFunc { get; set; } + public Func>? GetCustomUpStreamProxyFunc { get; set; } /// /// Callback for error events in this proxy instance. @@ -709,7 +709,7 @@ private void validateEndPointAsSystemProxy(ExplicitProxyEndPoint endPoint) /// /// The session. /// The external proxy as task result. - private Task getSystemUpStreamProxy(SessionEventArgsBase sessionEventArgs) + private Task getSystemUpStreamProxy(SessionEventArgsBase sessionEventArgs) { var proxy = systemProxyResolver!.GetProxy(sessionEventArgs.HttpClient.Request.RequestUri); return Task.FromResult(proxy); @@ -803,15 +803,8 @@ private async Task handleClient(TcpClient tcpClient, ProxyEndPoint endPoint) /// /// The client stream. /// The exception. - private void onException(CustomBufferedStream clientStream, Exception exception) + private void onException(HttpClientStream clientStream, Exception exception) { -#if DEBUG - if (clientStream is DebugCustomBufferedStream debugStream) - { - debugStream.LogException(exception); - } -#endif - ExceptionFunc(exception); } diff --git a/src/Titanium.Web.Proxy/RequestHandler.cs b/src/Titanium.Web.Proxy/RequestHandler.cs index 14ecbc241..021af3e17 100644 --- a/src/Titanium.Web.Proxy/RequestHandler.cs +++ b/src/Titanium.Web.Proxy/RequestHandler.cs @@ -31,13 +31,11 @@ public partial class ProxyServer /// The proxy endpoint. /// The client connection. /// The client stream. - /// The client stream writer. /// The cancellation token source for this async task. /// The Connect request if this is a HTTPS request from explicit endpoint. /// Prefetched server connection for current client using Connect/SNI headers. private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, TcpClientConnection clientConnection, - CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter, - CancellationTokenSource cancellationTokenSource, TunnelConnectSessionEventArgs? connectArgs = null, + HttpClientStream clientStream, CancellationTokenSource cancellationTokenSource, TunnelConnectSessionEventArgs? connectArgs = null, Task? prefetchConnectionTask = null) { var connectRequest = connectArgs?.HttpClient.ConnectRequest; @@ -66,7 +64,7 @@ private async Task handleHttpSessionRequest(ProxyEndPoint endPoint, TcpClientCon return; } - var args = new SessionEventArgs(this, endPoint, new ProxyClient(clientConnection, clientStream, clientStreamWriter), connectRequest, cancellationTokenSource) + var args = new SessionEventArgs(this, endPoint, clientConnection, clientStream, connectRequest, cancellationTokenSource) { UserData = connectArgs?.UserData }; @@ -101,7 +99,7 @@ await HeaderParser.ReadHeaders(clientStream, args.HttpClient.Request.Headers, await onBeforeResponse(args); // send the response - await clientStreamWriter.WriteResponseAsync(args.HttpClient.Response, + await clientStream.WriteResponseAsync(args.HttpClient.Response, cancellationToken: cancellationToken); return; } @@ -276,7 +274,7 @@ private async Task handleHttpSessionRequest(string requestHttpMetho // if upgrading to websocket then relay the request without reading the contents await handleWebSocketUpgrade(requestHttpMethod, requestHttpUrl, requestVersion, args, args.HttpClient.Request, - args.HttpClient.Response, args.ProxyClient.ClientStream, args.ProxyClient.ClientStreamWriter, + args.HttpClient.Response, args.ClientStream, connection, cancellationTokenSource, cancellationToken); return false; } @@ -304,13 +302,13 @@ await args.HttpClient.SendRequest(Enable100ContinueBehaviour, args.IsTransparent // If a successful 100 continue request was made, inform that to the client and reset response if (request.ExpectationSucceeded) { - var clientStreamWriter = args.ProxyClient.ClientStreamWriter; + var writer = args.ClientStream; var response = args.HttpClient.Response; var headerBuilder = new HeaderBuilder(); headerBuilder.WriteResponseLine(response.HttpVersion, response.StatusCode, response.StatusDescription); headerBuilder.WriteHeaders(response.Headers); - await clientStreamWriter.WriteHeadersAsync(headerBuilder, cancellationToken); + await writer.WriteHeadersAsync(headerBuilder, cancellationToken); await args.ClearResponse(cancellationToken); } @@ -320,14 +318,12 @@ await args.HttpClient.SendRequest(Enable100ContinueBehaviour, args.IsTransparent { if (request.IsBodyRead) { - var writer = args.HttpClient.Connection.StreamWriter; - await writer.WriteBodyAsync(body!, request.IsChunked, cancellationToken); + await args.HttpClient.Connection.Stream.WriteBodyAsync(body!, request.IsChunked, cancellationToken); } else if (!request.ExpectationFailed) { // get the request body unless an unsuccessful 100 continue request was made - HttpWriter writer = args.HttpClient.Connection.StreamWriter!; - await args.CopyRequestBodyAsync(writer, TransformationMode.None, cancellationToken); + await args.CopyRequestBodyAsync(args.HttpClient.Connection.Stream, TransformationMode.None, cancellationToken); } } @@ -357,7 +353,7 @@ private void prepareRequestHeaders(HeaderCollection requestHeaders) supportedAcceptEncoding.Add("identity"); requestHeaders.SetOrAddHeaderValue(KnownHeaders.AcceptEncoding, - string.Join(",", supportedAcceptEncoding)); + string.Join(", ", supportedAcceptEncoding)); } requestHeaders.FixProxyHeaders(); diff --git a/src/Titanium.Web.Proxy/ResponseHandler.cs b/src/Titanium.Web.Proxy/ResponseHandler.cs index 18ff4aa97..6bf2ad7c4 100644 --- a/src/Titanium.Web.Proxy/ResponseHandler.cs +++ b/src/Titanium.Web.Proxy/ResponseHandler.cs @@ -64,13 +64,13 @@ private async Task handleHttpSessionResponse(SessionEventArgs args) // it may changed in the user event response = args.HttpClient.Response; - var clientStreamWriter = args.ProxyClient.ClientStreamWriter; + var clientStream = args.ClientStream; // user set custom response by ignoring original response from server. if (response.Locked) { // write custom user response with body and return. - await clientStreamWriter.WriteResponseAsync(response, cancellationToken: cancellationToken); + await clientStream.WriteResponseAsync(response, cancellationToken: cancellationToken); if (args.HttpClient.HasConnection && !args.HttpClient.CloseServerConnection) { @@ -108,7 +108,7 @@ await handleHttpSessionRequest(args.HttpClient.Request.Method, args.HttpClient.R if (response.IsBodyRead) { - await clientStreamWriter.WriteResponseAsync(response, cancellationToken: cancellationToken); + await clientStream.WriteResponseAsync(response, cancellationToken: cancellationToken); } else { @@ -116,12 +116,12 @@ await handleHttpSessionRequest(args.HttpClient.Request.Method, args.HttpClient.R var headerBuilder = new HeaderBuilder(); headerBuilder.WriteResponseLine(response.HttpVersion, response.StatusCode, response.StatusDescription); headerBuilder.WriteHeaders(response.Headers); - await clientStreamWriter.WriteHeadersAsync(headerBuilder, cancellationToken); + await clientStream.WriteHeadersAsync(headerBuilder, cancellationToken); // Write body if exists if (response.HasBody) { - await args.CopyResponseBodyAsync(clientStreamWriter, TransformationMode.None, + await args.CopyResponseBodyAsync(clientStream, TransformationMode.None, cancellationToken); } } diff --git a/src/Titanium.Web.Proxy/StreamExtended/Network/CopyStream.cs b/src/Titanium.Web.Proxy/StreamExtended/Network/CopyStream.cs index 2e6b35ee8..886c05815 100644 --- a/src/Titanium.Web.Proxy/StreamExtended/Network/CopyStream.cs +++ b/src/Titanium.Web.Proxy/StreamExtended/Network/CopyStream.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Titanium.Web.Proxy.Helpers; using Titanium.Web.Proxy.StreamExtended.BufferPool; namespace Titanium.Web.Proxy.StreamExtended.Network @@ -11,9 +12,9 @@ namespace Titanium.Web.Proxy.StreamExtended.Network /// internal class CopyStream : ILineStream, IDisposable { - private readonly CustomBufferedStream reader; + private readonly IHttpStreamReader reader; - private readonly ICustomStreamWriter writer; + private readonly IHttpStreamWriter writer; private readonly IBufferPool bufferPool; @@ -27,7 +28,7 @@ internal class CopyStream : ILineStream, IDisposable public long ReadBytes { get; private set; } - public CopyStream(CustomBufferedStream reader, ICustomStreamWriter writer, IBufferPool bufferPool) + public CopyStream(IHttpStreamReader reader, IHttpStreamWriter writer, IBufferPool bufferPool) { this.reader = reader; this.writer = writer; @@ -61,7 +62,7 @@ public byte ReadByteFromBuffer() public ValueTask ReadLineAsync(CancellationToken cancellationToken = default) { - return CustomBufferedStream.ReadLineInternalAsync(this, bufferPool, cancellationToken); + return HttpStream.ReadLineInternalAsync(this, bufferPool, cancellationToken); } public void Dispose() diff --git a/src/Titanium.Web.Proxy/StreamExtended/Network/IHttpStreamReader.cs b/src/Titanium.Web.Proxy/StreamExtended/Network/IHttpStreamReader.cs new file mode 100644 index 000000000..5d13c0d04 --- /dev/null +++ b/src/Titanium.Web.Proxy/StreamExtended/Network/IHttpStreamReader.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Titanium.Web.Proxy.StreamExtended.Network +{ + public interface IHttpStreamReader : ILineStream + { + int Read(byte[] buffer, int offset, int count); + + Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); + } +} diff --git a/src/Titanium.Web.Proxy/StreamExtended/Network/ICustomStreamWriter.cs b/src/Titanium.Web.Proxy/StreamExtended/Network/IHttpStreamWriter.cs similarity index 59% rename from src/Titanium.Web.Proxy/StreamExtended/Network/ICustomStreamWriter.cs rename to src/Titanium.Web.Proxy/StreamExtended/Network/IHttpStreamWriter.cs index 919339f0b..a02143d2d 100644 --- a/src/Titanium.Web.Proxy/StreamExtended/Network/ICustomStreamWriter.cs +++ b/src/Titanium.Web.Proxy/StreamExtended/Network/IHttpStreamWriter.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; namespace Titanium.Web.Proxy.StreamExtended.Network @@ -6,10 +7,13 @@ namespace Titanium.Web.Proxy.StreamExtended.Network /// /// A concrete implementation of this interface is required when calling CopyStream. /// - public interface ICustomStreamWriter + internal interface IHttpStreamWriter { void Write(byte[] buffer, int i, int bufferLength); Task WriteAsync(byte[] buffer, int i, int bufferLength, CancellationToken cancellationToken); + + Task CopyBodyAsync(IHttpStreamReader streamReader, bool isChunked, long contentLength, + Action? onCopy, CancellationToken cancellationToken); } -} \ No newline at end of file +} diff --git a/src/Titanium.Web.Proxy/StreamExtended/Network/PeekStreamReader.cs b/src/Titanium.Web.Proxy/StreamExtended/Network/PeekStreamReader.cs index df5ba9a1b..a301f66e8 100644 --- a/src/Titanium.Web.Proxy/StreamExtended/Network/PeekStreamReader.cs +++ b/src/Titanium.Web.Proxy/StreamExtended/Network/PeekStreamReader.cs @@ -53,5 +53,5 @@ public byte[] ReadBytes(int length) return buffer; } - } + } } diff --git a/src/Titanium.Web.Proxy/StreamExtended/SslTools.cs b/src/Titanium.Web.Proxy/StreamExtended/SslTools.cs index 19a508dcb..3258be048 100644 --- a/src/Titanium.Web.Proxy/StreamExtended/SslTools.cs +++ b/src/Titanium.Web.Proxy/StreamExtended/SslTools.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Titanium.Web.Proxy.Helpers; using Titanium.Web.Proxy.StreamExtended.BufferPool; using Titanium.Web.Proxy.StreamExtended.Models; using Titanium.Web.Proxy.StreamExtended.Network; @@ -19,7 +20,7 @@ internal class SslTools /// /// /// - public static async Task PeekClientHello(CustomBufferedStream clientStream, IBufferPool bufferPool, CancellationToken cancellationToken = default) + public static async Task PeekClientHello(IPeekStream clientStream, IBufferPool bufferPool, CancellationToken cancellationToken = default) { // detects the HTTPS ClientHello message as it is described in the following url: // https://stackoverflow.com/questions/3897883/how-to-detect-an-incoming-ssl-https-handshake-ssl-wire-format @@ -127,11 +128,10 @@ internal class SslTools return null; } - byte[] ciphersData = peekStream.ReadBytes(length); - int[] ciphers = new int[ciphersData.Length / 2]; + int[] ciphers = new int[length / 2]; for (int i = 0; i < ciphers.Length; i++) { - ciphers[i] = (ciphersData[2 * i] << 8) + ciphersData[2 * i + 1]; + ciphers[i] = peekStream.ReadInt16(); } length = peekStream.ReadByte(); @@ -178,7 +178,7 @@ internal class SslTools /// /// /// - public static async Task IsServerHello(CustomBufferedStream stream, IBufferPool bufferPool, CancellationToken cancellationToken) + public static async Task IsServerHello(IPeekStream stream, IBufferPool bufferPool, CancellationToken cancellationToken) { var serverHello = await PeekServerHello(stream, bufferPool, cancellationToken); return serverHello != null; @@ -190,7 +190,7 @@ public static async Task IsServerHello(CustomBufferedStream stream, IBuffe /// /// /// - public static async Task PeekServerHello(CustomBufferedStream serverStream, IBufferPool bufferPool, CancellationToken cancellationToken = default) + public static async Task PeekServerHello(IPeekStream serverStream, IBufferPool bufferPool, CancellationToken cancellationToken = default) { // detects the HTTPS ClientHello message as it is described in the following url: // https://stackoverflow.com/questions/3897883/how-to-detect-an-incoming-ssl-https-handshake-ssl-wire-format @@ -286,11 +286,11 @@ public static async Task IsServerHello(CustomBufferedStream stream, IBuffe int cipherSuite = peekStream.ReadInt16(); byte compressionMethod = peekStream.ReadByte(); - int extenstionsStartPosition = peekStream.Position; + int extensionsStartPosition = peekStream.Position; Dictionary? extensions = null; - if (extenstionsStartPosition < recordLength + 5) + if (extensionsStartPosition < recordLength + 5) { extensions = await ReadExtensions(majorVersion, minorVersion, peekStream, bufferPool, cancellationToken); } @@ -298,7 +298,7 @@ public static async Task IsServerHello(CustomBufferedStream stream, IBuffe var serverHelloInfo = new ServerHelloInfo(3, majorVersion, minorVersion, random, sessionId, cipherSuite, peekStream.Position) { CompressionMethod = compressionMethod, - EntensionsStartPosition = extenstionsStartPosition, + EntensionsStartPosition = extensionsStartPosition, Extensions = extensions, }; diff --git a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.Mono.csproj b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.Mono.csproj index 3777fffbc..37cf96741 100644 --- a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.Mono.csproj +++ b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.Mono.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.NetCore.csproj b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.NetCore.csproj index ac8c9b734..de71aa587 100644 --- a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.NetCore.csproj +++ b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.NetCore.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj index 0002e0bad..c41718e92 100644 --- a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj +++ b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.nuspec b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.nuspec index ce61f46d4..a18c431b4 100644 --- a/src/Titanium.Web.Proxy/Titanium.Web.Proxy.nuspec +++ b/src/Titanium.Web.Proxy/Titanium.Web.Proxy.nuspec @@ -15,7 +15,7 @@ - + @@ -24,7 +24,7 @@ - + diff --git a/src/Titanium.Web.Proxy/TransparentClientHandler.cs b/src/Titanium.Web.Proxy/TransparentClientHandler.cs index ad0e106c9..6e0e8579c 100644 --- a/src/Titanium.Web.Proxy/TransparentClientHandler.cs +++ b/src/Titanium.Web.Proxy/TransparentClientHandler.cs @@ -33,8 +33,7 @@ private async Task handleClient(TransparentProxyEndPoint endPoint, TcpClientConn var cancellationTokenSource = new CancellationTokenSource(); var cancellationToken = cancellationTokenSource.Token; - var clientStream = new CustomBufferedStream(clientConnection.GetStream(), BufferPool); - var clientStreamWriter = new HttpResponseWriter(clientStream, BufferPool); + var clientStream = new HttpClientStream(clientConnection.GetStream(), BufferPool); SslStream? sslStream = null; @@ -75,14 +74,12 @@ private async Task handleClient(TransparentProxyEndPoint endPoint, TcpClientConn await sslStream.AuthenticateAsServerAsync(certificate, false, SslProtocols.Tls, false); // HTTPS server created - we can now decrypt the client's traffic - clientStream = new CustomBufferedStream(sslStream, BufferPool); - - clientStreamWriter = new HttpResponseWriter(clientStream, BufferPool); + clientStream = new HttpClientStream(sslStream, BufferPool); } catch (Exception e) { var certname = certificate?.GetNameInfo(X509NameType.SimpleName, false); - var session = new SessionEventArgs(this, endPoint, new ProxyClient(clientConnection, clientStream, clientStreamWriter), null, + var session = new SessionEventArgs(this, endPoint, clientConnection, clientStream, null, cancellationTokenSource); throw new ProxyConnectException( $"Couldn't authenticate host '{httpsHostName}' with certificate '{certname}'.", e, session); @@ -108,7 +105,7 @@ private async Task handleClient(TransparentProxyEndPoint endPoint, TcpClientConn { // clientStream.Available should be at most BufferSize because it is using the same buffer size await clientStream.ReadAsync(data, 0, available, cancellationToken); - await connection.StreamWriter.WriteAsync(data, 0, available, true, cancellationToken); + await connection.Stream.WriteAsync(data, 0, available, true, cancellationToken); } finally { @@ -133,7 +130,7 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, // HTTPS server created - we can now decrypt the client's traffic // Now create the request - await handleHttpSessionRequest(endPoint, clientConnection, clientStream, clientStreamWriter, cancellationTokenSource); + await handleHttpSessionRequest(endPoint, clientConnection, clientStream, cancellationTokenSource); } catch (ProxyException e) { @@ -155,11 +152,6 @@ await TcpHelper.SendRaw(clientStream, connection.Stream, BufferPool, { sslStream?.Dispose(); clientStream.Dispose(); - - if (!cancellationTokenSource.IsCancellationRequested) - { - cancellationTokenSource.Cancel(); - } } } } diff --git a/src/Titanium.Web.Proxy/WebSocketHandler.cs b/src/Titanium.Web.Proxy/WebSocketHandler.cs index 10ebe2a7b..5003be5fb 100644 --- a/src/Titanium.Web.Proxy/WebSocketHandler.cs +++ b/src/Titanium.Web.Proxy/WebSocketHandler.cs @@ -18,15 +18,14 @@ public partial class ProxyServer /// private async Task handleWebSocketUpgrade(string requestHttpMethod, string requestHttpUrl, Version requestVersion, SessionEventArgs args, Request request, Response response, - CustomBufferedStream clientStream, HttpResponseWriter clientStreamWriter, - TcpServerConnection serverConnection, + HttpClientStream clientStream, TcpServerConnection serverConnection, CancellationTokenSource cancellationTokenSource, CancellationToken cancellationToken) { // prepare the prefix content var headerBuilder = new HeaderBuilder(); headerBuilder.WriteRequestLine(requestHttpMethod, requestHttpUrl, requestVersion); headerBuilder.WriteHeaders(request.Headers); - await serverConnection.StreamWriter.WriteHeadersAsync(headerBuilder, cancellationToken); + await serverConnection.Stream.WriteHeadersAsync(headerBuilder, cancellationToken); string httpStatus; try @@ -51,7 +50,7 @@ await HeaderParser.ReadHeaders(serverConnection.Stream, response.Headers, if (!args.IsTransparent) { - await clientStreamWriter.WriteResponseAsync(response, + await clientStream.WriteResponseAsync(response, cancellationToken: cancellationToken); } diff --git a/src/Titanium.Web.Proxy/WinAuthHandler.cs b/src/Titanium.Web.Proxy/WinAuthHandler.cs index 72f13a592..c72d333e1 100644 --- a/src/Titanium.Web.Proxy/WinAuthHandler.cs +++ b/src/Titanium.Web.Proxy/WinAuthHandler.cs @@ -175,7 +175,7 @@ private async Task rewriteUnauthorizedResponse(SessionEventArgs args) // Add custom div to body to clarify that the proxy (not the client browser) failed authentication string authErrorMessage = "

NTLM authentication through Titanium.Web.Proxy (" + - args.ProxyClient.Connection.LocalEndPoint + + args.ClientConnection.LocalEndPoint + ") failed. Please check credentials.

"; string originalErrorMessage = "

Response from remote web server below.


"; diff --git a/tests/Titanium.Web.Proxy.IntegrationTests/NestedProxyTests.cs b/tests/Titanium.Web.Proxy.IntegrationTests/NestedProxyTests.cs index 7980c6012..2ed0d10e9 100644 --- a/tests/Titanium.Web.Proxy.IntegrationTests/NestedProxyTests.cs +++ b/tests/Titanium.Web.Proxy.IntegrationTests/NestedProxyTests.cs @@ -69,7 +69,7 @@ public async Task Smoke_Test_Nested_Proxy_UserData() }; var client = testSuite.GetClient(proxy1, true); - + var response = await client.PostAsync(new Uri(server.ListeningHttpsUrl), new StringContent("hello server. I am a client.")); diff --git a/tests/Titanium.Web.Proxy.IntegrationTests/Titanium.Web.Proxy.IntegrationTests.csproj b/tests/Titanium.Web.Proxy.IntegrationTests/Titanium.Web.Proxy.IntegrationTests.csproj index 279dcb57d..9764f7e8a 100644 --- a/tests/Titanium.Web.Proxy.IntegrationTests/Titanium.Web.Proxy.IntegrationTests.csproj +++ b/tests/Titanium.Web.Proxy.IntegrationTests/Titanium.Web.Proxy.IntegrationTests.csproj @@ -17,7 +17,7 @@ - + diff --git a/tests/Titanium.Web.Proxy.UnitTests/Titanium.Web.Proxy.UnitTests.csproj b/tests/Titanium.Web.Proxy.UnitTests/Titanium.Web.Proxy.UnitTests.csproj index fd26bfc64..c35f69cca 100644 --- a/tests/Titanium.Web.Proxy.UnitTests/Titanium.Web.Proxy.UnitTests.csproj +++ b/tests/Titanium.Web.Proxy.UnitTests/Titanium.Web.Proxy.UnitTests.csproj @@ -7,7 +7,7 @@ - +