1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT License.
3+
4+
5+ // Copyright (c) Microsoft Corporation.
6+ // Licensed under the MIT License.
7+
8+ using System . Text ;
9+ using System . Web ;
10+ using Microsoft . DevProxy . Abstractions ;
11+ using Microsoft . Extensions . Configuration ;
12+ using Microsoft . Extensions . Logging ;
13+
14+ namespace Microsoft . DevProxy . Plugins . RequestLogs ;
15+
16+ internal class HttpFile
17+ {
18+ public Dictionary < string , string > Variables { get ; set ; } = new ( ) ;
19+ public List < HttpFileRequest > Requests { get ; set ; } = new ( ) ;
20+
21+ public string Serialize ( )
22+ {
23+ var sb = new StringBuilder ( ) ;
24+
25+ foreach ( var variable in Variables )
26+ {
27+ sb . AppendLine ( $ "@{ variable . Key } = { variable . Value } ") ;
28+ }
29+
30+ foreach ( var request in Requests )
31+ {
32+ sb . AppendLine ( ) ;
33+ sb . AppendLine ( "###" ) ;
34+ sb . AppendLine ( ) ;
35+ sb . AppendLine ( $ "# @name { GetRequestName ( request ) } ") ;
36+ sb . AppendLine ( ) ;
37+
38+ sb . AppendLine ( $ "{ request . Method } { request . Url } ") ;
39+
40+ foreach ( var header in request . Headers )
41+ {
42+ sb . AppendLine ( $ "{ header . Name } : { header . Value } ") ;
43+ }
44+
45+ if ( ! string . IsNullOrEmpty ( request . Body ) )
46+ {
47+ sb . AppendLine ( ) ;
48+ sb . AppendLine ( request . Body ) ;
49+ }
50+ }
51+
52+ return sb . ToString ( ) ;
53+ }
54+
55+ private string GetRequestName ( HttpFileRequest request )
56+ {
57+ var url = new Uri ( request . Url ) ;
58+ return $ "{ request . Method . ToLower ( ) } { url . Segments . Last ( ) . Replace ( "/" , "" ) . ToPascalCase ( ) } ";
59+ }
60+ }
61+
62+ internal class HttpFileRequest
63+ {
64+ public string Method { get ; set ; } = string . Empty ;
65+ public string Url { get ; set ; } = string . Empty ;
66+ public string ? Body { get ; set ; }
67+ public List < HttpFileRequestHeader > Headers { get ; set ; } = new ( ) ;
68+ }
69+
70+ internal class HttpFileRequestHeader
71+ {
72+ public string Name { get ; set ; } = string . Empty ;
73+ public string Value { get ; set ; } = string . Empty ;
74+ }
75+
76+ public class HttpFileGeneratorPluginReport : List < string >
77+ {
78+ public HttpFileGeneratorPluginReport ( ) : base ( ) { }
79+
80+ public HttpFileGeneratorPluginReport ( IEnumerable < string > collection ) : base ( collection ) { }
81+ }
82+
83+ internal class HttpFileGeneratorPluginConfiguration
84+ {
85+ public bool IncludeOptionsRequests { get ; set ; } = false ;
86+ }
87+
88+ public class HttpFileGeneratorPlugin : BaseReportingPlugin
89+ {
90+ public override string Name => nameof ( HttpFileGeneratorPlugin ) ;
91+ public static readonly string GeneratedHttpFilesKey = "GeneratedHttpFiles" ;
92+ private HttpFileGeneratorPluginConfiguration _configuration = new ( ) ;
93+ private readonly string [ ] headersToExtract = [ "authorization" , "key" ] ;
94+ private readonly string [ ] queryParametersToExtract = [ "key" ] ;
95+
96+ public HttpFileGeneratorPlugin ( IPluginEvents pluginEvents , IProxyContext context , ILogger logger , ISet < UrlToWatch > urlsToWatch , IConfigurationSection ? configSection = null ) : base ( pluginEvents , context , logger , urlsToWatch , configSection )
97+ {
98+ }
99+
100+ public override void Register ( )
101+ {
102+ base . Register ( ) ;
103+
104+ ConfigSection ? . Bind ( _configuration ) ;
105+
106+ PluginEvents . AfterRecordingStop += AfterRecordingStop ;
107+ }
108+
109+ private async Task AfterRecordingStop ( object ? sender , RecordingArgs e )
110+ {
111+ Logger . LogInformation ( "Creating HTTP file from recorded requests..." ) ;
112+
113+ if ( ! e . RequestLogs . Any ( ) )
114+ {
115+ Logger . LogDebug ( "No requests to process" ) ;
116+ return ;
117+ }
118+
119+ var httpFile = await GetHttpRequests ( e . RequestLogs ) ;
120+ DeduplicateRequests ( httpFile ) ;
121+ ExtractVariables ( httpFile ) ;
122+
123+ var fileName = $ "requests_{ DateTime . Now : yyyyMMddHHmmss} .http";
124+ Logger . LogDebug ( "Writing HTTP file to {fileName}..." , fileName ) ;
125+ File . WriteAllText ( fileName , httpFile . Serialize ( ) ) ;
126+ Logger . LogInformation ( "Created HTTP file {fileName}" , fileName ) ;
127+
128+ var generatedHttpFiles = new [ ] { fileName } ;
129+ StoreReport ( new HttpFileGeneratorPluginReport ( generatedHttpFiles ) , e ) ;
130+
131+ // store the generated HTTP files in the global data
132+ // for use by other plugins
133+ e . GlobalData [ GeneratedHttpFilesKey ] = generatedHttpFiles ;
134+ }
135+
136+ private async Task < HttpFile > GetHttpRequests ( IEnumerable < RequestLog > requestLogs )
137+ {
138+ var httpFile = new HttpFile ( ) ;
139+
140+ foreach ( var request in requestLogs )
141+ {
142+ if ( request . MessageType != MessageType . InterceptedResponse ||
143+ request . Context is null ||
144+ request . Context . Session is null )
145+ {
146+ continue ;
147+ }
148+
149+ if ( ! _configuration . IncludeOptionsRequests &&
150+ request . Context . Session . HttpClient . Request . Method . ToUpperInvariant ( ) == "OPTIONS" )
151+ {
152+ Logger . LogDebug ( "Skipping OPTIONS request {url}..." , request . Context . Session . HttpClient . Request . RequestUri ) ;
153+ continue ;
154+ }
155+
156+ var methodAndUrlString = request . MessageLines . First ( ) ;
157+ Logger . LogDebug ( "Adding request {methodAndUrl}..." , methodAndUrlString ) ;
158+
159+ var methodAndUrl = methodAndUrlString . Split ( ' ' ) ;
160+ httpFile . Requests . Add ( new HttpFileRequest
161+ {
162+ Method = methodAndUrl [ 0 ] ,
163+ Url = methodAndUrl [ 1 ] ,
164+ Body = request . Context . Session . HttpClient . Request . HasBody ? await request . Context . Session . GetRequestBodyAsString ( ) : null ,
165+ Headers = request . Context . Session . HttpClient . Request . Headers
166+ . Select ( h => new HttpFileRequestHeader { Name = h . Name , Value = h . Value } )
167+ . ToList ( )
168+ } ) ;
169+ }
170+
171+ return httpFile ;
172+ }
173+
174+ private void DeduplicateRequests ( HttpFile httpFile )
175+ {
176+ Logger . LogDebug ( "Deduplicating requests..." ) ;
177+
178+ // remove duplicate requests
179+ // if the request doesn't have a body, dedupe on method + URL
180+ // if it has a body, dedupe on method + URL + body
181+ var uniqueRequests = new List < HttpFileRequest > ( ) ;
182+ foreach ( var request in httpFile . Requests )
183+ {
184+ Logger . LogDebug ( " Checking request {method} {url}..." , request . Method , request . Url ) ;
185+
186+ var existingRequest = uniqueRequests . FirstOrDefault ( r =>
187+ {
188+ if ( r . Method != request . Method || r . Url != request . Url )
189+ {
190+ return false ;
191+ }
192+
193+ if ( r . Body is null && request . Body is null )
194+ {
195+ return true ;
196+ }
197+
198+ if ( r . Body is not null && request . Body is not null )
199+ {
200+ return r . Body == request . Body ;
201+ }
202+
203+ return false ;
204+ } ) ;
205+
206+ if ( existingRequest is null )
207+ {
208+ Logger . LogDebug ( " Keeping request {method} {url}..." , request . Method , request . Url ) ;
209+ uniqueRequests . Add ( request ) ;
210+ }
211+ else
212+ {
213+ Logger . LogDebug ( " Skipping duplicate request {method} {url}..." , request . Method , request . Url ) ;
214+ }
215+ }
216+
217+ httpFile . Requests = uniqueRequests ;
218+ }
219+
220+ private void ExtractVariables ( HttpFile httpFile )
221+ {
222+ Logger . LogDebug ( "Extracting variables..." ) ;
223+
224+ foreach ( var request in httpFile . Requests )
225+ {
226+ Logger . LogDebug ( " Processing request {method} {url}..." , request . Method , request . Url ) ;
227+
228+ foreach ( var headerName in headersToExtract )
229+ {
230+ Logger . LogDebug ( " Extracting header {headerName}..." , headerName ) ;
231+
232+ var headers = request . Headers . Where ( h => h . Name . Contains ( headerName , StringComparison . OrdinalIgnoreCase ) ) ;
233+ if ( headers is not null )
234+ {
235+ Logger . LogDebug ( " Found {numHeaders} matching headers..." , headers . Count ( ) ) ;
236+
237+ foreach ( var header in headers )
238+ {
239+ var variableName = GetVariableName ( request , headerName ) ;
240+ Logger . LogDebug ( " Extracting variable {variableName}..." , variableName ) ;
241+ httpFile . Variables [ variableName ] = header . Value ;
242+ header . Value = $ "{{{{{variableName}}}}}";
243+ }
244+ }
245+ }
246+
247+ var url = new Uri ( request . Url ) ;
248+ var query = HttpUtility . ParseQueryString ( url . Query ) ;
249+ if ( query . Count > 0 )
250+ {
251+ Logger . LogDebug ( " Processing query parameters..." ) ;
252+
253+ foreach ( var queryParameterName in queryParametersToExtract )
254+ {
255+ Logger . LogDebug ( " Extracting query parameter {queryParameterName}..." , queryParameterName ) ;
256+
257+ var queryParams = query . AllKeys . Where ( k => k is not null && k . Contains ( queryParameterName , StringComparison . OrdinalIgnoreCase ) ) ;
258+ if ( queryParams is not null )
259+ {
260+ Logger . LogDebug ( " Found {numQueryParams} matching query parameters..." , queryParams . Count ( ) ) ;
261+
262+ foreach ( var queryParam in queryParams )
263+ {
264+ var variableName = GetVariableName ( request , queryParam ! ) ;
265+ Logger . LogDebug ( " Extracting variable {variableName}..." , variableName ) ;
266+ httpFile . Variables [ variableName ] = queryParam ! ;
267+ query [ queryParam ] = $ "{{{{{variableName}}}}}";
268+ }
269+ }
270+ }
271+ request . Url = $ "{ url . GetLeftPart ( UriPartial . Path ) } ?{ query } "
272+ . Replace ( "%7b" , "{" )
273+ . Replace ( "%7d" , "}" ) ;
274+ Logger . LogDebug ( " Updated URL to {url}..." , request . Url ) ;
275+ }
276+ else
277+ {
278+ Logger . LogDebug ( " No query parameters to process..." ) ;
279+ }
280+ }
281+ }
282+
283+ private string GetVariableName ( HttpFileRequest request , string variableName )
284+ {
285+ var url = new Uri ( request . Url ) ;
286+ return $ "{ url . Host . Replace ( "." , "_" ) . Replace ( "-" , "_" ) } _{ variableName . Replace ( "-" , "_" ) } ";
287+ }
288+ }
0 commit comments