aboutsummaryrefslogtreecommitdiff
path: root/libcurl_http_fetcher.cc
blob: b8d11f5a4b0461b963f26bea4a67644298d2f663 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
//
// Copyright (C) 2009 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#include "update_engine/libcurl_http_fetcher.h"

#include <netinet/in.h>
#include <resolv.h>
#include <sys/types.h>
#include <unistd.h>

#include <algorithm>
#include <string>

#include <base/bind.h>
#include <base/format_macros.h>
#include <base/location.h>
#include <base/logging.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/threading/thread_task_runner_handle.h>

#ifdef __ANDROID__
#include <cutils/qtaguid.h>
#include <private/android_filesystem_config.h>
#endif  // __ANDROID__

#include "update_engine/certificate_checker.h"
#include "update_engine/common/hardware_interface.h"
#include "update_engine/common/platform_constants.h"
#include "update_engine/common/utils.h"

using base::TimeDelta;
using brillo::MessageLoop;
using std::max;
using std::string;

// This is a concrete implementation of HttpFetcher that uses libcurl to do the
// http work.

namespace chromeos_update_engine {

namespace {

const int kNoNetworkRetrySeconds = 10;

// libcurl's CURLOPT_SOCKOPTFUNCTION callback function. Called after the socket
// is created but before it is connected. This callback tags the created socket
// so the network usage can be tracked in Android.
int LibcurlSockoptCallback(void* /* clientp */,
                           curl_socket_t curlfd,
                           curlsocktype /* purpose */) {
#ifdef __ANDROID__
  // Socket tag used by all network sockets. See qtaguid kernel module for
  // stats.
  const int kUpdateEngineSocketTag = 0x55417243;  // "CrAU" in little-endian.
  qtaguid_tagSocket(curlfd, kUpdateEngineSocketTag, AID_OTA_UPDATE);
#endif  // __ANDROID__
  return CURL_SOCKOPT_OK;
}

}  // namespace

// static
int LibcurlHttpFetcher::LibcurlCloseSocketCallback(void* clientp,
                                                   curl_socket_t item) {
#ifdef __ANDROID__
  qtaguid_untagSocket(item);
#endif  // __ANDROID__

  LibcurlHttpFetcher* fetcher = static_cast<LibcurlHttpFetcher*>(clientp);
  // Stop watching the socket before closing it.
  for (size_t t = 0; t < base::size(fetcher->fd_controller_maps_); ++t) {
    fetcher->fd_controller_maps_[t].erase(item);
  }

  // Documentation for this callback says to return 0 on success or 1 on error.
  if (!IGNORE_EINTR(close(item)))
    return 0;
  return 1;
}

LibcurlHttpFetcher::LibcurlHttpFetcher(HardwareInterface* hardware)
    : hardware_(hardware) {
  // Dev users want a longer timeout (180 seconds) because they may
  // be waiting on the dev server to build an image.
  if (!hardware_->IsOfficialBuild())
    low_speed_time_seconds_ = kDownloadDevModeLowSpeedTimeSeconds;
  if (hardware_->IsOOBEEnabled() && !hardware_->IsOOBEComplete(nullptr))
    max_retry_count_ = kDownloadMaxRetryCountOobeNotComplete;
}

LibcurlHttpFetcher::~LibcurlHttpFetcher() {
  LOG_IF(ERROR, transfer_in_progress_)
      << "Destroying the fetcher while a transfer is in progress.";
  CleanUp();
}

bool LibcurlHttpFetcher::GetProxyType(const string& proxy,
                                      curl_proxytype* out_type) {
  if (base::StartsWith(
          proxy, "socks5://", base::CompareCase::INSENSITIVE_ASCII) ||
      base::StartsWith(
          proxy, "socks://", base::CompareCase::INSENSITIVE_ASCII)) {
    *out_type = CURLPROXY_SOCKS5_HOSTNAME;
    return true;
  }
  if (base::StartsWith(
          proxy, "socks4://", base::CompareCase::INSENSITIVE_ASCII)) {
    *out_type = CURLPROXY_SOCKS4A;
    return true;
  }
  if (base::StartsWith(
          proxy, "http://", base::CompareCase::INSENSITIVE_ASCII) ||
      base::StartsWith(
          proxy, "https://", base::CompareCase::INSENSITIVE_ASCII)) {
    *out_type = CURLPROXY_HTTP;
    return true;
  }
  if (base::StartsWith(proxy, kNoProxy, base::CompareCase::INSENSITIVE_ASCII)) {
    // known failure case. don't log.
    return false;
  }
  LOG(INFO) << "Unknown proxy type: " << proxy;
  return false;
}

void LibcurlHttpFetcher::ResumeTransfer(const string& url) {
  LOG(INFO) << "Starting/Resuming transfer";
  CHECK(!transfer_in_progress_);
  url_ = url;
  curl_multi_handle_ = curl_multi_init();
  CHECK(curl_multi_handle_);

  curl_handle_ = curl_easy_init();
  CHECK(curl_handle_);
  ignore_failure_ = false;

  // Tag and untag the socket for network usage stats.
  curl_easy_setopt(
      curl_handle_, CURLOPT_SOCKOPTFUNCTION, LibcurlSockoptCallback);
  curl_easy_setopt(
      curl_handle_, CURLOPT_CLOSESOCKETFUNCTION, LibcurlCloseSocketCallback);
  curl_easy_setopt(curl_handle_, CURLOPT_CLOSESOCKETDATA, this);

  CHECK(HasProxy());
  bool is_direct = (GetCurrentProxy() == kNoProxy);
  LOG(INFO) << "Using proxy: " << (is_direct ? "no" : "yes");
  if (is_direct) {
    CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_PROXY, ""), CURLE_OK);
  } else {
    CHECK_EQ(curl_easy_setopt(
                 curl_handle_, CURLOPT_PROXY, GetCurrentProxy().c_str()),
             CURLE_OK);
    // Curl seems to require us to set the protocol
    curl_proxytype type{};
    if (GetProxyType(GetCurrentProxy(), &type)) {
      CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_PROXYTYPE, type),
               CURLE_OK);
    }
  }

  if (post_data_set_) {
    CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_POST, 1), CURLE_OK);
    CHECK_EQ(
        curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDS, post_data_.data()),
        CURLE_OK);
    CHECK_EQ(curl_easy_setopt(
                 curl_handle_, CURLOPT_POSTFIELDSIZE, post_data_.size()),
             CURLE_OK);
  }

  // Setup extra HTTP headers.
  if (curl_http_headers_) {
    curl_slist_free_all(curl_http_headers_);
    curl_http_headers_ = nullptr;
  }
  for (const auto& header : extra_headers_) {
    // curl_slist_append() copies the string.
    curl_http_headers_ =
        curl_slist_append(curl_http_headers_, header.second.c_str());
  }
  if (post_data_set_) {
    // Set the Content-Type HTTP header, if one was specifically set.
    if (post_content_type_ != kHttpContentTypeUnspecified) {
      const string content_type_attr = base::StringPrintf(
          "Content-Type: %s", GetHttpContentTypeString(post_content_type_));
      curl_http_headers_ =
          curl_slist_append(curl_http_headers_, content_type_attr.c_str());
    } else {
      LOG(WARNING) << "no content type set, using libcurl default";
    }
  }
  CHECK_EQ(
      curl_easy_setopt(curl_handle_, CURLOPT_HTTPHEADER, curl_http_headers_),
      CURLE_OK);

  if (bytes_downloaded_ > 0 || download_length_) {
    // Resume from where we left off.
    resume_offset_ = bytes_downloaded_;
    CHECK_GE(resume_offset_, 0);

    // Compute end offset, if one is specified. As per HTTP specification, this
    // is an inclusive boundary. Make sure it doesn't overflow.
    size_t end_offset = 0;
    if (download_length_) {
      end_offset = static_cast<size_t>(resume_offset_) + download_length_ - 1;
      CHECK_LE((size_t)resume_offset_, end_offset);
    }

    // Create a string representation of the desired range.
    string range_str = base::StringPrintf(
        "%" PRIu64 "-", static_cast<uint64_t>(resume_offset_));
    if (end_offset)
      range_str += std::to_string(end_offset);
    CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_RANGE, range_str.c_str()),
             CURLE_OK);
  }

  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_WRITEDATA, this), CURLE_OK);
  CHECK_EQ(
      curl_easy_setopt(curl_handle_, CURLOPT_WRITEFUNCTION, StaticLibcurlWrite),
      CURLE_OK);
  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_URL, url_.c_str()), CURLE_OK);

  // If the connection drops under |low_speed_limit_bps_| (10
  // bytes/sec by default) for |low_speed_time_seconds_| (90 seconds,
  // 180 on non-official builds), reconnect.
  CHECK_EQ(curl_easy_setopt(
               curl_handle_, CURLOPT_LOW_SPEED_LIMIT, low_speed_limit_bps_),
           CURLE_OK);
  CHECK_EQ(curl_easy_setopt(
               curl_handle_, CURLOPT_LOW_SPEED_TIME, low_speed_time_seconds_),
           CURLE_OK);
  CHECK_EQ(curl_easy_setopt(
               curl_handle_, CURLOPT_CONNECTTIMEOUT, connect_timeout_seconds_),
           CURLE_OK);

  // By default, libcurl doesn't follow redirections. Allow up to
  // |kDownloadMaxRedirects| redirections.
  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_FOLLOWLOCATION, 1), CURLE_OK);
  CHECK_EQ(
      curl_easy_setopt(curl_handle_, CURLOPT_MAXREDIRS, kDownloadMaxRedirects),
      CURLE_OK);

  // Lock down the appropriate curl options for HTTP or HTTPS depending on
  // the url.
  if (hardware_->IsOfficialBuild()) {
    if (base::StartsWith(
            url_, "http://", base::CompareCase::INSENSITIVE_ASCII)) {
      SetCurlOptionsForHttp();
    } else if (base::StartsWith(
                   url_, "https://", base::CompareCase::INSENSITIVE_ASCII)) {
      SetCurlOptionsForHttps();
#ifdef __ANDROID__
    } else if (base::StartsWith(
                   url_, "file://", base::CompareCase::INSENSITIVE_ASCII)) {
      SetCurlOptionsForFile();
#endif  // __ANDROID__
    } else {
      LOG(ERROR) << "Received invalid URI: " << url_;
      // Lock down to no protocol supported for the transfer.
      CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_PROTOCOLS, 0), CURLE_OK);
    }
  } else {
    LOG(INFO) << "Not setting http(s) curl options because we are "
              << "running a dev/test image";
  }

  CHECK_EQ(curl_multi_add_handle(curl_multi_handle_, curl_handle_), CURLM_OK);
  transfer_in_progress_ = true;
}

