summaryrefslogtreecommitdiff
path: root/app/main.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-14 14:28:15 -0300
committerGabriel Schneider <[email protected]>2026-09-16 11:18:48 -0300
commitae310a207534b33b7321dd2b9f423a73b1969159 (patch)
treeb4c2e85091a4ff6657e0a04ba1b2ef030d588ac8 /app/main.zig
parent5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9 (diff)
downloadcloud9-ae310a207534b33b7321dd2b9f423a73b1969159.tar.gz
cloud9-ae310a207534b33b7321dd2b9f423a73b1969159.zip
Add reusable HTTP transport, serial gateway, and WASM file browser
Diffstat (limited to 'app/main.zig')
-rw-r--r--app/main.zig204
1 files changed, 204 insertions, 0 deletions
diff --git a/app/main.zig b/app/main.zig
new file mode 100644
index 0000000..720503f
--- /dev/null
+++ b/app/main.zig
@@ -0,0 +1,204 @@
+//! HTTP application; no web or namespace policy is added to the cloud9 library.
+const std = @import("std");
+const c9 = @import("cloud9");
+const Io = std.Io;
+const max_frame = 65536;
+const max_connections = 32;
+var serial_busy: std.atomic.Value(bool) = .init(false);
+const Upstream = union(enum) { network: c9.transport.Address, file: []const u8 };
+var connections: std.atomic.Value(u32) = .init(0);
+
+const Config = struct {
+ upstream: Upstream,
+ host: []const u8,
+ origin: []const u8,
+ public_host: []const u8,
+ timeout_ms: u32,
+ browser_config: []const u8,
+};
+
+pub fn main(init: std.process.Init) !void {
+ const allocator = init.arena.allocator();
+ const args = try init.minimal.args.toSlice(allocator);
+ var listen_text: []const u8 = "127.0.0.1:8080";
+ var upstream_text: []const u8 = "tcp:127.0.0.1:564";
+ var timeout_ms: u32 = 300000;
+ var user: []const u8 = "user";
+ var tree: []const u8 = "";
+ var public_origin: ?[]const u8 = null;
+ var i: usize = 1;
+ while (i < args.len) : (i += 1) {
+ if (std.mem.eql(u8, args[i], "--help")) {
+ std.debug.print("Usage: cloud9-http [--listen IP:PORT] [--upstream tcp:IP:PORT|unix:PATH|file:PATH] [--origin https://HOST:PORT] [--timeout-ms 300000] [--user NAME] [--tree NAME]\nDefault: http://127.0.0.1:8080 -> tcp:127.0.0.1:564\n", .{});
+ return;
+ }
+ if (i + 1 == args.len) return error.MissingArgument;
+ if (std.mem.eql(u8, args[i], "--listen")) {
+ i += 1;
+ listen_text = args[i];
+ } else if (std.mem.eql(u8, args[i], "--upstream")) {
+ i += 1;
+ upstream_text = args[i];
+ } else if (std.mem.eql(u8, args[i], "--origin")) {
+ i += 1;
+ public_origin = args[i];
+ } else if (std.mem.eql(u8, args[i], "--timeout-ms")) {
+ i += 1;
+ timeout_ms = try std.fmt.parseInt(u32, args[i], 10);
+ } else if (std.mem.eql(u8, args[i], "--user")) {
+ i += 1;
+ user = args[i];
+ } else if (std.mem.eql(u8, args[i], "--tree")) {
+ i += 1;
+ tree = args[i];
+ } else return error.UnknownArgument;
+ }
+ const address = try Io.net.IpAddress.parseLiteral(listen_text);
+ const upstream: Upstream = if (std.mem.startsWith(u8, upstream_text, "tcp:"))
+ .{ .network = .{ .tcp = try Io.net.IpAddress.parseLiteral(upstream_text[4..]) } }
+ else if (std.mem.startsWith(u8, upstream_text, "unix:"))
+ .{ .network = .{ .unix = try allocator.dupeZ(u8, upstream_text[5..]) } }
+ else if (std.mem.startsWith(u8, upstream_text, "file:"))
+ .{ .file = upstream_text[5..] }
+ else
+ return error.InvalidUpstream;
+ const io = init.io;
+ var listener = c9.transport.listen(io, .{ .tcp = address }, max_connections) catch |err| {
+ if (err == error.AddressInUse) {
+ std.debug.print("cloud9-http: cannot listen on {s}: address already in use.\nUse --listen 127.0.0.1:0 to select a free port; the URL is printed at startup.\n", .{listen_text});
+ } else {
+ std.debug.print("cloud9-http: cannot listen on {s}: {s}\n", .{ listen_text, @errorName(err) });
+ }
+ return err;
+ };
+ defer listener.deinit(io);
+ const host = try std.fmt.allocPrint(allocator, "{f}", .{listener.socket.address});
+ const origin_text = public_origin orelse try std.fmt.allocPrint(allocator, "http://{s}", .{host});
+ const origin_uri = try std.Uri.parse(origin_text);
+ if ((!std.mem.eql(u8, origin_uri.scheme, "https") and !std.mem.eql(u8, origin_uri.scheme, "http")) or
+ origin_uri.host == null or origin_uri.user != null or origin_uri.password != null or
+ origin_uri.path.percent_encoded.len != 0 or origin_uri.query != null or origin_uri.fragment != null) return error.InvalidOrigin;
+ if (user.len > 256 or tree.len > 256) return error.AttachNameTooLong;
+ const browser_config = try std.json.Stringify.valueAlloc(allocator, .{ .user = user, .tree = tree }, .{});
+ const config: Config = .{ .upstream = upstream, .host = host, .origin = origin_text, .public_host = origin_text[origin_uri.scheme.len + 3 ..], .timeout_ms = timeout_ms, .browser_config = browser_config };
+ std.debug.print("cloud9-http {s} -> {s}\n", .{ config.origin, upstream_text });
+ var group: Io.Group = .init;
+ defer group.cancel(io);
+ while (true) {
+ const stream = try listener.accept(io);
+ if (connections.fetchAdd(1, .monotonic) >= max_connections) {
+ _ = connections.fetchSub(1, .monotonic);
+ stream.close(io);
+ continue;
+ }
+ group.concurrent(io, handle, .{ io, stream, config }) catch |err| {
+ _ = connections.fetchSub(1, .monotonic);
+ stream.close(io);
+ return err;
+ };
+ }
+}
+
+fn handle(io: Io, stream: Io.net.Stream, config: Config) void {
+ defer _ = connections.fetchSub(1, .monotonic);
+ defer stream.close(io);
+ var work: Connection = .{ .io = io, .stream = stream, .config = config };
+ var group: Io.Group = .init;
+ defer group.cancel(io);
+ group.concurrent(io, Connection.run, .{&work}) catch return;
+ const timeout: Io.Timeout = if (config.timeout_ms == 0) .none else .{ .duration = .{ .raw = .fromMilliseconds(config.timeout_ms), .clock = .awake } };
+ work.done.waitTimeout(io, timeout) catch {};
+}
+
+const Connection = struct {
+ io: Io,
+ stream: Io.net.Stream,
+ config: Config,
+ done: Io.Event = .unset,
+ fn run(connection: *Connection) void {
+ defer connection.done.set(connection.io);
+ serve(connection.io, connection.stream, connection.config) catch {};
+ }
+};
+
+fn respond(request: *std.http.Server.Request, content: []const u8, mime: []const u8, status: std.http.Status) !void {
+ try request.respond(content, .{ .status = status, .keep_alive = false, .extra_headers = &.{
+ .{ .name = "content-type", .value = mime },
+ .{ .name = "cache-control", .value = "no-store" },
+ .{ .name = "x-content-type-options", .value = "nosniff" },
+ .{ .name = "content-security-policy", .value = "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'" },
+ } });
+}
+
+fn serve(io: Io, stream: Io.net.Stream, config: Config) !void {
+ var input: [max_frame + 14]u8 = undefined;
+ var output: [8192]u8 = undefined;
+ var reader = stream.reader(io, &input);
+ var writer = stream.writer(io, &output);
+ var http: std.http.Server = .init(&reader.interface, &writer.interface);
+ http.reader.max_head_len = 8192;
+ var request = try http.receiveHead();
+ var host: ?[]const u8 = null;
+ var origin: ?[]const u8 = null;
+ var version: ?[]const u8 = null;
+ var headers = request.iterateHeaders();
+ while (headers.next()) |header| {
+ if (std.ascii.eqlIgnoreCase(header.name, "host")) host = header.value;
+ if (std.ascii.eqlIgnoreCase(header.name, "origin")) origin = header.value;
+ if (std.ascii.eqlIgnoreCase(header.name, "sec-websocket-version")) version = header.value;
+ }
+ if (!std.mem.eql(u8, host orelse "", config.host) and !std.mem.eql(u8, host orelse "", config.public_host)) return respond(&request, "Unexpected Host\n", "text/plain", .forbidden);
+ if (request.head.method != .GET) return respond(&request, "Use GET\n", "text/plain", .method_not_allowed);
+ const path = std.mem.sliceTo(request.head.target, '?');
+ if (std.mem.eql(u8, path, "/_cloud9/config.json")) return respond(&request, config.browser_config, "application/json", .ok);
+ if (std.mem.eql(u8, path, "/_cloud9/app.mjs")) return respond(&request, @embedFile("web/app.mjs"), "text/javascript; charset=utf-8", .ok);
+ if (std.mem.eql(u8, path, "/_cloud9/client.mjs")) return respond(&request, @embedFile("web/client.mjs"), "text/javascript; charset=utf-8", .ok);
+ if (std.mem.eql(u8, path, "/_cloud9/style.css")) return respond(&request, @embedFile("web/style.css"), "text/css; charset=utf-8", .ok);
+ if (std.mem.eql(u8, path, "/_cloud9/cloud9.wasm")) return respond(&request, @embedFile("client.wasm"), "application/wasm", .ok);
+ if (!std.mem.eql(u8, path, "/_cloud9/9p")) {
+ if (std.mem.eql(u8, path, "/_cloud9") or std.mem.startsWith(u8, path, "/_cloud9/"))
+ return respond(&request, "Not found\n", "text/plain", .not_found);
+ // File URLs boot the same browser client. The client resolves the path
+ // in the upstream 9P namespace, never in this machine's filesystem.
+ return respond(&request, @embedFile("web/index.html"), "text/html; charset=utf-8", .ok);
+ }
+ if (!std.mem.eql(u8, origin orelse "", config.origin)) return respond(&request, "Unexpected Origin\n", "text/plain", .forbidden);
+ if (!std.mem.eql(u8, version orelse "", "13")) return respond(&request, "WebSocket version 13 required\n", "text/plain", .bad_request);
+ switch (config.upstream) {
+ .network => |address| {
+ const upstream = c9.transport.connect(io, address) catch return respond(&request, "9P upstream unavailable\n", "text/plain", .bad_gateway);
+ defer upstream.close(io);
+ var in_buffer: [8192]u8 = undefined;
+ var out_buffer: [8192]u8 = undefined;
+ var upstream_reader = upstream.reader(io, &in_buffer);
+ var upstream_writer = upstream.writer(io, &out_buffer);
+ try bridge(io, &request, &upstream_reader.interface, &upstream_writer.interface);
+ },
+ .file => |path_name| {
+ if (serial_busy.swap(true, .acquire)) return respond(&request, "Device already in use\n", "text/plain", .service_unavailable);
+ defer serial_busy.store(false, .release);
+ const file = Io.Dir.cwd().openFile(io, path_name, .{ .mode = .read_write }) catch return respond(&request, "9P device unavailable\n", "text/plain", .bad_gateway);
+ defer file.close(io);
+ var in_buffer: [8192]u8 = undefined;
+ var out_buffer: [8192]u8 = undefined;
+ var upstream_reader = file.readerStreaming(io, &in_buffer);
+ var upstream_writer = file.writerStreaming(io, &out_buffer);
+ try bridge(io, &request, &upstream_reader.interface, &upstream_writer.interface);
+ },
+ }
+}
+
+fn bridge(io: Io, request: *std.http.Server.Request, reader: *Io.Reader, writer: *Io.Writer) !void {
+ var ws = try c9.http.accept(request);
+ var requests: [max_frame]u8 = undefined;
+ var replies: [max_frame]u8 = undefined;
+ var relay: c9.http.Bridge = .{
+ .socket = &ws,
+ .upstream_reader = reader,
+ .upstream_writer = writer,
+ .request_buffer = &requests,
+ .reply_buffer = &replies,
+ .frame_limit = max_frame,
+ };
+ try relay.run(io, .none);
+}