Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .clang-tidy
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
# clang-tidy config file
---
Checks: 'clang-diagnostic-*,clang-analyzer-*,modernize*,performance*,readability-*size*,readability*redundant*,-readability-redundant-member-init,-readability-redundant-access-specifiers,misc*,boost-use-to-string,cert*,google-explicit-constructor,-cert-err58-cpp,-modernize-concat-nested-namespaces,-modernize-use-nodiscard,-misc-non-private-member-variables-in-classes,-modernize-use-trailing-return-type,-misc-no-recursion,-misc-const-correctness,-misc-use-anonymous-namespace,-performance-avoid-endl'
# note DEV:
# disabled 'hicpp-signed-bitwise' because it's too noisy
# (IgnorePositiveIntegerLiterals doesn't seem to work, re-evaluate with Clang 11+)
Checks: 'clang-diagnostic-*,clang-analyzer-*,modernize*,performance*,readability-*size*,readability*redundant*,-readability-redundant-member-init,-readability-redundant-access-specifiers,misc*,boost-use-to-string,cert*,google-explicit-constructor,-cert-err58-cpp,-modernize-use-nodiscard,-misc-non-private-member-variables-in-classes,-modernize-use-trailing-return-type,-misc-no-recursion,-misc-const-correctness,-misc-use-anonymous-namespace,-readability-redundant-qualified-alias,-performance-enum-size'

