94 lines
3.2 KiB
Zig
94 lines
3.2 KiB
Zig
//! Network-dependent test for `doh_client.zig`.
|
|
//!
|
|
//! It lives in its own file because it needs `@import("build_options")`, which
|
|
//! only exists when the compilation is driven by build.zig. The test is
|
|
//! compiled by every `zig build test` run, so it cannot rot, and it skips at run
|
|
//! time unless `-Dlive` is passed. (`-Dintegration` stays hermetic: loopback
|
|
//! only. `-Dlive` is the gate for tests that leave the machine.)
|
|
|
|
const std = @import("std");
|
|
const build_options = @import("build_options");
|
|
|
|
const doh_client = @import("doh_client.zig");
|
|
const transport = @import("transport.zig");
|
|
const packet = @import("../dns/packet.zig");
|
|
|
|
/// No HTTP call in 0.16.0 takes a deadline, so the whole exchange runs as one
|
|
/// task raced against a sleep and the loser is canceled.
|
|
const budget: std.Io.Clock.Duration = .{ .raw = .fromSeconds(10), .clock = .awake };
|
|
|
|
const Outcome = union(enum) {
|
|
exchange: anyerror!usize,
|
|
expiry: std.Io.Cancelable!void,
|
|
};
|
|
|
|
/// A query for example.com A: id 0x1234, RD set, one question.
|
|
const query_bytes =
|
|
"\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" ++
|
|
"\x07example\x03com\x00\x00\x01\x00\x01";
|
|
|
|
const Params = struct {
|
|
gpa: std.mem.Allocator,
|
|
response_buf: []u8,
|
|
};
|
|
|
|
/// Returns the reply length; the bytes stay in the caller's `response_buf` so
|
|
/// they outlive this task.
|
|
fn runExchange(io: std.Io, params: Params) anyerror!usize {
|
|
var http: std.http.Client = .{ .allocator = params.gpa, .io = io };
|
|
defer http.deinit();
|
|
|
|
var request_buf: [1024]u8 = undefined;
|
|
var transfer_buf: [4096]u8 = undefined;
|
|
|
|
const endpoint = try transport.Endpoint.parse("https://cloudflare-dns.com/dns-query");
|
|
var doh = try doh_client.DohClient.init(&http, endpoint, &request_buf, &transfer_buf);
|
|
|
|
const reply = try doh.client().exchange(io, query_bytes, params.response_buf);
|
|
return reply.len;
|
|
}
|
|
|
|
fn expire(io: std.Io, duration: std.Io.Clock.Duration) std.Io.Cancelable!void {
|
|
return duration.sleep(io);
|
|
}
|
|
|
|
test "live DoH exchange against cloudflare-dns.com" {
|
|
if (!build_options.live) return error.SkipZigTest;
|
|
|
|
const gpa = std.testing.allocator;
|
|
|
|
var threaded: std.Io.Threaded = .init(gpa, .{});
|
|
defer threaded.deinit();
|
|
const io = threaded.io();
|
|
|
|
var response_buf: [4096]u8 = undefined;
|
|
|
|
var outcomes: [2]Outcome = undefined;
|
|
var race: std.Io.Select(Outcome) = .init(io, &outcomes);
|
|
defer race.cancelDiscard();
|
|
|
|
try race.concurrent(.exchange, runExchange, .{ io, Params{
|
|
.gpa = gpa,
|
|
.response_buf = &response_buf,
|
|
} });
|
|
try race.concurrent(.expiry, expire, .{ io, budget });
|
|
|
|
const len = switch (try race.await()) {
|
|
.exchange => |result| result catch |err| {
|
|
std.debug.print("DoH exchange failed: {s}\n", .{@errorName(err)});
|
|
return err;
|
|
},
|
|
.expiry => |result| {
|
|
try result;
|
|
return error.DohExchangeTimedOut;
|
|
},
|
|
};
|
|
|
|
// `exchange` already ran `validateResponse`; re-run it so the assertion is
|
|
// in the test and not only in the code under test.
|
|
try transport.validateResponse(query_bytes, response_buf[0..len]);
|
|
|
|
const reply = try packet.parse(response_buf[0..len]);
|
|
try std.testing.expect(reply.header.ancount >= 1);
|
|
}
|