Index: modules/simpletest/tests/session.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/session.test,v retrieving revision 1.16 diff -u -r1.16 session.test --- modules/simpletest/tests/session.test 13 Jul 2009 21:51:41 -0000 1.16 +++ modules/simpletest/tests/session.test 16 Aug 2009 03:57:05 -0000 @@ -250,3 +250,74 @@ } } } + +class SessionHttpsTestCase extends DrupalWebTestCase { + + public static function getInfo() { + return array( + 'name' => 'Session https handling', + 'description' => 'Ensure that when running under https two session cookies are generated.', + 'group' => 'Session' + ); + } + + public function setUp() { + parent::setUp(); + } + + protected function testHttpsSession() { + global $is_https; + + // Enable secure pages. + variable_set('https', TRUE); + + $user = $this->drupalCreateUser(array('access administration pages')); + + $this->drupalGet('user'); + $form = &$this->xpath('//form[@id="user-login"]'); + $this->assertEqual(substr($form[0]['action'], 0, 6), 'https:', 'Form action is secure'); + $form[0]['action'] = $this->httpsUrl('user'); + + $edit = array( + 'name' => $user->name, + 'pass' => $user->pass_raw, + ); + $this->drupalPost(NULL, $edit, t('Log in')); + if ($is_https) { + $insecure_session_name = substr(session_name(), 1); + $secure_session_name = session_name(); + } + else { + $insecure_session_name = session_name(); + $secure_session_name = "S$insecure_session_name"; + } + $args = array( + 'sid' => $this->cookies[$insecure_session_name], + 'ssid' => $this->cookies[$secure_session_name], + ); + $this->assertTrue(db_query('SELECT sid FROM {sessions} WHERE sid = :sid AND ssid = :ssid', $args)->fetchField(), 'Session has both SIDs'); + $cookies = array( + $insecure_session_name . '=' . $args['sid'], + $secure_session_name . '=' . $args['ssid'], + ); + + foreach ($cookies as $cookie_key => $cookie) { + foreach (array('admin', $this->httpsUrl('admin')) as $url_key => $url) { + $this->curlClose(); + + $this->drupalGet($url, array(), array('Cookie: ' . $cookie)); + if ($cookie_key == $url_key) { + $this->assertText(t('Administer')); + } + else { + $this->assertNoText(t('Administer')); + } + } + } + } + + protected function httpsUrl($url) { + global $base_url; + return $base_url . '/modules/simpletest/tests/https.php?q=' . $url; + } +} Index: modules/simpletest/tests/common.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/common.test,v retrieving revision 1.61 diff -u -r1.61 common.test --- modules/simpletest/tests/common.test 15 Aug 2009 06:30:38 -0000 1.61 +++ modules/simpletest/tests/common.test 16 Aug 2009 03:57:05 -0000 @@ -1260,3 +1260,21 @@ $this->assertIdentical(drupal_attributes(array()), '', t('Empty attributes array.')); } } + +/** + * Tests for the l() function. + */ +class SecurePagesUnitTest extends DrupalUnitTestCase { + + public static function getInfo() { + return array( + 'name' => 'Secure pages', + 'description' => '...', + 'group' => 'System', + ); + } + + protected function testSecurePages() { + + } +} Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.1021 diff -u -r1.1021 user.module --- modules/user/user.module 12 Aug 2009 12:36:05 -0000 1.1021 +++ modules/user/user.module 16 Aug 2009 03:57:07 -0000 @@ -985,7 +985,7 @@ function user_login_block() { $form = array( - '#action' => url($_GET['q'], array('query' => drupal_get_destination())), + '#action' => url($_GET['q'], array('secure' => TRUE, 'query' => drupal_get_destination())), '#id' => 'user-login-form', '#validate' => user_login_default_validators(), '#submit' => array('user_login_submit'), @@ -1576,6 +1576,7 @@ ); $form['#validate'] = user_login_default_validators(); $form['submit'] = array('#type' => 'submit', '#value' => t('Log in'), '#weight' => 2); + $form['#action'] = url($_GET['q'], array('secure' => TRUE)); return $form; } @@ -1659,7 +1660,7 @@ /** * The final validation handler on the login form. - * + * * Sets a form error if user has not been authenticated, or if too many * logins have been attempted. This validation function should always * be the last one. Index: includes/session.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/session.inc,v retrieving revision 1.70 diff -u -r1.70 session.inc --- includes/session.inc 1 Jul 2009 12:47:30 -0000 1.70 +++ includes/session.inc 16 Aug 2009 03:57:04 -0000 @@ -66,7 +66,7 @@ * was found or the user is anonymous. */ function _drupal_session_read($sid) { - global $user; + global $user, $is_https; // Write and Close handlers are called after destructing objects // since PHP 5.0.5. @@ -76,14 +76,29 @@ // Handle the case of first time visitors and clients that don't store // cookies (eg. web crawlers). - if (!isset($_COOKIE[session_name()])) { + $insecure_session_name = substr(session_name(), 1); + if (!isset($_COOKIE[session_name()]) && !isset($_COOKIE[$insecure_session_name])) { $user = drupal_anonymous_user(); return ''; } // Otherwise, if the session is still active, we have a record of the - // client's session in the database. - $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject(); + // client's session in the database. If it's HTTPS then we are either have + // a HTTPS session or we are about to log in so we check the sessions table + // for an anonymous session wth the non-HTTPS-only cookie. + if ($is_https) { + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => $sid))->fetchObject(); + if (!$user) { + if (isset($_COOKIE[$insecure_session_name])) { + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array( + ':sid' => $_COOKIE[$insecure_session_name])) + ->fetchObject(); + } + } + } + else { + $user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject(); + } // We found the client's session record and they are an authenticated user. if ($user && $user->uid > 0) { @@ -122,22 +137,27 @@ * This function will always return TRUE. */ function _drupal_session_write($sid, $value) { - global $user; + global $user, $is_https; if (!drupal_save_session()) { // We don't have anything to do if we are not allowed to save the session. return; } + $fields = array( + 'uid' => $user->uid, + 'cache' => isset($user->cache) ? $user->cache : 0, + 'hostname' => ip_address(), + 'session' => $value, + 'timestamp' => REQUEST_TIME, + ); + $insecure_session_name = substr(session_name(), 1); + if ($is_https && isset($_COOKIE[$insecure_session_name])) { + $fields['sid'] = $_COOKIE[$insecure_session_name]; + } db_merge('sessions') - ->key(array('sid' => $sid)) - ->fields(array( - 'uid' => $user->uid, - 'cache' => isset($user->cache) ? $user->cache : 0, - 'hostname' => ip_address(), - 'session' => $value, - 'timestamp' => REQUEST_TIME, - )) + ->key(array($is_https ? 'ssid' : 'sid' => $sid)) + ->fields($fields) ->execute(); // Last access time is updated no more frequently than once every 180 seconds. @@ -246,7 +266,14 @@ * Called when an anonymous user becomes authenticated or vice-versa. */ function drupal_session_regenerate() { - global $user; + global $user, $is_https; + if ($is_https && variable_get('https', FALSE)) { + $insecure_session_name = substr(session_name(), 1); + $params = session_get_cookie_params(); + $session_id = md5(uniqid(mt_rand(), TRUE)); + setcookie($insecure_session_name, $session_id, REQUEST_TIME + $params['lifetime'], $params['path'], $params['domain'], FALSE, $params['httponly']); + $_COOKIE[$insecure_session_name] = $session_id; + } if (drupal_session_started()) { $old_session_id = session_id(); @@ -264,7 +291,7 @@ if (isset($old_session_id)) { db_update('sessions') ->fields(array( - 'sid' => session_id() + $is_https ? 'ssid' : 'sid' => session_id() )) ->condition('sid', $old_session_id) ->execute(); @@ -304,11 +331,11 @@ * Session ID. */ function _drupal_session_destroy($sid) { - global $user; + global $user, $is_https; // Delete session data. db_delete('sessions') - ->condition('sid', $sid) + ->condition($is_https ? 'ssid' : 'sid', $sid) ->execute(); // Reset $_SESSION and $user to prevent a new session from being started @@ -316,11 +343,18 @@ $_SESSION = array(); $user = drupal_anonymous_user(); - // Unset the session cookie. - if (isset($_COOKIE[session_name()])) { + // Unset the session cookies. + _drupal_session_delete_cookie(session_name()); + if ($is_https) { + _drupal_session_delete_cookie(substr(session_name(), 1), TRUE); + } +} + +function _drupal_session_delete_cookie($name, $force_insecure = FALSE) { + if (isset($_COOKIE[$name])) { $params = session_get_cookie_params(); - setcookie(session_name(), '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); - unset($_COOKIE[session_name()]); + setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], !$force_insecure && $params['secure'], $params['httponly']); + unset($_COOKIE[$name]); } } Index: includes/bootstrap.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v retrieving revision 1.293 diff -u -r1.293 bootstrap.inc --- includes/bootstrap.inc 4 Aug 2009 04:02:25 -0000 1.293 +++ includes/bootstrap.inc 16 Aug 2009 03:57:03 -0000 @@ -518,7 +518,7 @@ global $base_url, $base_path, $base_root; // Export the following settings.php variables to the global namespace - global $databases, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url; + global $databases, $db_prefix, $cookie_domain, $conf, $installed_profile, $update_free_access, $db_url, $is_https, $base_secure_url, $base_insecure_url; $conf = array(); if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) { @@ -528,6 +528,7 @@ if (isset($base_url)) { // Parse fixed base URL from settings.php. $parts = parse_url($base_url); + $http_protocol = $parts['scheme']; if (!isset($parts['path'])) { $parts['path'] = ''; } @@ -537,9 +538,10 @@ } else { // Create base URL - $base_root = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http'; + $http_protocol = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 'https' : 'http'; + $base_root = $http_protocol . '://' . $_SERVER['HTTP_HOST']; - $base_url = $base_root .= '://' . $_SERVER['HTTP_HOST']; + $base_url = $base_root; // $_SERVER['SCRIPT_NAME'] can, in contrast to $_SERVER['PHP_SELF'], not // be modified by a visitor. @@ -552,6 +554,9 @@ $base_path = '/'; } } + $is_https = $http_protocol == 'https'; + $base_secure_url = str_replace('http://', 'https://', $base_url); + $base_insecure_url = str_replace('https://', 'http://', $base_url); if ($cookie_domain) { // If the user specifies the cookie domain, also use it for session name. @@ -566,15 +571,6 @@ $cookie_domain = check_plain($_SERVER['HTTP_HOST']); } } - // To prevent session cookies from being hijacked, a user can configure the - // SSL version of their website to only transfer session cookies via SSL by - // using PHP's session.cookie_secure setting. The browser will then use two - // separate session cookies for the HTTPS and HTTP versions of the site. So we - // must use different session identifiers for HTTPS and HTTP to prevent a - // cookie collision. - if (ini_get('session.cookie_secure')) { - $session_name .= 'SSL'; - } // Strip leading periods, www., and port numbers from cookie domain. $cookie_domain = ltrim($cookie_domain, '.'); if (strpos($cookie_domain, 'www.') === 0) { @@ -587,7 +583,17 @@ if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) { ini_set('session.cookie_domain', $cookie_domain); } - session_name('SESS' . md5($session_name)); + // To prevent session cookies from being hijacked, a user can configure the + // SSL version of their website to only transfer session cookies via SSL by + // using PHP's session.cookie_secure setting. The browser will then use two + // separate session cookies for the HTTPS and HTTP versions of the site. So we + // must use different session identifiers for HTTPS and HTTP to prevent a + // cookie collision. + if ($is_https) { + ini_set('session.cookie_secure', TRUE); + } + $prefix = ini_get('session.cookie_secure') ? 'SSESS' : 'SESS'; + session_name($prefix . md5($session_name)); } /** Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.961 diff -u -r1.961 common.inc --- includes/common.inc 15 Aug 2009 06:20:20 -0000 1.961 +++ includes/common.inc 16 Aug 2009 03:57:04 -0000 @@ -338,7 +338,7 @@ extract(parse_url(urldecode($_REQUEST['destination']))); } - $url = url($path, array('query' => $query, 'fragment' => $fragment, 'absolute' => TRUE)); + $url = url($path, array('query' => $query, 'fragment' => $fragment, 'absolute' => TRUE, 'secure' => FALSE)); // Allow modules to react to the end of the page request before redirecting. // We do not want this while running update.php. @@ -2065,6 +2065,10 @@ * - 'language' * An optional language object. Used to build the URL to link to and * look up the proper alias for the link. + * - 'secure' + * Whether this URL should point to a secure location. If not specified, + * the current scheme is used, so the user stays on http or https + * respectively. TRUE enforces HTTPS and FALSE enforces HTTP. * - 'base_url' * Only used internally, to modify the base URL when a language dependent * URL requires so. @@ -2078,12 +2082,14 @@ * alternative than url(). */ function url($path = NULL, array $options = array()) { + global $is_https; // Merge in defaults. $options += array( 'fragment' => '', 'query' => '', 'absolute' => FALSE, 'alias' => FALSE, + 'secure' => NULL, 'prefix' => '' ); if (!isset($options['external'])) { @@ -2121,7 +2127,7 @@ return $path . $options['fragment']; } - global $base_url; + global $base_url, $base_secure_url, $base_insecure_url; $script = &drupal_static(__FUNCTION__); if (!isset($script)) { @@ -2131,9 +2137,21 @@ $script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === FALSE) ? 'index.php' : ''; } + // The base_url might be rewritten from the language rewrite in domain mode. if (!isset($options['base_url'])) { - // The base_url might be rewritten from the language rewrite in domain mode. - $options['base_url'] = $base_url; + if (isset($options['secure']) && variable_get('https', FALSE)) { + if ($options['secure'] === TRUE) { + $options['base_url'] = $base_secure_url; + $options['absolute'] = TRUE; + } + elseif ($options['secure'] === FALSE) { + $options['base_url'] = $base_insecure_url; + $options['absolute'] = TRUE; + } + } + else { + $options['base_url'] = $base_url; + } } // Preserve the original path before aliasing. Index: modules/system/system.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.admin.inc,v retrieving revision 1.176 diff -u -r1.176 system.admin.inc --- modules/system/system.admin.inc 15 Aug 2009 06:27:49 -0000 1.176 +++ modules/system/system.admin.inc 16 Aug 2009 03:57:05 -0000 @@ -1296,6 +1296,18 @@ '#description' => t('Render all blocks on the default 404 (not found) page. Disabling blocks can help with performance but might leave users with a less functional site.'), '#default_value' => variable_get('site_404_blocks', 0) ); + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['advanced']['https'] = array( + '#type' => 'checkbox', + '#title' => t('Secure pages'), + '#description' => t('Use SSL for pages that are defined as requiring security. Anytime a secure page is accessed it will be done so over an SSL connection. Two user cookies are created and managed, one for https and the other for http.'), + '#default_value' => variable_get('https', FALSE), + ); $form['#validate'][] = 'system_site_information_settings_validate'; return system_settings_form($form); Index: modules/system/system.install =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.install,v retrieving revision 1.367 diff -u -r1.367 system.install --- modules/system/system.install 12 Aug 2009 23:51:19 -0000 1.367 +++ modules/system/system.install 16 Aug 2009 03:57:06 -0000 @@ -1272,6 +1272,13 @@ 'not null' => TRUE, 'default' => '', ), + 'ssid' => array( + 'description' => "Unique key: Secure session ID. The value is generated by PHP's Session API.", + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ), 'hostname' => array( 'description' => 'The IP address that last used this session ID (sid).', 'type' => 'varchar', @@ -1303,6 +1310,9 @@ 'timestamp' => array('timestamp'), 'uid' => array('uid'), ), + 'unique keys' => array( + 'ssid' => array('ssid'), + ), 'foreign keys' => array( 'uid' => array('users' => 'uid'), ), Index: modules/simpletest/drupal_web_test_case.php =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/drupal_web_test_case.php,v retrieving revision 1.136 diff -u -r1.136 drupal_web_test_case.php --- modules/simpletest/drupal_web_test_case.php 15 Aug 2009 17:52:53 -0000 1.136 +++ modules/simpletest/drupal_web_test_case.php 16 Aug 2009 03:57:04 -0000 @@ -1309,13 +1309,16 @@ call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1]))); } - // Save the session cookie, if set. - if (preg_match('/^Set-Cookie: ' . preg_quote($this->session_name) . '=([a-z90-9]+)/', $header, $matches)) { - if ($matches[1] != 'deleted') { - $this->session_id = $matches[1]; - } - else { - $this->session_id = NULL; + // Save cookies. + if (preg_match('/^Set-Cookie: ([^=]+)=([a-z90-9]+)/', $header, $matches)) { + $this->cookies[$matches[1]] = $matches[2]; + if ($matches[1] == $this->session_name) { + if ($matches[2] != 'deleted') { + $this->session_id = $matches[2]; + } + else { + $this->session_id = NULL; + } } } Index: modules/simpletest/tests/https.php =================================================================== RCS file: modules/simpletest/tests/https.php diff -N modules/simpletest/tests/https.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/simpletest/tests/https.php 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,24 @@ + $value) { + $_SERVER[$key] = str_replace('modules/simpletest/tests/https.php', 'index.php', $value); +} + +require_once 'index.php';