╭ serve.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 web = @import("./web.zig"); 6 const serve_common = @import("./serve_common.zig"); 7 const serve_ssh_protocol = @import("./serve_ssh_protocol.zig"); 8 const serve_ssh = @import("./serve_ssh.zig"); 9 const serve_http = @import("./serve_http.zig"); 10 11 pub const Options = struct { 12 http_listen: []const u8 = "127.0.0.1:8080", 13 ssh_listen: []const u8 = "127.0.0.1:8022", 14 wui_listen: []const u8 = "127.0.0.1:8000", 15 data_dir: []const u8 = ".", 16 17 // the port from `wui_listen`, for the TUI footer's url. 18 pub fn wuiPort(self: Options) !u16 { 19 return (try parseListenAddress(self.wui_listen)).port; 20 } 21 }; 22 23 const ListenAddress = struct { 24 host: []const u8, 25 port: u16, 26 }; 27 28 pub fn run( 29 comptime repo_kind: rp.RepoKind, 30 comptime any_repo_opts: rp.AnyRepoOpts(repo_kind), 31 io: std.Io, 32 allocator: std.mem.Allocator, 33 cwd_path: []const u8, 34 options: Options, 35 err: *std.Io.Writer, 36 runnable: anytype, 37 ) !void { 38 // create the data dir 39 40 const data_dir_path = try std.fs.path.resolve(allocator, &.{ cwd_path, options.data_dir }); 41 defer allocator.free(data_dir_path); 42 43 const data_dir = try std.Io.Dir.cwd().createDirPathOpen(io, data_dir_path, .{}); 44 defer data_dir.close(io); 45 46 // create the repos dir 47 48 const repo_root_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "repos" }); 49 defer allocator.free(repo_root_path); 50 51 try std.Io.Dir.cwd().createDirPath(io, repo_root_path); 52 53 // the admin repo path is where the user/repo metadata events live 54 55 const admin_repo_path = try std.fs.path.resolve(allocator, &.{ data_dir_path, "admin" }); 56 defer allocator.free(admin_repo_path); 57 58 // create http listener 59 60 const http_listen_address = try parseListenAddress(options.http_listen); 61 const http_address = try std.Io.net.IpAddress.parseIp4(http_listen_address.host, http_listen_address.port); 62 var http_server = try http_address.listen(io, .{ .reuse_address = true }); 63 defer http_server.deinit(io); 64 65 // create ssh listener 66 67 const ssh_listen_address = try parseListenAddress(options.ssh_listen); 68 const ssh_address = try std.Io.net.IpAddress.parseIp4(ssh_listen_address.host, ssh_listen_address.port); 69 var ssh_server = try ssh_address.listen(io, .{ .reuse_address = true }); 70 defer ssh_server.deinit(io); 71 72 // load or generate the SSH host key 73 const host_key = try serve_ssh_protocol.HostKey.loadOrGenerate(io, allocator, data_dir_path); 74 75 // disk-backed store for web login sessions 76 const session_store = try web.SessionStore.init(io, data_dir); 77 defer session_store.deinit(); 78 79 // create wui listener 80 81 const wui_listen_address = try parseListenAddress(options.wui_listen); 82 const wui_address = try std.Io.net.IpAddress.parseIp4(wui_listen_address.host, wui_listen_address.port); 83 var wui_server = try wui_address.listen(io, .{ .reuse_address = true }); 84 defer wui_server.deinit(io); 85 86 // start task group 87 88 var tasks: std.Io.Group = .init; 89 defer tasks.cancel(io); 90 91 // run listeners 92 93 try err.print("serving HTTP on {s}, repo root {s}\n", .{ options.http_listen, repo_root_path }); 94 try err.flush(); 95 96 serve_http.runListener(repo_kind, any_repo_opts, io, allocator, repo_root_path, admin_repo_path, &http_server, &tasks, err); 97 98 try err.print("serving SSH on {s}\n", .{options.ssh_listen}); 99 try err.flush(); 100 101 const ssh_session_handler = serve_ssh.SessionHandler{ 102 .admin_repo_path = admin_repo_path, 103 .repo_root_path = repo_root_path, 104 .wui_port = wui_listen_address.port, 105 .err = err, 106 }; 107 serve_ssh.runListener(io, allocator, &host_key, &ssh_session_handler, &ssh_server, &tasks, err); 108 109 try err.print("serving web UI on http://{s}/\n", .{options.wui_listen}); 110 try err.flush(); 111 112 runWebListener(io, allocator, &wui_server, &tasks, .{ .remote = .{ 113 .admin_repo_path = admin_repo_path, 114 .session_store = session_store, 115 } }, err); 116 117 if (@TypeOf(runnable) != void) { 118 try runnable.run(); 119 } else { 120 try tasks.await(io); 121 } 122 } 123 124 // serve just the web UI for a single local repo, running `runnable` (the 125 // local TUI) in the foreground while the listener runs in the background. 126 // when the default port is taken, a random ephemeral port is used instead; 127 // the bound port is passed to `runnable.run`. 128 pub fn runLocal( 129 io: std.Io, 130 allocator: std.mem.Allocator, 131 local: ui.RepoSource, 132 err: *std.Io.Writer, 133 runnable: anytype, 134 ) !void { 135 // no reuse_address here: on linux it also sets SO_REUSEPORT, which would 136 // let this bind silently share a port an already-running server holds 137 // instead of failing over to a random one. 138 const wui_listen_address = try parseListenAddress((Options{}).wui_listen); 139 const wui_address = try std.Io.net.IpAddress.parseIp4(wui_listen_address.host, wui_listen_address.port); 140 var wui_server = wui_address.listen(io, .{}) catch |listen_err| switch (listen_err) { 141 // the default port is taken; bind port 0 so the OS assigns a free one 142 error.AddressInUse => blk: { 143 const any_port = try std.Io.net.IpAddress.parseIp4(wui_listen_address.host, 0); 144 break :blk try any_port.listen(io, .{}); 145 }, 146 else => |e| return e, 147 }; 148 defer wui_server.deinit(io); 149 150 var tasks: std.Io.Group = .init; 151 defer tasks.cancel(io); 152 153 runWebListener(io, allocator, &wui_server, &tasks, .{ .local = local }, err); 154 155 try runnable.run(wui_server.socket.address.getPort()); 156 } 157 158 fn runWebListener( 159 io: std.Io, 160 allocator: std.mem.Allocator, 161 net_server: *std.Io.net.Server, 162 tasks: *std.Io.Group, 163 host: web.Host, 164 err: *std.Io.Writer, 165 ) void { 166 const Context = struct { 167 io: std.Io, 168 allocator: std.mem.Allocator, 169 host: web.Host, 170 err: *std.Io.Writer, 171 }; 172 173 const handle = struct { 174 fn h(ctx: Context, stream: std.Io.net.Stream) void { 175 defer stream.close(ctx.io); 176 web.handleConnection(ctx.io, ctx.allocator, stream, ctx.host, ctx.err) catch |request_err| { 177 serve_common.logError(ctx.io, ctx.err, "web ui request failed: {s}\n", .{@errorName(request_err)}); 178 }; 179 } 180 }.h; 181 182 serve_common.runListener(io, net_server, tasks, err, "web ui", Context{ 183 .io = io, 184 .allocator = allocator, 185 .host = host, 186 .err = err, 187 }, handle); 188 } 189 190 fn parseListenAddress(value: []const u8) !ListenAddress { 191 const colon = std.mem.lastIndexOfScalar(u8, value, ':') orelse return error.InvalidListenAddress; 192 if (colon == 0 or colon + 1 >= value.len) return error.InvalidListenAddress; 193 const port = try std.fmt.parseInt(u16, value[colon + 1 ..], 10); 194 return .{ .host = value[0..colon], .port = port }; 195 } 196