// Lock down only the protocol in case of HTTP.
void LibcurlHttpFetcher::SetCurlOptionsForHttp() {
  LOG(INFO) << "Setting up curl options for HTTP";
  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_PROTOCOLS, CURLPROTO_HTTP),
           CURLE_OK);
  CHECK_EQ(
      curl_easy_setopt(curl_handle_, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP),
      CURLE_OK);
}

// Security lock-down in official builds: makes sure that peer certificate
// verification is enabled, restricts the set of trusted certificates,
// restricts protocols to HTTPS, restricts ciphers to HIGH.
void LibcurlHttpFetcher::SetCurlOptionsForHttps() {
  LOG(INFO) << "Setting up curl options for HTTPS";
  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_SSL_VERIFYPEER, 1), CURLE_OK);
  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_SSL_VERIFYHOST, 2), CURLE_OK);
  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_CAINFO, nullptr), CURLE_OK);
  CHECK_EQ(curl_easy_setopt(
               curl_handle_, CURLOPT_CAPATH, constants::kCACertificatesPath),
           CURLE_OK);
  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS),
           CURLE_OK);
  CHECK_EQ(
      curl_easy_setopt(curl_handle_, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS),
      CURLE_OK);
  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_SSL_CIPHER_LIST, "HIGH:!ADH"),
           CURLE_OK);
  if (server_to_check_ != ServerToCheck::kNone) {
    CHECK_EQ(
        curl_easy_setopt(curl_handle_, CURLOPT_SSL_CTX_DATA, &server_to_check_),
        CURLE_OK);
    CHECK_EQ(curl_easy_setopt(curl_handle_,
                              CURLOPT_SSL_CTX_FUNCTION,
                              CertificateChecker::ProcessSSLContext),
             CURLE_OK);
  }
}

