httpclient.php 10.8 KB
Newer Older
1 2
<?php
/**
Evan Prodromou's avatar
Evan Prodromou committed
3
 * StatusNet, the distributed open-source microblogging tool
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *
 * Utility for doing HTTP-related things
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Action
Evan Prodromou's avatar
Evan Prodromou committed
23
 * @package   StatusNet
24
 * @author    Evan Prodromou <evan@status.net>
Evan Prodromou's avatar
Evan Prodromou committed
25
 * @copyright 2009 StatusNet, Inc.
26
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
Evan Prodromou's avatar
Evan Prodromou committed
27
 * @link      http://status.net/
28 29
 */

30
if (!defined('GNUSOCIAL')) {
31 32 33
    exit(1);
}

34 35 36
require_once 'HTTP/Request2.php';
require_once 'HTTP/Request2/Response.php';

37
/**
Evan Prodromou's avatar
Evan Prodromou committed
38
 * Useful structure for HTTP responses
39 40 41 42 43
 *
 * We make HTTP calls in several places, and we have several different
 * ways of doing them. This class hides the specifics of what underlying
 * library (curl or PHP-HTTP or whatever) that's used.
 *
44 45
 * This extends the HTTP_Request2_Response class with methods to get info
 * about any followed redirects.
46 47 48
 * 
 * Originally used the name 'HTTPResponse' to match earlier code, but
 * this conflicts with a class in in the PECL HTTP extension.
49
 *
50
 * @category HTTP
51 52 53 54 55
 * @package StatusNet
 * @author Evan Prodromou <evan@status.net>
 * @author Brion Vibber <brion@status.net>
 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link http://status.net/
56
 */
mmn's avatar
mmn committed
57
class GNUsocial_HTTPResponse extends HTTP_Request2_Response
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
    function __construct(HTTP_Request2_Response $response, $url, $redirects=0)
    {
        foreach (get_object_vars($response) as $key => $val) {
            $this->$key = $val;
        }
        $this->url = strval($url);
        $this->redirectCount = intval($redirects);
    }

    /**
     * Get the count of redirects that have been followed, if any.
     * @return int
     */
    function getRedirectCount()
    {
        return $this->redirectCount;
    }

    /**
     * Gets the final target URL, after any redirects have been followed.
     * @return string URL
     */
    function getUrl()
    {
        return $this->url;
    }

    /**
87
     * Check if the response is OK, generally a 200 or other 2xx status code.
88 89 90 91
     * @return bool
     */
    function isOk()
    {
92 93
        $status = $this->getStatus();
        return ($status >= 200 && $status < 300);
94
    }
95 96
}

Evan Prodromou's avatar
Evan Prodromou committed
97 98 99 100 101 102 103
/**
 * Utility class for doing HTTP client stuff
 *
 * We make HTTP calls in several places, and we have several different
 * ways of doing them. This class hides the specifics of what underlying
 * library (curl or PHP-HTTP or whatever) that's used.
 *
104 105 106 107 108 109 110
 * This extends the PEAR HTTP_Request2 package:
 * - sends StatusNet-specific User-Agent header
 * - 'follow_redirects' config option, defaulting off
 * - 'max_redirs' config option, defaulting to 10
 * - extended response class adds getRedirectCount() and getUrl() methods
 * - get() and post() convenience functions return body content directly
 *
Evan Prodromou's avatar
Evan Prodromou committed
111
 * @category HTTP
Evan Prodromou's avatar
Evan Prodromou committed
112
 * @package  StatusNet
113
 * @author   Evan Prodromou <evan@status.net>
114
 * @author   Brion Vibber <brion@status.net>
Evan Prodromou's avatar
Evan Prodromou committed
115
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
Evan Prodromou's avatar
Evan Prodromou committed
116
 * @link     http://status.net/
Evan Prodromou's avatar
Evan Prodromou committed
117 118
 */

