╭ serve.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const xit = @import("xit"); 3 const rp = xit.repo; 4 const web = @import("./web.zig"); 5 const serve_common = @import("./serve_common.zig"); 6 const serve_ssh_protocol = @import("./serve_ssh_protocol.zig"); 7 const serve_ssh = @import("./serve_ssh.zig"); 8 const serve_http = @import("./serve_http.zig"); 9 10 pub const Options = struct { 11 http_listen: []const u8 = "127.0.0.1:8080", 12 ssh_listen: []const u8 = "127.0.0.1:8022", 13 wui_listen: []const u8 = "127.0.0.1:8000", 14 data_dir: []const u8 = ".", 15 16 // the port from `wui_listen`, for the TUI footer's url. 17 pub fn wuiPort(self: Options) !u16 { 18 return (try parseListenAddress(self.wui_listen)).port; 19 } 20 }; 21 22 const ListenAddress = struct { 23 host: []const u8, 24 port: u16, 25 }; 26 27 pub fn run( 28 comptime repo_kind: rp.RepoKind, 29 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 30 io: std.Io, 31 allocator: std.mem.Allocator, 32 cwd_path: []const u8, 33 options: Options, 34 err: *std.Io.Writer, 35 runnable: anytype, 36 ) !void { 37 // create the data dir 38 39 const data_dir_path = try std.fs.path.resolve(allocator, &.{ cwd_path, options.data_dir }); 40 defer allocator.free(data_dir_path); 41 42 const data_dir = try std.Io.Dir.cwd().createDirPathOpen(io, data_dir_path, .{}); 43 defer data_dir.close(io); 44 45 // create the repos dir 46 47 const repo_root_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "repos" }); 48 defer allocator.free(repo_root_path); 49 50 try std.Io.Dir.cwd().createDirPath(io, repo_root_path); 51 52 // the admin repo path is where the user/repo metadata events live 53 54 const admin_repo_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "admin" }); 55 defer allocator.free(admin_repo_path); 56 57 // create http listener 58 59 const http_listen_address = try parseListenAddress(options.http_listen); 60 const http_address = try std.Io.net.IpAddress.parseIp4(http_listen_address.host, http_listen_address.port); 61 var http_server = try http_address.listen(io, .{ .reuse_address = true }); 62 defer http_server.deinit(io); 63 64 // create ssh listener 65 66 const ssh_listen_address = try parseListenAddress(options.ssh_listen); 67 const ssh_address = try std.Io.net.IpAddress.parseIp4(ssh_listen_address.host, ssh_listen_address.port); 68 var ssh_server = try ssh_address.listen(io, .{ .reuse_address = true }); 69 defer ssh_server.deinit(io); 70 71 // load or generate the SSH host key 72 const host_key = try serve_ssh_protocol.HostKey.loadOrGenerate(io, allocator, data_dir_path); 73 74 // disk-backed store for web login sessions 75 const session_store = try web.SessionStore.init(io, data_dir); 76 defer session_store.deinit(); 77 78 // create wui listener 79 80 const wui_listen_address = try parseListenAddress(options.wui_listen); 81 const wui_address = try std.Io.net.IpAddress.parseIp4(wui_listen_address.host, wui_listen_address.port); 82 var wui_server = try wui_address.listen(io, .{ .reuse_address = true }); 83 defer wui_server.deinit(io); 84 85 // start task group 86 87 var tasks: std.Io.Group = .init; 88 defer tasks.cancel(io); 89 90 // run listeners 91 92 try err.print("serving HTTP on {s}, repo root {s}\n", .{ options.http_listen, repo_root_path }); 93 try err.flush(); 94 95 serve_http.runListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, &http_server, &tasks, err); 96 97 try err.print("serving SSH on {s}\n", .{options.ssh_listen}); 98 try err.flush(); 99 100 const ssh_session_handler = serve_ssh.SessionHandler{ 101 .admin_repo_path = admin_repo_path, 102 .repo_root_path = repo_root_path, 103 .wui_port = wui_listen_address.port, 104 .err = err, 105 }; 106 serve_ssh.runListener(io, allocator, &host_key, &ssh_session_handler, &ssh_server, &tasks, err); 107 108 try err.print("serving web UI on http://{s}/\n", .{options.wui_listen}); 109 try err.flush(); 110 111 runWebListener(io, allocator, &wui_server, &tasks, admin_repo_path, session_store, err); 112 113 if (@TypeOf(runnable) != void) { 114 try runnable.run(); 115 } else { 116 try tasks.await(io); 117 } 118 } 119 120 fn runWebListener( 121 io: std.Io, 122 allocator: std.mem.Allocator, 123 net_server: *std.Io.net.Server, 124 tasks: *std.Io.Group, 125 admin_repo_path: []const u8, 126 session_store: web.SessionStore, 127 err: *std.Io.Writer, 128 ) void { 129 const Context = struct { 130 io: std.Io, 131 allocator: std.mem.Allocator, 132 admin_repo_path: []const u8, 133 session_store: web.SessionStore, 134 err: *std.Io.Writer, 135 }; 136 137 const handle = struct { 138 fn h(ctx: Context, stream: std.Io.net.Stream) void { 139 defer stream.close(ctx.io); 140 web.handleConnection(ctx.io, ctx.allocator, stream, ctx.admin_repo_path, ctx.session_store, ctx.err) catch |request_err| { 141 serve_common.logError(ctx.err, "web ui request failed: {s}\n", .{@errorName(request_err)}); 142 }; 143 } 144 }.h; 145 146 serve_common.runListener(io, net_server, tasks, err, "web ui", Context{ 147 .io = io, 148 .allocator = allocator, 149 .admin_repo_path = admin_repo_path, 150 .session_store = session_store, 151 .err = err, 152 }, handle); 153 } 154 155 fn parseListenAddress(value: []const u8) !ListenAddress { 156 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 157 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 158 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 159 return .{ .host = value[0..colon], .port = port }; 160 } 161