// Lock down only the protocol in case of a local file.
void LibcurlHttpFetcher::SetCurlOptionsForFile() {
  LOG(INFO) << "Setting up curl options for FILE";
  CHECK_EQ(curl_easy_setopt(curl_handle_, CURLOPT_PROTOCOLS, CURLPROTO_FILE),
           CURLE_OK);
  CHECK_EQ(
      curl_easy_setopt(curl_handle_, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_FILE),
      CURLE_OK);
}

// Begins the transfer, which must not have already been started.
void LibcurlHttpFetcher::BeginTransfer(const string& url) {
  CHECK(!transfer_in_progress_);
  url_ = url;

  transfer_size_ = -1;
  resume_offset_ = 0;
  retry_count_ = 0;
  no_network_retry_count_ = 0;
  http_response_code_ = 0;
  terminate_requested_ = false;
  sent_byte_ = false;

  // If we are paused, we delay these two operations until Unpause is called.
  if (transfer_paused_) {
    restart_transfer_on_unpause_ = true;
    return;
  }
  ResumeTransfer(url_);
  CurlPerformOnce();
}

void LibcurlHttpFetcher::ForceTransferTermination() {
  CleanUp();
  if (delegate_) {
    // Note that after the callback returns this object may be destroyed.
    delegate_->TransferTerminated(this);
  }
}

