1+ using System ;
2+ using System . IO ;
3+ using System . Net ;
4+ using System . Net . Http ;
5+ using System . Net . Sockets ;
6+ using System . Text ;
7+ using System . Threading . Tasks ;
8+ using Microsoft . AspNetCore . Builder ;
9+ using Microsoft . AspNetCore . Hosting ;
10+ using Microsoft . AspNetCore . Http ;
11+ using Microsoft . AspNetCore . Server . Kestrel . Core ;
12+ using Microsoft . Extensions . DependencyInjection ;
13+ using Microsoft . Extensions . Hosting ;
14+ using Microsoft . Extensions . Logging ;
15+ using Microsoft . Extensions . Primitives ;
16+ using Microsoft . Net . Http . Headers ;
17+ using Microsoft . ReverseProxy . Abstractions ;
18+ using Xunit ;
19+
20+ namespace Microsoft . ReverseProxy
21+ {
22+ public abstract class HttpProxyCookieTests
23+ {
24+ public const string CookieAKey = "testA" ;
25+ public const string CookieAValue = "A_Cookie" ;
26+ public const string CookieBKey = "testB" ;
27+ public const string CookieBValue = "B_Cookie" ;
28+
29+ public static readonly string CookieA = $ "{ CookieAKey } ={ CookieAValue } ";
30+ public static readonly string CookieB = $ "{ CookieBKey } ={ CookieBValue } ";
31+ public static readonly string Cookies = $ "{ CookieA } ; { CookieB } ";
32+
33+ public abstract HttpProtocols HttpProtocol { get ; }
34+ public abstract Task ProcessHttpRequest ( Uri proxyHostUri ) ;
35+
36+ [ Fact ]
37+ public async Task ProxyAsync_RequestWithCookieHeaders ( )
38+ {
39+ var tcs = new TaskCompletionSource < StringValues > ( ) ;
40+
41+ using var destinationHost = CreateDestinationHost (
42+ context =>
43+ {
44+ if ( context . Request . Headers . TryGetValue ( HeaderNames . Cookie , out var cookieHeaders ) )
45+ {
46+ tcs . SetResult ( cookieHeaders ) ;
47+ }
48+ else
49+ {
50+ tcs . SetException ( new Exception ( "Missing 'Cookie' header in request" ) ) ;
51+ }
52+ return Task . CompletedTask ;
53+ } ) ;
54+
55+ await destinationHost . StartAsync ( ) ;
56+ var destinationHostUrl = destinationHost . GetAddress ( ) ;
57+
58+ using var proxyHost = CreateReverseProxyHost ( HttpProtocol , destinationHostUrl ) ;
59+ await proxyHost . StartAsync ( ) ;
60+ var proxyHostUri = new Uri ( proxyHost . GetAddress ( ) ) ;
61+
62+ await ProcessHttpRequest ( proxyHostUri ) ;
63+
64+ Assert . True ( tcs . Task . IsCompleted ) ;
65+ var cookieHeaders = await tcs . Task ;
66+ var cookies = Assert . Single ( cookieHeaders ) ;
67+ Assert . Equal ( Cookies , cookies ) ;
68+
69+ await destinationHost . StopAsync ( ) ;
70+ await proxyHost . StopAsync ( ) ;
71+ }
72+
73+ private IHost CreateReverseProxyHost ( HttpProtocols httpProtocol , string destinationHostUrl )
74+ {
75+ return CreateHost ( httpProtocol ,
76+ services =>
77+ {
78+ var proxyRoute = new ProxyRoute
79+ {
80+ RouteId = "route1" ,
81+ ClusterId = "cluster1" ,
82+ Match = { Path = "/{**catchall}" }
83+ } ;
84+
85+ var cluster = new Cluster
86+ {
87+ Id = "cluster1" ,
88+ Destinations =
89+ {
90+ { "cluster1" , new Destination ( ) { Address = destinationHostUrl } }
91+ }
92+ } ;
93+
94+ services . AddReverseProxy ( ) . LoadFromMemory ( new [ ] { proxyRoute } , new [ ] { cluster } ) ;
95+ } ,
96+ app =>
97+ {
98+ app . UseMiddleware < CheckCookieHeaderMiddleware > ( ) ;
99+ app . UseRouting ( ) ;
100+ app . UseEndpoints ( builder =>
101+ {
102+ builder . MapReverseProxy ( ) ;
103+ } ) ;
104+ } ) ;
105+ }
106+
107+ private IHost CreateDestinationHost ( RequestDelegate getDelegate )
108+ {
109+ return CreateHost ( HttpProtocols . Http1AndHttp2 ,
110+ services =>
111+ {
112+ services . AddRouting ( ) ;
113+ } ,
114+ app =>
115+ {
116+ app . UseRouting ( ) ;
117+ app . UseEndpoints ( endpoints => endpoints . MapGet ( "/" , getDelegate ) ) ;
118+ } ) ;
119+ }
120+
121+ private IHost CreateHost ( HttpProtocols httpProtocols , Action < IServiceCollection > configureServices , Action < IApplicationBuilder > configureApp )
122+ {
123+ return new HostBuilder ( )
124+ . ConfigureWebHost ( webHostBuilder =>
125+ {
126+ webHostBuilder
127+ . ConfigureServices ( configureServices )
128+ . UseKestrel ( kestrel =>
129+ {
130+ kestrel . Listen ( IPAddress . Loopback , 0 , listenOptions =>
131+ {
132+ listenOptions . Protocols = httpProtocols ;
133+ } ) ;
134+ } )
135+ . Configure ( configureApp ) ;
136+ } ) . Build ( ) ;
137+ }
138+
139+ private class CheckCookieHeaderMiddleware
140+ {
141+ private readonly RequestDelegate _next ;
142+
143+ public CheckCookieHeaderMiddleware ( RequestDelegate next )
144+ {
145+ _next = next ;
146+ }
147+
148+ public async Task Invoke ( HttpContext context )
149+ {
150+ // Ensure that CookieA is the first and CookieB the last.
151+ Assert . True ( context . Request . Headers . TryGetValue ( HeaderNames . Cookie , out var headerValues ) ) ;
152+ Assert . StartsWith ( CookieA , headerValues ) ;
153+ Assert . EndsWith ( CookieB , headerValues ) ;
154+
155+ await _next . Invoke ( context ) ;
156+ }
157+ }
158+ }
159+ public class HttpProxyCookieTests_Http1 : HttpProxyCookieTests
160+ {
161+ public override HttpProtocols HttpProtocol => HttpProtocols . Http1 ;
162+
163+ // Using simple TcpClient since HttpClient will always concatenate cookies with comma separator.
164+ public override async Task ProcessHttpRequest ( Uri proxyHostUri )
165+ {
166+ using var client = new TcpClient ( proxyHostUri . Host , proxyHostUri . Port ) ;
167+ using var stream = client . GetStream ( ) ;
168+ using var writer = new StreamWriter ( stream , Encoding . ASCII ) ;
169+ using var reader = new StreamReader ( stream , Encoding . ASCII ) ;
170+
171+ await writer . WriteAsync ( $ "GET / HTTP/1.1\r \n ") ;
172+ await writer . WriteAsync ( $ "Host: { proxyHostUri . Authority } \r \n ") ;
173+ await writer . WriteAsync ( $ "Cookie: { Cookies } \r \n ") ;
174+ await writer . WriteAsync ( $ "Connection: close\r \n ") ;
175+ await writer . WriteAsync ( $ "\r \n ") ;
176+ await writer . FlushAsync ( ) ;
177+
178+ string line = null ;
179+ string statusLine = null ;
180+ while ( ( line = await reader . ReadLineAsync ( ) ) != null )
181+ {
182+ statusLine ??= line ;
183+ }
184+
185+ Assert . Equal ( $ "HTTP/1.1 200 OK", statusLine ) ;
186+ }
187+ }
188+
189+ #if NET5_0
190+ public class HttpProxyCookieTests_Http2 : HttpProxyCookieTests
191+ {
192+ public override HttpProtocols HttpProtocol => HttpProtocols . Http2 ;
193+
194+ // HttpClient for H/2 will use different header frames for cookies from a container and message headers.
195+ // It will first send message header cookie and than the container one and we expect them in the order of cookieA;cookieB.
196+ public override async Task ProcessHttpRequest ( Uri proxyHostUri )
197+ {
198+ var handler = new HttpClientHandler ( ) ;
199+ handler . CookieContainer . Add ( new System . Net . Cookie ( CookieBKey , CookieBValue , path : "/" , domain : proxyHostUri . Host ) ) ;
200+ var client = new HttpClient ( handler ) ;
201+ var message = new HttpRequestMessage ( HttpMethod . Get , proxyHostUri ) ;
202+ message . Version = HttpVersion . Version20 ;
203+ message . VersionPolicy = HttpVersionPolicy . RequestVersionExact ;
204+ message . Headers . Add ( HeaderNames . Cookie , $ "{ CookieA } ") ;
205+ using var response = await client . SendAsync ( message ) ;
206+ response . EnsureSuccessStatusCode ( ) ;
207+ }
208+ }
209+ #elif NETCOREAPP3_1
210+ // Do not test HTTP/2 on .NET Core 3.1
211+ #else
212+ #error A target framework was added to the project and needs to be added to this condition.
213+ #endif
214+ }
0 commit comments