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 // }