WarningsAsErrors: ''
HeaderFilterRegex: ''
Expand All @@ -22,4 +19,6 @@ CheckOptions:
value: llvm
- key: modernize-use-nullptr.NullMacros
value: 'NULL'
- key: performance-unnecessary-value-param.AllowedTypes
value: 'std::(__[A-Za-z0-9_]+::)*exception_ptr$'
...
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ It looks up the HTTPS records of each host in DNS ([RFC 9460](https://www.rfc-ed
and requests `https://<host>/` once, over TLS, in the way that the records say. It prints the status,
the route, the IP address it connected to, the time it took and the header fields of the response,
along with the host's records. A redirect is reported and not followed, and nothing else on the host
is requested. [Here is what that looks like](doc/README.md).
is requested. Its `Location` goes at the end of the status line, as in `301 … -> https://www.example.com/`,
as it came: not resolved against the URL, and not looked up. [Here is what that looks like](doc/README.md).

There is output for every host but those that do not exist, which `--all` adds. The counts at the end
look like this, on stderr:
Expand All @@ -37,7 +38,7 @@ look like this, on stderr:
| has a record that says the service is not available | out of reach, not connected to |
| does not exist | not found, not connected to |
| gets no answer from DNS, or has no address | lookup failed, so there is no telling, not connected to |
| has no record that offers `h3`, with `--h3` | without h3, not connected to |
| offers none of the protocols of `--h3`, `--h2` and `--h1`, with any of them | left out, not connected to |
| could be connected to, with `--dns-only` | resolved, not connected to |
| could be connected to, with `--each-address` | answered if all of its addresses did, otherwise failed |

Expand All @@ -46,10 +47,17 @@ record, HTTP/3 goes first, and then TCP on the same port: HTTP/2 if the record h
HTTP/1.1, which is in every record that does not say `no-default-alpn`. The route in the output is
what the server went for, which may be HTTP/1.1 where HTTP/2 was offered.

**With `--h3`,** only hosts with a compatible record that has `h3` in its `alpn` go ahead, on whichever
target, and they are treated as above, so that one can still end up out of reach, or going over TCP
when HTTP/3 can not be connected to. The others are left out after their HTTPS lookup, and printed
only with `--all`. The counts at the end then add how many there were, as `5 without h3`.
**With `--h3`, `--h2` or `--h1`,** only the protocols given are spoken, in any combination, and all
three without any of them. HTTP/3 needs a compatible record with `h3` in its `alpn`. HTTP/2 and
HTTP/1.1 need a record that offers them, or no compatible record at all, which means port 443. A host
that offers none of the protocols given, on whichever target, is left out after its HTTPS lookup,
printed only with `--all` and with a note on why, and counted at the end as `5 left out`. The others
are treated as above, with the routes of those protocols only, so that one can still end up out of
reach. With `--h3` alone, that means no going over TCP when HTTP/3 can not be connected to.

anyhttp offers HTTP/1.1 along with HTTP/2, and a server may take it, or not use ALPN at all, which
comes to the same. With `--h2` but not `--h1`, such a server is not sent the request. That counts as a
connection that failed, so the next address or route gets its turn.

**With `--dns-only`,** it stops at the point where it would connect, so that no host is connected to
or requested. A host that would have been goes by `resolved` and comes with its records and all of
Expand All @@ -64,7 +72,8 @@ example.com: resolved

All other hosts get the outcome they always get, which leaves `answered` and `failed` at 0. The addresses
are only looked up for a host that would have been connected to, so an `out of reach` one has none.
The counts at the end then add how many there were, as `5 resolved`, and `--h3` works as above.
The counts at the end then add how many there were, as `5 resolved`, and `--h3`, `--h2` and `--h1`
work as above.

**With `--each-address`,** every address of a host gets a request of its own instead of only the first
that can be connected to, one after the other, with a line per address for the status line of the
Expand All @@ -79,9 +88,12 @@ example.com: answered

All other hosts get the outcome they always get. A host counts as `answered` only if all of its
addresses did, whatever the status, and otherwise as `failed`. This is the one exception to one request
per host, and it can't go with `--dns-only`. `--timeout-ms` then is per address on each route. Beyond
per host, and it can't go with `--dns-only`. The addresses are all asked at the same time, so a host
takes as long as its slowest address, and they are printed in the order of trying them all the same.
`--timeout-ms` then is per address on each route. Beyond
that, the next paragraph holds for each address, and nothing is followed here either. HTTP/2 and HTTP/3
have no reason phrase, so there the `OK` is what that status is called.
have no reason phrase, so there the `OK` is what that status is called. A redirect has its `Location`
at the end of the line, as in `301 Moved Permanently -> /elsewhere`, and the header fields are left out.

**One request means one.** The next route is for when connecting failed. Once a request has gone out,
the host has been asked, so that a timeout or a response cut short ends it there, as a failure.
Expand Down
2 changes: 1 addition & 1 deletion doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ The two tokens in `set-cookie` and `report-to` are cut short to `…`.
21:01:20.687 debug [h3:[2606:4700::6810:84e5]:443] write_pkt: nghttp3_conn_writev_stream -> stream=-1 sveccnt=0 fin=0
--- 62 ------------------------------------------------------------------------
21:01:20.688 info [client] dtor
cloudflare.com: 301 [h3 cloudflare.com:443] at 2606:4700::6810:84e5, 167 bytes in 82ms
cloudflare.com: 301 [h3 cloudflare.com:443] at 2606:4700::6810:84e5, 167 bytes in 82ms -> https://www.cloudflare.com/
record: HTTPS 1 . alpn=h3,h2 ipv4hint=104.16.132.229,104.16.133.229 ipv6hint=2606:4700::6810:84e5,2606:4700::6810:85e5
content-type: text/html; charset=UTF-8
date: Tue, 03 Nov 2026 21:01:21 GMT
Expand Down
29 changes: 20 additions & 9 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

#include <boost/capy/ex/run_async.hpp>
#include <boost/corosio/io_context.hpp>
#include <boost/program_options.hpp>
#include <boost/program_options/errors.hpp>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/program_options/positional_options.hpp>
#include <boost/program_options/value_semantic.hpp>
#include <boost/program_options/variables_map.hpp>

#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>
Expand Down Expand Up @@ -158,16 +163,22 @@ po::options_description describe_options(Arguments& arguments)
config.timeout = std::chrono::milliseconds(not_negative("--timeout-ms", value));
}),
"how long one request may take");
opts("h3", po::bool_switch(&config.h3_only),
"connect only to the hosts with an HTTPS record that offers HTTP/3");
opts("h3", po::bool_switch(&config.protocols.h3),
"speak HTTP/3, where an HTTPS record offers it. Of --h3, --h2 and --h1, only those given "
"are spoken, all of them without any, and a host that offers none of them is left out");
opts("h2", po::bool_switch(&config.protocols.h2),
"speak HTTP/2, over TCP where a record offers it or there is none. Without --h1, a server "
"that chooses HTTP/1.1 is not sent the request");
opts("h1", po::bool_switch(&config.protocols.h1),
"speak HTTP/1.1, over TCP where a record offers it or there is none");
opts("dns-only", po::bool_switch(&config.dns_only),
"connect to nothing: stop after the lookups, and print the records and the addresses of "
"each host");
opts("each-address", po::bool_switch(&config.each_address),
"send a request to each address of a host and print the status line of each, instead of "
"one request per host");
opts("all", po::bool_switch(&arguments.all),
"also print the hosts that do not exist, and those that --h3 leaves out");
"also print the hosts that do not exist, and those that --h3, --h2 or --h1 leave out");
opts("color", po::bool_switch(&arguments.color),
"colour the output even where it does not go to a terminal, e.g. into 'less -R'");
opts("verbose,v", po::value<std::vector<std::string>>()->zero_tokens()->composing(),
Expand All @@ -186,7 +197,7 @@ void print_usage(std::ostream& out, const po::options_description& options)
<< wrap(exit_code, columns) << '\n';
}

