|
@@ -0,0 +1,427 @@
|
|
|
|
|
+using Invercargill;
|
|
|
|
|
+using Invercargill.DataStructures;
|
|
|
|
|
+using Inversion;
|
|
|
|
|
+using Astralis;
|
|
|
|
|
+
|
|
|
|
|
+// Regression tests for the security-audit fixes SA-2, SA-2b, SA-3 and SA-13
|
|
|
|
|
+// (see Spry/docs/security-audit.md). Mirrors Statum's harness: PASS/FAIL
|
|
|
|
|
+// lines and a non-zero exit on failure. The SA-3 checks run a real Server
|
|
|
|
|
+// (MHD daemon + SocketMonitor) on a background thread and speak plain HTTP
|
|
|
|
|
+// over a socket.
|
|
|
|
|
+
|
|
|
|
|
+namespace Astralis.Tests {
|
|
|
|
|
+
|
|
|
|
|
+ int failures = 0;
|
|
|
|
|
+ int passes = 0;
|
|
|
|
|
+
|
|
|
|
|
+ void check(bool condition, string label) {
|
|
|
|
|
+ if (condition) {
|
|
|
|
|
+ passes++;
|
|
|
|
|
+ print("PASS %s\n", label);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL %s\n", label);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ string root_dir;
|
|
|
|
|
+ string sibling_dir;
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // SA-2b: the "**" remainder must come from the REQUEST's segments
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ void test_wildcard_remainder_from_request() throws Error {
|
|
|
|
|
+ var route = new EndpointRoute("/assets/**");
|
|
|
|
|
+ var context = new RouteContext(route, request_segments("/assets/css/app.css"));
|
|
|
|
|
+
|
|
|
|
|
+ check(param(context, "**") == "css/app.css", "SA-2b: ** remainder is built from request segments (css/app.css)");
|
|
|
|
|
+
|
|
|
|
|
+ var context2 = new RouteContext(route, request_segments("/assets/a/b/c/d.js"));
|
|
|
|
|
+ check(param(context2, "**") == "a/b/c/d.js", "SA-2b: deep ** remainder (a/b/c/d.js)");
|
|
|
|
|
+
|
|
|
|
|
+ var context3 = new RouteContext(route, request_segments("/assets"));
|
|
|
|
|
+ check(param(context3, "**") == "", "SA-2b: prefix-only request yields empty remainder");
|
|
|
|
|
+
|
|
|
|
|
+ var user_route = new EndpointRoute("/user/{id}/files/**");
|
|
|
|
|
+ var context4 = new RouteContext(user_route, request_segments("/user/42/files/report/q3.csv"));
|
|
|
|
|
+ check(param(context4, "id") == "42", "SA-2b: named parameter in ** prefix resolves from request");
|
|
|
|
|
+ check(param(context4, "**") == "report/q3.csv", "SA-2b: ** after named prefix captures the tail");
|
|
|
|
|
+
|
|
|
|
|
+ var plain = new RouteContext(new EndpointRoute("/user/{id}"), request_segments("/user/7"));
|
|
|
|
|
+ check(param(plain, "id") == "7", "SA-2b: plain named route resolves from request");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ string? param(RouteContext context, string key) {
|
|
|
|
|
+ string? value = null;
|
|
|
|
|
+ context.mapped_parameters.try_get(key, out value);
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Request segments as HttpRequest would produce them: split, URL-decode
|
|
|
|
|
+ // per segment, drop empties.
|
|
|
|
|
+ Enumerable<string> request_segments(string path) {
|
|
|
|
|
+ return Wrap.array<string>(path.split("?")[0].split("/"))
|
|
|
|
|
+ .select<string>(p => Uri.unescape_string(p) ?? "")
|
|
|
|
|
+ .where(p => p.length != 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ HttpRequest get_request(string path) {
|
|
|
|
|
+ var headers = new Catalogue<string, string>();
|
|
|
|
|
+ return new HttpRequest(path, "GET", "HTTP/1.1", headers, new TestInput(),
|
|
|
|
|
+ new Catalogue<string, string>(), new Catalogue<string, string>(), "127.0.0.1");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// Small public wrapper so tests (outside the library's compilation
|
|
|
|
|
+ /// unit) can build an HttpContext around a crafted HttpRequest.
|
|
|
|
|
+ HttpContext context_for(string path) {
|
|
|
|
|
+ return new HttpContext.from_request(get_request(path));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // SA-2: traversal rejection, symlink escape, containment boundary
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ async HttpResult serve(string request_path) throws Error {
|
|
|
|
|
+ var resource = new FilesystemResource("/assets/**", root_dir);
|
|
|
|
|
+ var route_context = new RouteContext(new EndpointRoute("/assets/**"), request_segments(request_path));
|
|
|
|
|
+ return yield resource.handle_request(context_for(request_path), route_context);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async void run_traversal_tests() throws Error {
|
|
|
|
|
+ var r1 = yield serve("/assets/../../../etc/passwd");
|
|
|
|
|
+ check(r1.status == StatusCode.FORBIDDEN, "SA-2: /assets/../../../etc/passwd is 403");
|
|
|
|
|
+
|
|
|
|
|
+ var r2 = yield serve("/assets/%2e%2e%2f%2e%2e%2fetc%2fpasswd");
|
|
|
|
|
+ check(r2.status == StatusCode.FORBIDDEN, "SA-2: encoded %2e%2e%2f traversal is 403");
|
|
|
|
|
+
|
|
|
|
|
+ var r3 = yield serve("/assets/link/passwd");
|
|
|
|
|
+ check(r3.status == StatusCode.FORBIDDEN, "SA-2: symlink escape (root/link -> /etc) is 403");
|
|
|
|
|
+
|
|
|
|
|
+ var r4 = yield serve("/assets/../www2/secret.txt");
|
|
|
|
|
+ check(r4.status == StatusCode.FORBIDDEN, "SA-2: sibling directory via .. is 403");
|
|
|
|
|
+
|
|
|
|
|
+ var r6 = yield serve("/assets/css/app.css");
|
|
|
|
|
+ check(r6.status == StatusCode.OK, "SA-2: legitimate deep file serves 200");
|
|
|
|
|
+
|
|
|
|
|
+ var r7 = yield serve("/assets/a..b.txt");
|
|
|
|
|
+ check(r7.status == StatusCode.OK, "SA-2: filename with embedded dots (a..b.txt) serves 200");
|
|
|
|
|
+
|
|
|
|
|
+ var r8 = yield serve("/assets/nope.txt");
|
|
|
|
|
+ check(r8.status == StatusCode.NOT_FOUND, "SA-2: missing file is 404");
|
|
|
|
|
+
|
|
|
|
|
+ var r9 = yield serve("/assets/sub");
|
|
|
|
|
+ check(r9.status == StatusCode.OK, "SA-2: directory with index.html serves the index");
|
|
|
|
|
+
|
|
|
|
|
+ var r10 = yield serve("/assets/inner-link/app.css");
|
|
|
|
|
+ check(r10.status == StatusCode.OK, "SA-2: internal symlink (stays under root) serves 200");
|
|
|
|
|
+
|
|
|
|
|
+ // Containment boundary: a file in a sibling directory whose name
|
|
|
|
|
+ // shares the root's prefix must never be reachable. The fixture's
|
|
|
|
|
+ // root is "<tmp>/www" and the sibling "<tmp>/www2".
|
|
|
|
|
+ var r11 = yield serve("/assets/../../www2/secret.txt");
|
|
|
|
|
+ check(r11.status == StatusCode.FORBIDDEN, "SA-2: prefix-sharing sibling (/www vs /www2) is 403");
|
|
|
|
|
+
|
|
|
|
|
+ var r12 = yield serve("/assets");
|
|
|
|
|
+ check(r12.status == StatusCode.OK, "SA-2: root directory index serves 200");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // SA-13: security response headers
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ SecurityHeadersComponent headers_component(WebConfig config) {
|
|
|
|
|
+ return new SecurityHeadersComponent.with_config(config);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ string? header_of(HttpResult result, string name) {
|
|
|
|
|
+ string? value = null;
|
|
|
|
|
+ result.headers.try_get(name, out value);
|
|
|
|
|
+ return value;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async void run_header_tests() throws Error {
|
|
|
|
|
+ var component = headers_component(new WebConfig.empty());
|
|
|
|
|
+ var result = new HttpStringResult("hello");
|
|
|
|
|
+ component.apply_headers(result);
|
|
|
|
|
+ check(header_of(result, "X-Content-Type-Options") == "nosniff", "SA-13: nosniff header present");
|
|
|
|
|
+ check(header_of(result, "X-Frame-Options") == "SAMEORIGIN", "SA-13: X-Frame-Options SAMEORIGIN");
|
|
|
|
|
+ check(header_of(result, "Referrer-Policy") == "strict-origin-when-cross-origin", "SA-13: Referrer-Policy set");
|
|
|
|
|
+ check((header_of(result, "Content-Security-Policy") ?? "").contains("default-src 'self'"), "SA-13: CSP template applied");
|
|
|
|
|
+ check(header_of(result, "Strict-Transport-Security") == null, "SA-13: HSTS off by default");
|
|
|
|
|
+
|
|
|
|
|
+ // Application-set header must win over the default.
|
|
|
|
|
+ var result2 = new HttpStringResult("hello");
|
|
|
|
|
+ result2.set_header("X-Frame-Options", "DENY");
|
|
|
|
|
+ component.apply_headers(result2);
|
|
|
|
|
+ check(header_of(result2, "X-Frame-Options") == "DENY", "SA-13: endpoint-set header is not clobbered");
|
|
|
|
|
+
|
|
|
|
|
+ // Config overrides: HSTS opt-in, CSP extension, header disable.
|
|
|
|
|
+ var config = WebConfigLoader.load_and_merge_files({ Path.build_filename(root_dir, "headers-config.json") });
|
|
|
|
|
+ var configured = headers_component(config);
|
|
|
|
|
+ var result3 = new HttpStringResult("hello");
|
|
|
|
|
+ configured.apply_headers(result3);
|
|
|
|
|
+ check(header_of(result3, "Strict-Transport-Security") == "max-age=31536000", "SA-13: HSTS enabled via config");
|
|
|
|
|
+ check((header_of(result3, "Content-Security-Policy") ?? "").contains("frame-ancestors 'self'"), "SA-13: CSP extended via config");
|
|
|
|
|
+ check(header_of(result3, "X-Frame-Options") == null, "SA-13: header disabled via empty config value");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+ // SA-3: request body limits, end to end through the real server
|
|
|
|
|
+ // ------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ // A POST endpoint that echoes the body size; the server is configured
|
|
|
|
|
+ // with max_request_body = 64.
|
|
|
|
|
+ class EchoEndpoint : Object, Endpoint {
|
|
|
|
|
+ public async HttpResult handle_request(HttpContext http_context, RouteContext route_context) throws Error {
|
|
|
|
|
+ var body = yield http_context.request.request_body.read_all();
|
|
|
|
|
+ return new HttpStringResult(@"$(body.to_byte_buffer().length) bytes");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Server? test_server;
|
|
|
|
|
+ int server_port;
|
|
|
|
|
+ const uint64 TEST_BODY_LIMIT = 64;
|
|
|
|
|
+
|
|
|
|
|
+ int find_free_port() {
|
|
|
|
|
+ // Bind port 0, read the assigned ephemeral port, close. Racy in
|
|
|
|
|
+ // theory, fine in a test.
|
|
|
|
|
+ try {
|
|
|
|
|
+ var socket = new Socket(SocketFamily.IPV4, SocketType.STREAM, SocketProtocol.DEFAULT);
|
|
|
|
|
+ socket.bind(new InetSocketAddress(new InetAddress.from_string("127.0.0.1"), 0), false);
|
|
|
|
|
+ var addr = (InetSocketAddress) socket.get_local_address();
|
|
|
|
|
+ uint16 port = addr.get_port();
|
|
|
|
|
+ socket.close();
|
|
|
|
|
+ return port;
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ return 18099;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void start_server() throws Error {
|
|
|
|
|
+ var container = new Container();
|
|
|
|
|
+ var config = new WebConfig.empty();
|
|
|
|
|
+ container.register_singleton<WebConfig>(() => config);
|
|
|
|
|
+ var limits = new ServerLimits() { max_request_body = TEST_BODY_LIMIT };
|
|
|
|
|
+ container.register_singleton<ServerLimits>(() => limits);
|
|
|
|
|
+ var pipeline = new Pipeline(container);
|
|
|
|
|
+ pipeline.container.register_scoped<EchoEndpoint>().as<Endpoint>()
|
|
|
|
|
+ .with_metadata<EndpointRoute>(new EndpointRoute("/echo", Method.POST));
|
|
|
|
|
+ // Router so the route actually resolves.
|
|
|
|
|
+ pipeline.container.register_scoped<EndpointRouter>().as<PipelineComponent>();
|
|
|
|
|
+ test_server = new Server(server_port, pipeline, limits);
|
|
|
|
|
+
|
|
|
|
|
+ Thread.create<void>(() => {
|
|
|
|
|
+ test_server.run();
|
|
|
|
|
+ }, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ string? http_post(string path, size_t content_length, string? body) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var client = new SocketClient();
|
|
|
|
|
+ var connection = client.connect(new InetSocketAddress(
|
|
|
|
|
+ new InetAddress.from_string("127.0.0.1"), (uint16) server_port));
|
|
|
|
|
+ var output = connection.get_output_stream();
|
|
|
|
|
+ string request = "POST %s HTTP/1.1\r\nHost: localhost\r\nContent-Length: %s\r\nConnection: close\r\n\r\n".printf(path, content_length.to_string());
|
|
|
|
|
+ output.write_all(request.data, null);
|
|
|
|
|
+ if (body != null) {
|
|
|
|
|
+ output.write_all(body.data, null);
|
|
|
|
|
+ }
|
|
|
|
|
+ output.flush();
|
|
|
|
|
+ var input = new DataInputStream(connection.get_input_stream());
|
|
|
|
|
+ size_t read;
|
|
|
|
|
+ // Read until the full response (headers + body) has arrived;
|
|
|
|
|
+ // do not rely on the server closing the connection.
|
|
|
|
|
+ var response = new StringBuilder();
|
|
|
|
|
+ uint8 buffer[4096];
|
|
|
|
|
+ while (!response.str.contains("\r\n\r\n")) {
|
|
|
|
|
+ read = input.read(buffer);
|
|
|
|
|
+ if (read == 0) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ response.append_len((string) buffer, (ssize_t) read);
|
|
|
|
|
+ }
|
|
|
|
|
+ connection.close();
|
|
|
|
|
+ return response.str;
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ string? http_post_chunked(string path, size_t total_size) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var client = new SocketClient();
|
|
|
|
|
+ var connection = client.connect(new InetSocketAddress(
|
|
|
|
|
+ new InetAddress.from_string("127.0.0.1"), (uint16) server_port));
|
|
|
|
|
+ var output = connection.get_output_stream();
|
|
|
|
|
+ string request = "POST %s HTTP/1.1\r\nHost: localhost\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n".printf(path);
|
|
|
|
|
+ output.write_all(request.data, null);
|
|
|
|
|
+ // Send in 16-byte chunks until past the cap.
|
|
|
|
|
+ size_t sent = 0;
|
|
|
|
|
+ while (sent < total_size) {
|
|
|
|
|
+ size_t chunk = size_t.min(16, total_size - sent);
|
|
|
|
|
+ string header = @"%x\r\n".printf((uint) chunk);
|
|
|
|
|
+ output.write_all(header.data, null);
|
|
|
|
|
+ output.write_all(string.nfill((int) chunk, 'x').data, null);
|
|
|
|
|
+ output.write_all("\r\n".data, null);
|
|
|
|
|
+ sent += chunk;
|
|
|
|
|
+ }
|
|
|
|
|
+ output.write_all("0\r\n\r\n".data, null);
|
|
|
|
|
+ output.flush();
|
|
|
|
|
+ var input = new DataInputStream(connection.get_input_stream());
|
|
|
|
|
+ var response = new StringBuilder();
|
|
|
|
|
+ uint8 buffer[4096];
|
|
|
|
|
+ while (!response.str.contains("\r\n\r\n")) {
|
|
|
|
|
+ size_t read = input.read(buffer);
|
|
|
|
|
+ if (read == 0) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ response.append_len((string) buffer, (ssize_t) read);
|
|
|
|
|
+ }
|
|
|
|
|
+ connection.close();
|
|
|
|
|
+ return response.str;
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ string? status_of(string? response) {
|
|
|
|
|
+ if (response == null || !response.has_prefix("HTTP/")) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return response.split(" ")[1];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_body_limit_end_to_end() throws Error {
|
|
|
|
|
+ // Wait for the server to accept connections.
|
|
|
|
|
+ bool up = false;
|
|
|
|
|
+ for (int i = 0; i < 100; i++) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var client = new SocketClient();
|
|
|
|
|
+ client.connect(new InetSocketAddress(new InetAddress.from_string("127.0.0.1"), (uint16) server_port)).close();
|
|
|
|
|
+ up = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ Thread.usleep(50000);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ check(up, "SA-3: test server came up");
|
|
|
|
|
+
|
|
|
|
|
+ // Oversized declared Content-Length: rejected with 413 up front,
|
|
|
|
|
+ // without the body ever being sent or buffered.
|
|
|
|
|
+ var over = http_post("/echo", 1024, null);
|
|
|
|
|
+ check(status_of(over) == "413", "SA-3: oversized Content-Length answered 413 (got %s)".printf(status_of(over) ?? "none"));
|
|
|
|
|
+
|
|
|
|
|
+ // Within the limit: served normally.
|
|
|
|
|
+ // Within the limit: served normally (4 x 16 = 64 bytes exactly).
|
|
|
|
|
+ var small_body = new StringBuilder();
|
|
|
|
|
+ for (int i = 0; i < 4; i++) {
|
|
|
|
|
+ small_body.append("0123456789abcdef");
|
|
|
|
|
+ }
|
|
|
|
|
+ var ok = http_post("/echo", small_body.len, small_body.str);
|
|
|
|
|
+ check(status_of(ok) == "200", "SA-3: body exactly at the limit answered 200 (got %s)".printf(status_of(ok) ?? "none"));
|
|
|
|
|
+
|
|
|
|
|
+ // Lying/chunked-style client: no Content-Length, sends a chunked
|
|
|
|
|
+ // body larger than the cap. The streaming guard in ServerInput must
|
|
|
|
|
+ // cut it off with 413.
|
|
|
|
|
+ var lying = http_post_chunked("/echo", 256);
|
|
|
|
|
+ check(status_of(lying) == "413", "SA-3: chunked stream past the cap answered 413 (got %s)".printf(status_of(lying) ?? "none"));
|
|
|
|
|
+
|
|
|
|
|
+ if (test_server != null) {
|
|
|
|
|
+ test_server.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_limits_config() {
|
|
|
|
|
+ var defaults = ServerLimits.from_config(new WebConfig.empty());
|
|
|
|
|
+ check(defaults.max_request_body == 10 * 1024 * 1024, "SA-3: default max_request_body is 10 MiB");
|
|
|
|
|
+ check(defaults.connection_limit == 256, "SA-3: default connection limit 256");
|
|
|
|
|
+ check(defaults.per_ip_connection_limit == 32, "SA-3: default per-IP connection limit 32");
|
|
|
|
|
+ check(defaults.connection_memory_limit == 131072, "SA-3: default connection memory limit 128 KiB");
|
|
|
|
|
+
|
|
|
|
|
+ var config = WebConfigLoader.load_and_merge_files({ Path.build_filename(root_dir, "limits-config.json") });
|
|
|
|
|
+ var limits = ServerLimits.from_config(config);
|
|
|
|
|
+ check(limits.connection_limit == 64, "SA-3: connection_limit from config");
|
|
|
|
|
+ check(limits.per_ip_connection_limit == 8, "SA-3: per_ip_connection_limit from config");
|
|
|
|
|
+ check(limits.connection_memory_limit == 65536, "SA-3: connection_memory_limit from config");
|
|
|
|
|
+ check(limits.max_request_body == 2048, "SA-3: max_request_body from config");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ class TestInput : Object, AsyncInput {
|
|
|
|
|
+ public BinaryData? peek() { return null; }
|
|
|
|
|
+ public async BinaryData? read() throws Error { return null; }
|
|
|
|
|
+ public async BinaryData read_all() { return new ByteBuffer(0); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void main() {
|
|
|
|
|
+ string base_dir = DirUtils.make_tmp("astralis-tests-XXXXXX");
|
|
|
|
|
+ root_dir = Path.build_filename(base_dir, "www");
|
|
|
|
|
+ sibling_dir = Path.build_filename(base_dir, "www2");
|
|
|
|
|
+ DirUtils.create_with_parents(Path.build_filename(root_dir, "css"), 0755);
|
|
|
|
|
+ DirUtils.create_with_parents(Path.build_filename(root_dir, "sub"), 0755);
|
|
|
|
|
+ DirUtils.create_with_parents(sibling_dir, 0755);
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(root_dir, "css", "app.css"), "body{}");
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(root_dir, "a..b.txt"), "dots");
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(root_dir, "sub", "index.html"), "<html></html>");
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(root_dir, "index.html"), "<html></html>");
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(sibling_dir, "secret.txt"), "sibling secret");
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(root_dir, "headers-config.json"),
|
|
|
|
|
+ "{\"securityHeaders\": {\"hsts\": \"max-age=31536000\", \"xFrameOptions\": \"\", \"contentSecurityPolicy\": \"default-src 'self'; frame-ancestors 'self'\"}}");
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(root_dir, "limits-config.json"),
|
|
|
|
|
+ "{\"server\": {\"connection_limit\": 64, \"per_ip_connection_limit\": 8, \"connection_memory_limit\": 65536, \"max_request_body\": 2048}}");
|
|
|
|
|
+ if (!FileUtils.test(Path.build_filename(root_dir, "link"), FileTest.EXISTS)) {
|
|
|
|
|
+ File.new_for_path(Path.build_filename(root_dir, "link")).make_symbolic_link("/etc");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!FileUtils.test(Path.build_filename(root_dir, "inner-link"), FileTest.EXISTS)) {
|
|
|
|
|
+ File.new_for_path(Path.build_filename(root_dir, "inner-link")).make_symbolic_link("css");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_wildcard_remainder_from_request();
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL wildcard remainder threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var loop = new MainLoop();
|
|
|
|
|
+ run_traversal_tests.begin((obj, res) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ run_traversal_tests.end(res);
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL traversal tests threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ loop.quit();
|
|
|
|
|
+ });
|
|
|
|
|
+ loop.run();
|
|
|
|
|
+
|
|
|
|
|
+ var loop2 = new MainLoop();
|
|
|
|
|
+ run_header_tests.begin((obj, res) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ run_header_tests.end(res);
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL header tests threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ loop2.quit();
|
|
|
|
|
+ });
|
|
|
|
|
+ loop2.run();
|
|
|
|
|
+
|
|
|
|
|
+ test_limits_config();
|
|
|
|
|
+
|
|
|
|
|
+ // End-to-end server tests on a found-free port.
|
|
|
|
|
+ server_port = find_free_port();
|
|
|
|
|
+ try {
|
|
|
|
|
+ start_server();
|
|
|
|
|
+ test_body_limit_end_to_end();
|
|
|
|
|
+ } catch (Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL end-to-end server test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ print("\n%d passed, %d failed\n", passes, failures);
|
|
|
|
|
+ if (failures > 0) {
|
|
|
|
|
+ Process.exit(1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|