diff options
-rw-r--r-- | build.zig | 1 | ||||
-rw-r--r-- | build.zig.zon | 4 | ||||
-rw-r--r-- | src/main.zig | 278 | ||||
-rw-r--r-- | src/saprus.zig | 124 | ||||
-rw-r--r-- | src/saprus_message.zig | 211 |
5 files changed, 401 insertions, 217 deletions
@@ -27,6 +27,7 @@ pub fn build(b: *std.Build) void { }); exe_mod.addImport("network", b.dependency("network", .{}).module("network")); + exe_mod.addImport("clap", b.dependency("clap", .{}).module("clap")); // This creates another `std.Build.Step.Compile`, but this one builds an executable // rather than a static library. diff --git a/build.zig.zon b/build.zig.zon index da40722..f9d27ef 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -40,6 +40,10 @@ .url = "https://github.com/ikskuh/zig-network/archive/c76240d2240711a3dcbf1c0fb461d5d1f18be79a.zip", .hash = "network-0.1.0-AAAAAOwlAQAQ6zKPUrsibdpGisxld9ftUKGdMvcCSpaj", }, + .clap = .{ + .url = "git+https://github.com/Hejsil/zig-clap?ref=0.10.0#e47028deaefc2fb396d3d9e9f7bd776ae0b2a43a", + .hash = "clap-0.10.0-oBajB434AQBDh-Ei3YtoKIRxZacVPF1iSwp3IX_ZB8f0", + }, }, .paths = .{ "build.zig", diff --git a/src/main.zig b/src/main.zig index 9a0b8a4..e425e56 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,236 +1,80 @@ const is_debug = builtin.mode == .Debug; -const base64Enc = std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, '='); -const base64Dec = std.base64.Base64Decoder.init(std.base64.standard_alphabet_chars, '='); -/// Type tag for SaprusMessage union. -/// This is the first value in the actual packet sent over the network. -const SaprusPacketType = enum(u16) { - relay = 0x003C, - file_transfer = 0x8888, - connection = 0x00E9, - _, -}; +/// This creates a debug allocator that can only be referenced in debug mode. +/// You should check for is_debug around every reference to dba. +var dba: DebugAllocator = + if (is_debug) + DebugAllocator.init + else + @compileError("Should not use debug allocator in release mode"); -/// Reserved option values. -/// Currently unused. -const SaprusConnectionOptions = packed struct(u8) { - opt1: bool = false, - opt2: bool = false, - opt3: bool = false, - opt4: bool = false, - opt5: bool = false, - opt6: bool = false, - opt7: bool = false, - opt8: bool = false, -}; - -const SaprusError = error{ - NotImplementedSaprusType, - UnknownSaprusType, -}; - -/// All Saprus messages -const SaprusMessage = union(SaprusPacketType) { - const Relay = struct { - const Header = packed struct { - dest: @Vector(4, u8), - }; - header: Header, - payload: []const u8, - }; - const Connection = struct { - const Header = packed struct { - src_port: u16, - dest_port: u16, - seq_num: u32 = 0, - msg_id: u32 = 0, - reserved: u8 = 0, - options: SaprusConnectionOptions = .{}, - }; - header: Header, - payload: []const u8, +pub fn main() !void { + defer if (is_debug) { + _ = dba.deinit(); }; - relay: Relay, - file_transfer: void, // unimplemented - connection: Connection, - - /// Should be called for any SaprusMessage that was declared using a function that you pass an allocator to. - fn deinit(self: SaprusMessage, allocator: Allocator) void { - switch (self) { - .relay => |r| allocator.free(r.payload), - .connection => |c| allocator.free(c.payload), - else => unreachable, - } - } - - inline fn toBytesAux( - Header: type, - header: Header, - payload: []const u8, - w: std.ArrayList(u8).Writer, - allocator: Allocator, - ) !void { - // Create a growable string to store the base64 bytes in. - // Doing this first so I can use the length of the encoded bytes for the length field. - var payload_list = std.ArrayList(u8).init(allocator); - defer payload_list.deinit(); - const buf_w = payload_list.writer(); - - // Write the payload bytes as base64 to the growable string. - try base64Enc.encodeWriter(buf_w, payload); - - // Write the packet body to the output writer. - try w.writeStructEndian(header, .big); - try w.writeInt(u16, @intCast(payload_list.items.len), .big); - try w.writeAll(payload_list.items); - } - /// Caller is responsible for freeing the returned bytes. - fn toBytes(self: SaprusMessage, allocator: Allocator) ![]u8 { - // Create a growable list of bytes to store the output in. - var buf = std.ArrayList(u8).init(allocator); - // Create a writer for an easy interface to append arbitrary bytes. - const w = buf.writer(); - - // Start with writing the message type, which is the first 16 bits of every Saprus message. - try w.writeInt(u16, @intFromEnum(self), .big); - - // Write the proper header and payload for the given packet type. - switch (self) { - .relay => |r| try toBytesAux(Relay.Header, r.header, r.payload, w, allocator), - .connection => |c| try toBytesAux(Connection.Header, c.header, c.payload, w, allocator), - .file_transfer => return SaprusError.NotImplementedSaprusType, - } - - // Collect the growable list as a slice and return it. - return buf.toOwnedSlice(); - } - - inline fn fromBytesAux( - packet: SaprusPacketType, - Header: type, - r: std.io.FixedBufferStream([]const u8).Reader, - allocator: Allocator, - ) !SaprusMessage { - // Read the header for the current message type. - const header = try r.readStructEndian(Header, .big); - // Read the length of the base64 encoded payload. - const len = try r.readInt(u16, .big); - - // Read the base64 bytes into a list to be able to call the decoder on it. - var payload_buf = std.ArrayList(u8).init(allocator); - defer payload_buf.deinit(); - try r.readAllArrayList(&payload_buf, len); + const gpa = if (is_debug) dba.allocator() else std.heap.smp_allocator; + + // CLI parsing adapted from the example here + // https://github.com/Hejsil/zig-clap/blob/e47028deaefc2fb396d3d9e9f7bd776ae0b2a43a/README.md#examples + + // First we specify what parameters our program can take. + // We can use `parseParamsComptime` to parse a string into an array of `Param(Help)`. + const params = comptime clap.parseParamsComptime( + \\-h, --help Display this help and exit. + \\-r, --relay <str> A relay message to send. + \\-c, --connect <str> A connection message to send. + \\ + ); + + // Initialize our diagnostics, which can be used for reporting useful errors. + // This is optional. You can also pass `.{}` to `clap.parse` if you don't + // care about the extra information `Diagnostics` provides. + var diag = clap.Diagnostic{}; + var res = clap.parse(clap.Help, ¶ms, clap.parsers.default, .{ + .diagnostic = &diag, + .allocator = gpa, + }) catch |err| { + // Report useful error and exit. + diag.report(std.io.getStdErr().writer(), err) catch {}; + return err; + }; + defer res.deinit(); - // Create a buffer to store the payload in, and decode the base64 bytes into the payload field. - const payload = try allocator.alloc(u8, try base64Dec.calcSizeForSlice(payload_buf.items)); - try base64Dec.decode(payload, payload_buf.items); + try Saprus.init(); + defer Saprus.deinit(); - // Return the type of SaprusMessage specified by the `packet` argument. - return @unionInit(SaprusMessage, @tagName(packet), .{ - .header = header, - .payload = payload, - }); + if (res.args.help != 0) { + return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); } - /// Caller is responsible for calling .deinit on the returned value. - fn fromBytes(bytes: []const u8, allocator: Allocator) !SaprusMessage { - var s = std.io.fixedBufferStream(bytes); - const r = s.reader(); - - switch (@as(SaprusPacketType, @enumFromInt(try r.readInt(u16, .big)))) { - .relay => return fromBytesAux(.relay, Relay.Header, r, allocator), - .connection => return fromBytesAux(.connection, Connection.Header, r, allocator), - .file_transfer => return SaprusError.NotImplementedSaprusType, - else => return SaprusError.UnknownSaprusType, + if (res.args.relay) |r| { + try Saprus.sendRelay(if (r.len > 0) r else "Hello darkness my old friend", gpa); + std.debug.print("Sent: {s}\n", .{r}); + return; + } else if (res.args.connect) |c| { + const conn_res: ?SaprusMessage = Saprus.connect(if (c.len > 0) c else "Hello darkness my old friend", gpa) catch |err| switch (err) { + error.WouldBlock => null, + else => return err, + }; + defer if (conn_res) |r| r.deinit(gpa); + if (conn_res) |r| { + std.debug.print("{s}\n", .{r.connection.payload}); + } else { + std.debug.print("No response from connection request\n", .{}); } + return; } -}; -pub fn main() !void { - var dba: ?DebugAllocator = if (comptime is_debug) DebugAllocator.init else null; - defer if (dba) |*d| { - _ = d.deinit(); - }; - - var gpa = if (dba) |*d| d.allocator() else std.heap.smp_allocator; - - const msg = SaprusMessage{ - .relay = .{ - .header = .{ .dest = .{ 255, 255, 255, 255 } }, - .payload = "Hello darkness my old friend", - }, - }; - - const msg_bytes = try msg.toBytes(gpa); - defer gpa.free(msg_bytes); - - try network.init(); - defer network.deinit(); - - var sock = try network.Socket.create(.ipv4, .udp); - defer sock.close(); - - try sock.setBroadcast(true); - - // Bind to 0.0.0.0:0 - const bind_addr = network.EndPoint{ - .address = network.Address{ .ipv4 = network.Address.IPv4.any }, - .port = 0, - }; - - const dest_addr = network.EndPoint{ - .address = network.Address{ .ipv4 = network.Address.IPv4.broadcast }, - .port = 8888, - }; - - try sock.bind(bind_addr); - - _ = try sock.sendTo(dest_addr, msg_bytes); + return clap.help(std.io.getStdErr().writer(), clap.Help, ¶ms, .{}); } const builtin = @import("builtin"); const std = @import("std"); -const Allocator = std.mem.Allocator; const DebugAllocator = std.heap.DebugAllocator(.{}); +const ArrayList = std.ArrayList; -const network = @import("network"); - -test "Round trip Relay toBytes and fromBytes" { - const gpa = std.testing.allocator; - const msg = SaprusMessage{ - .relay = .{ - .header = .{ .dest = .{ 255, 255, 255, 255 } }, - .payload = "Hello darkness my old friend", - }, - }; - - const to_bytes = try msg.toBytes(gpa); - defer gpa.free(to_bytes); - - const from_bytes = try SaprusMessage.fromBytes(to_bytes, gpa); - defer from_bytes.deinit(gpa); - - try std.testing.expectEqualDeep(msg, from_bytes); -} - -test "Round trip Connection toBytes and fromBytes" { - const gpa = std.testing.allocator; - const msg = SaprusMessage{ - .connection = .{ - .header = .{ - .src_port = 0, - .dest_port = 0, - }, - .payload = "Hello darkness my old friend", - }, - }; +const Saprus = @import("./saprus.zig"); +const SaprusMessage = Saprus.SaprusMessage; - const to_bytes = try msg.toBytes(gpa); - defer gpa.free(to_bytes); - - const from_bytes = try SaprusMessage.fromBytes(to_bytes, gpa); - defer from_bytes.deinit(gpa); - - try std.testing.expectEqualDeep(msg, from_bytes); -} +const clap = @import("clap"); diff --git a/src/saprus.zig b/src/saprus.zig new file mode 100644 index 0000000..4bc4af0 --- /dev/null +++ b/src/saprus.zig @@ -0,0 +1,124 @@ +var rand: ?Random = null; + +pub fn init() !void { + var prng = Random.DefaultPrng.init(blk: { + var seed: u64 = undefined; + try posix.getrandom(mem.asBytes(&seed)); + break :blk seed; + }); + rand = prng.random(); + try network.init(); +} + +pub fn deinit() void { + network.deinit(); +} + +fn broadcastSaprusMessage(msg: SaprusMessage, udp_port: u16, allocator: Allocator) !void { + const msg_bytes = try msg.toBytes(allocator); + defer allocator.free(msg_bytes); + + var sock = try network.Socket.create(.ipv4, .udp); + defer sock.close(); + + try sock.setBroadcast(true); + + // Bind to 0.0.0.0:0 + const bind_addr = network.EndPoint{ + .address = network.Address{ .ipv4 = network.Address.IPv4.any }, + .port = 0, + }; + + const dest_addr = network.EndPoint{ + .address = network.Address{ .ipv4 = network.Address.IPv4.broadcast }, + .port = udp_port, + }; + + try sock.bind(bind_addr); + + _ = try sock.sendTo(dest_addr, msg_bytes); +} + +pub fn sendRelay(payload: []const u8, allocator: Allocator) !void { + const msg = SaprusMessage{ + .relay = .{ + .header = .{ .dest = .{ 255, 255, 255, 255 } }, + .payload = payload, + }, + }; + + try broadcastSaprusMessage(msg, 8888, allocator); +} + +fn randomPort() u16 { + var p: u16 = 0; + if (rand) |r| { + p = r.intRangeAtMost(u16, 1024, 65000); + } else unreachable; + + return p; +} + +pub fn sendInitialConnection(payload: []const u8, initial_port: u16, allocator: Allocator) !SaprusMessage { + const dest_port = randomPort(); + const msg = SaprusMessage{ + .connection = .{ + .header = .{ + .src_port = initial_port, + .dest_port = dest_port, + }, + .payload = payload, + }, + }; + + try broadcastSaprusMessage(msg, 8888, allocator); + + return msg; +} + +pub fn connect(payload: []const u8, allocator: Allocator) !?SaprusMessage { + var initial_port: u16 = 0; + if (rand) |r| { + initial_port = r.intRangeAtMost(u16, 1024, 65000); + } else unreachable; + + var initial_conn_res: ?SaprusMessage = null; + errdefer if (initial_conn_res) |c| c.deinit(allocator); + + var sock = try network.Socket.create(.ipv4, .udp); + defer sock.close(); + + // Bind to 255.255.255.255:8888 + const bind_addr = network.EndPoint{ + .address = network.Address{ .ipv4 = network.Address.IPv4.broadcast }, + .port = 8888, + }; + + // timeout 1s + try sock.setReadTimeout(1 * std.time.us_per_s); + try sock.bind(bind_addr); + + const msg = try sendInitialConnection(payload, initial_port, allocator); + + var response_buf: [4096]u8 = undefined; + _ = try sock.receive(&response_buf); // Ignore message that I sent. + const len = try sock.receive(&response_buf); + + initial_conn_res = try SaprusMessage.fromBytes(response_buf[0..len], allocator); + + // Complete handshake after awaiting response + try broadcastSaprusMessage(msg, randomPort(), allocator); + + return initial_conn_res; +} + +pub const SaprusMessage = @import("./saprus_message.zig").SaprusMessage; + +const std = @import("std"); +const Random = std.Random; +const posix = std.posix; +const mem = std.mem; + +const network = @import("network"); + +const Allocator = mem.Allocator; diff --git a/src/saprus_message.zig b/src/saprus_message.zig new file mode 100644 index 0000000..3d47f66 --- /dev/null +++ b/src/saprus_message.zig @@ -0,0 +1,211 @@ +const base64Enc = std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, '='); +const base64Dec = std.base64.Base64Decoder.init(std.base64.standard_alphabet_chars, '='); + +/// Type tag for SaprusMessage union. +/// This is the first value in the actual packet sent over the network. +pub const SaprusPacketType = enum(u16) { + relay = 0x003C, + file_transfer = 0x8888, + connection = 0x00E9, + _, +}; + +/// Reserved option values. +/// Currently unused. +pub const SaprusConnectionOptions = packed struct(u8) { + opt1: bool = false, + opt2: bool = false, + opt3: bool = false, + opt4: bool = false, + opt5: bool = false, + opt6: bool = false, + opt7: bool = false, + opt8: bool = false, +}; + +pub const SaprusError = error{ + NotImplementedSaprusType, + UnknownSaprusType, +}; + +/// All Saprus messages +pub const SaprusMessage = union(SaprusPacketType) { + pub const Relay = struct { + pub const Header = packed struct { + dest: @Vector(4, u8), + }; + header: Header, + payload: []const u8, + }; + pub const Connection = struct { + pub const Header = packed struct { + src_port: u16, // random number > 1024 + dest_port: u16, // random number > 1024 + seq_num: u32 = 0, + msg_id: u32 = 0, + reserved: u8 = 0, + options: SaprusConnectionOptions = .{}, + }; + header: Header, + payload: []const u8, + }; + relay: Relay, + file_transfer: void, // unimplemented + connection: Connection, + + /// Should be called for any SaprusMessage that was declared using a function that you pass an allocator to. + pub fn deinit(self: SaprusMessage, allocator: Allocator) void { + switch (self) { + .relay => |r| allocator.free(r.payload), + .connection => |c| allocator.free(c.payload), + else => unreachable, + } + } + + fn toBytesAux( + header: anytype, + payload: []const u8, + buf: *std.ArrayList(u8), + allocator: Allocator, + ) !void { + const Header = @TypeOf(header); + // Create a growable string to store the base64 bytes in. + // Doing this first so I can use the length of the encoded bytes for the length field. + var payload_list = std.ArrayList(u8).init(allocator); + defer payload_list.deinit(); + const buf_w = payload_list.writer(); + + // Write the payload bytes as base64 to the growable string. + try base64Enc.encodeWriter(buf_w, payload); + + // At this point, payload_list contains the base64 encoded payload. + + // Add the payload length to the output buf. + try buf.*.appendSlice( + asBytes(&nativeToBig(u16, @intCast(payload_list.items.len + @bitSizeOf(Header) / 8))), + ); + + // Add the header bytes to the output buf. + var header_buf: [@sizeOf(Header)]u8 = undefined; + var header_buf_stream = std.io.fixedBufferStream(&header_buf); + try header_buf_stream.writer().writeStructEndian(header, .big); + // Add the exact number of bits in the header without padding. + try buf.*.appendSlice(header_buf[0 .. @bitSizeOf(Header) / 8]); + + try buf.*.appendSlice(payload_list.items); + } + + /// Caller is responsible for freeing the returned bytes. + pub fn toBytes(self: SaprusMessage, allocator: Allocator) ![]u8 { + // Create a growable list of bytes to store the output in. + var buf = std.ArrayList(u8).init(allocator); + errdefer buf.deinit(); + + // Start with writing the message type, which is the first 16 bits of every Saprus message. + try buf.appendSlice(asBytes(&nativeToBig(u16, @intFromEnum(self)))); + + // Write the proper header and payload for the given packet type. + switch (self) { + .relay => |r| try toBytesAux(r.header, r.payload, &buf, allocator), + .connection => |c| try toBytesAux(c.header, c.payload, &buf, allocator), + .file_transfer => return SaprusError.NotImplementedSaprusType, + } + + // Collect the growable list as a slice and return it. + return buf.toOwnedSlice(); + } + + fn fromBytesAux( + comptime packet: SaprusPacketType, + len: u16, + r: std.io.FixedBufferStream([]const u8).Reader, + allocator: Allocator, + ) !SaprusMessage { + const Header = @field(@FieldType(SaprusMessage, @tagName(packet)), "Header"); + + // Read the header for the current message type. + var header_bytes: [@sizeOf(Header)]u8 = undefined; + _ = try r.read(header_bytes[0 .. @bitSizeOf(Header) / 8]); + var header_stream = std.io.fixedBufferStream(&header_bytes); + const header = try header_stream.reader().readStructEndian(Header, .big); + + // Read the base64 bytes into a list to be able to call the decoder on it. + const payload_buf = try allocator.alloc(u8, len - @bitSizeOf(Header) / 8); + defer allocator.free(payload_buf); + _ = try r.readAll(payload_buf); + + // Create a buffer to store the payload in, and decode the base64 bytes into the payload field. + const payload = try allocator.alloc(u8, try base64Dec.calcSizeForSlice(payload_buf)); + try base64Dec.decode(payload, payload_buf); + + // Return the type of SaprusMessage specified by the `packet` argument. + return @unionInit(SaprusMessage, @tagName(packet), .{ + .header = header, + .payload = payload, + }); + } + + /// Caller is responsible for calling .deinit on the returned value. + pub fn fromBytes(bytes: []const u8, allocator: Allocator) !SaprusMessage { + var s = std.io.fixedBufferStream(bytes); + const r = s.reader(); + + // Read packet type + const packet_type = @as(SaprusPacketType, @enumFromInt(try r.readInt(u16, .big))); + + // Read the length of the header + base64 encoded payload. + const len = try r.readInt(u16, .big); + + switch (packet_type) { + .relay => return fromBytesAux(.relay, len, r, allocator), + .connection => return fromBytesAux(.connection, len, r, allocator), + .file_transfer => return SaprusError.NotImplementedSaprusType, + else => return SaprusError.UnknownSaprusType, + } + } +}; + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const asBytes = std.mem.asBytes; +const nativeToBig = std.mem.nativeToBig; + +test "Round trip Relay toBytes and fromBytes" { + const gpa = std.testing.allocator; + const msg = SaprusMessage{ + .relay = .{ + .header = .{ .dest = .{ 255, 255, 255, 255 } }, + .payload = "Hello darkness my old friend", + }, + }; + + const to_bytes = try msg.toBytes(gpa); + defer gpa.free(to_bytes); + + const from_bytes = try SaprusMessage.fromBytes(to_bytes, gpa); + defer from_bytes.deinit(gpa); + + try std.testing.expectEqualDeep(msg, from_bytes); +} + +test "Round trip Connection toBytes and fromBytes" { + const gpa = std.testing.allocator; + const msg = SaprusMessage{ + .connection = .{ + .header = .{ + .src_port = 0, + .dest_port = 0, + }, + .payload = "Hello darkness my old friend", + }, + }; + + const to_bytes = try msg.toBytes(gpa); + defer gpa.free(to_bytes); + + const from_bytes = try SaprusMessage.fromBytes(to_bytes, gpa); + defer from_bytes.deinit(gpa); + + try std.testing.expectEqualDeep(msg, from_bytes); +} |