Commit 3806091a10

Andrew Kelley <andrew@ziglang.org>
2023-01-07 01:53:06
std.http.Client: fix handling of \r\n before next chunk size
1 parent 24b4e64
Changed files (1)
lib
std
lib/std/http/Client.zig
@@ -1,7 +1,3 @@
-//! This API is a barely-touched, barely-functional http client, just the
-//! absolute minimum thing I needed in order to test `std.crypto.tls`. Bear
-//! with me and I promise the API will become useful and streamlined.
-//!
 //! TODO: send connection: keep-alive and LRU cache a configurable number of
 //! open connections to skip DNS and TLS handshake for subsequent requests.
 
@@ -178,6 +174,8 @@ pub const Request = struct {
             seen_rnr,
             finished,
             /// Begin transfer-encoding: chunked parsing states.
+            chunk_size_prefix_r,
+            chunk_size_prefix_n,
             chunk_size,
             chunk_r,
             chunk_data,
@@ -382,6 +380,8 @@ pub const Request = struct {
                             continue :state;
                         },
                     },
+                    .chunk_size_prefix_r => unreachable,
+                    .chunk_size_prefix_n => unreachable,
                     .chunk_size => unreachable,
                     .chunk_r => unreachable,
                     .chunk_data => unreachable,
@@ -449,18 +449,6 @@ pub const Request = struct {
             try expectEqual(@as(u10, 999), parseInt3("999".*));
         }
 
-        inline fn int16(array: *const [2]u8) u16 {
-            return @bitCast(u16, array.*);
-        }
-
-        inline fn int32(array: *const [4]u8) u32 {
-            return @bitCast(u32, array.*);
-        }
-
-        inline fn int64(array: *const [8]u8) u64 {
-            return @bitCast(u64, array.*);
-        }
-
         test "find headers end basic" {
             var buffer: [1]u8 = undefined;
             var r = Response.initStatic(&buffer);
@@ -480,6 +468,29 @@ pub const Request = struct {
                 "\r\ncontent";
             try testing.expectEqual(@as(usize, 131), r.findHeadersEnd(example));
         }
+
+        test "find headers end bug" {
+            var buffer: [1]u8 = undefined;
+            var r = Response.initStatic(&buffer);
+            const trail = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
+            const example =
+                "HTTP/1.1 200 OK\r\n" ++
+                "Access-Control-Allow-Origin: https://render.githubusercontent.com\r\n" ++
+                "content-disposition: attachment; filename=zig-0.10.0.tar.gz\r\n" ++
+                "Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox\r\n" ++
+                "Content-Type: application/x-gzip\r\n" ++
+                "ETag: \"bfae0af6b01c7c0d89eb667cb5f0e65265968aeebda2689177e6b26acd3155ca\"\r\n" ++
+                "Strict-Transport-Security: max-age=31536000\r\n" ++
+                "Vary: Authorization,Accept-Encoding,Origin\r\n" ++
+                "X-Content-Type-Options: nosniff\r\n" ++
+                "X-Frame-Options: deny\r\n" ++
+                "X-XSS-Protection: 1; mode=block\r\n" ++
+                "Date: Fri, 06 Jan 2023 22:26:22 GMT\r\n" ++
+                "Transfer-Encoding: chunked\r\n" ++
+                "X-GitHub-Request-Id: 89C6:17E9:A7C9E:124B51:63B8A00E\r\n" ++
+                "connection: close\r\n\r\n" ++ trail;
+            try testing.expectEqual(@as(usize, example.len - trail.len), r.findHeadersEnd(example));
+        }
     };
 
     pub const Headers = struct {
@@ -536,8 +547,7 @@ pub const Request = struct {
     /// This one can return 0 without meaning EOF.
     /// TODO change to readvAdvanced
     pub fn readAdvanced(req: *Request, buffer: []u8) !usize {
-        const amt = try req.connection.read(buffer);
-        var in = buffer[0..amt];
+        var in = buffer[0..try req.connection.read(buffer)];
         var out_index: usize = 0;
         while (true) {
             switch (req.response.state) {
@@ -571,7 +581,8 @@ pub const Request = struct {
                             req.deinit();
                             req.* = new_req;
                             assert(out_index == 0);
-                            return readAdvanced(req, buffer);
+                            in = buffer[0..try req.connection.read(buffer)];
+                            continue;
                         }
 
                         if (req.response.headers.transfer_encoding) |transfer_encoding| {
@@ -598,8 +609,50 @@ pub const Request = struct {
                     return 0;
                 },
                 .finished => {
-                    mem.copy(u8, buffer[out_index..], in);
-                    return out_index + in.len;
+                    if (in.ptr == buffer.ptr) {
+                        return in.len;
+                    } else {
+                        mem.copy(u8, buffer[out_index..], in);
+                        return out_index + in.len;
+                    }
+                },
+                .chunk_size_prefix_r => switch (in.len) {
+                    0 => return out_index,
+                    1 => switch (in[0]) {
+                        '\r' => {
+                            req.response.state = .chunk_size_prefix_n;
+                            return out_index;
+                        },
+                        else => {
+                            req.response.state = .invalid;
+                            return error.HttpHeadersInvalid;
+                        },
+                    },
+                    else => switch (int16(in[0..2])) {
+                        int16("\r\n") => {
+                            in = in[2..];
+                            req.response.state = .chunk_size;
+                            continue;
+                        },
+                        else => {
+                            req.response.state = .invalid;
+                            return error.HttpHeadersInvalid;
+                        },
+                    },
+                },
+                .chunk_size_prefix_n => switch (in.len) {
+                    0 => return out_index,
+                    else => switch (in[0]) {
+                        '\n' => {
+                            in = in[1..];
+                            req.response.state = .chunk_size;
+                            continue;
+                        },
+                        else => {
+                            req.response.state = .invalid;
+                            return error.HttpHeadersInvalid;
+                        },
+                    },
                 },
                 .chunk_size, .chunk_r => {
                     const i = req.response.findChunkedLen(in);
@@ -619,20 +672,38 @@ pub const Request = struct {
                 },
                 .chunk_data => {
                     const sub_amt = @min(req.response.next_chunk_length, in.len);
-                    mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
-                    out_index += sub_amt;
                     req.response.next_chunk_length -= sub_amt;
-                    if (req.response.next_chunk_length == 0) {
-                        req.response.state = .chunk_size;
-                        in = in[sub_amt..];
-                        continue;
+                    if (req.response.next_chunk_length > 0) {
+                        if (in.ptr == buffer.ptr) {
+                            return sub_amt;
+                        } else {
+                            mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
+                            out_index += sub_amt;
+                            return out_index;
+                        }
                     }
-                    return out_index;
+                    mem.copy(u8, buffer[out_index..], in[0..sub_amt]);
+                    out_index += sub_amt;
+                    req.response.state = .chunk_size_prefix_r;
+                    in = in[sub_amt..];
+                    continue;
                 },
             }
         }
     }
 
+    inline fn int16(array: *const [2]u8) u16 {
+        return @bitCast(u16, array.*);
+    }
+
+    inline fn int32(array: *const [4]u8) u32 {
+        return @bitCast(u32, array.*);
+    }
+
+    inline fn int64(array: *const [8]u8) u64 {
+        return @bitCast(u64, array.*);
+    }
+
     test {
         _ = Response;
     }