Created
July 14, 2023 13:09
-
-
Save notcancername/636ffbbb86aa4f1f473e41ae4f08d49b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const std = @import("std"); | |
fn splice(src_rd: anytype, dst_wr: anytype, comptime buf_len: usize, lim: usize) !void { | |
var buf: [buf_len]u8 = undefined; | |
var left = lim; | |
while (true) { | |
const len = try src_rd.read(buf[0..@min(left, buf.len)]); | |
if (len == 0) break; | |
left = try std.math.sub(usize, left, len); | |
try dst_wr.writeAll(buf[0..len]); | |
} | |
if(left != 0) { | |
return error.PrematureEndOfStream; | |
} | |
} | |
// fn basename(s: []const u8) []const u8 { | |
// const pos = std.mem.lastIndexOfScalar(u8, s, '/') orelse 0; | |
// return s[pos + 1 ..]; | |
// } | |
const basename = std.fs.path.basenamePosix; | |
const default_safe_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 ._-,~#'\"[]{}:"; | |
fn writeHtmlEscaped(writer: anytype, safe_chars: []const u8, s: []const u8) !void { | |
std.debug.assert(std.unicode.utf8ValidateSlice(s)); | |
var buf = std.BoundedArray(u8, 128){}; | |
buf.appendSlice("&#") catch undefined; | |
var cur = s; | |
while (cur.len > 0) { | |
buf.len = 2; | |
const bad_ind = std.mem.indexOfNone(u8, cur, safe_chars) orelse break; | |
const safe = cur[0..bad_ind]; | |
const unsafe = cur[bad_ind..][0..std.unicode.utf8ByteSequenceLength(cur[bad_ind]) catch unreachable]; | |
const unsafe_cp = std.unicode.utf8Decode(unsafe) catch unreachable; | |
try writer.writeAll(safe); | |
try std.fmt.formatInt(unsafe_cp, 10, undefined, .{}, buf.writer()); | |
try buf.append(';'); | |
try writer.writeAll(buf.constSlice()); | |
cur = cur[bad_ind + unsafe.len..]; | |
} | |
try writer.writeAll(cur); | |
} | |
fn writeDirListing(writer: anytype, iter: std.fs.IterableDir.Iterator, dir_name: []const u8) !void { | |
var mut_iter = iter; | |
try writer.writeAll("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body><table>"); | |
while (mut_iter.next() catch return error.IterationFailed) |entry| { | |
try writer.writeAll("<tr><td><a href=\""); | |
try std.Uri.writeEscapedPath(writer, dir_name); | |
try writer.writeByte('/'); | |
try std.Uri.writeEscapedPath(writer, entry.name); | |
try writer.writeAll("\">"); | |
try writeHtmlEscaped(writer, default_safe_chars, entry.name); | |
try writer.writeAll(switch (entry.kind) { | |
.file => "</a></td><td>file</td></tr>", | |
.directory => "</a></td><td>dir</td></tr>", | |
else => "</a></td><td>?</td></tr>", | |
}); | |
} | |
try writer.writeAll("</table></body></html>"); | |
} | |
fn handleRequest(ally: std.mem.Allocator, resp: *std.http.Server.Response) !void { | |
try resp.headers.append("Server", "amogus"); | |
try resp.wait(); | |
switch (resp.request.method) { | |
.GET => {}, | |
else => return error.UnsupportedMethod, | |
} | |
std.log.info("GET `{s}' by {} headers:\n---\n{}---", .{resp.request.target, resp.address, resp.request.headers}); | |
const target_uri = b: { | |
if(std.mem.indexOf(u8, resp.request.target, "://") != null) { | |
break :b try std.Uri.parse(resp.request.target); | |
} else { | |
break :b try std.Uri.parseWithoutScheme(resp.request.target); | |
} | |
}; | |
const path = try std.Uri.unescapeString(ally, target_uri.path); | |
defer ally.free(path); | |
const resolved_path = try std.fs.path.resolvePosix(ally, &.{path}); | |
defer ally.free(resolved_path); | |
if (!std.fs.path.isAbsolutePosix(resolved_path) or std.mem.indexOf(u8, resolved_path, "..") != null) { | |
std.log.warn("bad path: {s}", .{resolved_path}); | |
return error.BadPath; | |
} | |
const file_path = if(resolved_path.len != 1) resolved_path[1..] else "."; | |
const sb = try std.fs.cwd().statFile(file_path); | |
switch (sb.kind) { | |
.file => { | |
const file = std.fs.cwd().openFile(file_path, .{ .mode = .read_only }) catch |err| return switch (err) { | |
error.FileNotFound, | |
error.AccessDenied, | |
error.NameTooLong, | |
=> err, | |
else => error.OpeningFailed, | |
}; | |
defer file.close(); | |
var did_request_range: bool = false; | |
// handle range requests | |
const len: usize = b: { | |
if(resp.request.headers.getFirstValue("Range")) |range| { | |
const trimmed = std.mem.trim(u8, range, &std.ascii.whitespace); | |
// FIXME HACK: handle properly | |
if(!std.mem.startsWith(u8, trimmed, "bytes=")) | |
break :b sb.size; | |
// FIXME HACK: handle properly | |
if(std.mem.indexOfScalar(u8, trimmed, ',') != null) | |
break :b sb.size; | |
did_request_range = true; | |
const byte_range = trimmed["bytes=".len..]; | |
var iter = std.mem.splitScalar(u8, byte_range, '-'); | |
const offset = std.fmt.parseInt(u64, iter.next() orelse break :b sb.size, 10) catch break :b sb.size; | |
const end: ?u64 = il: { | |
const end_str = iter.next() orelse return error.BadRequest; | |
if(end_str.len == 0) break :il null; | |
break :il std.fmt.parseInt(u64, end_str, 10) catch return error.BadRequest; | |
}; | |
// offset greater eof | |
if(offset > sb.size) { | |
return error.RangeNotSatisfiable; | |
} | |
if(end) |e| { | |
// user error | |
if(offset > e) { | |
return error.BadRequest; | |
} | |
// too long | |
if(e > sb.size) { | |
return error.RangeNotSatisfiable; | |
} | |
} | |
const res = if(end) |e| e - offset else sb.size - offset; | |
{ | |
var ba = std.BoundedArray(u8, 128){}; | |
const wr = ba.writer(); | |
try wr.print("bytes {d}-", .{offset}); | |
try wr.print("{d}", .{if(end) |e| e else res}); | |
try wr.print("/{d}", .{res}); | |
try resp.headers.append("Content-Range", ba.constSlice()); | |
} | |
// can't seek | |
file.seekTo(offset) catch return error.RangeNotSatisfiable; | |
break :b res; | |
} | |
break :b sb.size; | |
}; | |
resp.status = if(did_request_range) .partial_content else .ok; | |
resp.transfer_encoding = .{ .content_length = len }; | |
try resp.do(); | |
splice(file.reader(), resp.writer(), 32 << 10, len) catch |err| switch(err) { | |
error.ConnectionResetByPeer => { | |
return; | |
}, | |
else => { | |
std.log.warn("{}", .{err}); | |
return error.SplicingFailed; | |
}, | |
}; | |
try resp.finish(); | |
std.log.info("served file {} {s}", .{@intFromEnum(resp.status), if(resp.status.phrase()) |p| p else ""}); | |
}, | |
.directory => { | |
var dir = std.fs.cwd().openIterableDir(file_path, .{ .no_follow = true, .access_sub_paths = false }) catch |err| return switch (err) { | |
error.FileNotFound, | |
error.AccessDenied, | |
error.NameTooLong, | |
=> err, | |
else => error.OpeningFailed, | |
}; | |
defer dir.close(); | |
try resp.headers.append("Content-Type", "text/html"); | |
resp.status = .ok; | |
resp.transfer_encoding = .{ .chunked = {} }; | |
try resp.do(); | |
var buffered_writer = std.io.bufferedWriter(resp.writer()); | |
const writer = buffered_writer.writer(); | |
writeDirListing(writer, dir.iterateAssumeFirstIteration(), resolved_path) catch |err| { | |
std.log.warn("writing dir listing failed", .{}); | |
return err; | |
}; | |
buffered_writer.flush() catch |err| { | |
std.log.warn("flushing bw failed", .{}); | |
return err; | |
}; | |
try resp.finish(); | |
std.log.info("served dir {} {s}", .{@intFromEnum(resp.status), if(resp.status.phrase()) |p| p else ""}); | |
}, | |
else => return error.NotFileOrDir, | |
} | |
} | |
var should_stop = false; | |
export fn sigusr1_handler(_: c_int) void { | |
should_stop = true; | |
} | |
pub fn main() !void { | |
var ally_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); | |
defer ally_state.deinit(); | |
const ally = ally_state.allocator(); | |
// var ally_state = std.heap.GeneralPurposeAllocator(.{}){}; | |
// defer std.debug.assert(ally_state.deinit() == .ok); | |
// const ally = ally_state.allocator(); | |
{ | |
var set = std.os.empty_sigset; | |
std.os.linux.sigaddset(&set, std.os.linux.SIG.USR1); | |
try std.os.sigaction(std.os.SIG.USR1, &.{.handler = .{.handler = &sigusr1_handler}, .mask = set, .flags = 0}, null); | |
} | |
var server = std.http.Server.init(ally, .{ .reuse_address = true, .reuse_port = true }); | |
defer server.deinit(); | |
try server.listen(std.net.Address.resolveIp("0.0.0.0", 8080) catch unreachable); | |
{ | |
var tmp_ally_state = std.heap.ArenaAllocator.init(std.heap.page_allocator); | |
defer tmp_ally_state.deinit(); | |
const tmp_ally = tmp_ally_state.allocator(); | |
while (true) { | |
if(should_stop) break; | |
defer _ = tmp_ally_state.reset(.{ .retain_with_limit = 10 << 20 }); | |
var resp = server.accept(.{ .allocator = tmp_ally }) catch |e| { | |
std.log.warn("accept failed: {}", .{e}); | |
continue; | |
}; | |
defer resp.deinit(); | |
if(should_stop) break; | |
handleRequest(tmp_ally, &resp) catch |err| respond: { | |
switch (err) { | |
error.FileNotFound => { | |
resp.status = .not_found; | |
}, | |
error.AccessDenied => { | |
resp.status = .forbidden; | |
}, | |
error.BadRequest => { | |
resp.status = .bad_request; | |
}, | |
error.RangeNotSatisfiable => { | |
resp.status = .range_not_satisfiable; | |
}, | |
error.SplicingFailed, | |
error.IterationFailed, | |
error.ConnectionResetByPeer, | |
error.MessageNotCompleted, | |
=> |e| { | |
std.log.warn("{}", .{e}); | |
break :respond; | |
}, | |
else => |e| { | |
resp.status = .internal_server_error; | |
std.log.warn("{}", .{e}); | |
}, | |
} | |
resp.do() catch continue; | |
resp.finish() catch continue; | |
}; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment