lib/sys/src/x11/auth.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const sys = @import("../root.zig");
  3 
  4 const env = sys.env;
  5 const fs = sys.fs;
  6 
  7 pub const cookie_name = "MIT-MAGIC-COOKIE-1";
  8 pub const cookie_length = 16;
  9 pub const maximum_authority_file_byte_count = 64 * 1024;
 10 
 11 const family_local: u16 = 256;
 12 const family_wild: u16 = 65535;
 13 
 14 pub const Cookie = struct {
 15     data: [cookie_length]u8,
 16 };
 17 
 18 const Record = struct {
 19     family: u16,
 20     address: []const u8,
 21     number: []const u8,
 22     name: []const u8,
 23     data: []const u8,
 24 };
 25 
 26 const Reader = struct {
 27     bytes: []const u8,
 28     index: usize = 0,
 29 
 30     fn u16be(self: *Reader) ?u16 {
 31         if (self.index + 2 > self.bytes.len) return null;
 32         const value = std.mem.readInt(u16, self.bytes[self.index..][0..2], .big);
 33         self.index += 2;
 34         return value;
 35     }
 36 
 37     fn counted(self: *Reader) ?[]const u8 {
 38         const len = self.u16be() orelse return null;
 39         if (self.index + len > self.bytes.len) return null;
 40         const slice = self.bytes[self.index .. self.index + len];
 41         self.index += len;
 42         return slice;
 43     }
 44 
 45     fn record(self: *Reader) ?Record {
 46         const family = self.u16be() orelse return null;
 47         const address = self.counted() orelse return null;
 48         const number = self.counted() orelse return null;
 49         const name = self.counted() orelse return null;
 50         const data = self.counted() orelse return null;
 51         return .{
 52             .family = family,
 53             .address = address,
 54             .number = number,
 55             .name = name,
 56             .data = data,
 57         };
 58     }
 59 };
 60 
 61 pub fn findCookie(bytes: []const u8, display: u32) ?Cookie {
 62     var number_buffer: [10]u8 = undefined;
 63     const number = std.fmt.bufPrint(&number_buffer, "{d}", .{display}) catch return null;
 64 
 65     var reader = Reader{ .bytes = bytes };
 66     while (reader.record()) |record| {
 67         if (record.family != family_local and record.family != family_wild) continue;
 68         if (record.number.len != 0 and !std.mem.eql(u8, record.number, number)) continue;
 69         if (!std.mem.eql(u8, record.name, cookie_name)) continue;
 70         if (record.data.len != cookie_length) continue;
 71         var cookie: Cookie = undefined;
 72         @memcpy(&cookie.data, record.data);
 73         return cookie;
 74     }
 75     return null;
 76 }
 77 
 78 pub fn loadCookie(storage: []u8, display: u32) ?Cookie {
 79     std.debug.assert(storage.len >= maximum_authority_file_byte_count);
 80     var path_buffer: [512]u8 = undefined;
 81     const path = authorityPath(&path_buffer) orelse return null;
 82     var file = fs.cwd().openFile(fs.debugIo(), path, .{}) catch return null;
 83     defer file.close(fs.debugIo());
 84     const bytes = readAuthorityFile(file, storage[0..maximum_authority_file_byte_count]) orelse
 85         return null;
 86     return findCookie(bytes, display);
 87 }
 88 
 89 fn readAuthorityFile(file: std.Io.File, storage: []u8) ?[]const u8 {
 90     var filled: usize = 0;
 91     while (filled < storage.len) {
 92         const count = fs.readHandle(file, storage[filled..]) catch return null;
 93         if (count == 0) return storage[0..filled];
 94         filled += count;
 95     }
 96     var probe: [1]u8 = undefined;
 97     if ((fs.readHandle(file, &probe) catch return null) != 0) return null;
 98     return storage;
 99 }
100 
101 fn authorityPath(buffer: []u8) ?[]const u8 {
102     if (env.get("XAUTHORITY")) |explicit| {
103         if (explicit.len == 0 or explicit.len > buffer.len) return null;
104         @memcpy(buffer[0..explicit.len], explicit);
105         return buffer[0..explicit.len];
106     }
107     const home = env.get("HOME") orelse return null;
108     return std.fmt.bufPrint(buffer, "{s}/.Xauthority", .{home}) catch null;
109 }
110 
111 fn appendU16Be(list: *std.ArrayList(u8), allocator: std.mem.Allocator, value: u16) !void {
112     var scratch: [2]u8 = undefined;
113     std.mem.writeInt(u16, &scratch, value, .big);
114     try list.appendSlice(allocator, &scratch);
115 }
116 
117 fn appendCounted(list: *std.ArrayList(u8), allocator: std.mem.Allocator, bytes: []const u8) !void {
118     try appendU16Be(list, allocator, @intCast(bytes.len));
119     try list.appendSlice(allocator, bytes);
120 }
121 
122 fn appendRecord(
123     list: *std.ArrayList(u8),
124     allocator: std.mem.Allocator,
125     family: u16,
126     address: []const u8,
127     number: []const u8,
128     name: []const u8,
129     data: []const u8,
130 ) !void {
131     try appendU16Be(list, allocator, family);
132     try appendCounted(list, allocator, address);
133     try appendCounted(list, allocator, number);
134     try appendCounted(list, allocator, name);
135     try appendCounted(list, allocator, data);
136 }
137 
138 test "findCookie matches the display record" {
139     const allocator = std.testing.allocator;
140     var bytes: std.ArrayList(u8) = .empty;
141     defer bytes.deinit(allocator);
142 
143     const other = @as([cookie_length]u8, @splat(0xAA));
144     const wanted = @as([cookie_length]u8, @splat(0x5A));
145     try appendRecord(&bytes, allocator, family_local, "host", "0", cookie_name, &other);
146     try appendRecord(&bytes, allocator, family_local, "host", "7", cookie_name, &wanted);
147 
148     const cookie = findCookie(bytes.items, 7) orelse return error.TestExpectedCookie;
149     try std.testing.expectEqualSlices(u8, &wanted, &cookie.data);
150 }
151 
152 test "findCookie skips foreign families and short data" {
153     const allocator = std.testing.allocator;
154     var bytes: std.ArrayList(u8) = .empty;
155     defer bytes.deinit(allocator);
156 
157     const cookie = @as([cookie_length]u8, @splat(0x11));
158     try appendRecord(&bytes, allocator, 0, "10.0.0.1", "0", cookie_name, &cookie);
159     try appendRecord(&bytes, allocator, family_local, "host", "0", cookie_name, cookie[0..4]);
160     try std.testing.expect(findCookie(bytes.items, 0) == null);
161 }
162 
163 test "findCookie accepts wildcard records and truncated files" {
164     const allocator = std.testing.allocator;
165     var bytes: std.ArrayList(u8) = .empty;
166     defer bytes.deinit(allocator);
167 
168     const cookie = @as([cookie_length]u8, @splat(0x77));
169     try appendRecord(&bytes, allocator, family_wild, "", "", cookie_name, &cookie);
170     try std.testing.expect(findCookie(bytes.items, 3) != null);
171 
172     try std.testing.expect(findCookie(bytes.items[0 .. bytes.items.len - 1], 99) == null);
173 }
174 
175 test "authority file read accepts exact storage and rejects max plus one" {
176     const io = std.Options.debug_io;
177     var tmp = std.testing.tmpDir(.{});
178     defer tmp.cleanup();
179 
180     var exact = try tmp.dir.createFile(io, "exact", .{});
181     try fs.writeHandleAll(exact, "four");
182     exact.close(io);
183     var exact_reader = try tmp.dir.openFile(io, "exact", .{});
184     defer exact_reader.close(io);
185     var storage: [4]u8 = undefined;
186     try std.testing.expectEqualStrings(
187         "four",
188         readAuthorityFile(exact_reader, &storage).?,
189     );
190 
191     var oversized = try tmp.dir.createFile(io, "oversized", .{});
192     try fs.writeHandleAll(oversized, "fives");
193     oversized.close(io);
194     var oversized_reader = try tmp.dir.openFile(io, "oversized", .{});
195     defer oversized_reader.close(io);
196     try std.testing.expect(readAuthorityFile(oversized_reader, &storage) == null);
197 }