Skip to content

Commit 5d5b3a7

Browse files
Adds the RewritePlugin. Closes #926 (#933)
1 parent eec04e6 commit 5d5b3a7

File tree

5 files changed

+229
-2
lines changed

5 files changed

+229
-2
lines changed

dev-proxy-abstractions/IProxyLogger.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public enum MessageType
1717
Mocked,
1818
InterceptedResponse,
1919
FinishedProcessingRequest,
20-
Skipped
20+
Skipped,
21+
Processed
2122
}
2223

2324
public class LoggingContext(SessionEventArgs session)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Extensions.Configuration;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.DevProxy.Abstractions;
7+
using System.Text.RegularExpressions;
8+
9+
namespace Microsoft.DevProxy.Plugins.Processing;
10+
11+
public class RewriteRule
12+
{
13+
public string? Url { get; set; }
14+
}
15+
16+
public class RequestRewrite
17+
{
18+
public RewriteRule? In { get; set; }
19+
public RewriteRule? Out { get; set; }
20+
}
21+
22+
public class RewritePluginConfiguration
23+
{
24+
public IEnumerable<RequestRewrite> Rewrites { get; set; } = [];
25+
public string RewritesFile { get; set; } = "rewrites.json";
26+
}
27+
28+
public class RewritePlugin(IPluginEvents pluginEvents, IProxyContext context, ILogger logger, ISet<UrlToWatch> urlsToWatch, IConfigurationSection? configSection = null) : BaseProxyPlugin(pluginEvents, context, logger, urlsToWatch, configSection)
29+
{
30+
public override string Name => nameof(RewritePlugin);
31+
private readonly RewritePluginConfiguration _configuration = new();
32+
private RewritesLoader? _loader = null;
33+
34+
public override async Task RegisterAsync()
35+
{
36+
await base.RegisterAsync();
37+
38+
ConfigSection?.Bind(_configuration);
39+
_loader = new RewritesLoader(Logger, _configuration);
40+
41+
PluginEvents.BeforeRequest += BeforeRequestAsync;
42+
43+
// make the rewrites file path relative to the configuration file
44+
_configuration.RewritesFile = Path.GetFullPath(
45+
ProxyUtils.ReplacePathTokens(_configuration.RewritesFile),
46+
Path.GetDirectoryName(Context.Configuration.ConfigFile ?? string.Empty) ?? string.Empty
47+
);
48+
49+
_loader?.InitResponsesWatcher();
50+
}
51+
52+
private Task BeforeRequestAsync(object sender, ProxyRequestArgs e)
53+
{
54+
if (UrlsToWatch is null ||
55+
!e.HasRequestUrlMatch(UrlsToWatch))
56+
{
57+
Logger.LogRequest("URL not matched", MessageType.Skipped, new LoggingContext(e.Session));
58+
return Task.CompletedTask;
59+
}
60+
61+
if (_configuration.Rewrites is null ||
62+
!_configuration.Rewrites.Any())
63+
{
64+
Logger.LogRequest("No rewrites configured", MessageType.Skipped, new LoggingContext(e.Session));
65+
return Task.CompletedTask;
66+
}
67+
68+
var request = e.Session.HttpClient.Request;
69+
70+
foreach (var rewrite in _configuration.Rewrites)
71+
{
72+
if (string.IsNullOrEmpty(rewrite.In?.Url) ||
73+
string.IsNullOrEmpty(rewrite.Out?.Url))
74+
{
75+
continue;
76+
}
77+
78+
var newUrl = Regex.Replace(request.Url, rewrite.In.Url, rewrite.Out.Url, RegexOptions.IgnoreCase);
79+
80+
if (request.Url.Equals(newUrl, StringComparison.OrdinalIgnoreCase))
81+
{
82+
Logger.LogRequest($"{rewrite.In?.Url}", MessageType.Skipped, new LoggingContext(e.Session));
83+
}
84+
else
85+
{
86+
Logger.LogRequest($"{rewrite.In?.Url} > {newUrl}", MessageType.Processed, new LoggingContext(e.Session));
87+
request.Url = newUrl;
88+
}
89+
}
90+
91+
return Task.CompletedTask;
92+
}
93+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.DevProxy.Abstractions;
5+
using Microsoft.Extensions.Logging;
6+
using System.Text.Json;
7+
8+
namespace Microsoft.DevProxy.Plugins.Processing;
9+
10+
internal class RewritesLoader(ILogger logger, RewritePluginConfiguration configuration) : IDisposable
11+
{
12+
private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
13+
private readonly RewritePluginConfiguration _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
14+
15+
private string RewritesFilePath => _configuration.RewritesFile;
16+
private FileSystemWatcher? _watcher;
17+
18+
public void LoadRewrites()
19+
{
20+
if (!File.Exists(RewritesFilePath))
21+
{
22+
_logger.LogWarning("File {configurationFile} not found. No rewrites will be provided", _configuration.RewritesFile);
23+
_configuration.Rewrites = [];
24+
return;
25+
}
26+
27+
try
28+
{
29+
using var stream = new FileStream(RewritesFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
30+
using var reader = new StreamReader(stream);
31+
var RewritesString = reader.ReadToEnd();
32+
var rewritesConfig = JsonSerializer.Deserialize<RewritePluginConfiguration>(RewritesString, ProxyUtils.JsonSerializerOptions);
33+
IEnumerable<RequestRewrite>? configRewrites = rewritesConfig?.Rewrites;
34+
if (configRewrites is not null)
35+
{
36+
_configuration.Rewrites = configRewrites;
37+
_logger.LogInformation("Rewrites for {configResponseCount} url patterns loaded from {RewritesFile}", configRewrites.Count(), _configuration.RewritesFile);
38+
}
39+
}
40+
catch (Exception ex)
41+
{
42+
_logger.LogError(ex, "An error has occurred while reading {RewritesFile}:", _configuration.RewritesFile);
43+
}
44+
}
45+
46+
public void InitResponsesWatcher()
47+
{
48+
if (_watcher is not null)
49+
{
50+
return;
51+
}
52+
53+
string path = Path.GetDirectoryName(RewritesFilePath) ?? throw new InvalidOperationException($"{RewritesFilePath} is an invalid path");
54+
if (!File.Exists(RewritesFilePath))
55+
{
56+
_logger.LogWarning("File {RewritesFile} not found. No rewrites will be provided", _configuration.RewritesFile);
57+
_configuration.Rewrites = [];
58+
return;
59+
}
60+
61+
_watcher = new FileSystemWatcher(Path.GetFullPath(path))
62+
{
63+
NotifyFilter = NotifyFilters.CreationTime
64+
| NotifyFilters.FileName
65+
| NotifyFilters.LastWrite
66+
| NotifyFilters.Size,
67+
Filter = Path.GetFileName(RewritesFilePath)
68+
};
69+
_watcher.Changed += ResponsesFile_Changed;
70+
_watcher.Created += ResponsesFile_Changed;
71+
_watcher.Deleted += ResponsesFile_Changed;
72+
_watcher.Renamed += ResponsesFile_Changed;
73+
_watcher.EnableRaisingEvents = true;
74+
75+
LoadRewrites();
76+
}
77+
78+
private void ResponsesFile_Changed(object sender, FileSystemEventArgs e)
79+
{
80+
LoadRewrites();
81+
}
82+
83+
public void Dispose()
84+
{
85+
_watcher?.Dispose();
86+
}
87+
}

dev-proxy/Logging/ProxyConsoleFormatter.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,14 @@ private static string GetMessageTypeString(MessageType messageType)
230230
{
231231
MessageType.InterceptedRequest => "req",
232232
MessageType.InterceptedResponse => "res",
233-
MessageType.PassedThrough => "api",
233+
MessageType.PassedThrough => "pass",
234234
MessageType.Chaos => "oops",
235235
MessageType.Warning => "warn",
236236
MessageType.Mocked => "mock",
237237
MessageType.Failed => "fail",
238238
MessageType.Tip => "tip",
239239
MessageType.Skipped => "skip",
240+
MessageType.Processed => "proc",
240241
_ => " "
241242
};
242243
}
@@ -251,6 +252,7 @@ private static (ConsoleColor bg, ConsoleColor fg) GetMessageTypeColor(MessageTyp
251252
MessageType.InterceptedRequest => (bgColor, ConsoleColor.Gray),
252253
MessageType.PassedThrough => (ConsoleColor.Gray, fgColor),
253254
MessageType.Skipped => (bgColor, ConsoleColor.Gray),
255+
MessageType.Processed => (ConsoleColor.DarkGreen, fgColor),
254256
MessageType.Chaos => (ConsoleColor.DarkRed, fgColor),
255257
MessageType.Warning => (ConsoleColor.DarkYellow, fgColor),
256258
MessageType.Mocked => (ConsoleColor.DarkMagenta, fgColor),
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"title": "Dev Proxy RewritePlugin rewrite rules",
4+
"description": "Rewrite rules for the Dev Proxy RewritePlugin",
5+
"type": "object",
6+
"properties": {
7+
"$schema": {
8+
"type": "string"
9+
},
10+
"rewrites": {
11+
"type": "array",
12+
"items": {
13+
"type": "object",
14+
"properties": {
15+
"in": {
16+
"type": "object",
17+
"properties": {
18+
"url": {
19+
"type": "string",
20+
"pattern": "^.+$"
21+
}
22+
},
23+
"required": ["url"]
24+
},
25+
"out": {
26+
"type": "object",
27+
"properties": {
28+
"url": {
29+
"type": "string",
30+
"pattern": "^.*$"
31+
}
32+
},
33+
"required": ["url"]
34+
}
35+
},
36+
"required": ["in", "out"]
37+
}
38+
}
39+
},
40+
"required": [
41+
"rewrites"
42+
],
43+
"additionalProperties": false
44+
}

0 commit comments

Comments
 (0)