atproto tools in zig zat.dev
sdk atproto zig

Add DNS did resolving via DoH

could probably use threaded workers for trying both methods at the same time in the future.

authored by seiso.moe and committed by tangled.org cc5d48e0 4f9d4c88

+141 -3
+141 -3
src/internal/handle_resolver.zig
··· 15 pub const HandleResolver = struct { 16 allocator: std.mem.Allocator, 17 http_client: std.http.Client, 18 19 pub fn init(allocator: std.mem.Allocator) HandleResolver { 20 return .{ 21 .allocator = allocator, 22 .http_client = .{ .allocator = allocator }, 23 }; 24 } 25 ··· 29 30 /// resolve a handle to a DID via HTTP well-known 31 pub fn resolve(self: *HandleResolver, handle: Handle) ![]const u8 { 32 - return try self.resolveHttp(handle); 33 } 34 35 /// resolve via HTTP at https://{handle}/.well-known/atproto-did ··· 63 64 return try self.allocator.dupe(u8, did_str); 65 } 66 }; 67 68 // === integration tests === 69 // these actually hit the network - run with: zig test src/internal/handle_resolver.zig 70 71 - test "resolve handle - integration" { 72 // use arena for http client internals that may leak 73 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 74 defer arena.deinit(); ··· 78 79 // resolve a known handle that has .well-known/atproto-did 80 const handle = Handle.parse("jay.bsky.social") orelse return error.InvalidHandle; 81 - const did = resolver.resolve(handle) catch |err| { 82 // network errors are ok in CI without network access 83 std.debug.print("network error (expected in some CI): {}\n", .{err}); 84 return; ··· 88 try std.testing.expect(Did.parse(did) != null); 89 try std.testing.expect(std.mem.startsWith(u8, did, "did:plc:")); 90 }
··· 15 pub const HandleResolver = struct { 16 allocator: std.mem.Allocator, 17 http_client: std.http.Client, 18 + doh_endpoint: []const u8, 19 20 pub fn init(allocator: std.mem.Allocator) HandleResolver { 21 return .{ 22 .allocator = allocator, 23 .http_client = .{ .allocator = allocator }, 24 + .doh_endpoint = "https://cloudflare-dns.com/dns-query", 25 }; 26 } 27 ··· 31 32 /// resolve a handle to a DID via HTTP well-known 33 pub fn resolve(self: *HandleResolver, handle: Handle) ![]const u8 { 34 + if (self.resolveHttp(handle)) |did| { 35 + return did; 36 + } else |_| { 37 + return try self.resolveDns(handle); 38 + } 39 } 40 41 /// resolve via HTTP at https://{handle}/.well-known/atproto-did ··· 69 70 return try self.allocator.dupe(u8, did_str); 71 } 72 + 73 + /// resolve via DoH default: https://cloudflare-dns.com/dns-query 74 + pub fn resolveDns(self: *HandleResolver, handle: Handle) ![]const u8 { 75 + const dns_name = try std.fmt.allocPrint( 76 + self.allocator, 77 + "_atproto.{s}", 78 + .{handle.str()}, 79 + ); 80 + defer self.allocator.free(dns_name); 81 + 82 + const url = try std.fmt.allocPrint( 83 + self.allocator, 84 + "{s}?name={s}&type=TXT", 85 + .{ self.doh_endpoint, dns_name }, 86 + ); 87 + defer self.allocator.free(url); 88 + 89 + var aw: std.io.Writer.Allocating = .init(self.allocator); 90 + defer aw.deinit(); 91 + 92 + const result = self.http_client.fetch(.{ 93 + .location = .{ .url = url }, 94 + .extra_headers = &.{ 95 + .{ .name = "accept", .value = "application/dns-json" }, 96 + }, 97 + .response_writer = &aw.writer, 98 + }) catch return error.DnsResolutionFailed; 99 + 100 + if (result.status != .ok) { 101 + return error.DnsResolutionFailed; 102 + } 103 + 104 + const response_body = aw.toArrayList().items; 105 + const parsed = std.json.parseFromSlice( 106 + DnsResponse, 107 + self.allocator, 108 + response_body, 109 + .{}, 110 + ) catch return error.InvalidDnsResponse; 111 + defer parsed.deinit(); 112 + 113 + const dns_response = parsed.value; 114 + if (dns_response.Answer == null or dns_response.Answer.?.len == 0) { 115 + return error.NoDnsRecordsFound; 116 + } 117 + 118 + for (dns_response.Answer.?) |answer| { 119 + const data = answer.data orelse continue; 120 + const did_str = extractDidFromTxt(data) orelse continue; 121 + 122 + if (Did.parse(did_str) != null) { 123 + return try self.allocator.dupe(u8, did_str); 124 + } 125 + } 126 + 127 + return error.NoValidDidFound; 128 + } 129 + }; 130 + 131 + fn extractDidFromTxt(txt_data: []const u8) ?[]const u8 { 132 + var data = txt_data; 133 + if (data.len >= 2 and data[0] == '"' and data[data.len - 1] == '"') { 134 + data = data[1 .. data.len - 1]; 135 + } 136 + 137 + const prefix = "did="; 138 + if (std.mem.startsWith(u8, data, prefix)) { 139 + return data[prefix.len..]; 140 + } 141 + 142 + return null; 143 + } 144 + 145 + const DnsResponse = struct { 146 + Status: i32, 147 + TC: bool, 148 + RD: bool, 149 + RA: bool, 150 + AD: bool, 151 + CD: bool, 152 + Question: ?[]Question = null, 153 + Answer: ?[]Answer = null, 154 + }; 155 + 156 + const Question = struct { 157 + name: []const u8, 158 + type: i32, 159 + }; 160 + 161 + const Answer = struct { 162 + name: []const u8, 163 + type: i32, 164 + TTL: i32, 165 + data: ?[]const u8 = null, 166 }; 167 168 // === integration tests === 169 // these actually hit the network - run with: zig test src/internal/handle_resolver.zig 170 171 + test "resolve handle (http) - integration" { 172 // use arena for http client internals that may leak 173 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 174 defer arena.deinit(); ··· 178 179 // resolve a known handle that has .well-known/atproto-did 180 const handle = Handle.parse("jay.bsky.social") orelse return error.InvalidHandle; 181 + const did = resolver.resolveHttp(handle) catch |err| { 182 // network errors are ok in CI without network access 183 std.debug.print("network error (expected in some CI): {}\n", .{err}); 184 return; ··· 188 try std.testing.expect(Did.parse(did) != null); 189 try std.testing.expect(std.mem.startsWith(u8, did, "did:plc:")); 190 } 191 + 192 + test "resolve handle (dns over http) - integration" { 193 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 194 + defer arena.deinit(); 195 + 196 + var resolver = HandleResolver.init(arena.allocator()); 197 + defer resolver.deinit(); 198 + 199 + const handle = Handle.parse("seiso.moe") orelse return error.InvalidHandle; 200 + const did = resolver.resolveDns(handle) catch |err| { 201 + // network errors are ok in CI without network access 202 + std.debug.print("network error (expected in some CI): {}\n", .{err}); 203 + return; 204 + }; 205 + 206 + // should be a valid DID 207 + try std.testing.expect(Did.parse(did) != null); 208 + try std.testing.expect(std.mem.startsWith(u8, did, "did:")); 209 + } 210 + 211 + test "resolve handle - integration" { 212 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 213 + defer arena.deinit(); 214 + 215 + var resolver = HandleResolver.init(arena.allocator()); 216 + defer resolver.deinit(); 217 + 218 + const handle = Handle.parse("jay.bsky.social") orelse return error.InvalidHandle; 219 + const did = resolver.resolve(handle) catch |err| { 220 + // network errors are ok in CI without network access 221 + std.debug.print("network error (expected in some CI): {}\n", .{err}); 222 + return; 223 + }; 224 + 225 + // should be a valid DID 226 + try std.testing.expect(Did.parse(did) != null); 227 + try std.testing.expect(std.mem.startsWith(u8, did, "did:")); 228 + }