void LibcurlHttpFetcher::TerminateTransfer() {
  if (in_write_callback_) {
    terminate_requested_ = true;
  } else {
    ForceTransferTermination();
  }
}

void LibcurlHttpFetcher::SetHeader(const string& header_name,
                                   const string& header_value) {
  string header_line = header_name + ": " + header_value;
  // Avoid the space if no data on the right side of the semicolon.
  if (header_value.empty())
    header_line = header_name + ":";
  TEST_AND_RETURN(header_line.find('\n') == string::npos);
  TEST_AND_RETURN(header_name.find(':') == string::npos);
  extra_headers_[base::ToLowerASCII(header_name)] = header_line;
}

// Inputs: header_name, header_value
// Example:
//   extra_headers_ = { {"foo":"foo: 123"}, {"bar":"bar:"} }
//   string tmp = "gibberish";
//   Case 1:
//     GetHeader("foo", &tmp) -> tmp = "123", return true.
//   Case 2:
//     GetHeader("bar", &tmp) -> tmp = "", return true.
//   Case 3:
//     GetHeader("moo", &tmp) -> tmp = "", return false.
bool LibcurlHttpFetcher::GetHeader(const string& header_name,
                                   string* header_value) const {
  // Initially clear |header_value| to handle both success and failures without
  // leaving |header_value| in a unclear state.
  header_value->clear();
  auto header_key = base::ToLowerASCII(header_name);
  auto header_line_itr = extra_headers_.find(header_key);
  // If the |header_name| was never set, indicate so by returning false.
  if (header_line_itr == extra_headers_.end())
    return false;
  // From |SetHeader()| the check for |header_name| to not include ":" is
  // verified, so finding the first index of ":" is a safe operation.
  auto header_line = header_line_itr->second;
  *header_value = header_line.substr(header_line.find(':') + 1);
  // The following is neccessary to remove the leading ' ' before the header
  // value that was place only if |header_value| passed to |SetHeader()| was
  // a non-empty string.
  header_value->erase(0, 1);
  return true;
}

