~ubuntu-branches/ubuntu/vivid/phabricator/vivid

« back to all changes in this revision

Viewing changes to libphutil/src/future/http/HTTPSFuture.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2014-10-23 20:49:26 UTC
  • mfrom: (0.2.1) (0.1.1)
  • Revision ID: package-import@ubuntu.com-20141023204926-vq80u1op4df44azb
Tags: 0~git20141023-1
Initial release (closes: #703046)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
/**
 
4
 * Very basic HTTPS future.
 
5
 */
 
6
final class HTTPSFuture extends BaseHTTPFuture {
 
7
 
 
8
  private static $multi;
 
9
  private static $results = array();
 
10
  private static $pool = array();
 
11
  private static $globalCABundle;
 
12
  private static $blindTrustDomains = array();
 
13
 
 
14
  private $handle;
 
15
  private $profilerCallID;
 
16
  private $cabundle;
 
17
  private $followLocation = true;
 
18
  private $responseBuffer = '';
 
19
  private $responseBufferPos;
 
20
  private $files = array();
 
21
  private $temporaryFiles = array();
 
22
 
 
23
  /**
 
24
   * Create a temp file containing an SSL cert, and use it for this session.
 
25
   *
 
26
   * This allows us to do host-specific SSL certificates in whatever client
 
27
   * is using libphutil. e.g. in Arcanist, you could add an "ssl_cert" key
 
28
   * to a specific host in ~/.arcrc and use that.
 
29
   *
 
30
   * cURL needs this to be a file, it doesn't seem to be able to handle a string
 
31
   * which contains the cert. So we make a temporary file and store it there.
 
32
   *
 
33
   * @param string The multi-line, possibly lengthy, SSL certificate to use.
 
34
   * @return this
 
35
   */
 
36
  public function setCABundleFromString($certificate) {
 
37
    $temp = new TempFile();
 
38
    Filesystem::writeFile($temp, $certificate);
 
39
    $this->cabundle = $temp;
 
40
    return $this;
 
41
  }
 
42
 
 
43
  /**
 
44
   * Set the SSL certificate to use for this session, given a path.
 
45
   *
 
46
   * @param string The path to a valid SSL certificate for this session
 
47
   * @return this
 
48
   */
 
49
  public function setCABundleFromPath($path) {
 
50
    $this->cabundle = $path;
 
51
    return $this;
 
52
  }
 
53
 
 
54
  /**
 
55
   * Get the path to the SSL certificate for this session.
 
56
   *
 
57
   * @return string|null
 
58
   */
 
59
  public function getCABundle() {
 
60
    return $this->cabundle;
 
61
  }
 
62
 
 
63
  /**
 
64
   * Set whether Location headers in the response will be respected.
 
65
   * The default is true.
 
66
   *
 
67
   * @param boolean true to follow any Location header present in the response,
 
68
   *                false to return the request directly
 
69
   * @return this
 
70
   */
 
71
  public function setFollowLocation($follow) {
 
72
    $this->followLocation = $follow;
 
73
    return $this;
 
74
  }
 
75
 
 
76
  /**
 
77
   * Get whether Location headers in the response will be respected.
 
78
   *
 
79
   * @return boolean
 
80
   */
 
81
  public function getFollowLocation() {
 
82
    return $this->followLocation;
 
83
  }
 
84
 
 
85
  /**
 
86
   * Set the fallback CA certificate if one is not specified
 
87
   * for the session, given a path.
 
88
   *
 
89
   * @param string The path to a valid SSL certificate
 
90
   * @return void
 
91
   */
 
92
  public static function setGlobalCABundleFromPath($path) {
 
93
    self::$globalCABundle = $path;
 
94
  }
 
95
  /**
 
96
   * Set the fallback CA certificate if one is not specified
 
97
   * for the session, given a string.
 
98
   *
 
99
   * @param string The certificate
 
100
   * @return void
 
101
   */
 
102
  public static function setGlobalCABundleFromString($certificate) {
 
103
    $temp = new TempFile();
 
104
    Filesystem::writeFile($temp, $certificate);
 
105
    self::$globalCABundle = $temp;
 
106
  }
 
107
 
 
108
  /**
 
109
   * Get the fallback global CA certificate
 
110
   *
 
111
   * @return string
 
112
   */
 
113
  public static function getGlobalCABundle() {
 
114
    return self::$globalCABundle;
 
115
  }
 
116
 
 
117
  /**
 
118
   * Set a list of domains to blindly trust. Certificates for these domains
 
119
   * will not be validated.
 
120
   *
 
121
   * @param list<string> List of domain names to trust blindly.
 
122
   * @return void
 
123
   */
 
124
  public static function setBlindlyTrustDomains(array $domains) {
 
125
    self::$blindTrustDomains = array_fuse($domains);
 
126
  }
 
127
 
 
128
  /**
 
129
   * Load contents of remote URI. Behaves pretty much like
 
130
   * `@file_get_contents($uri)` but doesn't require `allow_url_fopen`.
 
131
   *
 
132
   * @param string
 
133
   * @param float
 
134
   * @return string|false
 
135
   */
 
136
  public static function loadContent($uri, $timeout = null) {
 
137
    $future = new HTTPSFuture($uri);
 
138
    if ($timeout !== null) {
 
139
      $future->setTimeout($timeout);
 
140
    }
 
141
    try {
 
142
      list($body) = $future->resolvex();
 
143
      return $body;
 
144
    } catch (HTTPFutureResponseStatus $ex) {
 
145
      return false;
 
146
    }
 
147
  }
 
148
 
 
149
  /**
 
150
   * Attach a file to the request.
 
151
   *
 
152
   * @param string  HTTP parameter name.
 
153
   * @param string  File content.
 
154
   * @param string  File name.
 
155
   * @param string  File mime type.
 
156
   * @return this
 
157
   */
 
158
  public function attachFileData($key, $data, $name, $mime_type) {
 
159
    if (isset($this->files[$key])) {
 
160
      throw new Exception(
 
161
        pht(
 
162
          'HTTPSFuture currently supports only one file attachment for each '.
 
163
          'parameter name. You are trying to attach two different files with '.
 
164
          'the same parameter, "%s".',
 
165
          $key));
 
166
    }
 
167
 
 
168
    $this->files[$key] = array(
 
169
      'data' => $data,
 
170
      'name' => $name,
 
171
      'mime' => $mime_type,
 
172
    );
 
173
 
 
174
    return $this;
 
175
  }
 
176
 
 
177
  public function isReady() {
 
178
    if (isset($this->result)) {
 
179
      return true;
 
180
    }
 
181
 
 
182
    $uri = $this->getURI();
 
183
    $domain = id(new PhutilURI($uri))->getDomain();
 
184
 
 
185
    if (!$this->handle) {
 
186
      $profiler = PhutilServiceProfiler::getInstance();
 
187
      $this->profilerCallID = $profiler->beginServiceCall(
 
188
        array(
 
189
          'type' => 'http',
 
190
          'uri' => $uri,
 
191
        ));
 
192
 
 
193
      if (!self::$multi) {
 
194
        self::$multi = curl_multi_init();
 
195
        if (!self::$multi) {
 
196
          throw new Exception('curl_multi_init() failed!');
 
197
        }
 
198
      }
 
199
 
 
200
      if (!empty(self::$pool[$domain])) {
 
201
        $curl = array_pop(self::$pool[$domain]);
 
202
      } else {
 
203
        $curl = curl_init();
 
204
        if (!$curl) {
 
205
          throw new Exception('curl_init() failed!');
 
206
        }
 
207
      }
 
208
 
 
209
      $this->handle = $curl;
 
210
      curl_multi_add_handle(self::$multi, $curl);
 
211
 
 
212
      curl_setopt($curl, CURLOPT_URL, $uri);
 
213
 
 
214
      if (defined('CURLOPT_PROTOCOLS')) {
 
215
        // cURL supports a lot of protocols, and by default it will honor
 
216
        // redirects across protocols (for instance, from HTTP to POP3). Beyond
 
217
        // being very silly, this also has security implications:
 
218
        //
 
219
        //   http://blog.volema.com/curl-rce.html
 
220
        //
 
221
        // Disable all protocols other than HTTP and HTTPS.
 
222
 
 
223
        $allowed_protocols = CURLPROTO_HTTPS | CURLPROTO_HTTP;
 
224
        curl_setopt($curl, CURLOPT_PROTOCOLS, $allowed_protocols);
 
225
        curl_setopt($curl, CURLOPT_REDIR_PROTOCOLS, $allowed_protocols);
 
226
      }
 
227
 
 
228
      $data = $this->formatRequestDataForCURL();
 
229
      curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
 
230
 
 
231
      $headers = $this->getHeaders();
 
232
 
 
233
      $saw_expect = false;
 
234
      for ($ii = 0; $ii < count($headers); $ii++) {
 
235
        list($name, $value) = $headers[$ii];
 
236
        $headers[$ii] = $name.': '.$value;
 
237
        if (!strncasecmp($name, 'Expect', strlen('Expect'))) {
 
238
          $saw_expect = true;
 
239
        }
 
240
      }
 
241
      if (!$saw_expect) {
 
242
        // cURL sends an "Expect" header by default for certain requests. While
 
243
        // there is some reasoning behind this, it causes a practical problem
 
244
        // in that lighttpd servers reject these requests with a 417. Both sides
 
245
        // are locked in an eternal struggle (lighttpd has introduced a
 
246
        // 'server.reject-expect-100-with-417' option to deal with this case).
 
247
        //
 
248
        // The ostensibly correct way to suppress this behavior on the cURL side
 
249
        // is to add an empty "Expect:" header. If we haven't seen some other
 
250
        // explicit "Expect:" header, do so.
 
251
        //
 
252
        // See here, for example, although this issue is fairly widespread:
 
253
        //   http://curl.haxx.se/mail/archive-2009-07/0008.html
 
254
        $headers[] = 'Expect:';
 
255
      }
 
256
      curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
 
257
 
 
258
      // Set the requested HTTP method, e.g. GET / POST / PUT.
 
259
      curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->getMethod());
 
260
 
 
261
      // Make sure we get the headers and data back.
 
262
      curl_setopt($curl, CURLOPT_HEADER, true);
 
263
      curl_setopt($curl, CURLOPT_WRITEFUNCTION,
 
264
        array($this, 'didReceiveDataCallback'));
 
265
 
 
266
      if ($this->followLocation) {
 
267
        curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
 
268
        curl_setopt($curl, CURLOPT_MAXREDIRS, 20);
 
269
      }
 
270
 
 
271
      if (defined('CURLOPT_TIMEOUT_MS')) {
 
272
        // If CURLOPT_TIMEOUT_MS is available, use the higher-precision timeout.
 
273
        $timeout = max(1, ceil(1000 * $this->getTimeout()));
 
274
        curl_setopt($curl, CURLOPT_TIMEOUT_MS, $timeout);
 
275
      } else {
 
276
        // Otherwise, fall back to the lower-precision timeout.
 
277
        $timeout = max(1, ceil($this->getTimeout()));
 
278
        curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
 
279
      }
 
280
 
 
281
      // We're going to try to set CAINFO below. This doesn't work at all on
 
282
      // OSX around Yosemite (see T5913). On these systems, we'll use the
 
283
      // system CA and then try to tell the user that their settings were
 
284
      // ignored and how to fix things if we encounter a CA-related error.
 
285
      // Assume we have custom CA settings to start with; we'll clear this
 
286
      // flag if we read the default CA info below.
 
287
 
 
288
      // Try some decent fallbacks here:
 
289
      // - First, check if a bundle is set explicitly for this request, via
 
290
      //   `setCABundle()` or similar.
 
291
      // - Then, check if a global bundle is set explicitly for all requests,
 
292
      //   via `setGlobalCABundle()` or similar.
 
293
      // - Then, if a local custom.pem exists, use that, because it probably
 
294
      //   means that the user wants to override everything (also because the
 
295
      //   user might not have access to change the box's php.ini to add
 
296
      //   curl.cainfo).
 
297
      // - Otherwise, try using curl.cainfo. If it's set explicitly, it's
 
298
      //   probably reasonable to try using it before we fall back to what
 
299
      //   libphutil ships with.
 
300
      // - Lastly, try the default that libphutil ships with. If it doesn't
 
301
      //   work, give up and yell at the user.
 
302
 
 
303
      if (!$this->getCABundle()) {
 
304
        $caroot = dirname(phutil_get_library_root('phutil')).'/resources/ssl/';
 
305
        $ini_val = ini_get('curl.cainfo');
 
306
        if (self::getGlobalCABundle()) {
 
307
          $this->setCABundleFromPath(self::getGlobalCABundle());
 
308
        } else if (Filesystem::pathExists($caroot.'custom.pem')) {
 
309
          $this->setCABundleFromPath($caroot.'custom.pem');
 
310
        } else if ($ini_val) {
 
311
          // TODO: We can probably do a pathExists() here, even.
 
312
          $this->setCABundleFromPath($ini_val);
 
313
        } else {
 
314
          $this->setCABundleFromPath($caroot.'default.pem');
 
315
        }
 
316
      }
 
317
 
 
318
      if ($this->canSetCAInfo()) {
 
319
        curl_setopt($curl, CURLOPT_CAINFO, $this->getCABundle());
 
320
      }
 
321
 
 
322
      $domain = id(new PhutilURI($uri))->getDomain();
 
323
      if (!empty(self::$blindTrustDomains[$domain])) {
 
324
        // Disable peer verification for domains that we blindly trust.
 
325
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
 
326
      } else {
 
327
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);
 
328
      }
 
329
 
 
330
      curl_setopt($curl, CURLOPT_SSLVERSION, 0);
 
331
    } else {
 
332
      $curl = $this->handle;
 
333
 
 
334
      if (!self::$results) {
 
335
        // NOTE: In curl_multi_select(), PHP calls curl_multi_fdset() but does
 
336
        // not check the return value of &maxfd for -1 until recent versions
 
337
        // of PHP (5.4.8 and newer). cURL may return -1 as maxfd in some unusual
 
338
        // situations; if it does, PHP enters select() with nfds=0, which blocks
 
339
        // until the timeout is reached.
 
340
        //
 
341
        // We could try to guess whether this will happen or not by examining
 
342
        // the version identifier, but we can also just sleep for only a short
 
343
        // period of time.
 
344
        curl_multi_select(self::$multi, 0.01);
 
345
      }
 
346
    }
 
347
 
 
348
    do {
 
349
      $active = null;
 
350
      $result = curl_multi_exec(self::$multi, $active);
 
351
    } while ($result == CURLM_CALL_MULTI_PERFORM);
 
352
 
 
353
    while ($info = curl_multi_info_read(self::$multi)) {
 
354
      if ($info['msg'] == CURLMSG_DONE) {
 
355
        self::$results[(int)$info['handle']] = $info;
 
356
      }
 
357
    }
 
358
 
 
359
    if (!array_key_exists((int)$curl, self::$results)) {
 
360
      return false;
 
361
    }
 
362
 
 
363
    // The request is complete, so release any temporary files we wrote
 
364
    // earlier.
 
365
    $this->temporaryFiles = array();
 
366
 
 
367
    $info = self::$results[(int)$curl];
 
368
    $result = $this->responseBuffer;
 
369
    $err_code = $info['result'];
 
370
 
 
371
    if ($err_code) {
 
372
      if (($err_code == CURLE_SSL_CACERT) && !$this->canSetCAInfo()) {
 
373
        $status = new HTTPFutureCertificateResponseStatus(
 
374
          HTTPFutureCertificateResponseStatus::ERROR_IMMUTABLE_CERTIFICATES,
 
375
          $uri);
 
376
      } else {
 
377
        $status = new HTTPFutureCURLResponseStatus($err_code, $uri);
 
378
      }
 
379
 
 
380
      $body = null;
 
381
      $headers = array();
 
382
      $this->result = array($status, $body, $headers);
 
383
    } else {
 
384
      // cURL returns headers of all redirects, we strip all but the final one.
 
385
      $redirects = curl_getinfo($curl, CURLINFO_REDIRECT_COUNT);
 
386
      $result = preg_replace('/^(.*\r\n\r\n){'.$redirects.'}/sU', '', $result);
 
387
      $this->result = $this->parseRawHTTPResponse($result);
 
388
    }
 
389
 
 
390
    curl_multi_remove_handle(self::$multi, $curl);
 
391
    unset(self::$results[(int)$curl]);
 
392
 
 
393
    // NOTE: We want to use keepalive if possible. Return the handle to a
 
394
    // pool for the domain; don't close it.
 
395
    self::$pool[$domain][] = $curl;
 
396
 
 
397
    $profiler = PhutilServiceProfiler::getInstance();
 
398
    $profiler->endServiceCall($this->profilerCallID, array());
 
399
 
 
400
    return true;
 
401
  }
 
