diff --git a/docs/README.md b/docs/README.md index d4f2ebb93..8116963d6 100755 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ here are prose; you might also want to check out the [API documentation][]. * [Authenticating your request][authentication] * Advanced Usage * [Custom authentication][authentication-custom] + * [Requests through proxy][proxy] * [Hooking system][hooks] [goals]: goals.md @@ -23,4 +24,5 @@ here are prose; you might also want to check out the [API documentation][]. [usage-advanced]: usage-advanced.md [authentication]: authentication.md [authentication-custom]: authentication-custom.md -[hooks]: hooks.md \ No newline at end of file +[hooks]: hooks.md +[proxy]: proxy.md \ No newline at end of file diff --git a/docs/proxy.md b/docs/proxy.md new file mode 100644 index 000000000..cb2675de2 --- /dev/null +++ b/docs/proxy.md @@ -0,0 +1,23 @@ +Proxy Support +============= + +You can easily make requests through HTTP proxies. + +To make requests through an open proxy, specify the following options: + +```php +$options = array( + 'proxy' => '127.0.0.1:3128' +); +Requests::get('http://httpbin.org/ip', array(), $options); +``` + +If your proxy needs you to authenticate, the option will become an array like +the following: + +```php +$options = array( + 'proxy' => array( '127.0.0.1:3128', 'my_username', 'my_password' ) +); +Requests::get('http://httpbin.org/ip', array(), $options); +``` diff --git a/examples/proxy.php b/examples/proxy.php new file mode 100644 index 000000000..21e4ebe04 --- /dev/null +++ b/examples/proxy.php @@ -0,0 +1,18 @@ + '127.0.0.1:8080', // syntax: host:port, eg 12.13.14.14:8080 or someproxy.com:3128 + // If you need to authenticate, use the following syntax: + // 'proxy' => array( '127.0.0.1:8080', 'username', 'password' ), +); +$request = Requests::get('http://httpbin.org/ip', array(), $options ); + +// See result +var_dump( $request->body ); \ No newline at end of file diff --git a/library/Requests.php b/library/Requests.php index d58c81aa0..eb2ac18f0 100755 --- a/library/Requests.php +++ b/library/Requests.php @@ -267,6 +267,8 @@ public static function patch($url, $headers, $data = array(), $options = array() * - `auth`: Authentication handler or array of user/password details to use * for Basic authentication * (Requests_Auth|array|boolean, default: false) + * - `proxy`: Proxy details to use for proxy by-passing and authentication + * (Requests_Proxy|array|boolean, default: false) * - `idn`: Enable IDN parsing * (boolean, default: true) * - `transport`: Custom transport. Either a class name, or a @@ -447,6 +449,7 @@ protected static function get_default_options($multirequest = false) { 'type' => self::GET, 'filename' => false, 'auth' => false, + 'proxy' => false, 'idn' => true, 'hooks' => null, 'transport' => null, @@ -477,7 +480,7 @@ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$optio if (empty($options['hooks'])) { $options['hooks'] = new Requests_Hooks(); } - + if (is_array($options['auth'])) { $options['auth'] = new Requests_Auth_Basic($options['auth']); } @@ -485,6 +488,13 @@ protected static function set_defaults(&$url, &$headers, &$data, &$type, &$optio $options['auth']->register($options['hooks']); } + if (!empty($options['proxy'])) { + $options['proxy'] = new Requests_Proxy_HTTP($options['proxy']); + } + if ($options['proxy'] !== false) { + $options['proxy']->register($options['hooks']); + } + if ($options['idn'] !== false) { $iri = new Requests_IRI($url); $iri->host = Requests_IDNAEncoder::encode($iri->ihost); diff --git a/library/Requests/Proxy.php b/library/Requests/Proxy.php new file mode 100644 index 000000000..ac7c1d6b0 --- /dev/null +++ b/library/Requests/Proxy.php @@ -0,0 +1,35 @@ +proxy = $args; + } elseif( is_array( $args ) ) { + if( count( $args ) == 1 ) { + list( $this->proxy ) = $args; + } elseif( count( $args ) == 3 ) { + list( $this->proxy, $this->user, $this->pass ) = $args; + $this->use_authentication = true; + } else { + throw new Requests_Exception( 'Invalid number of arguments', 'proxyhttpbadargs'); + } + } + + } + + /** + * Register the necessary callbacks + * + * @since 1.6 + * @see curl_before_send + * @see fsockopen_remote_socket + * @see fsockopen_remote_host_path + * @see fsockopen_header + * @param Requests_Hooks $hooks Hook system + */ + public function register(Requests_Hooks &$hooks) { + $hooks->register('curl.before_send', array(&$this, 'curl_before_send')); + + $hooks->register('fsockopen.remote_socket', array(&$this, 'fsockopen_remote_socket')); + $hooks->register('fsockopen.remote_host_path', array(&$this, 'fsockopen_remote_host_path')); + if( $this->use_authentication ) { + $hooks->register('fsockopen.after_headers', array(&$this, 'fsockopen_header')); + } + } + + /** + * Set cURL parameters before the data is sent + * + * @since 1.6 + * @param resource $handle cURL resource + */ + public function curl_before_send(&$handle) { + curl_setopt( $handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP ); + curl_setopt( $handle, CURLOPT_PROXY, $this->proxy ); + + if( $this->use_authentication ) { + curl_setopt( $handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY ); + curl_setopt( $handle, CURLOPT_PROXYUSERPWD, $this->getAuthString() ); + } + } + + /** + * Alter remote socket information before opening socket connection + * + * @since 1.6 + * @param string $out HTTP header string + */ + public function fsockopen_remote_socket( &$remote_socket ) { + $remote_socket = $this->proxy; + } + + /** + * Alter remote path before getting stream data + * + * @since 1.6 + * @param string $out HTTP header string + */ + public function fsockopen_remote_host_path( &$path, $url ) { + $path = $url; + } + + /** + * Add extra headers to the request before sending + * + * @since 1.6 + * @param string $out HTTP header string + */ + public function fsockopen_header( &$out ) { + $out .= "Proxy-Authorization: Basic " . base64_encode( $this->getAuthString() ) . "\r\n"; + } + + /** + * Get the authentication string (user:pass) + * + * @since 1.6 + * @return string + */ + public function getAuthString() { + return $this->user . ':' . $this->pass; + } +} \ No newline at end of file diff --git a/library/Requests/Transport/cURL.php b/library/Requests/Transport/cURL.php index b6f3136dd..442e53211 100755 --- a/library/Requests/Transport/cURL.php +++ b/library/Requests/Transport/cURL.php @@ -84,7 +84,7 @@ public function __construct() { */ public function request($url, $headers = array(), $data = array(), $options = array()) { $this->setup_handle($url, $headers, $data, $options); - + $options['hooks']->dispatch('curl.before_send', array(&$this->fp)); if ($options['filename'] !== false) { diff --git a/library/Requests/Transport/fsockopen.php b/library/Requests/Transport/fsockopen.php index 4b37839c0..e30026722 100755 --- a/library/Requests/Transport/fsockopen.php +++ b/library/Requests/Transport/fsockopen.php @@ -78,14 +78,21 @@ public function request($url, $headers = array(), $data = array(), $options = ar else { $remote_socket = 'tcp://' . $host; } - + + $proxy = isset( $options['proxy'] ); + $proxy_auth = $proxy && isset( $options['proxy_username'] ) && isset( $options['proxy_password'] ); + if (!isset($url_parts['port'])) { $url_parts['port'] = 80; } $remote_socket .= ':' . $url_parts['port']; set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE); - $fp = stream_socket_client($remote_socket, $errno, $errstr, $options['timeout'], STREAM_CLIENT_CONNECT, $context); + + $options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket)); + + $fp = stream_socket_client( $remote_socket, $errno, $errstr, $options['timeout'], STREAM_CLIENT_CONNECT, $context); + restore_error_handler(); if (!$fp) { @@ -114,7 +121,10 @@ public function request($url, $headers = array(), $data = array(), $options = ar else { $path = '/'; } + + $options['hooks']->dispatch( 'fsockopen.remote_host_path', array( &$path, $url ) ); $out = $options['type'] . " $path HTTP/1.0\r\n"; + if (is_array($data)) { $request_body = http_build_query($data, null, '&'); } @@ -131,8 +141,9 @@ public function request($url, $headers = array(), $data = array(), $options = ar case Requests::HEAD: case Requests::GET: case Requests::DELETE: - $get = self::format_get($url_parts, $data); - $out = $options['type'] . " $get HTTP/1.0\r\n"; + $path = self::format_get($url_parts, $data); + $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url)); + $out = $options['type'] . " $path HTTP/1.0\r\n"; break; } $out .= "Host: {$url_parts['host']}"; @@ -140,7 +151,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar if ($url_parts['port'] !== 80) { $out .= ":{$url_parts['port']}"; } - + $out .= "\r\n"; $out .= "User-Agent: {$options['useragent']}\r\n"; diff --git a/tests/Proxy/HTTP.php b/tests/Proxy/HTTP.php new file mode 100644 index 000000000..4165a9a6d --- /dev/null +++ b/tests/Proxy/HTTP.php @@ -0,0 +1,97 @@ +proxy = $proxy; + $this->user = $user; + $this->pass = $pass; + } + + public function register(Requests_Hooks &$hooks) { + $hooks->register('requests.before_request', array(&$this, 'before_request')); + } + + public function before_request(&$url, &$headers, &$data, &$type, &$options) { + $headers['X-Requests-Proxy'] = 'HTTP'; + if( isset( $this->user ) && isset( $this->pass ) ) { + $options = array( + 'proxy' => array( $this->proxy, $this->user, $this->pass ) + ); + } else { + $options = array( + 'proxy' => $this->proxy + ); + } + } +} + +class RequestsTest_Proxy_HTTP extends PHPUnit_Framework_TestCase { + + public static function transportProvider() { + $transports = array( + array('Requests_Transport_fsockopen'), + array('Requests_Transport_cURL'), + ); + return $transports; + } + + /** + * @dataProvider transportProvider + */ + public function testProxyNoAuth( $transport ) { + if (!call_user_func(array($transport, 'test'))) { + $this->markTestSkipped($transport . ' is not available'); + return; + } + + $options = array( + 'proxy' => new Requests_Proxy_Add_HTTP_XHeader( PROXYTEST_PROXY ), + 'transport' => $transport, + ); + + $request = Requests::get( PROXYTEST_URL, array(), $options ); + $this->assertEquals( 200, $request->status_code ); + + $result = json_decode( $request->body ); + $this->assertEquals( 'HTTP', $result->{"X-Requests-Proxy"} ); + $this->assertEquals( 'HTTP', $result->{"X-Requests-Proxied"} ); + } + + /** + * @dataProvider transportProvider + */ + public function testProxyWithAuth( $transport ) { + if (!call_user_func(array($transport, 'test'))) { + $this->markTestSkipped($transport . ' is not available'); + return; + } + + $options = array( + 'proxy' => new Requests_Proxy_Add_HTTP_XHeader( PROXYTEST_PROXY, PROXYTEST_USER, PROXYTEST_PASS ), + 'transport' => $transport, + ); + $request = Requests::get( PROXYTEST_URL, array(), $options ); + $this->assertEquals( 200, $request->status_code ); + + $result = json_decode( $request->body ); + $this->assertEquals( 'HTTP', $result->{"X-Requests-Proxy"} ); + $this->assertEquals( 'HTTP', $result->{"X-Requests-Proxied"} ); + } + + /** + * @expectedException Requests_Exception + */ + public function testMissingPassword() { + $test = new Requests_Proxy_HTTP( array( PROXYTEST_PROXY, PROXYTEST_USER ) ); + } + +} \ No newline at end of file diff --git a/tests/phpunit.xml.dist b/tests/phpunit.xml.dist index 3236d04ec..7993647fb 100755 --- a/tests/phpunit.xml.dist +++ b/tests/phpunit.xml.dist @@ -1,6 +1,9 @@ + + Proxy + Auth