summaryrefslogtreecommitdiff
path: root/app/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'app/main.zig')
-rw-r--r--app/main.zig204
1 files changed, 0 insertions, 204 deletions
diff --git a/app/main.zig b/app/main.zig
deleted file mode 100644
index 720503f..0000000
--- a/app/main.zig
+++ /dev/null
@@ -1,204 +0,0 @@
-//! 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);
-}