402
 
 
403
 
 
404
  /**
 
405
   * Callback invoked by cURL as it reads HTTP data from the response. We save
 
406
   * the data to a buffer.
 
407
   */
 
408
  public function didReceiveDataCallback($handle, $data) {
 
409
    $this->responseBuffer .= $data;
 
410
    return strlen($data);
 
411
  }
 
412
 
 
413
 
 
414
  /**
 
415
   * Read data from the response buffer.
 
416
   *
 
417
   * NOTE: Like @{class:ExecFuture}, this method advances a read cursor but
 
418
   * does not discard the data. The data will still be buffered, and it will
 
419
   * all be returned when the future resolves. To discard the data after
 
420
   * reading it, call @{method:discardBuffers}.
 
421
   *
 
422
   * @return string Response data, if available.
 
423
   */
 
424
  public function read() {
 
425
    $result = substr($this->responseBuffer, $this->responseBufferPos);
 
426
    $this->responseBufferPos = strlen($this->responseBuffer);
 
427
    return $result;
 
428
  }
 
429
 
 
430
 
 
431
  /**
 
432
   * Discard any buffered data. Normally, you call this after reading the
 
433
   * data with @{method:read}.
 
434
   *
 
435
   * @return this
 
436
   */
 
437
  public function discardBuffers() {
 
438
    $this->responseBuffer = '';
 
439
    $this->responseBufferPos = 0;
 
440
    return $this;
 
441
  }
 
