blob: cb3a1dc22f35bb49114fda2988a37a469b01f86d [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/test/test_doh_server.h"
#include <string.h>
#include <memory>
#include "base/base64url.h"
#include "base/bind.h"
#include "base/check.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/synchronization/lock.h"
#include "net/base/io_buffer.h"
#include "net/base/url_util.h"
#include "net/dns/dns_query.h"
#include "net/dns/dns_response.h"
#include "net/dns/dns_test_util.h"
#include "net/dns/dns_util.h"
#include "net/dns/public/dns_protocol.h"
#include "net/http/http_status_code.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "url/gurl.h"
namespace net {
namespace {
const char kPath[] = "/dns-query";
std::unique_ptr<test_server::HttpResponse> MakeHttpErrorResponse(
HttpStatusCode status,
base::StringPiece error) {
auto response = std::make_unique<test_server::BasicHttpResponse>();
response->set_code(status);
response->set_content(std::string(error));
response->set_content_type("text/plain;charset=utf-8");
return response;
}
std::unique_ptr<test_server::HttpResponse> MakeHttpResponseFromDns(
const DnsResponse& dns_response) {
if (!dns_response.IsValid()) {
return MakeHttpErrorResponse(HTTP_INTERNAL_SERVER_ERROR,
"error making DNS response");
}
auto response = std::make_unique<test_server::BasicHttpResponse>();
response->set_code(HTTP_OK);
response->set_content(std::string(dns_response.io_buffer()->data(),
dns_response.io_buffer_size()));
response->set_content_type("application/dns-message");
return response;
}
} // namespace
TestDohServer::TestDohServer() {
server_.RegisterRequestHandler(base::BindRepeating(
&TestDohServer::HandleRequest, base::Unretained(this)));
}
TestDohServer::~TestDohServer() = default;
void TestDohServer::SetHostname(base::StringPiece name) {
DCHECK(!server_.Started());
hostname_ = std::string(name);
}
void TestDohServer::SetFailRequests(bool fail_requests) {
base::AutoLock lock(lock_);
fail_requests_ = fail_requests;
}
void TestDohServer::AddAddressRecord(base::StringPiece name,
const IPAddress& address,
base::TimeDelta ttl) {
AddRecord(BuildTestAddressRecord(std::string(name), address, ttl));
}
void TestDohServer::AddRecord(const DnsResourceRecord& record) {
base::AutoLock lock(lock_);
records_.insert(
std::make_pair(std::make_pair(record.name, record.type), record));
}
bool TestDohServer::Start() {
if (!InitializeAndListen()) {
return false;
}
StartAcceptingConnections();
return true;
}
bool TestDohServer::InitializeAndListen() {
if (hostname_) {
EmbeddedTestServer::ServerCertificateConfig cert_config;
cert_config.dns_names = {*hostname_};
server_.SetSSLConfig(cert_config);
} else {
// `CERT_OK` is valid for 127.0.0.1.
server_.SetSSLConfig(EmbeddedTestServer::CERT_OK);
}
return server_.InitializeAndListen();
}
void TestDohServer::StartAcceptingConnections() {
server_.StartAcceptingConnections();
}
bool TestDohServer::ShutdownAndWaitUntilComplete() {
return server_.ShutdownAndWaitUntilComplete();
}
std::string TestDohServer::GetTemplate() {
GURL url =
hostname_ ? server_.GetURL(*hostname_, kPath) : server_.GetURL(kPath);
return url.spec() + "{?dns}";
}
std::string TestDohServer::GetPostOnlyTemplate() {
GURL url =
hostname_ ? server_.GetURL(*hostname_, kPath) : server_.GetURL(kPath);
return url.spec();
}
int TestDohServer::QueriesServed() {
base::AutoLock lock(lock_);
return queries_served_;
}
std::unique_ptr<test_server::HttpResponse> TestDohServer::HandleRequest(
const test_server::HttpRequest& request) {
GURL request_url = request.GetURL();
if (request_url.path_piece() != kPath) {
return nullptr;
}
base::AutoLock lock(lock_);
queries_served_++;
if (fail_requests_) {
return MakeHttpErrorResponse(HTTP_NOT_FOUND, "failed request");
}
// See RFC 8484, Section 4.1.
std::string query;
if (request.method == test_server::METHOD_GET) {
std::string query_b64;
if (!GetValueForKeyInQuery(request_url, "dns", &query_b64) ||
!base::Base64UrlDecode(
query_b64, base::Base64UrlDecodePolicy::IGNORE_PADDING, &query)) {
return MakeHttpErrorResponse(HTTP_BAD_REQUEST,
"could not decode query string");
}
} else if (request.method == test_server::METHOD_POST) {
auto content_type = request.headers.find("content-type");
if (content_type == request.headers.end() ||
content_type->second != "application/dns-message") {
return MakeHttpErrorResponse(HTTP_BAD_REQUEST,
"unsupported content type");
}
query = request.content;
} else {
return MakeHttpErrorResponse(HTTP_BAD_REQUEST, "invalid method");
}
// Parse the DNS query.
auto query_buf = base::MakeRefCounted<IOBufferWithSize>(query.size());
memcpy(query_buf->data(), query.data(), query.size());
DnsQuery dns_query(std::move(query_buf));
if (!dns_query.Parse(query.size())) {
return MakeHttpErrorResponse(HTTP_BAD_REQUEST, "invalid DNS query");
}
absl::optional<std::string> name =
DnsDomainToString(dns_query.qname(), /*require_complete=*/true);
if (!name) {
DnsResponse response(dns_query.id(), /*is_authoritative=*/false,
/*answers=*/{}, /*authority_records=*/{},
/*additional_records=*/{}, dns_query,
dns_protocol::kRcodeFORMERR);
return MakeHttpResponseFromDns(response);
}
auto range = records_.equal_range(std::make_pair(*name, dns_query.qtype()));
std::vector<DnsResourceRecord> answers;
for (auto i = range.first; i != range.second; ++i) {
answers.push_back(i->second);
}
VLOG(1) << "Serving " << answers.size() << " records for " << *name
<< ", qtype " << dns_query.qtype();
// Note `answers` may be empty. NOERROR with no answers is how to express
// NODATA, so there is no need handle it specially.
//
// For now, this server does not support configuring additional records. When
// testing more complex HTTPS record cases, this will need to be extended.
//
// TODO(crbug.com/1251204): Add SOA records to test the default TTL.
DnsResponse response(dns_query.id(), /*is_authoritative=*/true,
/*answers=*/answers, /*authority_records=*/{},
/*additional_records=*/{}, dns_query);
return MakeHttpResponseFromDns(response);
}
} // namespace net