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
12 changes: 7 additions & 5 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ endif()
#
# I/O runtime: Capy and Corosio, and only those. The libraries take either, which is their business:
# this is a program, and it gets to decide. These shadow the cache entries of the libraries, which
# go on saying ASIO in CMakeCache.txt.
# go on saying ASIO in CMakeCache.txt. c-ares-async is headers, and its runtime is the target a
# program links, c_ares_async::corosio here; its option only has it look for Corosio rather than
# insist on Asio.
#
set(ANYHTTP_API COROSIO)
set(C_ARES_ASYNC_API ${ANYHTTP_API})
Expand All @@ -48,16 +50,16 @@ set(C_ARES_ASYNC_TLS ${TLS_LIBRARY})
include(FetchContent)
FetchContent_Declare(anyhttp
GIT_REPOSITORY https://github.com/pgit/anyhttp.git
GIT_TAG 296e8ee11f2bec37aa1bf5edf345017d888e381a
GIT_TAG bc985a0e040d8b2dd027552b964bde19b0fbf38a
SYSTEM)
FetchContent_Declare(c_ares_async
GIT_REPOSITORY https://github.com/pgit/c-ares-async.git
GIT_TAG 6d46c6c8e65231f23a250bcec5dd00b3a1d9ba34
GIT_TAG 2bbeb48fcd7ca539fc1c6f3fc580984cde7ada4b
SYSTEM)

