1+ // Copyright (c) .NET Foundation. All rights reserved.
2+ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+ using System ;
5+ using System . Threading . Tasks ;
6+ using Microsoft . AspNetCore . Http ;
7+
8+ namespace Microsoft . VisualStudio . Web . BrowserLink
9+ {
10+ /// <summary>
11+ /// This class is created once, and invoked for each request. It puts a filter on
12+ /// the Response.Body, which will inject Browser Link's script links into the
13+ /// content (if the content is HTML).
14+ /// </summary>
15+ internal class BrowserLinkMiddleware
16+ {
17+ // Number of timeouts allowed before we stop trying to connect to the host
18+ private const int FilterRequestTimeoutLimit = 2 ;
19+
20+ // Number of timeouts that occurred while attempting to connect to the host
21+ private static int _filterRequestTimeouts = 0 ;
22+
23+ private RequestDelegate _next ;
24+ private string _applicationPath ;
25+
26+ internal BrowserLinkMiddleware ( string applicationPath , RequestDelegate next )
27+ {
28+ _applicationPath = applicationPath ;
29+ _next = next ;
30+ }
31+
32+ /// <summary>
33+ /// This method is called to process the response.
34+ /// </summary>
35+ internal Task Invoke ( HttpContext context )
36+ {
37+ string requestId = Guid . NewGuid ( ) . ToString ( "N" ) ;
38+
39+ IHttpSocketAdapter injectScriptSocket = GetSocketConnectionToHost ( _applicationPath , requestId , "injectScriptLink" , context . Request . IsHttps ) ;
40+
41+ if ( injectScriptSocket != null )
42+ {
43+ return ExecuteWithFilter ( injectScriptSocket , requestId , context ) ;
44+ }
45+ else
46+ {
47+ return ExecuteWithoutFilter ( context ) ;
48+ }
49+ }
50+
51+ private PageExecutionListenerFeature AddPageExecutionListenerFeatureTo ( HttpContext context , string requestId )
52+ {
53+ IHttpSocketAdapter mappingDataSocket = GetSocketConnectionToHost ( _applicationPath , requestId , "sendMappingData" , context . Request . IsHttps ) ;
54+
55+ if ( mappingDataSocket != null )
56+ {
57+ PageExecutionListenerFeature listener = new PageExecutionListenerFeature ( mappingDataSocket ) ;
58+
59+ context . Features . Set ( listener ) ;
60+
61+ return listener ;
62+ }
63+
64+ return null ;
65+ }
66+
67+ private async Task ExecuteWithFilter ( IHttpSocketAdapter injectScriptSocket , string requestId , HttpContext httpContext )
68+ {
69+ ScriptInjectionFilterContext filterContext = new ScriptInjectionFilterContext ( httpContext ) ;
70+
71+ using ( ScriptInjectionFilterStream filter = new ScriptInjectionFilterStream ( injectScriptSocket , filterContext ) )
72+ {
73+ httpContext . Response . Body = filter ;
74+ httpContext . Response . OnStarting ( delegate ( )
75+ {
76+ httpContext . Response . ContentLength = null ;
77+
78+ return StaticTaskResult . True ;
79+ } ) ;
80+
81+ using ( AddPageExecutionListenerFeatureTo ( httpContext , requestId ) )
82+ {
83+ await _next ( httpContext ) ;
84+
85+ await filter . WaitForFilterComplete ( ) ;
86+
87+ if ( filter . ScriptInjectionTimedOut )
88+ {
89+ _filterRequestTimeouts ++ ;
90+ }
91+ else
92+ {
93+ _filterRequestTimeouts = 0 ;
94+ }
95+ }
96+ }
97+ }
98+
99+ private Task ExecuteWithoutFilter ( HttpContext context )
100+ {
101+ return _next ( context ) ;
102+ }
103+
104+ private static IHttpSocketAdapter GetSocketConnectionToHost ( string applicationPath , string requestId , string rpcMethod , bool isHttps )
105+ {
106+ // The host should send an initial response immediately after
107+ // the connection is established. If it fails to do so multiple times,
108+ // stop trying. Each timeout is delaying a response to the browser.
109+ //
110+ // This will only reset when the server process is restarted.
111+ if ( _filterRequestTimeouts >= FilterRequestTimeoutLimit )
112+ {
113+ return null ;
114+ }
115+
116+ if ( FindAndSignalHostConnection ( applicationPath ) )
117+ {
118+ return new DelayConnectingHttpSocketAdapter ( async delegate ( )
119+ {
120+ Uri connectionString ;
121+
122+ if ( GetHostConnectionString ( applicationPath , out connectionString ) )
123+ {
124+ IHttpSocketAdapter httpSocket = await HttpSocketAdapter . OpenHttpSocketAsync ( "GET" , new Uri ( connectionString , rpcMethod ) ) ;
125+
126+ AddRequestHeaders ( httpSocket , requestId , isHttps ) ;
127+
128+ return httpSocket ;
129+ }
130+
131+ return null ;
132+ } ) ;
133+ }
134+
135+ return null ;
136+ }
137+
138+ private static void AddRequestHeaders ( IHttpSocketAdapter httpSocket , string requestId , bool isHttps )
139+ {
140+ httpSocket . AddRequestHeader ( BrowserLinkConstants . RequestIdHeaderName , requestId ) ;
141+
142+ if ( isHttps )
143+ {
144+ httpSocket . AddRequestHeader ( BrowserLinkConstants . RequestScheme , "https" ) ;
145+ }
146+ else
147+ {
148+ httpSocket . AddRequestHeader ( BrowserLinkConstants . RequestScheme , "http" ) ;
149+ }
150+ }
151+
152+ private static bool FindAndSignalHostConnection ( string applicationPath )
153+ {
154+ HostConnectionData connectionData ;
155+
156+ if ( HostConnectionUtil . FindHostConnection ( applicationPath , out connectionData ) )
157+ {
158+ return HostConnectionUtil . SignalHostForStartup ( connectionData , blockUntilStarted : false ) ;
159+ }
160+
161+ return false ;
162+ }
163+
164+ private static bool GetHostConnectionString ( string applicationPath , out Uri connectionString )
165+ {
166+ HostConnectionData connectionData ;
167+
168+ if ( GetHostConnectionData ( applicationPath , out connectionData ) )
169+ {
170+ return Uri . TryCreate ( connectionData . ConnectionString , UriKind . Absolute , out connectionString ) ;
171+ }
172+
173+ connectionString = null ;
174+ return false ;
175+ }
176+
177+ private static bool GetHostConnectionData ( string applicationPath , out HostConnectionData connectionData )
178+ {
179+ if ( HostConnectionUtil . FindHostConnection ( applicationPath , out connectionData ) )
180+ {
181+ return EnsureHostServerStarted ( applicationPath , ref connectionData ) ;
182+ }
183+
184+ connectionData = null ;
185+ return false ;
186+ }
187+
188+ private static bool EnsureHostServerStarted ( string applicationPath , ref HostConnectionData connectionData )
189+ {
190+ if ( String . IsNullOrEmpty ( connectionData . ConnectionString ) )
191+ {
192+ if ( ! HostConnectionUtil . SignalHostForStartup ( connectionData ) )
193+ {
194+ return false ;
195+ }
196+
197+ if ( ! HostConnectionUtil . FindHostConnection ( applicationPath , out connectionData ) )
198+ {
199+ return false ;
200+ }
201+
202+ if ( String . IsNullOrEmpty ( connectionData . ConnectionString ) )
203+ {
204+ return false ;
205+ }
206+ }
207+
208+ return true ;
209+ }
210+ }
211+ }
0 commit comments