void LibcurlHttpFetcher::CurlPerformOnce() {
  CHECK(transfer_in_progress_);
  int running_handles = 0;
  CURLMcode retcode = CURLM_CALL_MULTI_PERFORM;

  // libcurl may request that we immediately call curl_multi_perform after it
  // returns, so we do. libcurl promises that curl_multi_perform will not block.
  while (CURLM_CALL_MULTI_PERFORM == retcode) {
    retcode = curl_multi_perform(curl_multi_handle_, &running_handles);
    if (terminate_requested_) {
      ForceTransferTermination();
      return;
    }
  }

  // When retcode is not |CURLM_OK| at this point, libcurl has an internal error
  // that it is less likely to recover from (libcurl bug, out-of-memory, etc.).
  // In case of an update check, we send UMA metrics and log the error.
  if (is_update_check_ &&
      (retcode == CURLM_OUT_OF_MEMORY || retcode == CURLM_INTERNAL_ERROR)) {
    auxiliary_error_code_ = ErrorCode::kInternalLibCurlError;
    LOG(ERROR) << "curl_multi_perform is in an unrecoverable error condition: "
               << retcode;
  } else if (retcode != CURLM_OK) {
    LOG(ERROR) << "curl_multi_perform returns error: " << retcode;
  }

  // If the transfer completes while paused, we should ignore the failure once
  // the fetcher is unpaused.
  if (running_handles == 0 && transfer_paused_ && !ignore_failure_) {
    LOG(INFO) << "Connection closed while paused, ignoring failure.";
    ignore_failure_ = true;
  }

  if (running_handles != 0 || transfer_paused_) {
    // There's either more work to do or we are paused, so we just keep the
    // file descriptors to watch up to date and exit, until we are done with the
    // work and we are not paused.
    //
    // When there's no |base::SingleThreadTaskRunner| on current thread, it's
    // not possible to watch file descriptors. Just poll it later. This usually
    // happens if |brillo::FakeMessageLoop| is used.
    if (!base::ThreadTaskRunnerHandle::IsSet()) {
      MessageLoop::current()->PostDelayedTask(
          FROM_HERE,
          base::Bind(&LibcurlHttpFetcher::CurlPerformOnce,
                     base::Unretained(this)),
          TimeDelta::FromSeconds(1));
      return;
    }
    SetupMessageLoopSources();
    return;
  }

  // At this point, the transfer was completed in some way (error, connection
  // closed or download finished).

  GetHttpResponseCode();
  if (http_response_code_) {
    LOG(INFO) << "HTTP response code: " << http_response_code_;
    no_network_retry_count_ = 0;
    unresolved_host_state_machine_.UpdateState(false);
  } else {
    LOG(ERROR) << "Unable to get http response code.";
    CURLcode curl_code = GetCurlCode();
    LOG(ERROR) << "Return code for the transfer: " << curl_code;
    if (curl_code == CURLE_COULDNT_RESOLVE_HOST) {
      LOG(ERROR) << "libcurl can not resolve host.";
      unresolved_host_state_machine_.UpdateState(true);
      auxiliary_error_code_ = ErrorCode::kUnresolvedHostError;
    }
  }

  // we're done!
  CleanUp();

  if (unresolved_host_state_machine_.GetState() ==
      UnresolvedHostStateMachine::State::kRetry) {
    // Based on
    // https://curl.haxx.se/docs/todo.html#updated_DNS_server_while_running,
    // update_engine process should call res_init() and unconditionally retry.
    res_init();
    no_network_max_retries_++;
    LOG(INFO) << "Will retry after reloading resolv.conf because last attempt "
                 "failed to resolve host.";
  } else if (unresolved_host_state_machine_.GetState() ==
             UnresolvedHostStateMachine::State::kRetriedSuccess) {
    auxiliary_error_code_ = ErrorCode::kUnresolvedHostRecovered;
  }

  // TODO(petkov): This temporary code tries to deal with the case where the
  // update engine performs an update check while the network is not ready
  // (e.g., right after resume). Longer term, we should check if the network
  // is online/offline and return an appropriate error code.
  if (!sent_byte_ && http_response_code_ == 0 &&
      no_network_retry_count_ < no_network_max_retries_) {
    no_network_retry_count_++;
    retry_task_id_ = MessageLoop::current()->PostDelayedTask(
        FROM_HERE,
        base::Bind(&LibcurlHttpFetcher::RetryTimeoutCallback,
                   base::Unretained(this)),
        TimeDelta::FromSeconds(kNoNetworkRetrySeconds));
    LOG(INFO) << "No HTTP response, retry " << no_network_retry_count_;
  } else if ((!sent_byte_ && !IsHttpResponseSuccess()) ||
             IsHttpResponseError()) {
    // The transfer completed w/ error and we didn't get any bytes.
    // If we have another proxy to try, try that.
    //
    // TODO(garnold) in fact there are two separate cases here: one case is an
    // other-than-success return code (including no return code) and no
    // received bytes, which is necessary due to the way callbacks are
    // currently processing error conditions;  the second is an explicit HTTP
    // error code, where some data may have been received (as in the case of a
    // semi-successful multi-chunk fetch).  This is a confusing behavior and
    // should be unified into a complete, coherent interface.
    LOG(INFO) << "Transfer resulted in an error (" << http_response_code_
              << "), " << bytes_downloaded_ << " bytes downloaded";

    PopProxy();  // Delete the proxy we just gave up on.

    if (HasProxy()) {
      // We have another proxy. Retry immediately.
      LOG(INFO) << "Retrying with next proxy setting";
      retry_task_id_ = MessageLoop::current()->PostTask(
          FROM_HERE,
          base::Bind(&LibcurlHttpFetcher::RetryTimeoutCallback,
                     base::Unretained(this)));
    } else {
      // Out of proxies. Give up.
      LOG(INFO) << "No further proxies, indicating transfer complete";
      if (delegate_)
        delegate_->TransferComplete(this, false);  // signal fail
      return;
    }
  } else if ((transfer_size_ >= 0) && (bytes_downloaded_ < transfer_size_)) {
    if (!ignore_failure_)
      retry_count_++;
    LOG(INFO) << "Transfer interrupted after downloading " << bytes_downloaded_
              << " of " << transfer_size_ << " bytes. "
              << transfer_size_ - bytes_downloaded_ << " bytes remaining "
              << "after " << retry_count_ << " attempt(s)";

    if (retry_count_ > max_retry_count_) {
      LOG(INFO) << "Reached max attempts (" << retry_count_ << ")";
      if (delegate_)
        delegate_->TransferComplete(this, false);  // signal fail
      return;
    }
    // Need to restart transfer
    LOG(INFO) << "Restarting transfer to download the remaining bytes";
    retry_task_id_ = MessageLoop::current()->PostDelayedTask(
        FROM_HERE,
        base::Bind(&LibcurlHttpFetcher::RetryTimeoutCallback,
                   base::Unretained(this)),
        TimeDelta::FromSeconds(retry_seconds_));
  } else {
    LOG(INFO) << "Transfer completed (" << http_response_code_ << "), "
              << bytes_downloaded_ << " bytes downloaded";
    if (delegate_) {
      bool success = IsHttpResponseSuccess();
      delegate_->TransferComplete(this, success);
    }
    return;
  }
  // If we reach this point is because TransferComplete() was not called in any
  // of the previous branches. The delegate is allowed to destroy the object
  // once TransferComplete is called so this would be illegal.
  ignore_failure_ = false;
}

