blob 9e2ebd2d (17553B) - Raw
1 const std = @import("std"); 2 const http = std.http; 3 const Uri = std.Uri; 4 const mem = std.mem; 5 const assert = std.debug.assert; 6 7 const Client = @import("../Client.zig"); 8 const Connection = Client.Connection; 9 const ConnectionNode = Client.ConnectionPool.Node; 10 const Response = @import("Response.zig"); 11 12 const Request = @This(); 13 14 const read_buffer_size = 8192; 15 const ReadBufferIndex = std.math.IntFittingRange(0, read_buffer_size); 16 17 uri: Uri, 18 client: *Client, 19 connection: *ConnectionNode, 20 response: Response, 21 /// These are stored in Request so that they are available when following 22 /// redirects. 23 headers: Headers, 24 25 redirects_left: u32, 26 handle_redirects: bool, 27 compression_init: bool, 28 29 /// Used as a allocator for resolving redirects locations. 30 arena: std.heap.ArenaAllocator, 31 32 /// Read buffer for the connection. This is used to pull in large amounts of data from the connection even if the user asks for a small amount. This can probably be removed with careful planning. 33 read_buffer: [read_buffer_size]u8 = undefined, 34 read_buffer_start: ReadBufferIndex = 0, 35 read_buffer_len: ReadBufferIndex = 0, 36 37 pub const RequestTransfer = union(enum) { 38 content_length: u64, 39 chunked: void, 40 none: void, 41 }; 42 43 pub const Headers = struct { 44 version: http.Version = .@"HTTP/1.1", 45 method: http.Method = .GET, 46 user_agent: []const u8 = "zig (std.http)", 47 connection: http.Connection = .keep_alive, 48 transfer_encoding: RequestTransfer = .none, 49 50 custom: []const http.CustomHeader = &[_]http.CustomHeader{}, 51 }; 52 53 pub const Options = struct { 54 handle_redirects: bool = true, 55 max_redirects: u32 = 3, 56 header_strategy: HeaderStrategy = .{ .dynamic = 16 * 1024 }, 57 58 pub const HeaderStrategy = union(enum) { 59 /// In this case, the client's Allocator will be used to store the 60 /// entire HTTP header. This value is the maximum total size of 61 /// HTTP headers allowed, otherwise 62 /// error.HttpHeadersExceededSizeLimit is returned from read(). 63 dynamic: usize, 64 /// This is used to store the entire HTTP header. If the HTTP 65 /// header is too big to fit, `error.HttpHeadersExceededSizeLimit` 66 /// is returned from read(). When this is used, `error.OutOfMemory` 67 /// cannot be returned from `read()`. 68 static: []u8, 69 }; 70 }; 71 72 /// Frees all resources associated with the request. 73 pub fn deinit(req: *Request) void { 74 switch (req.response.compression) { 75 .none => {}, 76 .deflate => |*deflate| deflate.deinit(), 77 .gzip => |*gzip| gzip.deinit(), 78 .zstd => |*zstd| zstd.deinit(), 79 } 80 81 if (req.response.header_bytes_owned) { 82 req.response.header_bytes.deinit(req.client.allocator); 83 } 84 85 if (!req.response.done) { 86 // If the response wasn't fully read, then we need to close the connection. 87 req.connection.data.closing = true; 88 req.client.connection_pool.release(req.client, req.connection); 89 } 90 91 req.arena.deinit(); 92 req.* = undefined; 93 } 94 95 pub const ReadRawError = Connection.ReadError || Uri.ParseError || Client.RequestError || error{ 96 UnexpectedEndOfStream, 97 TooManyHttpRedirects, 98 HttpRedirectMissingLocation, 99 HttpHeadersInvalid, 100 }; 101 102 pub const ReaderRaw = std.io.Reader(*Request, ReadRawError, readRaw); 103 104 /// Read from the underlying stream, without decompressing or parsing the headers. Must be called 105 /// after waitForCompleteHead() has returned successfully. 106 pub fn readRaw(req: *Request, buffer: []u8) ReadRawError!usize { 107 assert(req.response.state.isContent()); 108 109 var index: usize = 0; 110 while (index == 0) { 111 const amt = try req.readRawAdvanced(buffer[index..]); 112 if (amt == 0 and req.response.done) break; 113 index += amt; 114 } 115 116 return index; 117 } 118 119 fn checkForCompleteHead(req: *Request, buffer: []u8) !usize { 120 switch (req.response.state) { 121 .invalid => unreachable, 122 .start, .seen_r, .seen_rn, .seen_rnr => {}, 123 else => return 0, // No more headers to read. 124 } 125 126 const i = req.response.findHeadersEnd(buffer[0..]); 127 if (req.response.state == .invalid) return error.HttpHeadersInvalid; 128 129 const headers_data = buffer[0..i]; 130 if (req.response.header_bytes.items.len + headers_data.len > req.response.max_header_bytes) { 131 return error.HttpHeadersExceededSizeLimit; 132 } 133 try req.response.header_bytes.appendSlice(req.client.allocator, headers_data); 134 135 if (req.response.state == .finished) { 136 req.response.headers = try Response.Headers.parse(req.response.header_bytes.items); 137 138 if (req.response.headers.upgrade) |_| { 139 req.connection.data.closing = false; 140 req.response.done = true; 141 return i; 142 } 143 144 if (req.response.headers.connection == .keep_alive) { 145 req.connection.data.closing = false; 146 } else { 147 req.connection.data.closing = true; 148 } 149 150 if (req.response.headers.transfer_encoding) |transfer_encoding| { 151 switch (transfer_encoding) { 152 .chunked => { 153 req.response.next_chunk_length = 0; 154 req.response.state = .chunk_size; 155 }, 156 } 157 } else if (req.response.headers.content_length) |content_length| { 158 req.response.next_chunk_length = content_length; 159 160 if (content_length == 0) req.response.done = true; 161 } else { 162 req.response.done = true; 163 } 164 165 return i; 166 } 167 168 return 0; 169 } 170 171 pub const WaitForCompleteHeadError = ReadRawError || error{ 172 UnexpectedEndOfStream, 173 174 HttpHeadersExceededSizeLimit, 175 ShortHttpStatusLine, 176 BadHttpVersion, 177 HttpHeaderContinuationsUnsupported, 178 HttpTransferEncodingUnsupported, 179 HttpConnectionHeaderUnsupported, 180 }; 181 182 /// Reads a complete response head. Any leftover data is stored in the request. This function is idempotent. 183 pub fn waitForCompleteHead(req: *Request) WaitForCompleteHeadError!void { 184 if (req.response.state.isContent()) return; 185 186 while (true) { 187 const nread = try req.connection.data.read(req.read_buffer[0..]); 188 const amt = try checkForCompleteHead(req, req.read_buffer[0..nread]); 189 190 if (amt != 0) { 191 req.read_buffer_start = @intCast(ReadBufferIndex, amt); 192 req.read_buffer_len = @intCast(ReadBufferIndex, nread); 193 return; 194 } else if (nread == 0) { 195 return error.UnexpectedEndOfStream; 196 } 197 } 198 } 199 200 /// This one can return 0 without meaning EOF. 201 fn readRawAdvanced(req: *Request, buffer: []u8) !usize { 202 assert(req.response.state.isContent()); 203 if (req.response.done) return 0; 204 205 // var in: []const u8 = undefined; 206 if (req.read_buffer_start == req.read_buffer_len) { 207 const nread = try req.connection.data.read(req.read_buffer[0..]); 208 if (nread == 0) return error.UnexpectedEndOfStream; 209 210 req.read_buffer_start = 0; 211 req.read_buffer_len = @intCast(ReadBufferIndex, nread); 212 } 213 214 var out_index: usize = 0; 215 while (true) { 216 switch (req.response.state) { 217 .invalid, .start, .seen_r, .seen_rn, .seen_rnr => unreachable, 218 .finished => { 219 // TODO https://github.com/ziglang/zig/issues/14039 220 const buf_avail = req.read_buffer_len - req.read_buffer_start; 221 const data_avail = req.response.next_chunk_length; 222 const out_avail = buffer.len; 223 224 if (req.handle_redirects and req.response.headers.status.class() == .redirect) { 225 const can_read = @intCast(usize, @min(buf_avail, data_avail)); 226 req.response.next_chunk_length -= can_read; 227 228 if (req.response.next_chunk_length == 0) { 229 req.client.connection_pool.release(req.client, req.connection); 230 req.connection = undefined; 231 req.response.done = true; 232 } 233 234 return 0; // skip over as much data as possible 235 } 236 237 const can_read = @intCast(usize, @min(@min(buf_avail, data_avail), out_avail)); 238 req.response.next_chunk_length -= can_read; 239 240 mem.copy(u8, buffer[0..], req.read_buffer[req.read_buffer_start..][0..can_read]); 241 req.read_buffer_start += @intCast(ReadBufferIndex, can_read); 242 243 if (req.response.next_chunk_length == 0) { 244 req.client.connection_pool.release(req.client, req.connection); 245 req.connection = undefined; 246 req.response.done = true; 247 } 248 249 return can_read; 250 }, 251 .chunk_size_prefix_r => switch (req.read_buffer_len - req.read_buffer_start) { 252 0 => return out_index, 253 1 => switch (req.read_buffer[req.read_buffer_start]) { 254 '\r' => { 255 req.response.state = .chunk_size_prefix_n; 256 return out_index; 257 }, 258 else => { 259 req.response.state = .invalid; 260 return error.HttpHeadersInvalid; 261 }, 262 }, 263 else => switch (int16(req.read_buffer[req.read_buffer_start..][0..2])) { 264 int16("\r\n") => { 265 req.read_buffer_start += 2; 266 req.response.state = .chunk_size; 267 continue; 268 }, 269 else => { 270 req.response.state = .invalid; 271 return error.HttpHeadersInvalid; 272 }, 273 }, 274 }, 275 .chunk_size_prefix_n => switch (req.read_buffer_len - req.read_buffer_start) { 276 0 => return out_index, 277 else => switch (req.read_buffer[req.read_buffer_start]) { 278 '\n' => { 279 req.read_buffer_start += 1; 280 req.response.state = .chunk_size; 281 continue; 282 }, 283 else => { 284 req.response.state = .invalid; 285 return error.HttpHeadersInvalid; 286 }, 287 }, 288 }, 289 .chunk_size, .chunk_r => { 290 const i = req.response.findChunkedLen(req.read_buffer[req.read_buffer_start..req.read_buffer_len]); 291 switch (req.response.state) { 292 .invalid => return error.HttpHeadersInvalid, 293 .chunk_data => { 294 if (req.response.next_chunk_length == 0) { 295 req.response.done = true; 296 req.client.connection_pool.release(req.client, req.connection); 297 req.connection = undefined; 298 299 return out_index; 300 } 301 302 req.read_buffer_start += @intCast(ReadBufferIndex, i); 303 continue; 304 }, 305 .chunk_size => return out_index, 306 else => unreachable, 307 } 308 }, 309 .chunk_data => { 310 // TODO https://github.com/ziglang/zig/issues/14039 311 const buf_avail = req.read_buffer_len - req.read_buffer_start; 312 const data_avail = req.response.next_chunk_length; 313 const out_avail = buffer.len - out_index; 314 315 if (req.handle_redirects and req.response.headers.status.class() == .redirect) { 316 const can_read = @intCast(usize, @min(buf_avail, data_avail)); 317 req.response.next_chunk_length -= can_read; 318 319 if (req.response.next_chunk_length == 0) { 320 req.client.connection_pool.release(req.client, req.connection); 321 req.connection = undefined; 322 req.response.done = true; 323 continue; 324 } 325 326 return 0; // skip over as much data as possible 327 } 328 329 const can_read = @intCast(usize, @min(@min(buf_avail, data_avail), out_avail)); 330 req.response.next_chunk_length -= can_read; 331 332 mem.copy(u8, buffer[out_index..], req.read_buffer[req.read_buffer_start..][0..can_read]); 333 req.read_buffer_start += @intCast(ReadBufferIndex, can_read); 334 out_index += can_read; 335 336 if (req.response.next_chunk_length == 0) { 337 req.response.state = .chunk_size_prefix_r; 338 339 continue; 340 } 341 342 return out_index; 343 }, 344 } 345 } 346 } 347 348 pub const ReadError = Client.DeflateDecompressor.Error || Client.GzipDecompressor.Error || Client.ZstdDecompressor.Error || WaitForCompleteHeadError || error{ BadHeader, InvalidCompression, StreamTooLong, InvalidWindowSize, CompressionNotSupported }; 349 350 pub const Reader = std.io.Reader(*Request, ReadError, read); 351 352 pub fn reader(req: *Request) Reader { 353 return .{ .context = req }; 354 } 355 356 pub fn read(req: *Request, buffer: []u8) ReadError!usize { 357 while (true) { 358 if (!req.response.state.isContent()) try req.waitForCompleteHead(); 359 360 if (req.handle_redirects and req.response.headers.status.class() == .redirect) { 361 assert(try req.readRaw(buffer) == 0); 362 363 if (req.redirects_left == 0) return error.TooManyHttpRedirects; 364 365 const location = req.response.headers.location orelse 366 return error.HttpRedirectMissingLocation; 367 const new_url = Uri.parse(location) catch try Uri.parseWithoutScheme(location); 368 369 var new_arena = std.heap.ArenaAllocator.init(req.client.allocator); 370 const resolved_url = try req.uri.resolve(new_url, false, new_arena.allocator()); 371 errdefer new_arena.deinit(); 372 373 req.arena.deinit(); 374 req.arena = new_arena; 375 376 const new_req = try req.client.request(resolved_url, req.headers, .{ 377 .max_redirects = req.redirects_left - 1, 378 .header_strategy = if (req.response.header_bytes_owned) .{ 379 .dynamic = req.response.max_header_bytes, 380 } else .{ 381 .static = req.response.header_bytes.unusedCapacitySlice(), 382 }, 383 }); 384 req.deinit(); 385 req.* = new_req; 386 } else { 387 break; 388 } 389 } 390 391 if (req.response.compression == .none) { 392 if (req.response.headers.transfer_compression) |compression| { 393 switch (compression) { 394 .compress => return error.CompressionNotSupported, 395 .deflate => req.response.compression = .{ 396 .deflate = try std.compress.zlib.zlibStream(req.client.allocator, ReaderRaw{ .context = req }), 397 }, 398 .gzip => req.response.compression = .{ 399 .gzip = try std.compress.gzip.decompress(req.client.allocator, ReaderRaw{ .context = req }), 400 }, 401 .zstd => req.response.compression = .{ 402 .zstd = std.compress.zstd.decompressStream(req.client.allocator, ReaderRaw{ .context = req }), 403 }, 404 } 405 } 406 } 407 408 return switch (req.response.compression) { 409 .deflate => |*deflate| try deflate.read(buffer), 410 .gzip => |*gzip| try gzip.read(buffer), 411 .zstd => |*zstd| try zstd.read(buffer), 412 else => try req.readRaw(buffer), 413 }; 414 } 415 416 pub fn readAll(req: *Request, buffer: []u8) !usize { 417 var index: usize = 0; 418 while (index < buffer.len) { 419 const amt = try read(req, buffer[index..]); 420 if (amt == 0) break; 421 index += amt; 422 } 423 return index; 424 } 425 426 pub const WriteError = Connection.WriteError || error{MessageTooLong}; 427 428 pub const Writer = std.io.Writer(*Request, WriteError, write); 429 430 pub fn writer(req: *Request) Writer { 431 return .{ .context = req }; 432 } 433 434 /// Write `bytes` to the server. The `transfer_encoding` request header determines how data will be sent. 435 pub fn write(req: *Request, bytes: []const u8) !usize { 436 switch (req.headers.transfer_encoding) { 437 .chunked => { 438 try req.connection.data.writer().print("{x}\r\n", .{bytes.len}); 439 try req.connection.data.writeAll(bytes); 440 try req.connection.data.writeAll("\r\n"); 441 442 return bytes.len; 443 }, 444 .content_length => |*len| { 445 if (len.* < bytes.len) return error.MessageTooLong; 446 447 const amt = try req.connection.data.write(bytes); 448 len.* -= amt; 449 return amt; 450 }, 451 .none => return error.NotWriteable, 452 } 453 } 454 455 /// Finish the body of a request. This notifies the server that you have no more data to send. 456 pub fn finish(req: *Request) !void { 457 switch (req.headers.transfer_encoding) { 458 .chunked => try req.connection.data.writeAll("0\r\n"), 459 .content_length => |len| if (len != 0) return error.MessageNotCompleted, 460 .none => {}, 461 } 462 } 463 464 inline fn int16(array: *const [2]u8) u16 { 465 return @bitCast(u16, array.*); 466 } 467 468 inline fn int32(array: *const [4]u8) u32 { 469 return @bitCast(u32, array.*); 470 } 471 472 inline fn int64(array: *const [8]u8) u64 { 473 return @bitCast(u64, array.*); 474 } 475 476 test { 477 const builtin = @import("builtin"); 478 479 if (builtin.os.tag == .wasi) return error.SkipZigTest; 480 481 _ = Response; 482 }