#
# Both libraries pin these as well. The first declaration wins, so these are the ones both of them
# get, and here they are SYSTEM, too. They follow anyhttp's pins, which c-ares-async's lag behind.
# get, and here they are SYSTEM, too. They are anyhttp's pins, and c-ares-async has the same.
#
FetchContent_Declare(capy
GIT_REPOSITORY https://github.com/cppalliance/capy.git
Expand Down Expand Up @@ -85,7 +87,7 @@ set(http_probe_warnings -Wall -Wextra -Werror -Wno-missing-designated-field-init

add_library(http_probe_core STATIC src/output.cpp src/prober.cpp src/routes.cpp)
target_include_directories(http_probe_core PUBLIC src)
target_link_libraries(http_probe_core PUBLIC anyhttp::anyhttp c_ares_async::c_ares_async)
target_link_libraries(http_probe_core PUBLIC anyhttp::anyhttp c_ares_async::corosio)
target_compile_options(http_probe_core PRIVATE ${http_probe_warnings})

add_executable(http-probe src/main.cpp)
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ http-probe --dot --dns-server one.one.one.one cloudflare.com

It looks up the HTTPS records of each host in DNS ([RFC 9460](https://www.rfc-editor.org/rfc/rfc9460)),
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. Its `Location` goes at the end of the status line, as in `301 … -> https://www.example.com/`,
the route, the IP address it connected to and the time it took, on one line, along with the host's
records. `--all-headers` adds the header fields of the response below that line. A redirect is
reported and not followed, and nothing else on the host 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
Expand Down Expand Up @@ -93,7 +93,8 @@ takes as long as its slowest address, and they are printed in the order of tryin
`--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. A redirect has its `Location`
at the end of the line, as in `301 Moved Permanently -> /elsewhere`, and the header fields are left out.
at the end of the line, as in `301 Moved Permanently -> /elsewhere`, and `--all-headers` adds the
header fields of each response below its line.

**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 @@ -3,7 +3,7 @@
One host, asked for its records over DNS over TLS. This is how the result looks in a terminal:

```sh
build/http-probe --dot --dns-server one.one.one.one cloudflare.com
build/http-probe --all-headers --dot --dns-server one.one.one.one cloudflare.com
```

![The result for cloudflare.com: 301 over HTTP/3, its HTTPS record and response header fields, and the summary line, in colour](sample-output.png)
Expand Down
12 changes: 8 additions & 4 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ constexpr std::string_view usage = R"(usage: http-probe [options] HOST...

constexpr std::string_view about =
"Looks up the HTTPS records of each host (RFC 9460), and requests https://HOST/ once, the way "
"they say or over TCP on port 443 if they don't, printing the status and header fields of the "
"response. Redirects are reported and not followed, and nothing else on the host is requested.";
"they say or over TCP on port 443 if they don't, printing the status of the response on one "
"line. Redirects are reported and not followed, and nothing else on the host is requested.";

constexpr std::string_view exit_code =
"The exit code is 0 if every host that was connected to answered, whatever the status, and "
Expand All @@ -59,6 +59,7 @@ struct Arguments
std::vector<std::string> files;
bool help = false;
bool all = false;
bool all_headers = false;
bool color = false;
int verbosity = 0;
};
Expand Down Expand Up @@ -177,6 +178,8 @@ po::options_description describe_options(Arguments& arguments)
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-headers", po::bool_switch(&arguments.all_headers),
"also print the header fields of each response, after its status line");
opts("all", po::bool_switch(&arguments.all),
"also print the hosts that do not exist, and those that --h3, --h2 or --h1 leave out");
opts("color", po::bool_switch(&arguments.color),
Expand Down Expand Up @@ -305,7 +308,7 @@ int main(int argc, char* argv[])
return 0;
}

auto& [config, files, help, all, color, verbosity] = arguments;
auto& [config, files, help, all, all_headers, color, verbosity] = arguments;
config.logger = set_up_logging(verbosity, color);

const bool own_dns_server = !config.dns_server.empty();
Expand All @@ -318,7 +321,8 @@ int main(int argc, char* argv[])
++with_records;
if (all || (result.outcome != Outcome::not_found && result.outcome != Outcome::left_out))
{
std::print("{}", http_probe::format_result(result, color_results));
std::print("{}", http_probe::format_result(
result, {.color = color_results, .all_headers = all_headers}));
std::ignore = std::fflush(stdout); // a run can take a while, and it is worth watching
}
};
Expand Down
22 changes: 19 additions & 3 deletions src/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,26 @@ std::string record_text(const c_ares_async::https_record& record, bool color)
return text;
}

//
// The header fields of a response, a line each, \p indent deep.
//
std::string fields_text(const HeaderFields& fields, std::string_view indent, bool color)
{
std::string text;
for (const auto& [name, value] : fields)
text += std::format("{}{}: {}\n", indent, colored(name, color_of::field, color), value);
return text;
}

// =================================================================================================

} // namespace

std::string format_result(const Result& result, bool color)
std::string format_result(const Result& result, const Style& style)
{
using Outcome = Result::Outcome;

const bool color = style.color;
const auto host = colored(result.host, color_of::name, color);
std::string text;
switch (result.outcome)
Expand Down Expand Up @@ -154,12 +166,16 @@ std::string format_result(const Result& result, bool color)
for (const auto& address : result.addresses)
text += std::format(" address: {}\n", address_text(address, color));
for (const auto& reply : result.replies)
{
text += std::format(" address: {} {}\n", address_text(reply.address, color),
reply_text(reply, color));
if (style.all_headers)
text += fields_text(reply.fields, " ", color);
}
for (const auto& note : result.notes)
text += colored(std::format(" note: {}", note), color_of::note, color) + "\n";
for (const auto& [name, value] : result.fields)
text += std::format(" {}: {}\n", colored(name, color_of::field, color), value);
if (style.all_headers)
text += fields_text(result.fields, " ", color);
return text;
}

Expand Down
12 changes: 10 additions & 2 deletions src/output.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@ namespace http_probe

using Counts = std::map<Result::Outcome, std::size_t>;

/// All there is to say about one host, a line each and with a line break at the end.
std::string format_result(const Result& result, bool color);
/// What to print of a result, and how.
struct Style
{
bool color = false;
bool all_headers = false; ///< the header fields of each response, not only its status line
};

/// All there is to say about one host, a line each and with a line break at the end. A response
/// has a line of its own, and with Style::all_headers its header fields after that.
std::string format_result(const Result& result, const Style& style);

/// How it went overall, without a line break.
std::string format_summary(Counts counts, bool color);
Expand Down
12 changes: 10 additions & 2 deletions src/prober.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ std::string redirect_location(const anyhttp::client::Message& message)
: std::string();
}

HeaderFields header_fields(const anyhttp::client::Message& message)
{
HeaderFields fields;
for (const auto& field : message)
fields.emplace_back(field.name_string(), field.value());
return fields;
}

//
// The resolver does not write anywhere by itself. Its messages go where our own go.
//
Expand Down Expand Up @@ -465,8 +473,7 @@ capy::task<Result> Prober::probe(const std::string& host)
auto spoken = route;
spoken.protocol = response.protocol;
result.route = spoken.to_string();
for (const auto& field : response.message)
result.fields.emplace_back(field.name_string(), field.value());
result.fields = header_fields(response.message);
result.body_size = response.message.body().size();
result.duration = response.duration;
co_return result;
Expand Down Expand Up @@ -500,6 +507,7 @@ capy::task<capy::results<AddressReply>> Prober::ask_address(const std::vector<Ro
reply.status = static_cast<int>(response.message.result_int());
reply.reason = response.message.reason();
reply.location = redirect_location(response.message);
reply.fields = header_fields(response.message);
reply.error.clear();
co_return {std::error_code(), std::move(reply)};
}
Expand Down
6 changes: 5 additions & 1 deletion src/prober.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,17 @@ std::vector<std::string> read_hosts(std::istream& list, std::vector<std::string>
/// Moves the IPv6 addresses to the front, each family keeping the order it was in.
void prefer_ipv6(std::vector<corosio::endpoint>& addresses);

/// The header fields of a response, in the order they came.
using HeaderFields = std::vector<std::pair<std::string, std::string>>;

/// What came of the request to one address of a host, see Config::each_address.
struct AddressReply
{
std::string address;
int status = 0; ///< without \c error
std::string reason; ///< without \c error: "OK", which HTTP/2 and HTTP/3 only know by the status
std::string location; ///< with a 3xx status: the Location field as it came, see Result::location
HeaderFields fields; ///< without \c error
std::string error; ///< what went wrong on the last route, if no response could be had
};

Expand All @@ -130,7 +134,7 @@ struct Result

int status = 0; ///< with \c answered
std::string route; ///< with \c answered: what the response came over, as the server chose it
std::vector<std::pair<std::string, std::string>> fields; ///< with \c answered
HeaderFields fields; ///< with \c answered
/// With \c answered and a 3xx status: the Location field as it came, neither resolved nor
/// followed, and empty without one.
std::string location;
Expand Down
9 changes: 8 additions & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ add_library(
fake_dns STATIC ${c_ares_async_SOURCE_DIR}/test/fake_dns_server.cpp
${c_ares_async_SOURCE_DIR}/test/test_certificate.cpp)
target_include_directories(fake_dns SYSTEM PUBLIC ${c_ares_async_SOURCE_DIR}/test)
target_link_libraries(fake_dns PUBLIC c_ares_async::c_ares_async anyhttp::anyhttp OpenSSL::SSL
target_link_libraries(fake_dns PUBLIC c_ares_async::corosio anyhttp::anyhttp OpenSSL::SSL
OpenSSL::Crypto)

add_library(test_site STATIC site.cpp)
Expand Down Expand Up @@ -99,6 +99,13 @@ add_test(NAME http_probe_refuses_each_address_with_dns_only
set_tests_properties(http_probe_refuses_each_address_with_dns_only PROPERTIES
PASS_REGULAR_EXPRESSION "rules out --each-address")

# --all is a prefix of --all-headers, which program_options takes abbreviations of, and is still
# itself rather than ambiguous.
add_test(NAME http_probe_tells_all_from_all_headers
COMMAND http-probe --all --dns-server 127.0.0.1:1 localhost)
set_tests_properties(http_probe_tells_all_from_all_headers PROPERTIES
PASS_REGULAR_EXPRESSION "1 lookups failed")

# An empty list gets as far as having no hosts, and goes nowhere.
add_test(NAME http_probe_takes_each_address_with_a_list
COMMAND http-probe --each-address --file /dev/null)
Expand Down
Loading