╭ web.zig ──────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const rp = xit.repo; 4 const ui = @import("./ui.zig"); 5 const pg = @import("./page.zig"); 6 7 const Asset = struct { 8 path: []const u8, 9 content_type: []const u8, 10 body: []const u8, 11 }; 12 13 const assets = [_]Asset{ 14 .{ .path = "index.html", .content_type = "text/html; charset=utf-8", .body = @embedFile("assets/index.html") }, 15 .{ .path = "script.js", .content_type = "text/javascript; charset=utf-8", .body = @embedFile("assets/script.js") }, 16 .{ .path = "term.ttf", .content_type = "font/ttf", .body = @embedFile("assets/term.ttf") }, 17 .{ .path = "haxy.wasm", .content_type = "application/wasm", .body = @embedFile("assets/haxy.wasm") }, 18 }; 19 20 pub fn run( 21 io: std.Io, 22 allocator: std.mem.Allocator, 23 net_server: *std.Io.net.Server, 24 tasks: *std.Io.Group, 25 admin_repo_path: []const u8, 26 err: *std.Io.Writer, 27 ) void { 28 const Listener = struct { 29 io: std.Io, 30 allocator: std.mem.Allocator, 31 net_server: *std.Io.net.Server, 32 tasks: *std.Io.Group, 33 admin_repo_path: []const u8, 34 err: *std.Io.Writer, 35 36 fn run(ctx: @This()) void { 37 while (true) { 38 const stream = ctx.net_server.accept(ctx.io) catch |accept_err| { 39 if (accept_err == error.Canceled) return; 40 logError(ctx.err, "web ui accept failed: {s}\n", .{@errorName(accept_err)}); 41 continue; 42 }; 43 44 const Connection = struct { 45 io: std.Io, 46 allocator: std.mem.Allocator, 47 stream: std.Io.net.Stream, 48 admin_repo_path: []const u8, 49 err: *std.Io.Writer, 50 51 fn run(conn: @This()) void { 52 defer conn.stream.close(conn.io); 53 handleConnection(conn.io, conn.allocator, conn.stream, conn.admin_repo_path, conn.err) catch |request_err| { 54 logError(conn.err, "web ui request failed: {s}\n", .{@errorName(request_err)}); 55 }; 56 } 57 }; 58 59 ctx.tasks.async(ctx.io, Connection.run, .{Connection{ 60 .io = ctx.io, 61 .allocator = ctx.allocator, 62 .stream = stream, 63 .admin_repo_path = ctx.admin_repo_path, 64 .err = ctx.err, 65 }}); 66 } 67 } 68 }; 69 70 tasks.async(io, Listener.run, .{Listener{ 71 .io = io, 72 .allocator = allocator, 73 .net_server = net_server, 74 .tasks = tasks, 75 .admin_repo_path = admin_repo_path, 76 .err = err, 77 }}); 78 } 79 80 fn renderIndexHtml(io: std.Io, allocator: std.mem.Allocator, admin_repo_path: []const u8) ![]const u8 { 81 const template = (findAsset("/index.html") orelse return error.MissingIndexAsset).body; 82 83 // open the admin repo to read live user/repo data; if it doesn't exist 84 // yet (e.g. a fresh `haxy serve` with no admin repo) fall back to empty. 85 const repo_opts: rp.RepoOpts(.xit) = .{}; 86 const Repo = rp.Repo(.xit, repo_opts); 87 var repo_or_err = Repo.open(io, allocator, .{ .path = admin_repo_path }); 88 var page_arena = std.heap.ArenaAllocator.init(allocator); 89 defer page_arena.deinit(); 90 const page: pg.Page = if (repo_or_err) |*repo| blk: { 91 defer repo.deinit(io, allocator); 92 break :blk .{ .user_repo = try .init(repo_opts, &page_arena, repo) }; 93 } else |_| .{ .user_repo = .empty() }; 94 95 var root = try ui.initRoot(allocator, &page); 96 defer root.deinit(); 97 98 const content = try ui.generateHtml(allocator, &root); 99 defer allocator.free(content); 100 101 const needle = "{{{ HAXY_CONTENT }}}"; 102 const index = std.mem.indexOf(u8, template, needle) orelse return error.MissingTemplateToken; 103 104 var out: std.ArrayList(u8) = .empty; 105 errdefer out.deinit(allocator); 106 try out.appendSlice(allocator, template[0..index]); 107 try out.appendSlice(allocator, content); 108 try out.appendSlice(allocator, template[index + needle.len ..]); 109 return try out.toOwnedSlice(allocator); 110 } 111 112 fn handleConnection( 113 io: std.Io, 114 allocator: std.mem.Allocator, 115 stream: std.Io.net.Stream, 116 admin_repo_path: []const u8, 117 err: *std.Io.Writer, 118 ) !void { 119 var send_buffer = [_]u8{0} ** 4096; 120 var recv_buffer = [_]u8{0} ** 4096; 121 var conn_br = stream.reader(io, &recv_buffer); 122 var conn_bw = stream.writer(io, &send_buffer); 123 var http_server = std.http.Server.init(&conn_br.interface, &conn_bw.interface); 124 125 while (http_server.reader.state == .ready) { 126 var request = http_server.receiveHead() catch |receive_err| switch (receive_err) { 127 error.HttpConnectionClosing => break, 128 error.ReadFailed => break, 129 else => |e| return e, 130 }; 131 132 handleRequest(io, &http_server, &request, allocator, admin_repo_path) catch |request_err| { 133 try err.print("web ui request failed: {s}\n", .{@errorName(request_err)}); 134 try err.flush(); 135 if (http_server.reader.state == .received_head) { 136 http_server.reader.state = .ready; 137 } 138 try writeSimpleResponse(&http_server, 500, "Internal Server Error", "text/plain", @errorName(request_err)); 139 }; 140 try http_server.out.flush(); 141 break; 142 } 143 } 144 145 fn handleRequest( 146 io: std.Io, 147 http_server: *std.http.Server, 148 request: *std.http.Server.Request, 149 allocator: std.mem.Allocator, 150 admin_repo_path: []const u8, 151 ) !void { 152 const method = if (request.head.method == .HEAD) .GET else request.head.method; 153 if (method != .GET) { 154 if (http_server.reader.state == .received_head) { 155 http_server.reader.state = .ready; 156 } 157 try writeSimpleResponse(http_server, 405, "Method Not Allowed", "text/plain", "method not allowed"); 158 return; 159 } 160 161 const uri = try std.Uri.parseAfterScheme("", request.head.target); 162 const path = uri.path.percent_encoded; 163 164 if (std.mem.eql(u8, path, "/favicon.ico")) { 165 if (http_server.reader.state == .received_head) { 166 http_server.reader.state = .ready; 167 } 168 try writeStaticResponse(http_server, 204, "No Content", "image/x-icon", "", true); 169 return; 170 } 171 172 const asset = findAsset(path) orelse { 173 if (http_server.reader.state == .received_head) { 174 http_server.reader.state = .ready; 175 } 176 try writeSimpleResponse(http_server, 404, "Not Found", "text/plain", "not found"); 177 return; 178 }; 179 180 if (http_server.reader.state == .received_head) { 181 http_server.reader.state = .ready; 182 } 183 184 if (std.mem.eql(u8, asset.path, "index.html")) { 185 const index_html = try renderIndexHtml(io, allocator, admin_repo_path); 186 defer allocator.free(index_html); 187 try writeStaticResponse(http_server, 200, "OK", asset.content_type, index_html, request.head.method == .HEAD); 188 } else { 189 try writeStaticResponse(http_server, 200, "OK", asset.content_type, asset.body, request.head.method == .HEAD); 190 } 191 } 192 193 fn findAsset(request_path: []const u8) ?Asset { 194 const path = if (std.mem.eql(u8, request_path, "/")) 195 "index.html" 196 else if (request_path.len > 1 and request_path[0] == '/') 197 request_path[1..] 198 else 199 return null; 200 201 for (assets) |asset| { 202 if (std.mem.eql(u8, path, asset.path)) return asset; 203 } 204 return null; 205 } 206 207 fn writeSimpleResponse( 208 http_server: *std.http.Server, 209 code: u16, 210 message: []const u8, 211 content_type: []const u8, 212 body: []const u8, 213 ) !void { 214 try http_server.out.print( 215 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n{s}", 216 .{ code, message, content_type, body.len, body }, 217 ); 218 } 219 220 fn writeStaticResponse( 221 http_server: *std.http.Server, 222 code: u16, 223 message: []const u8, 224 content_type: []const u8, 225 body: []const u8, 226 head_only: bool, 227 ) !void { 228 try http_server.out.print( 229 "HTTP/1.1 {d} {s}\r\nContent-Type: {s}\r\nContent-Length: {d}\r\n\r\n", 230 .{ code, message, content_type, body.len }, 231 ); 232 if (!head_only) { 233 try http_server.out.writeAll(body); 234 } 235 } 236 237 fn logError(err: *std.Io.Writer, comptime fmt: []const u8, args: anytype) void { 238 err.print(fmt, args) catch return; 239 err.flush() catch {}; 240 } 241