1+ // Licensed to the .NET Foundation under one or more agreements.
2+ // The .NET Foundation licenses this file to you under the MIT license.
3+ // See the LICENSE file in the project root for more information.
4+
5+ using DevProxy . Abstractions . Plugins ;
6+ using DevProxy . Abstractions . Proxy ;
7+ using DevProxy . Abstractions . Utils ;
8+ using DevProxy . Plugins . Models ;
9+ using Microsoft . Extensions . Configuration ;
10+ using Microsoft . Extensions . Logging ;
11+ using System . Diagnostics ;
12+ using System . Text . Json ;
13+ using System . Web ;
14+
15+ namespace DevProxy . Plugins . Generation ;
16+
17+ public sealed class HarGeneratorPluginConfiguration
18+ {
19+ public bool IncludeSensitiveInformation { get ; set ; }
20+ public bool IncludeResponse { get ; set ; }
21+ }
22+
23+ public sealed class HarGeneratorPlugin (
24+ HttpClient httpClient ,
25+ ILogger < HarGeneratorPlugin > logger ,
26+ ISet < UrlToWatch > urlsToWatch ,
27+ IProxyConfiguration proxyConfiguration ,
28+ IConfigurationSection pluginConfigurationSection ) :
29+ BaseReportingPlugin < HarGeneratorPluginConfiguration > (
30+ httpClient ,
31+ logger ,
32+ urlsToWatch ,
33+ proxyConfiguration ,
34+ pluginConfigurationSection )
35+ {
36+ public override string Name => nameof ( HarGeneratorPlugin ) ;
37+
38+ public override async Task AfterRecordingStopAsync ( RecordingArgs e , CancellationToken cancellationToken )
39+ {
40+ Logger . LogTrace ( "{Method} called" , nameof ( AfterRecordingStopAsync ) ) ;
41+
42+ ArgumentNullException . ThrowIfNull ( e ) ;
43+
44+ if ( ! e . RequestLogs . Any ( ) )
45+ {
46+ Logger . LogDebug ( "No requests to process" ) ;
47+ return ;
48+ }
49+
50+ Logger . LogInformation ( "Creating HAR file from recorded requests..." ) ;
51+
52+ var harFile = new HarFile
53+ {
54+ Log = new HarLog
55+ {
56+ Creator = new HarCreator
57+ {
58+ Name = "DevProxy" ,
59+ Version = ProxyUtils . ProductVersion
60+ } ,
61+ Entries = [ .. e . RequestLogs . Where ( r =>
62+ r . MessageType == MessageType . InterceptedResponse &&
63+ r is not null &&
64+ r . Context is not null &&
65+ r . Context . Session is not null &&
66+ ProxyUtils . MatchesUrlToWatch ( UrlsToWatch , r . Context . Session . HttpClient . Request . RequestUri . AbsoluteUri ) ) . Select ( CreateHarEntry ) ]
67+ }
68+ } ;
69+
70+ Logger . LogDebug ( "Serializing HAR file..." ) ;
71+ var harFileJson = JsonSerializer . Serialize ( harFile , ProxyUtils . JsonSerializerOptions ) ;
72+ var fileName = $ "devproxy-{ DateTime . Now : yyyyMMddHHmmss} .har";
73+
74+ Logger . LogDebug ( "Writing HAR file to {FileName}..." , fileName ) ;
75+ await File . WriteAllTextAsync ( fileName , harFileJson , cancellationToken ) ;
76+
77+ Logger . LogInformation ( "Created HAR file {FileName}" , fileName ) ;
78+
79+ StoreReport ( fileName , e ) ;
80+
81+ Logger . LogTrace ( "Left {Name}" , nameof ( AfterRecordingStopAsync ) ) ;
82+ }
83+
84+ private string GetHeaderValue ( string headerName , string originalValue )
85+ {
86+ if ( ! Configuration . IncludeSensitiveInformation &&
87+ Http . SensitiveHeaders . Contains ( headerName , StringComparer . OrdinalIgnoreCase ) )
88+ {
89+ return "REDACTED" ;
90+ }
91+ return originalValue ;
92+ }
93+
94+ private HarEntry CreateHarEntry ( RequestLog log )
95+ {
96+ Debug . Assert ( log is not null ) ;
97+ Debug . Assert ( log . Context is not null ) ;
98+
99+ var request = log . Context . Session . HttpClient . Request ;
100+ var response = log . Context . Session . HttpClient . Response ;
101+ var currentTime = DateTime . UtcNow ;
102+
103+ var entry = new HarEntry
104+ {
105+ StartedDateTime = currentTime . ToString ( "o" ) ,
106+ Time = 0 , // We don't have actual timing data in RequestLog
107+ Request = new HarRequest
108+ {
109+ Method = request . Method ,
110+ Url = request . RequestUri ? . ToString ( ) ,
111+ HttpVersion = $ "HTTP/{ request . HttpVersion } ",
112+ Headers = [ .. request . Headers . Select ( h => new HarHeader { Name = h . Name , Value = GetHeaderValue ( h . Name , string . Join ( ", " , h . Value ) ) } ) ] ,
113+ QueryString = [ .. HttpUtility . ParseQueryString ( request . RequestUri ? . Query ?? "" )
114+ . AllKeys
115+ . Where ( key => key is not null )
116+ . Select ( key => new HarQueryParam { Name = key ! , Value = HttpUtility . ParseQueryString ( request . RequestUri ? . Query ?? "" ) [ key ] ?? "" } ) ] ,
117+ Cookies = [ .. request . Headers
118+ . Where ( h => string . Equals ( h . Name , "Cookie" , StringComparison . OrdinalIgnoreCase ) )
119+ . Select ( h => h . Value )
120+ . SelectMany ( v => v . Split ( ';' ) )
121+ . Select ( c =>
122+ {
123+ var parts = c . Split ( '=' , 2 ) ;
124+ return new HarCookie { Name = parts [ 0 ] . Trim ( ) , Value = parts . Length > 1 ? parts [ 1 ] . Trim ( ) : "" } ;
125+ } ) ] ,
126+ HeadersSize = request . Headers ? . ToString ( ) ? . Length ?? 0 ,
127+ BodySize = request . HasBody ? ( request . BodyString ? . Length ?? 0 ) : 0 ,
128+ PostData = request . HasBody ? new HarPostData
129+ {
130+ MimeType = request . ContentType ,
131+ Text = request . BodyString ?? ""
132+ }
133+ : null
134+ } ,
135+ Response = response is not null ? new HarResponse
136+ {
137+ Status = response . StatusCode ,
138+ StatusText = response . StatusDescription ,
139+ HttpVersion = $ "HTTP/{ response . HttpVersion } ",
140+ Headers = [ .. response . Headers . Select ( h => new HarHeader { Name = h . Name , Value = GetHeaderValue ( h . Name , string . Join ( ", " , h . Value ) ) } ) ] ,
141+ Cookies = [ .. response . Headers
142+ . Where ( h => string . Equals ( h . Name , "Set-Cookie" , StringComparison . OrdinalIgnoreCase ) )
143+ . Select ( h => h . Value )
144+ . Select ( sc =>
145+ {
146+ var parts = sc . Split ( ';' ) [ 0 ] . Split ( '=' , 2 ) ;
147+ return new HarCookie { Name = parts [ 0 ] . Trim ( ) , Value = parts . Length > 1 ? parts [ 1 ] . Trim ( ) : "" } ;
148+ } ) ] ,
149+ Content = new HarContent
150+ {
151+ Size = response . HasBody ? ( response . BodyString ? . Length ?? 0 ) : 0 ,
152+ MimeType = response . ContentType ?? "" ,
153+ Text = Configuration . IncludeResponse && response . HasBody ? response . BodyString : null
154+ } ,
155+ HeadersSize = response . Headers ? . ToString ( ) ? . Length ?? 0 ,
156+ BodySize = response . HasBody ? ( response . BodyString ? . Length ?? 0 ) : 0
157+ } : null
158+ } ;
159+
160+ return entry ;
161+ }
162+ }
0 commit comments