diff --git a/src/main.zig b/src/main.zig index ac19d7a..4fe4933 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,40 +1,65 @@ const std = @import("std"); -const http = @import("apple_pie"); +const web = @import("zhp"); const hzzp = @import("hzzp"); const mimetypes = @import("mimetypes"); +const Request = web.Request; +const Response = web.Response; + const images_dir_path = "./images"; var registry: mimetypes.Registry = undefined; -pub fn main() anyerror!void { +var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + +const IndexHandler = struct { + pub fn get(self: *@This(), req: *Request, resp: *Response) !void { + try resp.stream.writeAll("Hello, World!"); + } +}; + +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), +}; + +pub const middleware = [_]web.Middleware{ + web.Middleware.create(web.middleware.LoggingMiddleware), +}; + +pub fn main() !void { std.log.info("welcome to webscale", .{}); - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); + defer std.debug.assert(!gpa.deinit()); + const allocator = &gpa.allocator; registry = mimetypes.Registry.init(std.heap.page_allocator); defer registry.deinit(); try registry.load(); - // TODO: configurable addr via env var - const bind_addr = try std.net.Address.parseIp("0.0.0.0", 8080); - std.log.info("serving on {}", .{bind_addr}); - // TODO: configurable path via env var try std.fs.cwd().makePath(images_dir_path); - 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), - }), - ); + var app = web.Application.init(allocator, .{ .debug = true }); + + defer app.deinit(); + 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!"); } diff --git a/src/main_old_apple_pie.zig b/src/main_old_apple_pie.zig new file mode 100644 index 0000000..ac19d7a --- /dev/null +++ b/src/main_old_apple_pie.zig @@ -0,0 +1,270 @@ +const std = @import("std"); +const http = @import("apple_pie"); +const hzzp = @import("hzzp"); +const mimetypes = @import("mimetypes"); + +const images_dir_path = "./images"; + +var registry: mimetypes.Registry = undefined; + +pub fn main() anyerror!void { + std.log.info("welcome to webscale", .{}); + + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + + registry = mimetypes.Registry.init(std.heap.page_allocator); + defer registry.deinit(); + try registry.load(); + + // TODO: configurable addr via env var + const bind_addr = try std.net.Address.parseIp("0.0.0.0", 8080); + std.log.info("serving on {}", .{bind_addr}); + + // TODO: configurable path via env var + try std.fs.cwd().makePath(images_dir_path); + + 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; + + const seed = @truncate(u64, @bitCast(u128, std.time.nanoTimestamp())); + var r = std.rand.DefaultPrng.init(seed); + + while (i < 16) : (i += 1) { + // random ascii lowercase char + var idx = @intCast(u8, r.random.uintLessThan(u5, 24)); + var letter = @as(u8, 97) + idx; + buffer[i] = letter; + } + + 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 a7359dd..d6bcd3c 100644 --- a/zig.mod +++ b/zig.mod @@ -3,10 +3,6 @@ name: yet-another-pomf-clone main: src/main.zig dev_dependencies: - - src: git https://github.com/Luukdegram/apple_pie - name: apple_pie - main: src/apple_pie.zig - - src: git https://github.com/truemedian/hzzp name: hzzp main: src/main.zig @@ -14,3 +10,7 @@ dev_dependencies: - src: git https://github.com/frmdstryr/zig-mimetypes name: mimetypes main: mimetypes.zig + + - src: git https://github.com/frmdstryr/zhp + name: zhp + main: src/zhp.zig