Skip to content

Commit 24d9c25

Browse files
committed
wip
1 parent c6e51cc commit 24d9c25

File tree

10 files changed

+269
-185
lines changed

10 files changed

+269
-185
lines changed

extensions/Sisk.SslProxy/ProxyGateway.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ namespace Sisk.Ssl;
1313

1414
class ProxyGateway : IDisposable {
1515
HttpClient client;
16+
HttpClientHandler httpHandler;
1617

1718
public IPEndPoint GatewayEndpoint { get; }
1819

1920
public ProxyGateway ( IPEndPoint endpoint ) {
20-
var httpHandler = new HttpClientHandler () {
21+
httpHandler = new HttpClientHandler () {
2122
AllowAutoRedirect = false,
2223
AutomaticDecompression = DecompressionMethods.None,
2324
ServerCertificateCustomValidationCallback = ( message, cert, chain, errors ) => {
@@ -34,6 +35,7 @@ public ProxyGateway ( IPEndPoint endpoint ) {
3435
}
3536

3637
public void Dispose () {
38+
httpHandler.Dispose ();
3739
client.Dispose ();
3840
}
3941
}

extensions/Sisk.SslProxy/SslProxyContextHandler.cs

Lines changed: 112 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,30 @@
88
// Repository: https://github.com/sisk-http/core
99

1010
using System.Buffers;
11-
using System.Diagnostics;
1211
using System.Net.Sockets;
13-
using System.Reflection;
12+
using System.Text;
1413
using Sisk.Cadente;
1514
using Sisk.Core.Http;
1615

1716
namespace Sisk.Ssl;
1817

1918
class SslProxyContextHandler : HttpHostHandler {
19+
// Helpers
20+
private static readonly byte [] CRLF = Encoding.ASCII.GetBytes ( "\r\n" );
21+
private static readonly HashSet<string> HopByHopRequestHeaders = new ( StringComparer.OrdinalIgnoreCase )
22+
{
23+
"Connection", "Keep-Alive", "Proxy-Connection", "Transfer-Encoding", "TE", "Trailer", "Upgrade"
24+
};
25+
private static readonly HashSet<string> HopByHopResponseHeaders = new ( StringComparer.OrdinalIgnoreCase )
26+
{
27+
"Connection", "Keep-Alive", "Proxy-Connection", "Transfer-Encoding", "TE", "Trailer", "Upgrade",
28+
"Proxy-Authenticate", "Proxy-Authorization"
29+
};
30+
private static readonly HashSet<string> StripAlwaysResponseHeaders = new ( StringComparer.OrdinalIgnoreCase )
31+
{
32+
"Server", "Date", "Host"
33+
};
2034

21-
internal readonly static byte [] ChunkedEOF = "0\r\n\r\n"u8.ToArray ();
22-
internal readonly static string [] UnnalowedProxiedHeaders = [ "Server", "Date", "Host", "Connection" ];
2335
readonly SslProxy ProxyHost;
2436

2537
public SslProxyContextHandler ( SslProxy proxy ) {
@@ -39,20 +51,31 @@ public override Task OnClientDisconnectedAsync ( HttpHost host, HttpHostClient c
3951
public override async Task OnContextCreatedAsync ( HttpHost host, HttpHostContext context ) {
4052
ProxyGateway state = (ProxyGateway) context.Client.State!;
4153

42-
CancellationTokenSource gatewayCancellation = new CancellationTokenSource ();
54+
using CancellationTokenSource gatewayCancellation = new CancellationTokenSource ();
4355
gatewayCancellation.CancelAfter ( ProxyHost.GatewayTimeout );
4456
gatewayCancellation.Token.ThrowIfCancellationRequested ();
4557

58+
// Monta request upstream
4659
HttpMethod requestMethod = new HttpMethod ( context.Request.Method );
4760
string requestPath = context.Request.Path;
4861
string requestUri =
49-
ProxyHost.UseGatewayHttps ?
50-
$"https://{ProxyHost.GatewayEndpoint.Address}:{ProxyHost.GatewayEndpoint.Port}{requestPath}" :
51-
$"http://{ProxyHost.GatewayEndpoint.Address}:{ProxyHost.GatewayEndpoint.Port}{requestPath}";
62+
ProxyHost.UseGatewayHttps
63+
? $"https://{ProxyHost.GatewayEndpoint.Address}:{ProxyHost.GatewayEndpoint.Port}{requestPath}"
64+
: $"http://{ProxyHost.GatewayEndpoint.Address}:{ProxyHost.GatewayEndpoint.Port}{requestPath}";
5265

53-
bool isWebsocketConnection = context.Request.Headers.Any ( c => c.Name.Equals ( HttpKnownHeaderNames.SecWebSocketKey, StringComparison.OrdinalIgnoreCase ) );
66+
bool isWebsocketConnection = context.Request.Headers.Any ( h =>
67+
h.Name.Equals ( HttpKnownHeaderNames.SecWebSocketKey, StringComparison.OrdinalIgnoreCase ) );
5468

55-
HttpRequestMessage proxyRequest = new HttpRequestMessage ( requestMethod, requestUri );
69+
if (isWebsocketConnection) {
70+
context.Response.StatusCode = 501;
71+
context.Response.StatusDescription = "Not Implemented (use ClientWebSocket upstream)";
72+
await context.Response.GetResponseStream ().FlushAsync ( gatewayCancellation.Token );
73+
return;
74+
}
75+
76+
using var proxyRequest = new HttpRequestMessage ( requestMethod, requestUri ) {
77+
Version = new Version ( 1, 1 )
78+
};
5679
proxyRequest.Headers.Host = ProxyHost.GatewayHostname;
5780

5881
if (context.Request.ContentLength > 0) {
@@ -63,151 +86,123 @@ public override async Task OnContextCreatedAsync ( HttpHost host, HttpHostContex
6386

6487
for (int i = 0; i < context.Request.Headers.Length; i++) {
6588
HttpHeader header = context.Request.Headers [ i ];
66-
67-
if (UnnalowedProxiedHeaders.Contains ( header.Name, StringComparer.OrdinalIgnoreCase )) {
89+
if (HopByHopRequestHeaders.Contains ( header.Name ) ||
90+
header.Name.Equals ( "Host", StringComparison.OrdinalIgnoreCase )) {
6891
continue;
6992
}
70-
else {
71-
proxyRequest.Headers.TryAddWithoutValidation ( header.Name, header.Value );
72-
}
93+
proxyRequest.Headers.TryAddWithoutValidation ( header.Name, header.Value );
7394
}
7495

75-
if (isWebsocketConnection) {
76-
proxyRequest.Headers.Connection.Add ( "Upgrade" );
77-
}
7896
if (ProxyHost.ProxyAuthorization != null) {
7997
proxyRequest.Headers.TryAddWithoutValidation ( HttpKnownHeaderNames.ProxyAuthorization, ProxyHost.ProxyAuthorization );
8098
}
8199

82-
HttpResponseMessage proxyResponse = await state.SendMessageAsync ( proxyRequest, gatewayCancellation.Token );
100+
using HttpResponseMessage proxyResponse = await state.SendMessageAsync ( proxyRequest, gatewayCancellation.Token );
83101

84102
context.Response.StatusCode = (int) proxyResponse.StatusCode;
85-
context.Response.StatusDescription = proxyResponse.ReasonPhrase ?? HttpStatusInformation.GetStatusCodeDescription ( proxyResponse.StatusCode );
103+
context.Response.StatusDescription =
104+
proxyResponse.ReasonPhrase ?? HttpStatusInformation.GetStatusCodeDescription ( proxyResponse.StatusCode );
86105

87106
IEnumerable<KeyValuePair<string, IEnumerable<string>>> proxyResponseHeaders =
88107
[ .. proxyResponse.Headers, .. proxyResponse.Content.Headers ];
89108

90-
foreach (var header in proxyResponseHeaders) {
91-
if (UnnalowedProxiedHeaders.Contains ( header.Key, StringComparer.OrdinalIgnoreCase ))
92-
continue;
93-
94-
foreach (var headerValue in header.Value)
95-
context.Response.Headers.Add ( new HttpHeader ( header.Key, headerValue ) );
96-
}
109+
long? contentLength = proxyResponse.Content.Headers.ContentLength;
97110

98-
Stream gatewayStream = ResolveRawResponseStream ( await proxyResponse.Content.ReadAsStreamAsync (), out bool isChunked );
111+
var announcedTrailers = proxyResponse.Headers.Trailer;
99112

100-
if (isWebsocketConnection) {
113+
foreach (var header in proxyResponseHeaders) {
114+
string name = header.Key;
101115

102-
context.Response.Headers.Add ( new HttpHeader ( HttpKnownHeaderNames.Connection, "Upgrade" ) );
103-
Stream responseStream = context.Response.GetResponseStream ();
116+
if (HopByHopResponseHeaders.Contains ( name ) || StripAlwaysResponseHeaders.Contains ( name ))
117+
continue;
104118

105-
Task copyToProxy = CopyToAsyncUnchecked ( responseStream, gatewayStream, eof: null, gatewayCancellation.Token );
106-
Task copyFromProxy = CopyToAsyncUnchecked ( gatewayStream, responseStream, eof: null, gatewayCancellation.Token );
119+
if (name.Equals ( "Content-Length", StringComparison.OrdinalIgnoreCase )) {
120+
if (contentLength is null)
121+
continue;
122+
}
107123

108-
await Task.WhenAny ( copyToProxy, copyFromProxy );
109-
await gatewayCancellation.CancelAsync ();
110-
;
124+
foreach (var headerValue in header.Value) {
125+
context.Response.Headers.Add ( new HttpHeader ( name, headerValue ) );
126+
}
127+
}
111128

129+
if (announcedTrailers != null && announcedTrailers.Count > 0) {
130+
foreach (var trailersName in announcedTrailers) {
131+
context.Response.Headers.Add ( new HttpHeader ( "Trailer", trailersName ) );
132+
}
112133
}
113-
else {
114-
Stream responseStream = context.Response.GetResponseStream ();
115134

116-
byte []? eof = isChunked ? ChunkedEOF : null;
135+
await using Stream contentStream = await proxyResponse.Content.ReadAsStreamAsync ( gatewayCancellation.Token );
136+
Stream responseStream = context.Response.GetResponseStream ();
117137

118-
await CopyToAsyncUnchecked ( gatewayStream, responseStream, eof, gatewayCancellation.Token );
119-
;
138+
if (contentLength.HasValue) {
139+
context.Response.Headers.Add ( new HttpHeader ( "Content-Length", contentLength.Value.ToString () ) );
120140

141+
await CopyRawAsync ( contentStream, responseStream, gatewayCancellation.Token );
121142
}
143+
else {
144+
context.Response.Headers.Add ( new HttpHeader ( "Transfer-Encoding", "chunked" ) );
145+
await CopyAsChunkedAsync ( contentStream, responseStream, gatewayCancellation.Token );
146+
147+
if (proxyResponse.TrailingHeaders != null && proxyResponse.TrailingHeaders.Any ()) {
148+
// Escreve chunk de tamanho zero + CRLF já foi feito em CopyAsChunkedAsync,
149+
// então aqui temos que escrever corretamente os trailers antes do CRLF final.
150+
// Para simplificar, vamos ajustar CopyAsChunkedAsync para NÃO escrever o final,
151+
// e fazê-lo aqui com trailers. Implementação abaixo assume esse contrato.
152+
// Caso já tenha escrito "0\r\n\r\n", devemos adaptar a função para omitir esse final.
153+
}
154+
}
155+
156+
await responseStream.FlushAsync ( gatewayCancellation.Token );
122157
}
123158

124-
static async Task CopyToAsyncUnchecked ( Stream from, Stream to, byte []? eof, CancellationToken cancellationToken ) {
159+
static async Task CopyRawAsync ( Stream from, Stream to, CancellationToken cancellationToken ) {
125160
try {
126161
const int DefaultCopySize = 81920;
127162
byte [] buffer = ArrayPool<byte>.Shared.Rent ( DefaultCopySize );
128163
try {
129-
if (eof is null) {
130-
// Non-chunked path: just copy until the stream ends. This relies on the source
131-
// stream being properly bounded by Content-Length, which HttpClient should ensure.
132-
int bytesRead;
133-
while ((bytesRead = await from.ReadAsync ( buffer, cancellationToken ).ConfigureAwait ( false )) != 0) {
134-
await to.WriteAsync ( buffer.AsMemory ( 0, bytesRead ), cancellationToken ).ConfigureAwait ( false );
135-
}
136-
}
137-
else {
138-
// Chunked path: copy until we see the EOF marker ("0\r\n\r\n").
139-
// This implementation is robust against the marker being split across multiple reads.
140-
int eofMatchIndex = 0;
141-
int bytesRead;
142-
while ((bytesRead = await from.ReadAsync ( buffer, cancellationToken ).ConfigureAwait ( false )) != 0) {
143-
await to.WriteAsync ( buffer.AsMemory ( 0, bytesRead ), cancellationToken ).ConfigureAwait ( false );
144-
145-
for (int i = 0; i < bytesRead; i++) {
146-
if (buffer [ i ] == eof [ eofMatchIndex ]) {
147-
eofMatchIndex++;
148-
if (eofMatchIndex == eof.Length) {
149-
return;
150-
}
151-
}
152-
else {
153-
eofMatchIndex = (buffer [ i ] == eof [ 0 ]) ? 1 : 0;
154-
}
155-
}
156-
}
164+
int bytesRead;
165+
while ((bytesRead = await from.ReadAsync ( buffer, 0, buffer.Length, cancellationToken ).ConfigureAwait ( false )) != 0) {
166+
await to.WriteAsync ( buffer.AsMemory ( 0, bytesRead ), cancellationToken ).ConfigureAwait ( false );
157167
}
158168
}
159169
finally {
160170
ArrayPool<byte>.Shared.Return ( buffer );
161171
}
162172
}
163-
catch (IOException) {
164-
}
165-
catch (SocketException) {
166-
}
167-
catch (OperationCanceledException) {
168-
}
173+
catch (IOException) { }
174+
catch (SocketException) { }
175+
catch (OperationCanceledException) { }
169176
}
170177

171-
static FieldInfo? ResolveRawResponseStream__f_connection;
172-
static FieldInfo? ResolveRawResponseStream__f_stream;
173-
174-
static Stream ResolveRawResponseStream ( Stream gatewayStream, out bool isChunked ) {
175-
176-
/*
177-
The HttpClient places the response stream into a Stream to deserialize the chunked encoding
178-
and retrieve the deserialized content.
179-
180-
The problem with this is that the proxy sends the transfer-encoding: chunked header and a
181-
non-chunked response, which causes a deserialization issue.
182-
183-
The code below uses reflection to get the underlying NetworkStream of the connection between
184-
the proxy and the gateway to send a raw response without data decompression,
185-
to the proxy client.
186-
*/
187-
188-
Type typeName = gatewayStream.GetType ();
189-
if (typeName.FullName == "System.Net.Http.HttpConnection+ChunkedEncodingReadStream") {
190-
191-
ResolveRawResponseStream__f_connection ??= typeName.GetField ( "_connection", BindingFlags.NonPublic | BindingFlags.Instance );
192-
Debug.Assert ( ResolveRawResponseStream__f_connection != null );
193-
194-
object? _connection = ResolveRawResponseStream__f_connection.GetValue ( gatewayStream );
195-
Debug.Assert ( _connection != null );
196-
197-
ResolveRawResponseStream__f_stream ??= _connection.GetType ()?.GetField ( "_stream", BindingFlags.NonPublic | BindingFlags.Instance );
198-
Debug.Assert ( ResolveRawResponseStream__f_stream != null );
199-
200-
object? stream = ResolveRawResponseStream__f_stream.GetValue ( _connection );
201-
Debug.Assert ( stream != null );
202-
203-
isChunked = true;
204-
205-
return (Stream) stream;
206-
}
207-
else {
208-
isChunked = false;
209-
210-
return gatewayStream;
178+
static async Task CopyAsChunkedAsync ( Stream from, Stream to, CancellationToken cancellationToken ) {
179+
try {
180+
const int DefaultCopySize = 81920;
181+
byte [] buffer = ArrayPool<byte>.Shared.Rent ( DefaultCopySize );
182+
try {
183+
int bytesRead;
184+
while ((bytesRead = await from.ReadAsync ( buffer, 0, buffer.Length, cancellationToken ).ConfigureAwait ( false )) > 0) {
185+
// size
186+
string sizeHex = bytesRead.ToString ( "x" );
187+
await WriteAsciiAsync ( to, sizeHex, cancellationToken ).ConfigureAwait ( false );
188+
await to.WriteAsync ( CRLF, 0, CRLF.Length, cancellationToken ).ConfigureAwait ( false );
189+
190+
// payload
191+
await to.WriteAsync ( buffer.AsMemory ( 0, bytesRead ), cancellationToken ).ConfigureAwait ( false );
192+
193+
// crlf
194+
await to.WriteAsync ( CRLF, 0, CRLF.Length, cancellationToken ).ConfigureAwait ( false );
195+
}
196+
}
197+
finally {
198+
ArrayPool<byte>.Shared.Return ( buffer );
199+
}
211200
}
201+
catch (IOException) { }
202+
catch (SocketException) { }
203+
catch (OperationCanceledException) { }
212204
}
213-
}
205+
206+
static Task WriteAsciiAsync ( Stream to, string s, CancellationToken ct )
207+
=> to.WriteAsync ( Encoding.ASCII.GetBytes ( s ), 0, s.Length, ct );
208+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// The Sisk Framework source code
2+
// Copyright (c) 2024- PROJECT PRINCIPIUM and all Sisk contributors
3+
//
4+
// The code below is licensed under the MIT license as
5+
// of the date of its publication, available at
6+
//
7+
// File name: GZipRotatingLogPolicyCompressor.cs
8+
// Repository: https://github.com/sisk-http/core
9+
10+
using System.IO.Compression;
11+
12+
namespace Sisk.Core.Http;
13+
14+
sealed class GZipRotatingLogPolicyCompressor : RotatingLogPolicyCompressor {
15+
16+
public override string GetCompressedFileName ( string preFormattedName ) {
17+
return $"{preFormattedName}.gz";
18+
}
19+
20+
public override Stream GetCompressingStream ( Stream logFileStream ) {
21+
return new GZipStream ( logFileStream, CompressionMode.Compress );
22+
}
23+
}

src/Http/LogStream.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using System.Text;
1111
using System.Threading.Channels;
1212
using Sisk.Core.Entity;
13-
using Sisk.Core.Internal;
1413

1514
namespace Sisk.Core.Http {
1615
/// <summary>
@@ -207,7 +206,7 @@ public void StopBuffering () {
207206

208207
/// <summary>
209208
/// Defines the time interval and size threshold for starting the task, and then starts the task. This method is an
210-
/// shortcut for calling <see cref="RotatingLogPolicy.Configure(long, TimeSpan)"/> of this defined <see cref="RotatingPolicy"/> method.
209+
/// shortcut for calling <see cref="RotatingLogPolicy.Configure(long, TimeSpan, RotatingLogPolicyCompressor)"/> of this defined <see cref="RotatingPolicy"/> method.
211210
/// </summary>
212211
/// <remarks>
213212
/// The first run is performed immediately after calling this method.

0 commit comments

Comments
 (0)