mfashby.net

Website mfashby.net
git clone git://code.mfashby.net:/mfashby.net
Log | Files | Refs | Submodules | README

main.zig (15690B)


      1 const std = @import("std");
      2 const log = std.log.scoped(.server);
      3 
      4 // Web server for mfashby.net
      5 // It does _not_ follow the unix philosophy (:
      6 // Initially it replaces caddy, and it needs to be capable of these things 
      7 // to actually be usable
      8 //  - http(s) (otherwise it's not a web server...) ✅ https isn't supported because zig http server (in fact there's no TLS server implementation). For now I'll have to use haproxy
      9 //  - routing, including virtual host              ✅
     10 //  - serving static content from selected folders ✅
     11 //  - executing CGI programs                       ✅
     12 //  - reverse proxy                                ❌
     13 
     14 // And I should probably test it thoroughly before exposing is to the 'net
     15 
     16 // Future possibilities: 
     17 //  - subsume some CGI programs directly into the web server e.g. my comments program
     18 //  - and maybe even cgit (although I might scrap it in favour of stagit if I can figure out archive downloads
     19 //  - do something about efficiency :) it's thread-per-request right now
     20 pub fn main() !void {
     21     var gpa = std.heap.GeneralPurposeAllocator(.{ .thread_safe = true }){};
     22     defer _ = gpa.deinit();
     23     const a = gpa.allocator();
     24     var pool: std.Thread.Pool = undefined;
     25     try std.Thread.Pool.init(&pool, .{ .allocator = a, .n_jobs = 32 });
     26     defer pool.deinit();
     27     var svr = std.http.Server.init(.{ .reuse_address = true, .reuse_port = true });
     28     defer svr.deinit();
     29     const port = 8081;
     30     const addr = std.net.Address.initIp4(.{0,0,0,0}, port);
     31     try svr.listen(addr);
     32     log.info("listening on {}", .{addr});
     33     try acceptLoop(a, &svr, &pool, struct{
     34         fn cancelled() bool {
     35             return false;
     36         }
     37     });
     38 }
     39 
     40 fn acceptLoop(a: std.mem.Allocator, svr: *std.http.Server, pool: *std.Thread.Pool, cancel: anytype) !void {
     41     while (!cancel.cancelled()) {
     42         // Create the Response into the heap so that the ownership goes to the handling thread
     43         const res = try a.create(std.http.Server.Response);
     44         errdefer a.destroy(res);
     45         res.* = try svr.accept(.{ .allocator = a });
     46         errdefer res.deinit();
     47         if (cancel.cancelled()) {
     48             res.deinit();
     49             a.destroy(res);
     50             break;
     51         }
     52         try pool.spawn(handle, .{res});
     53     }
     54 }
     55 
     56 fn handle(res: *std.http.Server.Response) void {
     57     defer res.allocator.destroy(res);
     58     defer res.deinit();
     59     handleRoute(res) catch |e| {
     60         log.info("error {}", .{e});
     61         if (@errorReturnTrace()) |trace| {
     62             std.debug.dumpStackTrace(trace.*);
     63         }
     64         sendError(res, e) catch |e2| {
     65             log.info("error sending error {}", .{e2});
     66             if (@errorReturnTrace()) |trace| {
     67                 std.debug.dumpStackTrace(trace.*);
     68             }
     69         };
     70     };
     71 }
     72 
     73 fn handleRoute(res: *std.http.Server.Response) !void {
     74     try res.wait();
     75 
     76     // Route, virtual host first
     77     var host: []const u8 = "";
     78     if (res.request.headers.getFirstValue("Host")) |host_header| {
     79         var spl = std.mem.splitScalar(u8, host_header, ':');
     80         host = spl.first();
     81     }
     82 
     83     const uri = std.Uri.parseWithoutScheme(res.request.target) catch {
     84         res.status = .bad_request;
     85         const msg = "bad request target";
     86         res.transfer_encoding = .{ .content_length = msg.len };
     87         try res.send();
     88         try res.writeAll(msg);
     89         try res.finish();
     90         return;
     91     };
     92 
     93     if (std.mem.eql(u8, host, "localhost")) {
     94         if (std.mem.startsWith(u8,  uri.path, "/api")) {
     95             try serveCgi(res, "/home/martin/dev/mfashby.net/comments/zig-out/bin/comments",
     96                 &.{"DATABASE_URL","SMTP_USERNAME","SMTP_PASSWORD","NOTIFICATION_ADDRESS","SMTP_SERVER"});
     97         } else {
     98             try serveStatic(res, "public");
     99         }
    100     } else {
    101         const ans = "You have reached mfashby.net ... but did you mean to?";
    102         res.status = .ok;
    103         res.transfer_encoding = .{ .content_length = ans.len };
    104         try res.send();
    105         try res.writeAll(ans);
    106         try res.finish();
    107     }
    108 }
    109 
    110 fn serveCgi(res: *std.http.Server.Response, executable: []const u8,
    111             comptime env_copy: []const []const u8) !void {
    112     var child = std.ChildProcess.init(&.{executable}, res.allocator);
    113     child.stdin_behavior = .Pipe;
    114     child.stdout_behavior = .Pipe;
    115     child.stderr_behavior = .Pipe;
    116 
    117     var env = std.process.EnvMap.init(res.allocator);
    118     try env.put("REQUEST_METHOD", @tagName(res.request.method));
    119     try env.put("REQUEST_URI", res.request.target);
    120     inline for (env_copy) |key| {
    121         try env.put(key, std.os.getenv(key) orelse "");
    122     }
    123     var clb = [_]u8{0}**30;
    124     if (res.request.method == .POST) {
    125         if (res.request.content_length) |cl| {
    126             try env.put("CONTENT_LENGTH", try std.fmt.bufPrint(&clb, "{}", .{cl}));
    127         }
    128     }
    129     child.env_map = &env;
    130     try child.spawn();
    131     
    132     if (res.request.method == .POST) {
    133         if (res.request.content_length) |cl| {
    134             log.info("sending {} data as CGI body", .{cl});
    135             try pump(res.reader(), child.stdin.?.writer(), cl);
    136         } else {
    137             _ = try pumpUnknown(res.reader(), child.stdin.?.writer());
    138         }
    139     }
    140     var stdout = std.ArrayList(u8).init(res.allocator);
    141     var stderr = std.ArrayList(u8).init(res.allocator);
    142     defer stdout.deinit();
    143     defer stderr.deinit();
    144     defer {
    145         if (stderr.items.len>0) {
    146             log.err("CGI error: {s}", .{stderr.items});
    147         }
    148     }
    149     try child.collectOutput(&stdout, &stderr, 1_000_000);
    150     const term = try child.wait();
    151     if (term.Exited != 0) {
    152         return error.ProcessError;
    153     }
    154 
    155     var fbs = std.io.fixedBufferStream(stdout.items);
    156     var reader = fbs.reader();
    157     var headerLine = std.ArrayList(u8).init(res.allocator);
    158     defer headerLine.deinit();
    159     while (true) {
    160         headerLine.clearRetainingCapacity();
    161         try reader.streamUntilDelimiter(headerLine.writer(), '\r', 8192);
    162         _ = try reader.skipBytes(1, .{}); // \n
    163         if (headerLine.items.len == 0) {
    164             break;
    165         }
    166         var spl = std.mem.splitScalar(u8, headerLine.items, ':');
    167         const key = try std.ascii.allocLowerString(res.allocator, spl.first());
    168         defer res.allocator.free(key);
    169         if (std.mem.eql(u8, key, "status")) {
    170             const value = spl.rest();
    171             var spl2 = std.mem.splitScalar(u8, std.mem.trim(u8, value, " "), ' ');
    172             res.status = @enumFromInt(try std.fmt.parseInt(u16, spl2.first(), 10));
    173             log.info("status from CGI {}", .{res.status});
    174         } else if (std.mem.eql(u8, key, "content-length")) {
    175             const value = spl.rest();
    176             res.transfer_encoding = .{.content_length = try std.fmt.parseInt(usize, value, 10)};
    177             log.info("transfer_encoding from CGI {}", .{res.transfer_encoding});
    178         } else {
    179             const value = spl.rest();
    180             try res.headers.append(key, std.mem.trim(u8, value, " "));
    181             log.info("header from CGI {s}: {s}", .{key, value});
    182         }
    183     }
    184 
    185     if (res.transfer_encoding == .content_length) {
    186         try res.send();
    187         try pump(reader, res.writer(), res.transfer_encoding.content_length);
    188     } else {
    189         res.transfer_encoding = .chunked;
    190         try res.send();
    191         _ = try pumpUnknown(reader, res.writer());
    192     }
    193     try res.finish();
    194 }
    195 
    196 fn serveStatic(res: *std.http.Server.Response, dirname: []const u8) !void {
    197     const dirpath = try std.fs.realpathAlloc(res.allocator, dirname);
    198     defer res.allocator.free(dirpath);
    199 
    200     // Path massaging
    201     const uri = std.Uri.parseWithoutScheme(res.request.target) catch {
    202         res.status = .bad_request;
    203         const msg = "bad request target";
    204         res.transfer_encoding = .{ .content_length = msg.len };
    205         try res.send();
    206         try res.writeAll(msg);
    207         try res.finish();
    208         return;
    209     };
    210     var requested_path = uri.path;
    211     requested_path = try std.fs.path.join(res.allocator, &.{ dirpath, requested_path });
    212     defer res.allocator.free(requested_path);
    213 
    214 
    215     const path = std.fs.realpathAlloc(res.allocator, requested_path) catch |e| {
    216         res.status = switch (e) {
    217             error.FileNotFound => .not_found,
    218             error.AccessDenied => .forbidden,
    219             error.BadPathName => .bad_request,
    220             else => .internal_server_error,
    221         };
    222         const msg = try std.fmt.allocPrint(res.allocator, "error: {}", .{e});
    223         defer res.allocator.free(msg);
    224         res.transfer_encoding = .{ .content_length = msg.len };
    225         try res.send();
    226         try res.writeAll(msg);
    227         try res.finish();
    228         return;
    229     };
    230 
    231     defer res.allocator.free(path);
    232     if (!std.mem.startsWith(u8, path, dirpath)) {
    233         res.status = .bad_request;
    234         const msg = try std.fmt.allocPrint(res.allocator, "Trying to escape the root directory {s}", .{path});
    235         defer res.allocator.free(msg);
    236         res.transfer_encoding = .{ .content_length = msg.len };
    237         try res.send();
    238         try res.writeAll(msg);
    239         try res.finish();
    240         return;
    241     }
    242     const f = std.fs.openFileAbsolute(path, .{}) catch |e| {
    243         res.status = switch (e) {
    244             error.FileNotFound => .not_found,
    245             error.AccessDenied => .forbidden,
    246             error.BadPathName, error.NameTooLong => .bad_request,
    247             else => .internal_server_error,
    248         };
    249         const msg = try std.fmt.allocPrint(res.allocator, "error: {}", .{e});
    250         defer res.allocator.free(msg);
    251         res.transfer_encoding = .{ .content_length = msg.len };
    252         try res.send();
    253         try res.writeAll(msg);
    254         try res.finish();
    255         return;
    256     };
    257     defer f.close();
    258     const stat = try f.stat();
    259     switch (stat.kind) {
    260         .file => {
    261             res.transfer_encoding = .{ .content_length = stat.size };
    262             try res.send();
    263             try pump(f.reader(), res.writer(), stat.size);
    264             try res.finish();
    265         },
    266         .directory => {
    267             const index_path = try std.fs.path.join(res.allocator, &.{path, "index.html"});
    268             defer res.allocator.free(index_path);
    269             const index_f = std.fs.openFileAbsolute(index_path, .{}) catch |e| {
    270                 res.status = switch (e) {
    271                     error.FileNotFound => .not_found,
    272                     error.AccessDenied => .forbidden,
    273                     error.BadPathName, error.NameTooLong => .bad_request,
    274                     else => .internal_server_error,
    275                 };
    276                 const msg = try std.fmt.allocPrint(res.allocator, "error: {}", .{e});
    277                 defer res.allocator.free(msg);
    278                 res.transfer_encoding = .{ .content_length = msg.len };
    279                 try res.send();
    280                 try res.writeAll(msg);
    281                 try res.finish();
    282                 return;
    283             };
    284             defer index_f.close();
    285             const index_stat = try index_f.stat();
    286             res.transfer_encoding = .{ .content_length = index_stat.size };
    287             try res.send();
    288             try pump(index_f.reader(), res.writer(), index_stat.size);
    289             try res.finish();  
    290         },
    291         else => {
    292             const msg = "unable to serve unsupported file kind";
    293             res.status = .unavailable_for_legal_reasons;
    294             res.transfer_encoding = .{.content_length = msg.len};
    295             try res.send();
    296             try res.writeAll(msg);
    297             try res.finish();
    298         }
    299     }
    300     
    301 }
    302 
    303 fn pumpUnknown(reader: anytype, writer: anytype) !usize {
    304     var read: usize = 0;
    305     var buf: [1024]u8 = undefined;
    306     while (true) {
    307         const sz = try reader.read(&buf);
    308         if (sz == 0) break;
    309         read += sz;
    310         try writer.writeAll(buf[0..sz]);
    311     }
    312     return read;
    313 }
    314 
    315 fn pump(reader: anytype, writer: anytype, expected: usize) !void {
    316     var read: usize = 0;
    317     var buf: [1024]u8 = undefined;
    318     while (true) {
    319         const sz = try reader.read(&buf);
    320         if (sz == 0) break;
    321         read += sz;
    322         if (read > expected) return error.TooMuchData;
    323         try writer.writeAll(buf[0..sz]);
    324     }
    325     if (read != expected) {
    326         return error.NotEnoughData;
    327     }
    328 }
    329 
    330 
    331 fn sendError(res: *std.http.Server.Response, e: anyerror) !void {
    332     switch (res.state) {
    333         .first, .start, .waited => {
    334             if (res.state != .waited) {
    335                 try res.wait();
    336             }
    337             const errmsg = try std.fmt.allocPrint(res.allocator, "Error: {}", .{e});
    338             defer res.allocator.free(errmsg);
    339             // Now send an error
    340             res.status = .internal_server_error;
    341             res.transfer_encoding = .{ .content_length = errmsg.len };
    342             try res.send();
    343             try res.writeAll(errmsg);
    344             try res.finish();
    345         },
    346         .responded, .finished => {
    347             // Too late!
    348             log.err("can't send an error, response already sent, state {}", .{res.state});
    349         },
    350     }
    351 }
    352 
    353 const Fixture = struct {
    354     a: std.mem.Allocator,
    355     port: u16,
    356     t: std.Thread,
    357     cancel: std.atomic.Value(bool),
    358     base_url: []const u8,
    359     thread_pool: std.Thread.Pool,
    360     server: std.http.Server,
    361     client: std.http.Client,
    362 
    363     fn init(a: std.mem.Allocator) !*Fixture {
    364         var res: *Fixture = try a.create(Fixture);
    365         errdefer a.destroy(res);
    366         res.a = a;
    367         try std.Thread.Pool.init(&res.thread_pool, .{ .allocator = a, .n_jobs = 32 });
    368         errdefer res.thread_pool.deinit();
    369         res.server = std.http.Server.init(.{ .reuse_address = true, .reuse_port = true });
    370         errdefer res.server.deinit();
    371         const addr = std.net.Address.initIp4(.{127,0,0,1}, 0);
    372         try res.server.listen(addr);
    373         res.port = res.server.socket.listen_address.in.getPort();
    374         res.base_url = try std.fmt.allocPrint(a, "http://localhost:{}", .{res.port});
    375         errdefer a.free(res.base_url);
    376         res.cancel = std.atomic.Value(bool).init(false);
    377         res.t = try std.Thread.spawn(.{}, acceptLoop, .{a, &res.server, &res.thread_pool, res});
    378         res.client = .{.allocator = a};
    379         return res;
    380     }
    381 
    382     fn deinit(self: *Fixture) void {
    383         self.client.deinit();
    384         self.a.free(self.base_url);
    385         self.cancel.store(true, .Unordered);
    386         // Trigger the server's accept() method to wake it up
    387         if (std.net.tcpConnectToAddress(std.net.Address.initIp4(.{127,0,0,1}, self.port))) |stream| {
    388             stream.close();
    389         } else |_| {}
    390         self.t.join();
    391         self.thread_pool.deinit();
    392         self.server.deinit();
    393         self.a.destroy(self);
    394     }
    395 
    396     fn cancelled(self: *Fixture) bool {
    397         return self.cancel.load(.Unordered);
    398     }
    399 };
    400 
    401 
    402 test "static pages" {
    403     const a = std.testing.allocator;
    404     var f = try Fixture.init(a);
    405     defer f.deinit();
    406     const TestCase = struct {
    407         path: []const u8,
    408         file: []const u8,
    409     };
    410     const test_cases = [_]TestCase{
    411         .{ .path = "/", .file = "public/index.html"},
    412         .{ .path = "/posts/2018-05-31-new-site/", .file = "public/posts/2018-05-31-new-site/index.html"},
    413     };
    414 
    415     for (test_cases) |test_case| {
    416         const url = try std.fmt.allocPrint(a, "{s}{s}", .{f.base_url, test_case.path});
    417         defer a.free(url);
    418         var fr = try f.client.fetch(a, .{
    419             .location = .{.url = url}
    420             });
    421         defer fr.deinit();
    422         try std.testing.expectEqual(std.http.Status.ok, fr.status);
    423         const expected = try std.fs.cwd().readFileAlloc(a, test_case.file, 1_000_000);
    424         defer a.free(expected);
    425         try std.testing.expectEqualStrings(expected, fr.body.?);
    426     }
    427 }
    428 
    429 // test "404" {
    430 
    431 // }