void parse_arguments(int argc, char* argv[], const po::options_description& options,
void parse_arguments(int argc, char** argv, const po::options_description& options,
Arguments& arguments)
{
try
Expand Down Expand Up @@ -305,10 +316,10 @@ int main(int argc, char* argv[])
++counts[result.outcome];
if (!result.records.empty() || result.aliased)
++with_records;
if (all || (result.outcome != Outcome::not_found && result.outcome != Outcome::no_h3))
if (all || (result.outcome != Outcome::not_found && result.outcome != Outcome::left_out))
{
std::print("{}", http_probe::format_result(result, color_results));
std::fflush(stdout); // a run can take a while, and it is worth watching
std::ignore = std::fflush(stdout); // a run can take a while, and it is worth watching
}
};

Expand All @@ -320,9 +331,9 @@ int main(int argc, char* argv[])
// What a handler throws ends the process, so this one leaves it for after run().
//
boost::capy::run_async(
context.get_executor(), []() noexcept {},
context.get_executor(), [] noexcept {},
[&](std::exception_ptr ex) noexcept {
failed = ex;
failed = std::move(ex);
context.stop();
})(prober.run());
if (verbosity >= 2)
Expand Down
32 changes: 20 additions & 12 deletions src/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ std::string address_text(std::string_view address, bool color)
return colored(address, v6 ? color_of::v6 : color_of::v4, color);
}

//
// Where a redirect points, as the server put it, or nothing.
//
std::string location_text(std::string_view location)
{
return location.empty() ? std::string() : std::format(" -> {}", location);
}

//
// What came of the request to one address: the status line, as far as it goes without the version,
// which the routes say more about, or what went wrong.
Expand All @@ -51,9 +59,9 @@ std::string reply_text(const AddressReply& reply, bool color)
{
if (!reply.error.empty())
return std::format("{}: {}", colored("failed", color_of::bad, color), reply.error);
return reply.reason.empty()
? status_text(reply.status, color)
: std::format("{} {}", status_text(reply.status, color), reply.reason);
return std::format("{}{}{}", status_text(reply.status, color),
reply.reason.empty() ? "" : std::format(" {}", reply.reason),
location_text(reply.location));
}

//
Expand Down Expand Up @@ -108,12 +116,12 @@ std::string format_result(const Result& result, bool color)
switch (result.outcome)
{
case Outcome::answered:
text =
!result.replies.empty()
? std::format("{}: {}\n", host, colored("answered", color_of::good, color))
: std::format("{}: {} [{}] at {}, {} bytes in {}\n", host,
status_text(result.status, color), result.route,
address_text(result.address, color), result.body_size, result.duration);
text = !result.replies.empty()
? std::format("{}: {}\n", host, colored("answered", color_of::good, color))
: std::format("{}: {} [{}] at {}, {} bytes in {}{}\n", host,
status_text(result.status, color), result.route,
address_text(result.address, color), result.body_size,
result.duration, location_text(result.location));
break;
case Outcome::failed:
text = !result.replies.empty()
Expand All @@ -133,8 +141,8 @@ std::string format_result(const Result& result, bool color)
case Outcome::lookup_failed:
text = std::format("{}: {}\n", host, colored("lookup failed", color_of::bad, color));
break;
case Outcome::no_h3:
text = std::format("{}: no h3 record\n", host);
case Outcome::left_out:
text = std::format("{}: left out\n", host);
break;
case Outcome::resolved:
text = std::format("{}: {}\n", host, colored("resolved", color_of::good, color));
Expand Down Expand Up @@ -168,7 +176,7 @@ std::string format_summary(Counts counts, bool color)
count(Outcome::failed, "failed", color_of::bad),
count(Outcome::out_of_reach, "out of reach", color_of::moved), counts[Outcome::not_found],
count(Outcome::lookup_failed, "lookups failed", color_of::bad),
counts[Outcome::no_h3] ? std::format(", {} without h3", counts[Outcome::no_h3]) : "",
counts[Outcome::left_out] ? std::format(", {} left out", counts[Outcome::left_out]) : "",
counts[Outcome::resolved]
? std::format(", {}", count(Outcome::resolved, "resolved", color_of::good))
: "");
Expand Down
Loading