size_t LibcurlHttpFetcher::LibcurlWrite(void* ptr, size_t size, size_t nmemb) {
  // Update HTTP response first.
  GetHttpResponseCode();
  const size_t payload_size = size * nmemb;

  // Do nothing if no payload or HTTP response is an error.
  if (payload_size == 0 || !IsHttpResponseSuccess()) {
    LOG(INFO) << "HTTP response unsuccessful (" << http_response_code_
              << ") or no payload (" << payload_size << "), nothing to do";
    return 0;
  }

  sent_byte_ = true;
  {
    double transfer_size_double{};
    CHECK_EQ(curl_easy_getinfo(curl_handle_,
                               CURLINFO_CONTENT_LENGTH_DOWNLOAD,
                               &transfer_size_double),
             CURLE_OK);
    off_t new_transfer_size = static_cast<off_t>(transfer_size_double);
    if (new_transfer_size > 0) {
      transfer_size_ = resume_offset_ + new_transfer_size;
    }
  }
  bytes_downloaded_ += payload_size;
  if (delegate_) {
    in_write_callback_ = true;
    auto should_terminate = !delegate_->ReceivedBytes(this, ptr, payload_size);
    in_write_callback_ = false;
    if (should_terminate) {
      LOG(INFO) << "Requesting libcurl to terminate transfer.";
      // Returning an amount that differs from the received size signals an
      // error condition to libcurl, which will cause the transfer to be
      // aborted.
      return 0;
    }
  }
  return payload_size;
}

void LibcurlHttpFetcher::Pause() {
  if (transfer_paused_) {
    LOG(ERROR) << "Fetcher already paused.";
    return;
  }
  transfer_paused_ = true;
  if (!transfer_in_progress_) {
    // If pause before we started a connection, we don't need to notify curl
    // about that, we will simply not start the connection later.
    return;
  }
  CHECK(curl_handle_);
  CHECK_EQ(curl_easy_pause(curl_handle_, CURLPAUSE_ALL), CURLE_OK);
}

void LibcurlHttpFetcher::Unpause() {
  if (!transfer_paused_) {
    LOG(ERROR) << "Resume attempted when fetcher not paused.";
    return;
  }
  transfer_paused_ = false;
  if (restart_transfer_on_unpause_) {
    restart_transfer_on_unpause_ = false;
    ResumeTransfer(url_);
    CurlPerformOnce();
    return;
  }
  if (!transfer_in_progress_) {
    // If resumed before starting the connection, there's no need to notify
    // anybody. We will simply start the connection once it is time.
    return;
  }
  CHECK(curl_handle_);
  CHECK_EQ(curl_easy_pause(curl_handle_, CURLPAUSE_CONT), CURLE_OK);
  // Since the transfer is in progress, we need to dispatch a CurlPerformOnce()
  // now to let the connection continue, otherwise it would be called by the
  // TimeoutCallback but with a delay.
  CurlPerformOnce();
}

