diff --git a/src/main.zig b/src/main.zig index d457a74..4fe4933 100644 --- a/src/main.zig +++ b/src/main.zig @@ -18,83 +18,12 @@ const IndexHandler = struct { } }; -const UploadHandler = struct { - const start = std.mem.indexOf(u8, template, key).?; - const end = start + key.len; - - pub fn post(self: *@This(), req: *Request, resp: *Response) !void { - var content_type = req.headers.getDefault("Content-Type", ""); - if (!std.mem.startsWith(u8, content_type, "multipart/form-data")) { - resp.status = web.responses.BAD_REQUEST; - return; - } - - var form = web.forms.Form.init(resp.allocator); - form.parse(req) catch |err| switch (err) { - error.NotImplemented => { - resp.status = web.responses.REQUEST_ENTITY_TOO_LARGE; - try resp.stream.writeAll("TODO: Handle large uploads"); - return; - }, - else => return err, - }; - - var part = form.files.get("file").?; - - var extensions = registry.getExtensionsByType(part.content_type); - if (extensions == null) return error.InvalidContentMimeType; - const extension = extensions.?.items[0]; - - var image_id_buffer: [256]u8 = undefined; - const image_id = generateImageId(&image_id_buffer); - - var image_path_buffer: [512]u8 = undefined; - const image_path = try std.fmt.bufPrint( - &image_path_buffer, - "{s}/{s}{s}", - .{ images_dir_path, image_id, extension }, - ); - - const image_file = try std.fs.cwd().createFile(image_path, .{}); - try image_file.writer().writeAll(part.body); - - try resp.stream.writeAll(image_path); - return; - } -}; - -const FetchHandler = struct { - pub fn get(self: *@This(), req: *Request, resp: *Response) !void { - const filename = req.args.?[0].?; - std.log.info("got name: {s}", .{filename}); - var image_path_buffer: [512]u8 = undefined; - - const images_dir = try std.fs.cwd().openDir(images_dir_path, .{}); - const image_path = try std.fmt.bufPrint( - &image_path_buffer, - "{s}/{s}", - .{ images_dir_path, filename }, - ); - - // TODO return 404 on error - const image_file = try std.fs.cwd().openFile(image_path, .{ .read = false }); - while (true) { - var file_write_buffer: [1024]u8 = undefined; - const bytes_read = try image_file.read(&file_write_buffer); - if (bytes_read == 0) return; - try resp.stream.writeAll(&file_write_buffer); - } - } -}; - pub const io_mode = .evented; pub const log_level = .debug; // The routes must be defined in the "root" pub const routes = [_]web.Route{ web.Route.create("index", "/", IndexHandler), - web.Route.create("upload", "/api/upload", UploadHandler), - web.Route.create("fetch", "/i/(.*)", FetchHandler), }; pub const middleware = [_]web.Middleware{ @@ -120,6 +49,21 @@ pub fn main() !void { try app.listen("127.0.0.1", 8080); try app.start(); } + +// try http.listenAndServe( +// &gpa.allocator, +// bind_addr, +// comptime http.router.router(&[_]http.router.Route{ +// http.router.get("/", index), +// http.router.post("/api/upload", uploadFile), +// http.router.get("/i/:filename", fetchFile), +// }), +// ); + +fn index(response: *http.Response, request: http.Request) !void { + try response.writer().writeAll("Hello Zig!"); +} + fn generateImageId(buffer: []u8) []const u8 { var i: usize = 0; @@ -135,3 +79,217 @@ fn generateImageId(buffer: []u8) []const u8 { return buffer[0..i]; } + +const StreamT = std.io.FixedBufferStream([]const u8); + +const ContentDisposition = struct { + name: []const u8, + filename: []const u8, +}; + +const Part = struct { + disposition: ContentDisposition, + content_type: []const u8, + body: []const u8, +}; + +const Multipart = struct { + stream: StreamT, + boundary: []const u8, + cursor: usize = 0, + + const Self = @This(); + + // TODO: move boundary_buffer to allocator + pub fn init(body: []const u8, content_type: []const u8, boundary_buffer: []u8) !Multipart { + // parse content_type into what we want (the boundary) + var it = std.mem.split(content_type, ";"); + const should_be_multipart = it.next() orelse return error.MissingContentType; + std.log.debug("should be multipart: {s}", .{should_be_multipart}); + if (!std.mem.eql(u8, should_be_multipart, "multipart/form-data")) + return error.InvalidContentType; + + const should_be_boundary = it.next() orelse return error.MissingBoundary; + std.log.debug("should be boundary: {s} {d}", .{ should_be_boundary, should_be_boundary.len }); + if (!std.mem.startsWith(u8, should_be_boundary, " boundary=")) + return error.InvalidBoundary; + + var boundary_it = std.mem.split(should_be_boundary, "="); + _ = boundary_it.next(); + const boundary_value = boundary_it.next() orelse return error.InvalidBoundary; + std.log.debug("boundary value: {s} {d}", .{ boundary_value, boundary_value.len }); + + const actual_boundary_value = try std.fmt.bufPrint(boundary_buffer, "--{s}", .{boundary_value}); + std.log.debug("actual boundary value: {s} {d}", .{ actual_boundary_value, actual_boundary_value.len }); + + return Self{ + .stream = StreamT{ .buffer = body, .pos = 0 }, + .boundary = actual_boundary_value, + }; + } + + pub fn next(self: *Self, hzzp_buffer: []u8) !?Part { + var reader = self.stream.reader(); + // first self.boundary.len+2 bytes MUST be boundary + \r + \n + var boundary_buffer: [512]u8 = undefined; + const maybe_boundary_raw = (try reader.readUntilDelimiterOrEof(&boundary_buffer, '\n')).?; + + const maybe_boundary_strip1 = std.mem.trimRight(u8, maybe_boundary_raw, "\n"); + const maybe_boundary_strip2 = std.mem.trimRight(u8, maybe_boundary_strip1, "\r"); + if (!std.mem.eql(u8, maybe_boundary_strip2, self.boundary)) { + std.log.err("expected '{s}' {}, got '{s}' {}", .{ self.boundary, self.boundary.len, maybe_boundary_strip2, maybe_boundary_strip2.len }); + return error.InvalidBoundaryBody; + } + std.log.debug("got successful boundary {s}", .{maybe_boundary_strip2}); + + // from there ownwards, its just http! + var parser = hzzp.parser.request.create(hzzp_buffer, reader); + + // This is a hack so that it doesnt try to parse an http header. + parser.state = .header; + + var content_disposition: ?ContentDisposition = null; + var content_type: ?[]const u8 = null; + + std.log.debug("next bytes: {any}", .{self.stream.buffer[self.stream.pos..(self.stream.pos + 50)]}); + + while (try parser.next()) |event| { + std.log.debug("got event: {}", .{event}); + switch (event) { + .status => unreachable, + .end => break, + .head_done => {}, + .header => |header| { + // TODO lowercase header name + if (std.mem.eql(u8, header.name, "Content-Disposition")) { + // parse disposition + var disposition_it = std.mem.split(header.value, ";"); + _ = disposition_it.next(); + + var dispo_name: []const u8 = undefined; + var dispo_filename: []const u8 = undefined; + + while (disposition_it.next()) |disposition_part_raw| { + const disposition_part = std.mem.trim(u8, disposition_part_raw, " "); + + if (std.mem.eql(u8, disposition_part, "form-data")) continue; + + // we have an A=B thing + var single_part_it = std.mem.split(disposition_part, "="); + + const inner_part_name = single_part_it.next().?; + const inner_part_value_quoted = single_part_it.next().?; + + const inner_part_value = std.mem.trim(u8, inner_part_value_quoted, "\""); + + if (std.mem.eql(u8, inner_part_name, "name")) dispo_name = inner_part_value; + if (std.mem.eql(u8, inner_part_name, "filename")) dispo_filename = inner_part_value; + } + + content_disposition = ContentDisposition{ + .name = dispo_name, + .filename = dispo_filename, + }; + std.log.debug("got content disposition for part! {}", .{content_disposition}); + } else if (std.mem.eql(u8, header.name, "Content-Type")) { + content_type = header.value; + std.log.debug("got content type for part! {s}", .{content_type}); + } + }, + else => { + std.log.err("unexpected event: {}", .{event}); + @panic("shit"); + }, + } + } + + // the rest of the reader until we find a matching boundary is the part body. + // hzzp does not do it for us because it cant find a body encoding + // (content-length, content-encoding) + // + // we can use the fact that we know the reader is FixedBufferStream + // to extract the remaining body, then trim the boundary! + // + // THIS ASSUMES ONLY ONE FILE IS IN THE WIRE. + + const remaining_body = self.stream.buffer[self.stream.pos..self.stream.buffer.len]; + + var end_boundary_buf: [512]u8 = undefined; + const boundary_end_marker = try std.fmt.bufPrint(&end_boundary_buf, "{s}--\r\n", .{self.boundary}); + const actual_body = std.mem.trim(u8, remaining_body, boundary_end_marker); + + return Part{ + .disposition = content_disposition.?, + .content_type = content_type.?, + .body = actual_body, + }; + } +}; + +fn uploadFile(response: *http.Response, request: http.Request) !void { + std.log.info("upload! got {d} bytes", .{request.body.len}); + + // find content-type header + var it = request.iterator(); + var content_type: ?[]const u8 = null; + while (it.next()) |header| { + if (std.mem.eql(u8, header.key, "Content-Type")) { + content_type = header.value; + } + } + + if (content_type == null) return error.InvalidContentType; + + // parse multipart data + var boundary_buffer: [512]u8 = undefined; + var multipart = try Multipart.init(request.body, content_type.?, &boundary_buffer); + var hzzp_buffer: [1024]u8 = undefined; + + while (try multipart.next(&hzzp_buffer)) |part| { + std.log.info( + "got part from multipart request! name='{s}' filename='{s}' content_type='{s}' length={d}", + .{ part.disposition.name, part.disposition.filename, part.content_type, part.body.len }, + ); + + var extensions = registry.getExtensionsByType(part.content_type); + if (extensions == null) return error.InvalidContentMimeType; + const extension = extensions.?.items[0]; + + var image_id_buffer: [256]u8 = undefined; + const image_id = generateImageId(&image_id_buffer); + + var image_path_buffer: [512]u8 = undefined; + const image_path = try std.fmt.bufPrint( + &image_path_buffer, + "{s}/{s}{s}", + .{ images_dir_path, image_id, extension }, + ); + + const image_file = try std.fs.cwd().createFile(image_path, .{}); + try image_file.writer().writeAll(part.body); + + try response.writer().writeAll(image_path); + return; + } +} + +fn fetchFile(response: *http.Response, request: http.Request, filename: []const u8) !void { + std.log.info("got name: {s}", .{filename}); + var image_path_buffer: [512]u8 = undefined; + + const images_dir = try std.fs.cwd().openDir(images_dir_path, .{}); + const image_path = try std.fmt.bufPrint( + &image_path_buffer, + "{s}/{s}", + .{ images_dir_path, filename }, + ); + + // TODO return 404 on error + const image_file = try std.fs.cwd().openFile(image_path, .{ .read = false }); + while (true) { + var file_write_buffer: [1024]u8 = undefined; + const bytes_read = try image_file.read(&file_write_buffer); + if (bytes_read == 0) return; + try response.writer().writeAll(&file_write_buffer); + } +} diff --git a/zig.mod b/zig.mod index 12f6bbc..d6bcd3c 100644 --- a/zig.mod +++ b/zig.mod @@ -11,6 +11,6 @@ dev_dependencies: name: mimetypes main: mimetypes.zig - - src: git https://github.com/lun-4/zhp commit-fb44af9 + - src: git https://github.com/frmdstryr/zhp name: zhp main: src/zhp.zig