Index: includes/browser.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/browser.inc,v retrieving revision 1.3 diff -u -9 -p -r1.3 browser.inc --- includes/browser.inc 31 Aug 2009 05:56:54 -0000 1.3 +++ includes/browser.inc 10 Sep 2009 19:23:28 -0000 @@ -631,37 +631,80 @@ class Browser { public function request($method, $url, array $additional) { if (!$this->isMethodSupported($method)) { return FALSE; } // TODO } /** + * Check whether the request should be made through a proxy server. + * + * @param $url + * The URL to request. + */ + public function shouldUseProxy($url) { + if (!variable_get('proxy_hostname')) { + return FALSE; + } + $hostname = parse_url($url, PHP_URL_HOST); + foreach (explode(',', variable_get('proxy_exceptions')) as $exception) { + $exception = trim($exception); + if ($exception) { + if ($hostname == $exception) { + return FALSE; + } + // If the exception begins with a period, it should match the end of + // the hostname, e.g. ".example.com" matches "foo.example.com" but not + // "foo.example.com.example.org". + elseif ($exception[0] == '.' && substr($hostname, -strlen($exception)) == $exception) { + return FALSE; + } + // If the exception is an IP address, resolve the hostname and compare + // its address. If $hostname is already an IP address, gethostbyname() + // returns it unmodified. + elseif (filter_var($exception, FILTER_VALIDATE_IP, FILTER_VALIDATE_IP) && $exception == gethostbyname($hostname)) { + return FALSE; + } + } + } + return TRUE; + } + + /** * Perform the request using the PHP stream wrapper. * * @param $url - * The url to request. + * The URL to request. * @param $options * The HTTP stream context options to be passed to * stream_context_set_params(). */ protected function streamExecute($url, array $options) { // Global variable provided by PHP stream wapper. global $http_response_header; if (!isset($options['header'])) { $options['header'] = array(); } - // Merge default request headers with the passed headers and generate - // header string to be sent in http request. + // Merge default request headers with the passed headers. $headers = $this->requestHeaders + $options['header']; + + if ($this->shouldUseProxy($url)) { + $options['proxy'] = 'tcp://' . variable_get('proxy_hostname') . ':' . variable_get('proxy_port', 8080); + $options['request_fulluri'] = TRUE; + if (variable_get('proxy_username')) { + $headers['Proxy-Authorization'] = 'Basic ' . base64_encode(variable_get('proxy_username') . ':' . variable_get('proxy_password')); + } + } + + // Generate header string to be sent in HTTP request. $options['header'] = $this->headerString($headers); // Update the handler options. stream_context_set_params($this->handle, array( 'options' => array( 'http' => $options, ) )); @@ -685,18 +728,29 @@ class Browser { $this->headers = array(); // Ensure that request headers are up to date. if ($this->getHttpAuthentication()) { curl_setopt($this->handle, CURLOPT_USERPWD, $this->getHttpAuthentication()); } curl_setopt($this->handle, CURLOPT_USERAGENT, $this->requestHeaders['User-Agent']); curl_setopt($this->handle, CURLOPT_HTTPHEADER, $this->requestHeaders); + if ($this->shouldUseProxy($options[CURLOPT_URL])) { + $options[CURLOPT_PROXY] = variable_get('proxy_hostname'); + $options[CURLOPT_PROXYPORT] = variable_get('proxy_port', 8080); + if (variable_get('proxy_username')) { + $options[CURLOPT_PROXYUSERPWD] = variable_get('proxy_username') . ':' . variable_get('proxy_password'); + } + } + else { + $options[CURLOPT_PROXY] = FALSE; + } + curl_setopt_array($this->handle, $options); $this->content = curl_exec($this->handle); $this->url = curl_getinfo($this->handle, CURLINFO_EFFECTIVE_URL); // $this->headers should be filled by $this->curlHeaderCallback(). unset($this->page); } /** Index: modules/simpletest/tests/browser.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/browser.test,v retrieving revision 1.1 diff -u -9 -p -r1.1 browser.test --- modules/simpletest/tests/browser.test 17 Aug 2009 06:08:47 -0000 1.1 +++ modules/simpletest/tests/browser.test 10 Sep 2009 19:23:28 -0000 @@ -35,18 +35,44 @@ class BrowserTestCase extends DrupalWebT // Check browser refresh, both meta tag and HTTP header. $request = $browser->get(url('browser_test/refresh/meta', array('absolute' => TRUE))); $this->assertEqual($request['content'], 'Refresh successful', 'Meta refresh successful ($request)'); $this->assertEqual($browser->getContent(), 'Refresh successful', 'Meta refresh successful ($browser)'); $request = $browser->get(url('browser_test/refresh/header', array('absolute' => TRUE))); $this->assertEqual($request['content'], 'Refresh successful', 'Meta refresh successful ($request)'); $this->assertEqual($browser->getContent(), 'Refresh successful', 'Meta refresh successful ($browser)'); } + + /** + * Test the proxy exception list. + */ + public function testShouldUseProxy() { + $browser = new Browser(); + $this->assertFalse($browser->shouldUseProxy('http://example.com/'), t('Proxy is not used when a proxy hostname is specified.')); + + variable_set('proxy_hostname', 'proxy.example.com'); + $this->assertTrue($browser->shouldUseProxy('http://example.com/'), t('Proxy is used when a proxy hostname is specified.')); + + variable_set('proxy_exceptions', '.example.com,example.org'); + $this->assertTrue($browser->shouldUseProxy('http://example.com/'), t('Proxy is used for host in exception list.')); + $this->assertFalse($browser->shouldUseProxy('http://example.org/'), t('Proxy is not used for host in exception list.')); + $this->assertTrue($browser->shouldUseProxy('http://www.example.org/'), t('Proxy is used for host not in exception list.')); + $this->assertFalse($browser->shouldUseProxy('http://www.example.com/'), t('Proxy is not used for host that matches exception suffix.')); + $this->assertTrue($browser->shouldUseProxy('http://www.example.com.example.org/'), t('Proxy is used for host not in exception list.')); + + variable_set('proxy_exceptions', '127.0.0.1'); + $this->assertFalse($browser->shouldUseProxy('http://localhost/'), t('Proxy is not used for host that resolves to an IP address in exception list.')); + $this->assertFalse($browser->shouldUseProxy('http://127.0.0.1/'), t('Proxy is not used for IP address in exception list.')); + $this->assertTrue($browser->shouldUseProxy('http://127.0.0.2/'), t('Proxy is used for IP address in exception list.')); + + variable_set('proxy_exceptions', '10.0.0.1'); + $this->assertTrue($browser->shouldUseProxy('http://localhost/'), t('Proxy is used for host that does not resolve to an IP address in exception list.')); + } } /** * Test browser backend wrappers. */ class BrowserBackendTestCase extends DrupalWebTestCase { public static function getInfo() { return array( Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.199 diff -u -9 -p -r1.199 system.admin.inc --- modules/system/system.admin.inc 10 Sep 2009 06:38:20 -0000 1.199 +++ modules/system/system.admin.inc 10 Sep 2009 19:23:28 -0000 @@ -1461,18 +1461,93 @@ function system_performance_settings() { * * @ingroup forms */ function system_clear_cache_submit($form, &$form_state) { drupal_flush_all_caches(); drupal_set_message(t('Caches cleared.')); } /** + * Form builder; Configure the site proxy settings. + * + * @ingroup forms + * @see system_settings_form() + */ +function system_proxy_settings() { + + $form['forward_proxy'] = array( + '#type' => 'fieldset', + '#title' => t('Outgoing proxy server'), + '#description' => t('Specify a proxy server to be used when Drupal connects to other websites on the internet.'), + ); + $form['forward_proxy']['proxy_hostname'] = array( + '#type' => 'textfield', + '#title' => t('Hostname'), + '#default_value' => '', + '#description' => t('The hostname of the proxy server, e.g. localhost. Leave this field empty, if Drupal should connect directly to the internet.'), + '#element_validate' => array('system_proxy_hostname_validate'), + ); + $form['forward_proxy']['proxy_port'] = array( + '#type' => 'textfield', + '#title' => t('Port number'), + '#size' => 5, + '#maxlength' => 5, + '#default_value' => 8080, + '#description' => t('The port number of the proxy server, e.g. 3128 or 8080'), + '#element_validate' => array('system_proxy_port_validate'), + ); + $form['forward_proxy']['proxy_username'] = array( + '#type' => 'textfield', + '#title' => t('Username'), + '#default_value' => '', + '#description' => t('The username used to authenticate with the proxy server. Leave this field empty, if the proxy server does not require authentication.'), + ); + $form['forward_proxy']['proxy_password'] = array( + '#type' => 'textfield', + '#title' => t('Password'), + '#default_value' => '', + '#description' => t('The password used to connect to the proxy server. This is stored as plain text.'), + ); + $form['forward_proxy']['proxy_exceptions'] = array( + '#type' => 'textfield', + '#title' => t('No proxy for'), + '#default_value' => 'localhost', + '#description' => t('Example: .example.com,localhost,192.168.1.2'), + ); + + return system_settings_form($form); +} + +/** + * Element validate function for proxy hostname. + */ +function system_proxy_hostname_validate($element, &$form_state) { + // Check that an IP address or a valid hostname is specified. + if (strlen($element['#value']) > 0 + && !filter_var($element['#value'], FILTER_VALIDATE_IP) + && gethostbyname($element['#value']) == $element['#value']) { + form_error($element, t('The proxy host name cannot be found.')); + } +} + +/** + * Element validate function for proxy port number. + */ +function system_proxy_port_validate($element, &$form_state) { + if (strlen($element['#value']) > 0) { + // TCP allows the port to be between 0 and 65535 inclusive + if (!is_numeric($element['#value']) || ($element['#value'] < 0) || ($element['#value'] > 65535)) { + form_set_error('proxy_port', t('The proxy port is invalid. It must be a number between 0 and 65535.')); + } + } +} + +/** * Form builder; Configure the site file handling. * * @ingroup forms * @see system_settings_form() */ function system_file_system_settings() { $form['file_public_path'] = array( '#type' => 'textfield', Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.788 diff -u -9 -p -r1.788 system.module --- modules/system/system.module 10 Sep 2009 06:32:54 -0000 1.788 +++ modules/system/system.module 10 Sep 2009 19:23:28 -0000 @@ -842,18 +842,26 @@ function system_menu() { $items['admin/config/system/site-information'] = array( 'title' => 'Site information', 'description' => 'Change basic site name, e-mail address, slogan, default front page, number of posts per page, error pages and cron.', 'page callback' => 'drupal_get_form', 'page arguments' => array('system_site_information_settings'), 'access arguments' => array('administer site configuration'), 'file' => 'system.admin.inc', 'weight' => -10, ); + $items['admin/config/system/proxy'] = array( + 'title' => 'Proxy server', + 'description' => 'Configure how Drupal connects to other websites.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('system_proxy_settings'), + 'access arguments' => array('administer site configuration'), + 'file' => 'system.admin.inc', + ); // Reports. $items['admin/reports'] = array( 'title' => 'Reports', 'description' => 'View reports from system logs and other status information.', 'page callback' => 'system_admin_menu_block_page', 'access arguments' => array('access site reports'), 'weight' => 5, 'position' => 'left',