// This method sets up callbacks with the MessageLoop.
void LibcurlHttpFetcher::SetupMessageLoopSources() {
  fd_set fd_read;
  fd_set fd_write;
  fd_set fd_exc;

  FD_ZERO(&fd_read);
  FD_ZERO(&fd_write);
  FD_ZERO(&fd_exc);

  int fd_max = 0;

  // Ask libcurl for the set of file descriptors we should track on its
  // behalf.
  CHECK_EQ(curl_multi_fdset(
               curl_multi_handle_, &fd_read, &fd_write, &fd_exc, &fd_max),
           CURLM_OK);

  // We should iterate through all file descriptors up to libcurl's fd_max or
  // the highest one we're tracking, whichever is larger.
  for (size_t t = 0; t < base::size(fd_controller_maps_); ++t) {
    if (!fd_controller_maps_[t].empty())
      fd_max = max(fd_max, fd_controller_maps_[t].rbegin()->first);
  }

  // For each fd, if we're not tracking it, track it. If we are tracking it, but
  // libcurl doesn't care about it anymore, stop tracking it. After this loop,
  // there should be exactly as many tasks scheduled in
  // fd_controller_maps_[0|1] as there are read/write fds that we're tracking.
  for (int fd = 0; fd <= fd_max; ++fd) {
    // Note that fd_exc is unused in the current version of libcurl so is_exc
    // should always be false.
    bool is_exc = FD_ISSET(fd, &fd_exc) != 0;
    bool must_track[2] = {
        is_exc || (FD_ISSET(fd, &fd_read) != 0),  // track 0 -- read
        is_exc || (FD_ISSET(fd, &fd_write) != 0)  // track 1 -- write
    };

    for (size_t t = 0; t < base::size(fd_controller_maps_); ++t) {
      bool tracked =
          fd_controller_maps_[t].find(fd) != fd_controller_maps_[t].end();

      if (!must_track[t]) {
        // If we have an outstanding io_channel, remove it.
        fd_controller_maps_[t].erase(fd);
        continue;
      }

      // If we are already tracking this fd, continue -- nothing to do.
      if (tracked)
        continue;

      // Track a new fd.
      switch (t) {
        case 0:  // Read
          fd_controller_maps_[t][fd] =
              base::FileDescriptorWatcher::WatchReadable(
                  fd,
                  base::BindRepeating(&LibcurlHttpFetcher::CurlPerformOnce,
                                      base::Unretained(this)));
          break;
        case 1:  // Write
          fd_controller_maps_[t][fd] =
              base::FileDescriptorWatcher::WatchWritable(
                  fd,
                  base::BindRepeating(&LibcurlHttpFetcher::CurlPerformOnce,
                                      base::Unretained(this)));
      }
      static int io_counter = 0;
      io_counter++;
      if (io_counter % 50 == 0) {
        LOG(INFO) << "io_counter = " << io_counter;
      }
    }
  }

  // Set up a timeout callback for libcurl.
  if (timeout_id_ == MessageLoop::kTaskIdNull) {
    VLOG(1) << "Setting up timeout source: " << idle_seconds_ << " seconds.";
    timeout_id_ = MessageLoop::current()->PostDelayedTask(
        FROM_HERE,
        base::Bind(&LibcurlHttpFetcher::TimeoutCallback,
                   base::Unretained(this)),
        TimeDelta::FromSeconds(idle_seconds_));
  }
}

void LibcurlHttpFetcher::RetryTimeoutCallback() {
  retry_task_id_ = MessageLoop::kTaskIdNull;
  if (transfer_paused_) {
    restart_transfer_on_unpause_ = true;
    return;
  }
  ResumeTransfer(url_);
  CurlPerformOnce();
}

