| ... | ... | @@ -4,6 +4,7 @@ const net = @import("net"); |
| 4 | 4 | const url = @import("url"); |
| 5 | 5 | const nio = @import("nio"); |
| 6 | 6 | const extras = @import("extras"); |
| 7 | const nfs = @import("nfs"); |
| 7 | 8 | |
| 8 | 9 | pub const Method = enum { |
| 9 | 10 | GET, |
| ... | ... | @@ -160,6 +161,14 @@ pub const Status = enum(u10) { |
| 160 | 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 | 174 | pub fn open(allocator: std.mem.Allocator, method: Method, input: []const u8) !ClientRequest { |
| ... | ... | @@ -302,6 +311,46 @@ pub const HeadersMap = struct { |
| 302 | 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 | 354 | pub fn count(map: *const HeadersMap) usize { |
| 306 | 355 | return map.data.lengths.items.len / 4; |
| 307 | 356 | } |
| ... | ... | @@ -323,3 +372,184 @@ pub const HeadersMap = struct { |
| 323 | 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 | }; |