Skip to content

Instantly share code, notes, and snippets.

@notcancername
Created July 14, 2023 13:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save notcancername/636ffbbb86aa4f1f473e41ae4f08d49b to your computer and use it in GitHub Desktop.
Save notcancername/636ffbbb86aa4f1f473e41ae4f08d49b to your computer and use it in GitHub Desktop.
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