88// Repository: https://github.com/sisk-http/core
99
1010using System . Buffers ;
11- using System . Diagnostics ;
1211using System . Net . Sockets ;
13- using System . Reflection ;
12+ using System . Text ;
1413using Sisk . Cadente ;
1514using Sisk . Core . Http ;
1615
1716namespace Sisk . Ssl ;
1817
1918class 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+ }
0 commit comments