blob: eed1ca28ac28c9b7ee03877de9763adb328336eb [file] [log] [blame]
Jason Jeremy Iman15c32782021-01-27 04:19:43 +09001// 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 Iman845f2932021-01-31 16:12:13 +09009#include <base/bind.h>
Jason Jeremy Iman15c32782021-01-27 04:19:43 +090010#include <base/strings/string_util.h>
Jason Jeremy Iman845f2932021-01-31 16:12:13 +090011#include <base/threading/thread_task_runner_handle.h>
Jason Jeremy Iman15c32782021-01-27 04:19:43 +090012
13namespace dns_proxy {
14namespace {
15constexpr 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";
18constexpr std::array<const char*, 2> kDoHHeaderList{
19 {"Accept: application/dns-message",
20 "Content-Type: application/dns-message"}};
21} // namespace
22
Jason Jeremy Iman845f2932021-01-31 16:12:13 +090023DoHCurlClient::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 Iman90512982021-01-31 18:43:58 +090030DoHCurlClient::State::State(CURL* curl,
31 const QueryCallback& callback,
32 void* ctx,
33 int request_id)
Jason Jeremy Iman15c32782021-01-27 04:19:43 +090034 : curl(curl),
Jason Jeremy Iman90512982021-01-31 18:43:58 +090035 callback(callback),
Jason Jeremy Iman15c32782021-01-27 04:19:43 +090036 ctx(ctx),
Jason Jeremy Iman90512982021-01-31 18:43:58 +090037 header_list(nullptr),
38 request_id(request_id) {}
Jason Jeremy Iman15c32782021-01-27 04:19:43 +090039
40DoHCurlClient::State::~State() {
41 curl_easy_cleanup(curl);
42 curl_slist_free_all(header_list);
43}
44
Jason Jeremy Iman90512982021-01-31 18:43:58 +090045void DoHCurlClient::State::RunCallback(CURLMsg* curl_msg, int64_t http_code) {
Jason Jeremy Iman845f2932021-01-31 16:12:13 +090046 // TODO(jasongustaman): Use HTTP 429, Retry-After header value.
47 CurlResult res(curl_msg->data.result, http_code, 0 /* retry_delay_ms */);
Jason Jeremy Iman90512982021-01-31 18:43:58 +090048 callback.Run(ctx, res, response.data(), response.size());
Jason Jeremy Iman845f2932021-01-31 16:12:13 +090049}
50
51void 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 Iman15c32782021-01-27 04:19:43 +090057}
58
Jason Jeremy Iman90512982021-01-31 18:43:58 +090059DoHCurlClient::DoHCurlClient(base::TimeDelta timeout,
60 int max_concurrent_queries)
61 : timeout_seconds_(timeout.InSeconds()),
62 max_concurrent_queries_(max_concurrent_queries) {
Jason Jeremy Iman15c32782021-01-27 04:19:43 +090063 // 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
82DoHCurlClient::~DoHCurlClient() {
Jason Jeremy Iman90512982021-01-31 18:43:58 +090083 // Cancel all in-flight queries.
84 for (const auto& requests : requests_) {
85 CancelRequest(requests.second);
86 }
Jason Jeremy Iman15c32782021-01-27 04:19:43 +090087 curl_global_cleanup();
88}
89
90void DoHCurlClient::HandleResult(CURLMsg* curl_msg) {
Jason Jeremy Iman90512982021-01-31 18:43:58 +090091 // `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 Iman15c32782021-01-27 04:19:43 +090094 if (!base::Contains(states_, curl_msg->easy_handle)) {
Jason Jeremy Iman15c32782021-01-27 04:19:43 +090095 return;
96 }
97
98 CURL* curl = curl_msg->easy_handle;
Jason Jeremy Iman15c32782021-01-27 04:19:43 +090099 State* state = states_[curl].get();
Jason Jeremy Iman15c32782021-01-27 04:19:43 +0900100
Jason Jeremy Iman90512982021-01-31 18:43:58 +0900101 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 Iman15c32782021-01-27 04:19:43 +0900112 // TODO(jasongustaman): Get and save curl metrics.
Jason Jeremy Iman15c32782021-01-27 04:19:43 +0900113}
114
115void 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
126void 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
137void 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
148void 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
159void 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
170void DoHCurlClient::RemoveWatcher(curl_socket_t socket_fd) {
171 read_watchers_.erase(socket_fd);
172 write_watchers_.erase(socket_fd);
173}
174
175int 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 Iman845f2932021-01-31 16:12:13 +0900197void DoHCurlClient::TimeoutCallback() {
198 int still_running;
199 curl_multi_socket_action(curlm_, CURL_SOCKET_TIMEOUT, 0, &still_running);
200 CheckMultiInfo();
201}
202
Jason Jeremy Iman15c32782021-01-27 04:19:43 +0900203int DoHCurlClient::TimerCallback(CURLM* multi,
204 int64_t timeout_ms,
205 void* userp) {
206 DoHCurlClient* client = static_cast<DoHCurlClient*>(userp);
Jason Jeremy Iman845f2932021-01-31 16:12:13 +0900207 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 Iman15c32782021-01-27 04:19:43 +0900215 }
Jason Jeremy Iman15c32782021-01-27 04:19:43 +0900216 return 0;
217}
218
219size_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 Iman845f2932021-01-31 16:12:13 +0900225 state->SetResponse(ptr, len);
226 return len;
227}
228
229size_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 Iman15c32782021-01-27 04:19:43 +0900237 return len;
238}
239
240void DoHCurlClient::SetNameServers(
241 const std::vector<std::string>& name_servers) {
242 name_servers_ = base::JoinString(name_servers, ",");
243}
244
245void DoHCurlClient::SetDoHProviders(
246 const std::vector<std::string>& doh_providers) {
247 doh_providers_ = doh_providers;
248}
249
Jason Jeremy Iman90512982021-01-31 18:43:58 +0900250void 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 Iman15c32782021-01-27 04:19:43 +0900254 }
Jason Jeremy Iman90512982021-01-31 18:43:58 +0900255}
Jason Jeremy Iman15c32782021-01-27 04:19:43 +0900256
Jason Jeremy Iman90512982021-01-31 18:43:58 +0900257void 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
267std::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 Iman15c32782021-01-27 04:19:43 +0900273 CURL* curl;
274 curl = curl_easy_init();
275 if (!curl) {
276 LOG(ERROR) << "Failed to initialize curl";
Jason Jeremy Iman90512982021-01-31 18:43:58 +0900277 return nullptr;
Jason Jeremy Iman15c32782021-01-27 04:19:43 +0900278 }
279
Jason Jeremy Iman90512982021-01-31 18:43:58 +0900280 // Allocate a state for the request.
Jason Jeremy Iman15c32782021-01-27 04:19:43 +0900281 std::unique_ptr<State> state =
Jason Jeremy Iman90512982021-01-31 18:43:58 +0900282 std::make_unique<State>(curl, callback, ctx, next_request_id_);
Jason Jeremy Iman15c32782021-01-27 04:19:43 +0900283
284 // Set the target URL which is the DoH provider to query to.
Jason Jeremy Iman90512982021-01-31 18:43:58 +0900285 curl_easy_setopt(curl, CURLOPT_URL, doh_provider.c_str());
Jason Jeremy Iman15c32782021-01-27 04:19:43 +0900286
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 Iman90512982021-01-31 18:43:58 +0900322 return state;
323}
324
325bool 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 Iman15c32782021-01-27 04:19:43 +0900366 return true;
367}
368} // namespace dns_proxy