1<?php 2/** 3 * Copyright 2014 Facebook, Inc. 4 * 5 * You are hereby granted a non-exclusive, worldwide, royalty-free license to 6 * use, copy, modify, and distribute this software in source code or binary 7 * form for use in connection with the web services and APIs provided by 8 * Facebook. 9 * 10 * As with any software that integrates with the Facebook platform, your use 11 * of this software is subject to the Facebook Developer Principles and 12 * Policies [http://developers.facebook.com/policy/]. This copyright notice 13 * shall be included in all copies or substantial portions of the software. 14 * 15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 * DEALINGS IN THE SOFTWARE. 22 * 23 */ 24namespace Facebook\HttpClients; 25 26use Facebook\Http\GraphRawResponse; 27use Facebook\Exceptions\FacebookSDKException; 28 29/** 30 * Class FacebookCurlHttpClient 31 * 32 * @package Facebook 33 */ 34class FacebookCurlHttpClient implements FacebookHttpClientInterface 35{ 36 /** 37 * @var string The client error message 38 */ 39 protected $curlErrorMessage = ''; 40 41 /** 42 * @var int The curl client error code 43 */ 44 protected $curlErrorCode = 0; 45 46 /** 47 * @var string|boolean The raw response from the server 48 */ 49 protected $rawResponse; 50 51 /** 52 * @var FacebookCurl Procedural curl as object 53 */ 54 protected $facebookCurl; 55 56 /** 57 * @const Curl Version which is unaffected by the proxy header length error. 58 */ 59 const CURL_PROXY_QUIRK_VER = 0x071E00; 60 61 /** 62 * @const "Connection Established" header text 63 */ 64 const CONNECTION_ESTABLISHED = "HTTP/1.0 200 Connection established\r\n\r\n"; 65 66 /** 67 * @param FacebookCurl|null Procedural curl as object 68 */ 69 public function __construct(FacebookCurl $facebookCurl = null) 70 { 71 $this->facebookCurl = $facebookCurl ?: new FacebookCurl(); 72 } 73 74 /** 75 * @inheritdoc 76 */ 77 public function send($url, $method, $body, array $headers, $timeOut) 78 { 79 $this->openConnection($url, $method, $body, $headers, $timeOut); 80 $this->sendRequest(); 81 82 if ($curlErrorCode = $this->facebookCurl->errno()) { 83 throw new FacebookSDKException($this->facebookCurl->error(), $curlErrorCode); 84 } 85 86 // Separate the raw headers from the raw body 87 list($rawHeaders, $rawBody) = $this->extractResponseHeadersAndBody(); 88 89 $this->closeConnection(); 90 91 return new GraphRawResponse($rawHeaders, $rawBody); 92 } 93 94 /** 95 * Opens a new curl connection. 96 * 97 * @param string $url The endpoint to send the request to. 98 * @param string $method The request method. 99 * @param string $body The body of the request. 100 * @param array $headers The request headers. 101 * @param int $timeOut The timeout in seconds for the request. 102 */ 103 public function openConnection($url, $method, $body, array $headers, $timeOut) 104 { 105 $options = [ 106 CURLOPT_CUSTOMREQUEST => $method, 107 CURLOPT_HTTPHEADER => $this->compileRequestHeaders($headers), 108 CURLOPT_URL => $url, 109 CURLOPT_CONNECTTIMEOUT => 10, 110 CURLOPT_TIMEOUT => $timeOut, 111 CURLOPT_RETURNTRANSFER => true, // Follow 301 redirects 112 CURLOPT_HEADER => true, // Enable header processing 113 CURLOPT_SSL_VERIFYHOST => 2, 114 CURLOPT_SSL_VERIFYPEER => true, 115 CURLOPT_CAINFO => __DIR__ . '/certs/DigiCertHighAssuranceEVRootCA.pem', 116 ]; 117 118 if ($method !== "GET") { 119 $options[CURLOPT_POSTFIELDS] = $body; 120 } 121 122 $this->facebookCurl->init(); 123 $this->facebookCurl->setoptArray($options); 124 } 125 126 /** 127 * Closes an existing curl connection 128 */ 129 public function closeConnection() 130 { 131 $this->facebookCurl->close(); 132 } 133 134 /** 135 * Send the request and get the raw response from curl 136 */ 137 public function sendRequest() 138 { 139 $this->rawResponse = $this->facebookCurl->exec(); 140 } 141 142 /** 143 * Compiles the request headers into a curl-friendly format. 144 * 145 * @param array $headers The request headers. 146 * 147 * @return array 148 */ 149 public function compileRequestHeaders(array $headers) 150 { 151 $return = []; 152 153 foreach ($headers as $key => $value) { 154 $return[] = $key . ': ' . $value; 155 } 156 157 return $return; 158 } 159 160 /** 161 * Extracts the headers and the body into a two-part array 162 * 163 * @return array 164 */ 165 public function extractResponseHeadersAndBody() 166 { 167 $headerSize = $this->getHeaderSize(); 168 169 $rawHeaders = mb_substr($this->rawResponse, 0, $headerSize); 170 $rawBody = mb_substr($this->rawResponse, $headerSize); 171 172 return [trim($rawHeaders), trim($rawBody)]; 173 } 174 175 /** 176 * Return proper header size 177 * 178 * @return integer 179 */ 180 private function getHeaderSize() 181 { 182 $headerSize = $this->facebookCurl->getinfo(CURLINFO_HEADER_SIZE); 183 // This corrects a Curl bug where header size does not account 184 // for additional Proxy headers. 185 if ($this->needsCurlProxyFix()) { 186 // Additional way to calculate the request body size. 187 if (preg_match('/Content-Length: (\d+)/', $this->rawResponse, $m)) { 188 $headerSize = mb_strlen($this->rawResponse) - $m[1]; 189 } elseif (stripos($this->rawResponse, self::CONNECTION_ESTABLISHED) !== false) { 190 $headerSize += mb_strlen(self::CONNECTION_ESTABLISHED); 191 } 192 } 193 194 return $headerSize; 195 } 196 197 /** 198 * Detect versions of Curl which report incorrect header lengths when 199 * using Proxies. 200 * 201 * @return boolean 202 */ 203 private function needsCurlProxyFix() 204 { 205 $ver = $this->facebookCurl->version(); 206 $version = $ver['version_number']; 207 208 return $version < self::CURL_PROXY_QUIRK_VER; 209 } 210} 211