442
 
 
443
 
 
444
  /**
 
445
   * Produces a value safe to pass to `CURLOPT_POSTFIELDS`.
 
446
   *
 
447
   * @return wild   Some value, suitable for use in `CURLOPT_POSTFIELDS`.
 
448
   */
 
449
  private function formatRequestDataForCURL() {
 
450
    // We're generating a value to hand to cURL as CURLOPT_POSTFIELDS. The way
 
451
    // cURL handles this value has some tricky caveats.
 
452
 
 
453
    // First, we can return either an array or a query string. If we return
 
454
    // an array, we get a "multipart/form-data" request. If we return a
 
455
    // query string, we get an "application/x-www-form-urlencoded" request.
 
456
 
 
457
    // Second, if we return an array we can't duplicate keys. The user might
 
458
    // want to send the same parameter multiple times.
 
459
 
 
460
    // Third, if we return an array and any of the values start with "@",
 
461
    // cURL includes arbitrary files off disk and sends them to an untrusted
 
462
    // remote server. For example, an array like:
 
463
    //
 
464
    //   array('name' => '@/usr/local/secret')
 
465
    //
 
466
    // ...will attempt to read that file off disk and transmit its contents with
 
467
    // the request. This behavior is pretty surprising, and it can easily
 
468
    // become a relatively severe security vulnerability which allows an
 
469
    // attacker to read any file the HTTP process has access to. Since this
 
470
    // feature is very dangerous and not particularly useful, we prevent its
 
471
    // use. Broadly, this means we must reject some requests because they
 
472
    // contain an "@" in an inconvenient place.
 
473
 
 
474
    // Generally, to avoid the "@" case and because most servers usually
 
475
    // expect "application/x-www-form-urlencoded" data, we try to return a
 
476
    // string unless there are files attached to this request.
 
477
 
 
478
    $data = $this->getData();
 
479
    $files = $this->files;
 
480
 
 
481
    $any_data = ($data || (is_string($data) && strlen($data)));
 
482
    $any_files = (bool)$this->files;
 
483
 
 
484
    if (!$any_data && !$any_files) {
 
485
      // No files or data, so just bail.
 
486
      return null;
 
487
    }
 
488
 
 
489
    if (!$any_files) {
 
490
      // If we don't have any files, just encode the data as a query string,
 
491
      // make sure it's not including any files, and we're good to go.
 
492
      if (is_array($data)) {
 
493
        $data = http_build_query($data, '', '&');
 
494
      }
 
495
 
 
496
      $this->checkForDangerousCURLMagic($data, $is_query_string = true);
 
497
 
 
498
      return $data;
 
499
    }
 
500
 
 
501
    // If we've made it this far, we have some files, so we need to return
 
502
    // an array. First, convert the other data into an array if it isn't one
 
503
    // already.
 
504
 
 
505
    if (is_string($data)) {
 
506
      // NOTE: We explicitly don't want fancy array parsing here, so just
 
507
      // do a basic parse and then convert it into a dictionary ourselves.
 
508
      $parser = new PhutilQueryStringParser();
 
509
      $pairs = $parser->parseQueryStringToPairList($data);
 
510
 
 
511
      $map = array();
 
512
      foreach ($pairs as $pair) {
 
513
        list($key, $value) = $pair;
 
514
        if (array_key_exists($key, $map)) {
 
515
          throw new Exception(
 
516
            pht(
 
517
              'Request specifies two values for key "%s", but parameter '.
 
518
              'names must be unique if you are posting file data due to '.
 
519
              'limitations with cURL.'));
 
520
        }
 
521
        $map[$key] = $value;
 
522
      }
 
523
 
 
524
      $data = $map;
 
525
    }
 
526
 
 
527
    foreach ($data as $key => $value) {
 
528
      $this->checkForDangerousCURLMagic($value, $is_query_string = false);
 
529
    }
 
530
 
 
531
    foreach ($this->files as $name => $info) {
 
532
      if (array_key_exists($name, $data)) {
 
533
        throw new Exception(
 
534
          pht(
 
535
            'Request specifies a file with key "%s", but that key is '.
 
536
            'also defined by normal request data. Due to limitations '.
 
537
            'with cURL, requests that post file data must use unique '.
 
538
            'keys.'));
 
539
      }
 
540
 
 
541
      $tmp = new TempFile($info['name']);
 
542
      Filesystem::writeFile($tmp, $info['data']);
 
543
      $this->temporaryFiles[] = $tmp;
 
544
 
 
545
      // In 5.5.0 and later, we can use CURLFile. Prior to that, we have to
 
546
      // use this "@" stuff.
 
547
 
 
548
      if (class_exists('CURLFile', false)) {
 
549
        $file_value = new CURLFile((string)$tmp, $info['mime'], $info['name']);
 
550
      } else {
 
551
        $file_value = '@'.(string)$tmp;
 
552
      }
 
553
 
 
554
      $data[$name] = $file_value;
 
555
    }
 
556
 
 
557
    return $data;
 
558
  }
 
