Skip to content

Commit 3f7c96f

Browse files
authored
Add Dart dart:io HttpServer analyzer (#2151)
* Add Dart HttpServer analyzer * Fix Dart HttpServer route scoping
1 parent c10a59a commit 3f7c96f

9 files changed

Lines changed: 641 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
Future<void> main() async {
5+
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080);
6+
7+
await for (final HttpRequest request in server) {
8+
if (request.method == 'GET' && request.uri.path == '/health') {
9+
final name = request.uri.queryParameters['name'];
10+
final traceId = request.headers.value('X-Trace-Id');
11+
final session = request.cookies.firstWhere((cookie) => cookie.name == 'session');
12+
_writeText(request, 'healthy $name $traceId ${session.value}');
13+
} else if (request.uri.path == '/users' && request.method == 'POST') {
14+
final body = await utf8.decoder.bind(request).join();
15+
_writeText(request, body);
16+
} else if (request.method == 'PATCH') {
17+
if (request.uri.path == '/profiles') {
18+
final mode = request.headers['X-Profile-Mode'];
19+
_writeText(request, 'profile $mode');
20+
}
21+
}
22+
23+
if (request.uri.path.startsWith('/files/')) {
24+
_writeText(request, 'file');
25+
}
26+
27+
if (request.uri.path == '/reports') {
28+
if (request.method == 'DELETE') {
29+
_writeText(request, 'deleted');
30+
}
31+
}
32+
33+
if (request.method == 'POST') {
34+
final uploadBody = await utf8.decoder.bind(request).join();
35+
if (request.uri.path == '/uploads') {
36+
_writeText(request, uploadBody);
37+
}
38+
}
39+
40+
switch (request.method) {
41+
case 'PUT':
42+
if (request.uri.path == '/switch-users') {
43+
_writeText(request, 'updated');
44+
}
45+
break;
46+
}
47+
48+
switch (request.uri.path) {
49+
case '/status':
50+
_writeText(request, 'ok');
51+
break;
52+
}
53+
}
54+
}
55+
56+
void _writeText(HttpRequest request, String body) {
57+
request.response
58+
..headers.contentType = ContentType.text
59+
..write(body);
60+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import 'dart:io';
2+
3+
void main() {
4+
final fake = '/test-only';
5+
print('${HttpServer.bind} $fake');
6+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require "../../func_spec.cr"
2+
3+
expected_endpoints = [
4+
Endpoint.new("/health", "GET", [
5+
Param.new("name", "", "query"),
6+
Param.new("X-Trace-Id", "", "header"),
7+
Param.new("session", "", "cookie"),
8+
]),
9+
Endpoint.new("/users", "POST", [
10+
Param.new("body", "", "json"),
11+
]),
12+
Endpoint.new("/profiles", "PATCH", [
13+
Param.new("X-Profile-Mode", "", "header"),
14+
]),
15+
Endpoint.new("/files", "GET"),
16+
Endpoint.new("/reports", "DELETE"),
17+
Endpoint.new("/uploads", "POST", [
18+
Param.new("body", "", "json"),
19+
]),
20+
Endpoint.new("/switch-users", "PUT"),
21+
Endpoint.new("/status", "GET"),
22+
]
23+
24+
tester = FunctionalTester.new("fixtures/dart/http/", {
25+
:techs => 1,
26+
:endpoints => expected_endpoints.size,
27+
}, expected_endpoints)
28+
tester.perform_tests
29+
30+
it "does not leak pre-route body reads into previous endpoints" do
31+
reports = tester.app.endpoints.find { |found| found.url == "/reports" && found.method == "DELETE" }
32+
reports.should_not be_nil
33+
reports.try do |endpoint|
34+
endpoint.params.any? { |param| param.name == "body" && param.param_type == "json" }.should be_false
35+
end
36+
37+
uploads = tester.app.endpoints.find { |found| found.url == "/uploads" && found.method == "POST" }
38+
uploads.should_not be_nil
39+
uploads.try do |endpoint|
40+
endpoint.params.any? { |param| param.name == "body" && param.param_type == "json" }.should be_true
41+
end
42+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
require "../../../spec_helper"
2+
require "../../../../src/detector/detectors/dart/*"
3+
4+
describe "Detect Dart HttpServer" do
5+
options = create_test_options
6+
instance = Detector::Dart::Http.new options
7+
8+
it "detects dart io HttpServer usage" do
9+
content = <<-DART
10+
import 'dart:io';
11+
12+
Future<void> main() async {
13+
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 8080);
14+
await for (final HttpRequest request in server) {
15+
request.response.write('ok');
16+
}
17+
}
18+
DART
19+
instance.detect("bin/server.dart", content).should be_true
20+
end
21+
22+
it "detects aliased dart io HttpServer usage" do
23+
content = <<-DART
24+
import 'dart:io' as io;
25+
26+
Future<void> main() async {
27+
final server = await io.HttpServer.bind(io.InternetAddress.loopbackIPv4, 8080);
28+
server.listen((io.HttpRequest request) {});
29+
}
30+
DART
31+
instance.detect("bin/server.dart", content).should be_true
32+
end
33+
34+
it "does not detect unrelated dart io usage" do
35+
content = <<-DART
36+
import 'dart:io';
37+
38+
void main() {
39+
final file = File('README.md');
40+
print(file.path);
41+
}
42+
DART
43+
instance.detect("bin/tool.dart", content).should be_false
44+
end
45+
46+
it "does not detect HttpServer without dart io import" do
47+
instance.detect("bin/server.dart", "void main() { print('HttpServer'); }").should be_false
48+
end
49+
50+
it "does not detect non Dart files" do
51+
instance.detect("notes.txt", "import 'dart:io'; HttpServer.bind();").should be_false
52+
end
53+
end

src/analyzer/analyzer.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def initialize_analyzers(logger : NoirLogger)
3939
{"dart_angel3", Dart::Angel3},
4040
{"dart_get_server", Dart::GetServer},
4141
{"dart_frog", Dart::DartFrog},
42+
{"dart_http", Dart::Http},
4243
{"dart_serverpod", Dart::Serverpod},
4344
{"dart_shelf", Dart::Shelf},
4445
{"elixir_bandit", Elixir::Bandit},

0 commit comments

Comments
 (0)