void LibcurlHttpFetcher::TimeoutCallback() {
  // We always re-schedule the callback, even if we don't want to be called
  // anymore. We will remove the event source separately if we don't want to
  // be called back.
  timeout_id_ = MessageLoop::current()->PostDelayedTask(
      FROM_HERE,
      base::Bind(&LibcurlHttpFetcher::TimeoutCallback, base::Unretained(this)),
      TimeDelta::FromSeconds(idle_seconds_));

  // CurlPerformOnce() may call CleanUp(), so we need to schedule our callback
  // first, since it could be canceled by this call.
  if (transfer_in_progress_)
    CurlPerformOnce();
}

void LibcurlHttpFetcher::CleanUp() {
  MessageLoop::current()->CancelTask(retry_task_id_);
  retry_task_id_ = MessageLoop::kTaskIdNull;

  MessageLoop::current()->CancelTask(timeout_id_);
  timeout_id_ = MessageLoop::kTaskIdNull;

  for (size_t t = 0; t < base::size(fd_controller_maps_); ++t) {
    fd_controller_maps_[t].clear();
  }

  if (curl_http_headers_) {
    curl_slist_free_all(curl_http_headers_);
    curl_http_headers_ = nullptr;
  }
  if (curl_handle_) {
    if (curl_multi_handle_) {
      CHECK_EQ(curl_multi_remove_handle(curl_multi_handle_, curl_handle_),
               CURLM_OK);
    }
    curl_easy_cleanup(curl_handle_);
    curl_handle_ = nullptr;
  }
  if (curl_multi_handle_) {
    CHECK_EQ(curl_multi_cleanup(curl_multi_handle_), CURLM_OK);
    curl_multi_handle_ = nullptr;
  }
  transfer_in_progress_ = false;
  transfer_paused_ = false;
  restart_transfer_on_unpause_ = false;
}

void LibcurlHttpFetcher::GetHttpResponseCode() {
  long http_response_code = 0;  // NOLINT(runtime/int) - curl needs long.
  if (base::StartsWith(url_, "file://", base::CompareCase::INSENSITIVE_ASCII)) {
    // Fake out a valid response code for file:// URLs.
    http_response_code_ = 299;
  } else if (curl_easy_getinfo(curl_handle_,
                               CURLINFO_RESPONSE_CODE,
                               &http_response_code) == CURLE_OK) {
    http_response_code_ = static_cast<int>(http_response_code);
  } else {
    LOG(ERROR) << "Unable to get http response code from curl_easy_getinfo";
  }
}

CURLcode LibcurlHttpFetcher::GetCurlCode() {
  CURLcode curl_code = CURLE_OK;
  while (true) {
    // Repeated calls to |curl_multi_info_read| will return a new struct each
    // time, until a NULL is returned as a signal that there is no more to get
    // at this point.
    int msgs_in_queue{};
    CURLMsg* curl_msg =
        curl_multi_info_read(curl_multi_handle_, &msgs_in_queue);
    if (curl_msg == nullptr)
      break;
    // When |curl_msg| is |CURLMSG_DONE|, a transfer of an easy handle is done,
    // and then data contains the return code for this transfer.
    if (curl_msg->msg == CURLMSG_DONE) {
      // Make sure |curl_multi_handle_| has one and only one easy handle
      // |curl_handle_|.
      CHECK_EQ(curl_handle_, curl_msg->easy_handle);
      // Transfer return code reference:
      // https://curl.haxx.se/libcurl/c/libcurl-errors.html
      curl_code = curl_msg->data.result;
    }
  }

  // Gets connection error if exists.
  long connect_error = 0;  // NOLINT(runtime/int) - curl needs long.
  CURLcode res =
      curl_easy_getinfo(curl_handle_, CURLINFO_OS_ERRNO, &connect_error);
  if (res == CURLE_OK && connect_error) {
    LOG(ERROR) << "Connect error code from the OS: " << connect_error;
  }

  return curl_code;
}

void UnresolvedHostStateMachine::UpdateState(bool failed_to_resolve_host) {
  switch (state_) {
    case State::kInit:
      if (failed_to_resolve_host) {
        state_ = State::kRetry;
      }
      break;
    case State::kRetry:
      if (failed_to_resolve_host) {
        state_ = State::kNotRetry;
      } else {
        state_ = State::kRetriedSuccess;
      }
      break;
    case State::kNotRetry:
      break;
    case State::kRetriedSuccess:
      break;
    default:
      NOTREACHED();
      break;
  }
}

}  // namespace chromeos_update_engine