559
 
 
560
 
 
561
  /**
 
562
   * Detect strings which will cause cURL to do horrible, insecure things.
 
563
   *
 
564
   * @param string  Possibly dangerous string.
 
565
   * @param bool    True if this string is being used as part of a query string.
 
566
   * @return void
 
567
   */
 
568
  private function checkForDangerousCURLMagic($string, $is_query_string) {
 
569
    if (empty($string[0]) || ($string[0] != '@')) {
 
570
      // This isn't an "@..." string, so it's fine.
 
571
      return;
 
572
    }
 
573
 
 
574
    if ($is_query_string) {
 
575
      if (version_compare(phpversion(), '5.2.0', '<')) {
 
576
        throw new Exception(
 
577
          pht(
 
578
            'Attempting to make an HTTP request, but query string data begins '.
 
579
            'with "@". Prior to PHP 5.2.0 this reads files off disk, which '.
 
580
            'creates a wide attack window for security vulnerabilities. '.
 
581
            'Upgrade PHP or avoid making cURL requests which begin with "@".'));
 
582
      }
 
583
 
 
584
      // This is safe if we're on PHP 5.2.0 or newer.
 
585
      return;
 
586
    }
 
587
 
 
588
    throw new Exception(
 
589
      pht(
 
590
        'Attempting to make an HTTP request which includes file data, but '.
 
591
        'the value of a query parameter begins with "@". PHP interprets '.
 
592
        'these values to mean that it should read arbitrary files off disk '.
 
593
        'and transmit them to remote servers. Declining to make this '.
 
594
        'request.'));
 
595
  }
 
596
 
 
597
 
 
598
  /**
 
599
   * Determine whether CURLOPT_CAINFO is usable on this system.
 
600
   */
 
601
  private function canSetCAInfo() {
 
602
    // We cannot set CAInfo on OSX after Yosemite.
 
603
 
 
604
    $osx_version = PhutilExecutionEnvironment::getOSXVersion();
 
605
    if ($osx_version) {
 
606
      if (version_compare($osx_version, 14, '>=')) {
 
607
        return false;
 
608
      }
 
609
    }
 
610
 
 
611
    return true;
 
612
  }
 
613
 
 
614
}