Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 1 | // Copyright 2021 The Chromium OS Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "dns-proxy/doh_curl_client.h" |
| 6 | |
| 7 | #include <utility> |
| 8 | |
Jason Jeremy Iman | 845f293 | 2021-01-31 16:12:13 +0900 | [diff] [blame] | 9 | #include <base/bind.h> |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 10 | #include <base/strings/string_util.h> |
Jason Jeremy Iman | 845f293 | 2021-01-31 16:12:13 +0900 | [diff] [blame] | 11 | #include <base/threading/thread_task_runner_handle.h> |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 12 | |
| 13 | namespace dns_proxy { |
| 14 | namespace { |
| 15 | constexpr char kLinuxUserAgent[] = |
| 16 | "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (kHTML, like Gecko) " |
| 17 | "Chrome/7.0.38.09.132 Safari/537.36"; |
| 18 | constexpr std::array<const char*, 2> kDoHHeaderList{ |
| 19 | {"Accept: application/dns-message", |
| 20 | "Content-Type: application/dns-message"}}; |
| 21 | } // namespace |
| 22 | |
Jason Jeremy Iman | 845f293 | 2021-01-31 16:12:13 +0900 | [diff] [blame] | 23 | DoHCurlClient::CurlResult::CurlResult(CURLcode curl_code, |
| 24 | int64_t http_code, |
| 25 | int64_t retry_delay_ms) |
| 26 | : curl_code(curl_code), |
| 27 | http_code(http_code), |
| 28 | retry_delay_ms(retry_delay_ms) {} |
| 29 | |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 30 | DoHCurlClient::State::State(CURL* curl, |
| 31 | const QueryCallback& callback, |
| 32 | void* ctx, |
| 33 | int request_id) |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 34 | : curl(curl), |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 35 | callback(callback), |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 36 | ctx(ctx), |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 37 | header_list(nullptr), |
| 38 | request_id(request_id) {} |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 39 | |
| 40 | DoHCurlClient::State::~State() { |
| 41 | curl_easy_cleanup(curl); |
| 42 | curl_slist_free_all(header_list); |
| 43 | } |
| 44 | |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 45 | void DoHCurlClient::State::RunCallback(CURLMsg* curl_msg, int64_t http_code) { |
Jason Jeremy Iman | 845f293 | 2021-01-31 16:12:13 +0900 | [diff] [blame] | 46 | // TODO(jasongustaman): Use HTTP 429, Retry-After header value. |
| 47 | CurlResult res(curl_msg->data.result, http_code, 0 /* retry_delay_ms */); |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 48 | callback.Run(ctx, res, response.data(), response.size()); |
Jason Jeremy Iman | 845f293 | 2021-01-31 16:12:13 +0900 | [diff] [blame] | 49 | } |
| 50 | |
| 51 | void DoHCurlClient::State::SetResponse(char* msg, size_t len) { |
| 52 | if (len <= 0) { |
| 53 | LOG(ERROR) << "Unexpected length: " << len; |
| 54 | return; |
| 55 | } |
| 56 | response.insert(response.end(), msg, msg + len); |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 57 | } |
| 58 | |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 59 | DoHCurlClient::DoHCurlClient(base::TimeDelta timeout, |
| 60 | int max_concurrent_queries) |
| 61 | : timeout_seconds_(timeout.InSeconds()), |
| 62 | max_concurrent_queries_(max_concurrent_queries) { |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 63 | // Initialize CURL. |
| 64 | curl_global_init(CURL_GLOBAL_DEFAULT); |
| 65 | curlm_ = curl_multi_init(); |
| 66 | |
| 67 | // Set socket callback to `SocketCallback(...)`. This function will be called |
| 68 | // whenever a CURL socket state is changed. DoHCurlClient class |this| will |
| 69 | // passed as a parameter of the callback. |
| 70 | curl_multi_setopt(curlm_, CURLMOPT_SOCKETDATA, this); |
| 71 | curl_multi_setopt(curlm_, CURLMOPT_SOCKETFUNCTION, |
| 72 | &DoHCurlClient::SocketCallback); |
| 73 | |
| 74 | // Set timer callback to `TimerCallback(...)`. This function will be called |
| 75 | // whenever a timeout change happened. DoHCurlClient class |this| will be |
| 76 | // passed as a parameter of the callback. |
| 77 | curl_multi_setopt(curlm_, CURLMOPT_TIMERDATA, this); |
| 78 | curl_multi_setopt(curlm_, CURLMOPT_TIMERFUNCTION, |
| 79 | &DoHCurlClient::TimerCallback); |
| 80 | } |
| 81 | |
| 82 | DoHCurlClient::~DoHCurlClient() { |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 83 | // Cancel all in-flight queries. |
| 84 | for (const auto& requests : requests_) { |
| 85 | CancelRequest(requests.second); |
| 86 | } |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 87 | curl_global_cleanup(); |
| 88 | } |
| 89 | |
| 90 | void DoHCurlClient::HandleResult(CURLMsg* curl_msg) { |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 91 | // `HandleResult(...)` may be called even after `CancelRequest(...)` is |
| 92 | // called. This happens if a query is completed while queries are being |
| 93 | // cancelled. On such case, do nothing. |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 94 | if (!base::Contains(states_, curl_msg->easy_handle)) { |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 95 | return; |
| 96 | } |
| 97 | |
| 98 | CURL* curl = curl_msg->easy_handle; |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 99 | State* state = states_[curl].get(); |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 100 | |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 101 | int64_t http_code = 0; |
| 102 | curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); |
| 103 | |
| 104 | // Run the callback if the current request is the first successful request |
| 105 | // or the current request is the last request (noted by the number of request |
| 106 | // with the same |request_id| is 1). |
| 107 | if (http_code == kHTTPOk || requests_[state->request_id].size() == 1) { |
| 108 | state->RunCallback(curl_msg, http_code); |
| 109 | CancelRequest(state->request_id); |
| 110 | return; |
| 111 | } |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 112 | // TODO(jasongustaman): Get and save curl metrics. |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 113 | } |
| 114 | |
| 115 | void DoHCurlClient::CheckMultiInfo() { |
| 116 | CURLMsg* curl_msg = nullptr; |
| 117 | int msgs_left = 0; |
| 118 | while ((curl_msg = curl_multi_info_read(curlm_, &msgs_left))) { |
| 119 | if (curl_msg->msg != CURLMSG_DONE) { |
| 120 | continue; |
| 121 | } |
| 122 | HandleResult(curl_msg); |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | void DoHCurlClient::OnFileCanReadWithoutBlocking(curl_socket_t socket_fd) { |
| 127 | int still_running; |
| 128 | CURLMcode rc = curl_multi_socket_action(curlm_, socket_fd, CURL_CSELECT_IN, |
| 129 | &still_running); |
| 130 | if (rc != CURLM_OK) { |
| 131 | LOG(INFO) << "Failed to read from socket: " << curl_multi_strerror(rc); |
| 132 | return; |
| 133 | } |
| 134 | CheckMultiInfo(); |
| 135 | } |
| 136 | |
| 137 | void DoHCurlClient::OnFileCanWriteWithoutBlocking(curl_socket_t socket_fd) { |
| 138 | int still_running; |
| 139 | CURLMcode rc = curl_multi_socket_action(curlm_, socket_fd, CURL_CSELECT_OUT, |
| 140 | &still_running); |
| 141 | if (rc != CURLM_OK) { |
| 142 | LOG(INFO) << "Failed to write to socket: " << curl_multi_strerror(rc); |
| 143 | return; |
| 144 | } |
| 145 | CheckMultiInfo(); |
| 146 | } |
| 147 | |
| 148 | void DoHCurlClient::AddReadWatcher(curl_socket_t socket_fd) { |
| 149 | if (!base::Contains(read_watchers_, socket_fd)) { |
| 150 | read_watchers_.emplace( |
| 151 | socket_fd, |
| 152 | base::FileDescriptorWatcher::WatchReadable( |
| 153 | socket_fd, |
| 154 | base::BindRepeating(&DoHCurlClient::OnFileCanReadWithoutBlocking, |
| 155 | weak_factory_.GetWeakPtr(), socket_fd))); |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | void DoHCurlClient::AddWriteWatcher(curl_socket_t socket_fd) { |
| 160 | if (!base::Contains(write_watchers_, socket_fd)) { |
| 161 | write_watchers_.emplace( |
| 162 | socket_fd, |
| 163 | base::FileDescriptorWatcher::WatchReadable( |
| 164 | socket_fd, |
| 165 | base::BindRepeating(&DoHCurlClient::OnFileCanWriteWithoutBlocking, |
| 166 | weak_factory_.GetWeakPtr(), socket_fd))); |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | void DoHCurlClient::RemoveWatcher(curl_socket_t socket_fd) { |
| 171 | read_watchers_.erase(socket_fd); |
| 172 | write_watchers_.erase(socket_fd); |
| 173 | } |
| 174 | |
| 175 | int DoHCurlClient::SocketCallback( |
| 176 | CURL* easy, curl_socket_t socket_fd, int what, void* userp, void* socketp) { |
| 177 | DoHCurlClient* client = static_cast<DoHCurlClient*>(userp); |
| 178 | switch (what) { |
| 179 | case CURL_POLL_IN: |
| 180 | client->AddReadWatcher(socket_fd); |
| 181 | return 0; |
| 182 | case CURL_POLL_OUT: |
| 183 | client->AddWriteWatcher(socket_fd); |
| 184 | return 0; |
| 185 | case CURL_POLL_INOUT: |
| 186 | client->AddReadWatcher(socket_fd); |
| 187 | client->AddWriteWatcher(socket_fd); |
| 188 | return 0; |
| 189 | case CURL_POLL_REMOVE: |
| 190 | client->RemoveWatcher(socket_fd); |
| 191 | return 0; |
| 192 | default: |
| 193 | return 0; |
| 194 | } |
| 195 | } |
| 196 | |
Jason Jeremy Iman | 845f293 | 2021-01-31 16:12:13 +0900 | [diff] [blame] | 197 | void DoHCurlClient::TimeoutCallback() { |
| 198 | int still_running; |
| 199 | curl_multi_socket_action(curlm_, CURL_SOCKET_TIMEOUT, 0, &still_running); |
| 200 | CheckMultiInfo(); |
| 201 | } |
| 202 | |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 203 | int DoHCurlClient::TimerCallback(CURLM* multi, |
| 204 | int64_t timeout_ms, |
| 205 | void* userp) { |
| 206 | DoHCurlClient* client = static_cast<DoHCurlClient*>(userp); |
Jason Jeremy Iman | 845f293 | 2021-01-31 16:12:13 +0900 | [diff] [blame] | 207 | if (timeout_ms > 0) { |
| 208 | base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| 209 | FROM_HERE, |
| 210 | base::BindRepeating(&DoHCurlClient::TimeoutCallback, |
| 211 | base::Unretained(client)), |
| 212 | base::TimeDelta::FromMilliseconds(timeout_ms)); |
| 213 | } else if (timeout_ms == 0) { |
| 214 | client->TimeoutCallback(); |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 215 | } |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 216 | return 0; |
| 217 | } |
| 218 | |
| 219 | size_t DoHCurlClient::WriteCallback(char* ptr, |
| 220 | size_t size, |
| 221 | size_t nmemb, |
| 222 | void* userdata) { |
| 223 | State* state = static_cast<State*>(userdata); |
| 224 | size_t len = size * nmemb; |
Jason Jeremy Iman | 845f293 | 2021-01-31 16:12:13 +0900 | [diff] [blame] | 225 | state->SetResponse(ptr, len); |
| 226 | return len; |
| 227 | } |
| 228 | |
| 229 | size_t DoHCurlClient::HeaderCallback(void* data, |
| 230 | size_t size, |
| 231 | size_t nitems, |
| 232 | void* userp) { |
| 233 | State* state = static_cast<State*>(userp); |
| 234 | size_t len = size * nitems; |
| 235 | std::string header(static_cast<char*>(data), len); |
| 236 | state->header.emplace_back(header); |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 237 | return len; |
| 238 | } |
| 239 | |
| 240 | void DoHCurlClient::SetNameServers( |
| 241 | const std::vector<std::string>& name_servers) { |
| 242 | name_servers_ = base::JoinString(name_servers, ","); |
| 243 | } |
| 244 | |
| 245 | void DoHCurlClient::SetDoHProviders( |
| 246 | const std::vector<std::string>& doh_providers) { |
| 247 | doh_providers_ = doh_providers; |
| 248 | } |
| 249 | |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 250 | void DoHCurlClient::CancelRequest(const std::set<State*>& states) { |
| 251 | for (const auto& state : states) { |
| 252 | curl_multi_remove_handle(curlm_, state->curl); |
| 253 | states_.erase(state->curl); |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 254 | } |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 255 | } |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 256 | |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 257 | void DoHCurlClient::CancelRequest(int request_id) { |
| 258 | auto requests = requests_.find(request_id); |
| 259 | if (requests == requests_.end()) { |
| 260 | return; |
| 261 | } |
| 262 | // Cancel in-flight queries and delete the state. |
| 263 | CancelRequest(requests->second); |
| 264 | requests_.erase(request_id); |
| 265 | } |
| 266 | |
| 267 | std::unique_ptr<DoHCurlClient::State> DoHCurlClient::InitCurl( |
| 268 | const std::string& doh_provider, |
| 269 | const char* msg, |
| 270 | int len, |
| 271 | const QueryCallback& callback, |
| 272 | void* ctx) { |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 273 | CURL* curl; |
| 274 | curl = curl_easy_init(); |
| 275 | if (!curl) { |
| 276 | LOG(ERROR) << "Failed to initialize curl"; |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 277 | return nullptr; |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 278 | } |
| 279 | |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 280 | // Allocate a state for the request. |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 281 | std::unique_ptr<State> state = |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 282 | std::make_unique<State>(curl, callback, ctx, next_request_id_); |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 283 | |
| 284 | // Set the target URL which is the DoH provider to query to. |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 285 | curl_easy_setopt(curl, CURLOPT_URL, doh_provider.c_str()); |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 286 | |
| 287 | // Set the DNS name servers to resolve the URL(s) / DoH provider(s). |
| 288 | // This uses ares and will be done asynchronously. |
| 289 | curl_easy_setopt(curl, CURLOPT_DNS_SERVERS, name_servers_.c_str()); |
| 290 | |
| 291 | // Set the HTTP header to the needed DoH header. The stored value needs to |
| 292 | // be released when query is finished. |
| 293 | for (int i = 0; i < kDoHHeaderList.size(); i++) { |
| 294 | state.get()->header_list = |
| 295 | curl_slist_append(state.get()->header_list, kDoHHeaderList[i]); |
| 296 | } |
| 297 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, state.get()->header_list); |
| 298 | |
| 299 | // Stores the data to be sent through HTTP POST and its length. |
| 300 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, msg); |
| 301 | curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len); |
| 302 | |
| 303 | // Set the user agent for the query. |
| 304 | curl_easy_setopt(curl, CURLOPT_USERAGENT, kLinuxUserAgent); |
| 305 | |
| 306 | // Ignore signals SIGPIPE to be sent when the other end of CURL socket is |
| 307 | // closed. |
| 308 | curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 0); |
| 309 | |
| 310 | // Set timeout of the query. |
| 311 | curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_seconds_); |
| 312 | |
| 313 | // Set the callback to be called whenever CURL got a response. The data |
| 314 | // needs to be copied to the write data. |
| 315 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &DoHCurlClient::WriteCallback); |
| 316 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, state.get()); |
| 317 | |
| 318 | // Handle redirection automatically. |
| 319 | curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, 1L); |
| 320 | curl_easy_setopt(curl, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); |
| 321 | |
Jason Jeremy Iman | 9051298 | 2021-01-31 18:43:58 +0900 | [diff] [blame] | 322 | return state; |
| 323 | } |
| 324 | |
| 325 | bool DoHCurlClient::Resolve(const char* msg, |
| 326 | int len, |
| 327 | const QueryCallback& callback, |
| 328 | void* ctx) { |
| 329 | if (name_servers_.empty() || doh_providers_.empty()) { |
| 330 | LOG(DFATAL) << "DNS and DoH server must not be empty"; |
| 331 | return false; |
| 332 | } |
| 333 | |
| 334 | std::set<State*> requests; |
| 335 | int num_concurrent_queries = 0; |
| 336 | for (const auto& doh_provider : doh_providers_) { |
| 337 | std::unique_ptr<State> state = |
| 338 | InitCurl(doh_provider, msg, len, callback, ctx); |
| 339 | if (!state.get()) { |
| 340 | continue; |
| 341 | } |
| 342 | State* state_ptr = state.get(); |
| 343 | |
| 344 | // Create state structure to store required data of each query. |
| 345 | states_.emplace(state_ptr->curl, std::move(state)); |
| 346 | requests.emplace(state_ptr); |
| 347 | |
| 348 | // Runs the query asynchronously. |
| 349 | curl_multi_add_handle(curlm_, state_ptr->curl); |
| 350 | |
| 351 | // Queries at most |max_concurrent_queries_| times concurrently. |
| 352 | num_concurrent_queries++; |
| 353 | if (num_concurrent_queries >= max_concurrent_queries_) { |
| 354 | break; |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | if (requests.empty()) { |
| 359 | LOG(ERROR) << "No requests for query"; |
| 360 | return false; |
| 361 | } |
| 362 | |
| 363 | // Store the concurrent requests and increment |next_request_id_|. |
| 364 | requests_.emplace(next_request_id_, requests); |
| 365 | next_request_id_++; |
Jason Jeremy Iman | 15c3278 | 2021-01-27 04:19:43 +0900 | [diff] [blame] | 366 | return true; |
| 367 | } |
| 368 | } // namespace dns_proxy |