Skip to content

Commit d750f55

Browse files
authored
feat(csharp): add System.Net.HttpListener analyzer (#2154)
* feat(csharp): add HttpListener analyzer * refactor(csharp): lex HttpListener source once for tokens + masked_lines Reuse a single Noir::CSharpLexer instance to provide both comment spans (for comment stripping) and masked_lines (for brace counting), instead of lexing the source twice per file. Addresses Copilot review feedback.
1 parent bb09997 commit d750f55

8 files changed

Lines changed: 749 additions & 1 deletion

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.IO;
3+
using System.Net;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
public static class Program
8+
{
9+
public static async Task Main()
10+
{
11+
var listener = new HttpListener();
12+
listener.Prefixes.Add("http://localhost:8080/");
13+
listener.Start();
14+
15+
while (true)
16+
{
17+
var context = await listener.GetContextAsync();
18+
await Handle(context);
19+
}
20+
}
21+
22+
private static async Task Handle(HttpListenerContext context)
23+
{
24+
var request = context.Request;
25+
var response = context.Response;
26+
var method = request.HttpMethod;
27+
var path = request.Url?.AbsolutePath ?? "/";
28+
29+
if (method == "GET" && path == "/health")
30+
{
31+
var trace = request.Headers["X-Trace-Id"];
32+
Write(response, $"ok {trace}");
33+
}
34+
else if (path == "/search" && method.Equals("GET", StringComparison.OrdinalIgnoreCase))
35+
{
36+
var query = request.QueryString;
37+
var q = query["q"];
38+
var page = query.Get("page");
39+
Write(response, $"{q}:{page}");
40+
}
41+
else if (method == "POST" && path == "/users")
42+
{
43+
var contentType = request.Headers.Get("Content-Type");
44+
using var reader = new StreamReader(request.InputStream, request.ContentEncoding);
45+
var body = await reader.ReadToEndAsync();
46+
Write(response, $"{contentType}:{body}");
47+
}
48+
else if ("DELETE".Equals(method, StringComparison.OrdinalIgnoreCase) && path == "/users/delete")
49+
{
50+
var sid = request.Cookies["sid"]?.Value;
51+
Write(response, sid ?? "");
52+
}
53+
else if (path == "/ready")
54+
{
55+
Write(response, "ready");
56+
}
57+
58+
switch (path)
59+
{
60+
case "/status":
61+
if (method == "HEAD")
62+
{
63+
response.StatusCode = 204;
64+
}
65+
break;
66+
}
67+
68+
switch (method)
69+
{
70+
case "PUT":
71+
if (path == "/users")
72+
{
73+
var requestId = request.Headers["X-Request-Id"];
74+
Write(response, requestId ?? "");
75+
}
76+
break;
77+
}
78+
79+
switch ((method, path))
80+
{
81+
case ("PATCH", "/users/profile"):
82+
var include = request.QueryString["include"];
83+
Write(response, include ?? "");
84+
break;
85+
}
86+
}
87+
88+
private static void Write(HttpListenerResponse response, string value)
89+
{
90+
var bytes = Encoding.UTF8.GetBytes(value);
91+
response.OutputStream.Write(bytes, 0, bytes.Length);
92+
}
93+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
require "../../func_spec.cr"
2+
3+
expected_endpoints = [
4+
Endpoint.new("/health", "GET", [
5+
Param.new("X-Trace-Id", "", "header"),
6+
]),
7+
Endpoint.new("/search", "GET", [
8+
Param.new("q", "", "query"),
9+
Param.new("page", "", "query"),
10+
]),
11+
Endpoint.new("/users", "POST", [
12+
Param.new("Content-Type", "", "header"),
13+
Param.new("body", "", "json"),
14+
]),
15+
Endpoint.new("/users/delete", "DELETE", [
16+
Param.new("sid", "", "cookie"),
17+
]),
18+
Endpoint.new("/ready", "GET"),
19+
Endpoint.new("/status", "HEAD"),
20+
Endpoint.new("/users", "PUT", [
21+
Param.new("X-Request-Id", "", "header"),
22+
]),
23+
Endpoint.new("/users/profile", "PATCH", [
24+
Param.new("include", "", "query"),
25+
]),
26+
]
27+
28+
tester = FunctionalTester.new("fixtures/csharp/httplistener/", {
29+
:techs => 1,
30+
:endpoints => expected_endpoints.size,
31+
}, expected_endpoints)
32+
33+
tester.perform_tests
34+
35+
describe "C# HttpListener analyzer edge cases" do
36+
it "marks endpoints with the dedicated technology" do
37+
health = tester.app.endpoints.find { |e| e.url == "/health" && e.method == "GET" }
38+
health.should_not be_nil
39+
health.as(Endpoint).details.technology.should eq "cs_httplistener"
40+
end
41+
42+
it "does not emit a default GET for a path switch case that contains method dispatch" do
43+
tester.app.endpoints.any? { |e| e.url == "/status" && e.method == "GET" }.should be_false
44+
end
45+
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
require "../../../../src/detector/detectors/csharp/*"
2+
3+
describe "Detect C# System.Net.HttpListener" do
4+
config_init = ConfigInitializer.new
5+
options = config_init.default_options
6+
instance = Detector::CSharp::HttpListener.new options
7+
8+
it "detects bare HttpListener server setup" do
9+
instance.detect("Program.cs", <<-CS).should be_true
10+
using System.Net;
11+
12+
var listener = new HttpListener();
13+
listener.Prefixes.Add("http://localhost:8080/");
14+
listener.Start();
15+
var context = await listener.GetContextAsync();
16+
CS
17+
end
18+
19+
it "detects fully qualified HttpListener construction" do
20+
instance.detect("Program.cs", <<-CS).should be_true
21+
var listener = new System.Net.HttpListener();
22+
listener.Prefixes.Add("http://localhost:8080/");
23+
CS
24+
end
25+
26+
it "ignores handler-only references without server setup" do
27+
instance.detect("Handler.cs", "void Handle(HttpListenerContext context) { }").should be_false
28+
end
29+
30+
it "ignores unrelated HttpListener type names" do
31+
instance.detect("Errors.cs", "catch (HttpListenerException ex) { Console.WriteLine(ex); }").should be_false
32+
end
33+
34+
it "ignores non-C# files" do
35+
instance.detect("notes.txt", "var listener = new HttpListener(); listener.Prefixes.Add(\"http://localhost:8080/\");").should be_false
36+
end
37+
end

src/analyzer/analyzer.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def initialize_analyzers(logger : NoirLogger)
2929
{"cs_aspnet_core_minimal_api", CSharp::MinimalApis},
3030
{"cs_carter", CSharp::Carter},
3131
{"cs_fastendpoints", CSharp::FastEndpoints},
32+
{"cs_httplistener", CSharp::HttpListener},
3233
{"crystal_amber", Crystal::Amber},
3334
{"crystal_grip", Crystal::Grip},
3435
{"crystal_kemal", Crystal::Kemal},

0 commit comments

Comments
 (0)