| ... | @@ -4,6 +4,7 @@ const net = @import("net"); | ... | @@ -4,6 +4,7 @@ const net = @import("net"); |
| 4 | const url = @import("url"); | 4 | const url = @import("url"); |
| 5 | const nio = @import("nio"); | 5 | const nio = @import("nio"); |
| 6 | const extras = @import("extras"); | 6 | const extras = @import("extras"); |
| | 7 | const nfs = @import("nfs"); |
| 7 | | 8 | |
| 8 | pub const Method = enum { | 9 | pub const Method = enum { |
| 9 | GET, | 10 | GET, |
| ... | @@ -160,6 +161,14 @@ pub const Status = enum(u10) { | ... | @@ -160,6 +161,14 @@ pub const Status = enum(u10) { |
| 160 | .network_authentication_required => "Network Authentication Required", | 161 | .network_authentication_required => "Network Authentication Required", |
| 161 | }; | 162 | }; |
| 162 | } | 163 | } |
| | 164 | |
| | 165 | pub fn digits(self: Status) [3]u8 { |
| | 166 | var result: [3]u8 = undefined; |
| | 167 | result[0] = @intCast((@intFromEnum(self) / 100) + '0'); |
| | 168 | result[1] = @intCast((@intFromEnum(self) / 10 % 10) + '0'); |
| | 169 | result[2] = @intCast((@intFromEnum(self) % 10) + '0'); |
| | 170 | return result; |
| | 171 | } |
| 163 | }; | 172 | }; |
| 164 | | 173 | |
| 165 | pub fn open(allocator: std.mem.Allocator, method: Method, input: []const u8) !ClientRequest { | 174 | pub fn open(allocator: std.mem.Allocator, method: Method, input: []const u8) !ClientRequest { |
| ... | @@ -302,6 +311,46 @@ pub const HeadersMap = struct { | ... | @@ -302,6 +311,46 @@ pub const HeadersMap = struct { |
| 302 | map.data.deinit(); | 311 | map.data.deinit(); |
| 303 | } | 312 | } |
| 304 | | 313 | |
| | 314 | fn findIndex(map: *const HeadersMap, n: []const u8) ?usize { |
| | 315 | for (0..map.count()) |i| { |
| | 316 | if (std.mem.eql(u8, map.name(i), n)) { |
| | 317 | return i; |
| | 318 | } |
| | 319 | } |
| | 320 | return null; |
| | 321 | } |
| | 322 | |
| | 323 | pub fn append(map: *HeadersMap, n: []const u8, v: []const u8) !void { |
| | 324 | if (map.findIndex(n)) |i| { |
| | 325 | try map.data.appendSlice(i * 4 + 2, ", "); |
| | 326 | try map.data.appendSlice(i * 4 + 2, v); |
| | 327 | return; |
| | 328 | } |
| | 329 | try map.data.appendSlice(try map.data.add(), n); |
| | 330 | try map.data.appendSlice(try map.data.add(), ": "); |
| | 331 | try map.data.appendSlice(try map.data.add(), v); |
| | 332 | try map.data.appendSlice(try map.data.add(), "\r\n"); |
| | 333 | } |
| | 334 | |
| | 335 | pub fn set(map: *HeadersMap, n: []const u8, v: []const u8) !void { |
| | 336 | if (map.findIndex(n)) |i| { |
| | 337 | try map.data.set(i * 4 + 2, v); |
| | 338 | return; |
| | 339 | } |
| | 340 | try map.data.appendSlice(try map.data.add(), n); |
| | 341 | try map.data.appendSlice(try map.data.add(), ": "); |
| | 342 | try map.data.appendSlice(try map.data.add(), v); |
| | 343 | try map.data.appendSlice(try map.data.add(), "\r\n"); |
| | 344 | } |
| | 345 | |
| | 346 | pub fn remove(map: *HeadersMap, n: []const u8) void { |
| | 347 | const i = map.findIndex(n) orelse return; |
| | 348 | map.data.remove(i * 4); |
| | 349 | map.data.remove(i * 4); |
| | 350 | map.data.remove(i * 4); |
| | 351 | map.data.remove(i * 4); |
| | 352 | } |
| | 353 | |
| 305 | pub fn count(map: *const HeadersMap) usize { | 354 | pub fn count(map: *const HeadersMap) usize { |
| 306 | return map.data.lengths.items.len / 4; | 355 | return map.data.lengths.items.len / 4; |
| 307 | } | 356 | } |
| ... | @@ -323,3 +372,184 @@ pub const HeadersMap = struct { | ... | @@ -323,3 +372,184 @@ pub const HeadersMap = struct { |
| 323 | return null; | 372 | return null; |
| 324 | } | 373 | } |
| 325 | }; | 374 | }; |
| | 375 | |
| | 376 | pub const Server = struct { |
| | 377 | conn: net.Server.Connection, |
| | 378 | reader: nio.BufferedReader(4096, net.Stream), |
| | 379 | writer: nio.BufferedWriter(4096, net.Stream), |
| | 380 | state: enum { |
| | 381 | ready, |
| | 382 | receiving_head, |
| | 383 | received_head, |
| | 384 | }, |
| | 385 | |
| | 386 | pub fn init(conn: net.Server.Connection) Server { |
| | 387 | return .{ |
| | 388 | .conn = conn, |
| | 389 | .reader = .init(conn.stream), |
| | 390 | .writer = .init(conn.stream), |
| | 391 | .state = .ready, |
| | 392 | }; |
| | 393 | } |
| | 394 | |
| | 395 | pub fn receiveHead(server: *Server, allocator: std.mem.Allocator) !ServerRequest { |
| | 396 | std.debug.assert(server.state == .ready); |
| | 397 | server.state = .receiving_head; |
| | 398 | var scratch_buffer: [8192]u8 = undefined; |
| | 399 | |
| | 400 | const method_s = try server.reader.readUntilDelimitersBuf(&scratch_buffer, " "); |
| | 401 | const method = std.meta.stringToEnum(Method, method_s) orelse return error.InvalidRequest; |
| | 402 | |
| | 403 | const target_s = try server.reader.readUntilDelimitersBuf(&scratch_buffer, " "); |
| | 404 | const target_url_root: url.URL = .{ |
| | 405 | .href = "file:///", |
| | 406 | .protocol = "file:", |
| | 407 | .username = "", |
| | 408 | .password = "", |
| | 409 | .hostname = "", |
| | 410 | .hostname_kind = .unset, |
| | 411 | .port = "", |
| | 412 | .host = "", |
| | 413 | .pathname = "/", |
| | 414 | .search = "", |
| | 415 | .hash = "", |
| | 416 | .has_opaque_path = false, |
| | 417 | }; |
| | 418 | const target_url = try url.URL.parseBasic(allocator, target_s, &target_url_root, null); |
| | 419 | errdefer allocator.free(target_url.href); |
| | 420 | |
| | 421 | const version_s = try server.reader.readUntilDelimitersBuf(&scratch_buffer, "\r\n"); |
| | 422 | if (version_s.len != 8) return error.InvalidRequest; |
| | 423 | if (std.mem.bytesToValue(u64, version_s[0..8]) != comptime std.mem.bytesToValue(u64, "HTTP/1.1")) return error.InvalidRequest; |
| | 424 | |
| | 425 | var headers: HeadersMap = .init(allocator); |
| | 426 | errdefer headers.deinit(); |
| | 427 | try headers.data.list.ensureUnusedCapacity(allocator, 512); |
| | 428 | try headers.data.lengths.ensureUnusedCapacity(allocator, 40); |
| | 429 | |
| | 430 | while (true) { |
| | 431 | const line = try server.reader.readUntilDelimitersBuf(&scratch_buffer, "\r\n"); |
| | 432 | if (line.len == 0) break; |
| | 433 | const name_end = std.mem.indexOfScalar(u8, line, ':') orelse return error.InvalidRequest; |
| | 434 | const name = line[0..name_end]; |
| | 435 | for (name) |*c| { |
| | 436 | switch (c.*) { |
| | 437 | 'A'...'Z' => {}, |
| | 438 | 'a'...'z' => {}, |
| | 439 | '0'...'9' => {}, |
| | 440 | '-' => {}, |
| | 441 | else => return error.InvalidRequest, |
| | 442 | } |
| | 443 | switch (c.*) { |
| | 444 | 'A'...'Z' => c.* = c.* - 'A' + 'a', |
| | 445 | else => {}, |
| | 446 | } |
| | 447 | } |
| | 448 | const value = std.mem.trim(u8, line[name_end + 1 ..], " "); |
| | 449 | try headers.append(name, value); |
| | 450 | } |
| | 451 | server.state = .received_head; |
| | 452 | |
| | 453 | return .{ |
| | 454 | .server = server, |
| | 455 | .method = method, |
| | 456 | .target = target_url, |
| | 457 | .headers = headers, |
| | 458 | }; |
| | 459 | } |
| | 460 | }; |
| | 461 | |
| | 462 | pub const ServerRequest = struct { |
| | 463 | server: *Server, |
| | 464 | method: Method, |
| | 465 | target: url.URL, |
| | 466 | headers: HeadersMap, |
| | 467 | |
| | 468 | pub fn deinit(req: *ServerRequest, allocator: std.mem.Allocator) void { |
| | 469 | allocator.free(req.target.href); |
| | 470 | req.headers.deinit(); |
| | 471 | } |
| | 472 | |
| | 473 | pub fn readAllAlloc(req: *ServerRequest, allocator: std.mem.Allocator, max_size: usize) ![]u8 { |
| | 474 | if (req.headers.find("content-length")) |s| { |
| | 475 | const content_length = try extras.parseDigits(u64, s, 10); |
| | 476 | if (content_length > max_size) return error.StreamTooLong; |
| | 477 | var list: std.ArrayListUnmanaged(u8) = .{}; |
| | 478 | try list.ensureUnusedCapacity(allocator, content_length); |
| | 479 | var total: usize = 0; |
| | 480 | while (total < content_length) { |
| | 481 | const len = try req.server.reader.read(list.items.ptr[total..list.capacity]); |
| | 482 | if (len == 0) break; |
| | 483 | total += len; |
| | 484 | list.items.len += len; |
| | 485 | } |
| | 486 | return list.toOwnedSlice(allocator); |
| | 487 | } |
| | 488 | if (req.headers.find("transfer-encoding")) |s| { |
| | 489 | if (std.mem.eql(u8, s, "chunked")) { |
| | 490 | return error.TEChunked; |
| | 491 | } |
| | 492 | return error.TE; |
| | 493 | } |
| | 494 | return ""; |
| | 495 | } |
| | 496 | |
| | 497 | pub fn pipeTo(req: *ServerRequest, writable: anytype, max_size: ?usize) !void { |
| | 498 | if (req.headers.find("content-length")) |s| { |
| | 499 | const content_length = try extras.parseDigits(u64, s, 10); |
| | 500 | if (max_size) |max| if (content_length > max) return error.StreamTooLong; |
| | 501 | var total: usize = 0; |
| | 502 | var scratch_buffer: [4096]u8 = undefined; |
| | 503 | while (total < content_length) { |
| | 504 | const len = try req.server.reader.read(&scratch_buffer); |
| | 505 | if (len == 0) break; |
| | 506 | total += len; |
| | 507 | try writable.writeAll(scratch_buffer[0..len]); |
| | 508 | } |
| | 509 | return; |
| | 510 | } |
| | 511 | if (req.headers.find("transfer-encoding")) |s| { |
| | 512 | if (std.mem.eql(u8, s, "chunked")) { |
| | 513 | return error.TEChunked; |
| | 514 | } |
| | 515 | return error.TE; |
| | 516 | } |
| | 517 | } |
| | 518 | |
| | 519 | pub fn respondFull(req: *ServerRequest, status: Status, headers: *HeadersMap, body: []const u8) !void { |
| | 520 | try req.server.writer.writevAll(&.{ "HTTP/1.1", " ", &status.digits(), " ", status.phrase(), "\r\n" }); |
| | 521 | |
| | 522 | try headers.set("connection", "close"); |
| | 523 | headers.remove("content-length"); |
| | 524 | try req.server.writer.writeAll(headers.data.list.items); |
| | 525 | try req.server.writer.writeAll("content-length: "); |
| | 526 | try req.server.writer.writeIntPretty(body.len, 10, .lower); |
| | 527 | try req.server.writer.writeAll("\r\n"); |
| | 528 | |
| | 529 | try req.server.writer.writeAll("\r\n"); |
| | 530 | try req.server.writer.writeAll(body); |
| | 531 | } |
| | 532 | |
| | 533 | pub fn respondStreaming(req: *ServerRequest, status: Status, headers: *HeadersMap, body_length: ?u64) !void { |
| | 534 | try req.server.writer.writevAll(&.{ "HTTP/1.1", " ", &status.digits(), " ", status.phrase(), "\r\n" }); |
| | 535 | |
| | 536 | headers.remove("connection"); |
| | 537 | headers.remove("content-length"); |
| | 538 | headers.remove("transfer-encoding"); |
| | 539 | try req.server.writer.writeAll(headers.data.list.items); |
| | 540 | try req.server.writer.writeAll("connection: close\r\n"); |
| | 541 | if (body_length) |len| { |
| | 542 | try req.server.writer.writeAll("content-length: "); |
| | 543 | try req.server.writer.writeIntPretty(len, 10, .lower); |
| | 544 | try req.server.writer.writeAll("\r\n"); |
| | 545 | } else { |
| | 546 | try req.server.writer.writeAll("transfer-encoding: chunked\r\n"); |
| | 547 | } |
| | 548 | |
| | 549 | try req.server.writer.writeAll("\r\n"); |
| | 550 | } |
| | 551 | |
| | 552 | pub fn sendfile(req: *ServerRequest, file: nfs.File, offset: net.off_t, count: ?usize) !void { |
| | 553 | return req.server.conn.stream.sendfile(file, offset, count); |
| | 554 | } |
| | 555 | }; |