119
class HTTPClient extends HTTP_Request2
120
{
Evan Prodromou's avatar
Evan Prodromou committed
121

122
    function __construct($url=null, $method=self::METHOD_GET, $config=array())
123
    {
124 125
        $this->config['max_redirs'] = 10;
        $this->config['follow_redirects'] = true;
126 127 128 129 130 131 132 133 134
        
        // We've had some issues with keepalive breaking with
        // HEAD requests, such as to youtube which seems to be
        // emitting chunked encoding info for an empty body
        // instead of not emitting anything. This may be a
        // bug on YouTube's end, but the upstream libray
        // ought to be investigated to see if we can handle
        // it gracefully in that case as well.
        $this->config['protocol_version'] = '1.0';
135 136 137 138 139 140 141 142 143 144 145 146 147

        // Default state of OpenSSL seems to have no trusted
        // SSL certificate authorities, which breaks hostname
        // verification and means we have a hard time communicating
        // with other sites' HTTPS interfaces.
        //
        // Turn off verification unless we've configured a CA bundle.
        if (common_config('http', 'ssl_cafile')) {
            $this->config['ssl_cafile'] = common_config('http', 'ssl_cafile');
        } else {
            $this->config['ssl_verify_peer'] = false;
        }

148 149 150 151
        // This means "verify the cert hostname against what we connect to", it does not
        // imply CA trust or anything like that. Just the hostname.
        $this->config['ssl_verify_host'] = common_config('http', 'ssl_verify_host');

152 153 154 155
        if (common_config('http', 'curl') && extension_loaded('curl')) {
            $this->config['adapter'] = 'HTTP_Request2_Adapter_Curl';
        }

156 157 158 159 160 161 162 163
        foreach (array('host', 'port', 'user', 'password', 'auth_scheme') as $cf) {
            $k = 'proxy_'.$cf;
            $v = common_config('http', $k); 
            if (!empty($v)) {
                $this->config[$k] = $v;
            }
        }

164
        parent::__construct($url, $method, $config);
165
        $this->setHeader('User-Agent', self::userAgent());
166 167
    }

168 169 170 171 172
    /**
     * Convenience/back-compat instantiator
     * @return HTTPClient
     */
    public static function start()
173
    {
174
        return new HTTPClient();
175 176
    }

mmn's avatar
mmn committed
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
    /**
     * Quick static function to GET a URL
     */
    public static function quickGet($url, $accept='text/html,application/xhtml+xml')
    {
        $client = new HTTPClient();
        $client->setHeader('Accept', $accept);
        $response = $client->get($url);
        if (!$response->isOk()) {
            // TRANS: Exception. %s is a profile URL.
            throw new Exception(sprintf(_m('Could not GET URL %s.'), $url), $response->getStatus());
        }
        return $response->getBody();
    }

192 193 194
    /**
     * Convenience function to run a GET request.
     *
mmn's avatar
mmn committed
195
     * @return GNUsocial_HTTPResponse
196 197 198
     * @throws HTTP_Request2_Exception
     */
    public function get($url, $headers=array())
199
    {
200
        return $this->doRequest($url, self::METHOD_GET, $headers);
201 202
    }

203 204 205
    /**
     * Convenience function to run a HEAD request.
     *
mmn's avatar
mmn committed
206
     * @return GNUsocial_HTTPResponse
207 208 209
     * @throws HTTP_Request2_Exception
     */
    public function head($url, $headers=array())
210
    {
211
        return $this->doRequest($url, self::METHOD_HEAD, $headers);
212
    }
213

214 215 216 217 218 219
    /**
     * Convenience function to POST form data.
     *
     * @param string $url
     * @param array $headers optional associative array of HTTP headers
     * @param array $data optional associative array or blob of form data to submit
mmn's avatar
mmn committed
220
     * @return GNUsocial_HTTPResponse
221 222 223
     * @throws HTTP_Request2_Exception
     */
    public function post($url, $headers=array(), $data=array())
224
    {
225 226 227 228
        if ($data) {
            $this->addPostParameter($data);
        }
        return $this->doRequest($url, self::METHOD_POST, $headers);
229 230
    }

231
    /**
mmn's avatar
mmn committed
232
     * @return GNUsocial_HTTPResponse
233 234 235
     * @throws HTTP_Request2_Exception
     */
    protected function doRequest($url, $method, $headers)
236
    {
237
        $this->setUrl($url);
238 239 240 241 242 243 244 245 246

        // Workaround for HTTP_Request2 not setting up SNI in socket contexts;
        // This fixes cert validation for SSL virtual hosts using SNI.
        // Requires PHP 5.3.2 or later and OpenSSL with SNI support.
        if ($this->url->getScheme() == 'https' && defined('OPENSSL_TLSEXT_SERVER_NAME')) {
            $this->config['ssl_SNI_enabled'] = true;
            $this->config['ssl_SNI_server_name'] = $this->url->getHost();
        }

247 248 249 250 251 252 253 254 255 256 257 258 259 260
        $this->setMethod($method);
        if ($headers) {
            foreach ($headers as $header) {
                $this->setHeader($header);
            }
        }
        $response = $this->send();
        return $response;
    }
    
    protected function log($level, $detail) {
        $method = $this->getMethod();
        $url = $this->getUrl();
        common_log($level, __CLASS__ . ": HTTP $method $url - $detail");
261
    }
262

263
    /**
264
     * Pulls up GNU Social's customized user-agent string, so services
265 266 267 268
     * we hit can track down the responsible software.
     *
     * @return string
     */
269
    static public function userAgent()
270
    {
271 272
        return GNUSOCIAL_ENGINE . '/' . GNUSOCIAL_VERSION
                . ' (' . GNUSOCIAL_CODENAME . ')';
273
    }
274 275

    /**
276
     * Actually performs the HTTP request and returns a
mmn's avatar
mmn committed
277
     * GNUsocial_HTTPResponse object with response body and header info.
278 279 280
     *
     * Wraps around parent send() to add logging and redirection processing.
     *
mmn's avatar
mmn committed
281
     * @return GNUsocial_HTTPResponse
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
     * @throw HTTP_Request2_Exception
     */
    public function send()
    {
        $maxRedirs = intval($this->config['max_redirs']);
        if (empty($this->config['follow_redirects'])) {
            $maxRedirs = 0;
        }
        $redirs = 0;
        do {
            try {
                $response = parent::send();
            } catch (HTTP_Request2_Exception $e) {
                $this->log(LOG_ERR, $e->getMessage());
                throw $e;
            }
            $code = $response->getStatus();
            if ($code >= 200 && $code < 300) {
                $reason = $response->getReasonPhrase();
                $this->log(LOG_INFO, "$code $reason");
            } elseif ($code >= 300 && $code < 400) {
                $url = $this->getUrl();
                $target = $response->getHeader('Location');
                
                if (++$redirs >= $maxRedirs) {
                    common_log(LOG_ERR, __CLASS__ . ": Too many redirects: skipping $code redirect from $url to $target");
                    break;
                }
                try {
                    $this->setUrl($target);
                    $this->setHeader('Referer', $url);
                    common_log(LOG_INFO, __CLASS__ . ": Following $code redirect from $url to $target");
                    continue;
                } catch (HTTP_Request2_Exception $e) {
                    common_log(LOG_ERR, __CLASS__ . ": Invalid $code redirect from $url to $target");
                }
            } else {
                $reason = $response->getReasonPhrase();
                $this->log(LOG_ERR, "$code $reason");
            }
            break;
        } while ($maxRedirs);
mmn's avatar
mmn committed
324
        return new GNUsocial_HTTPResponse($response, $this->getUrl(), $redirs);
325
    }
326
}