Commit 6586b764 authored by Dries's avatar Dries

- Patch by #1577 by chx, boombatower, Bèr Kessels, kkaefer: made SSL support a...

- Patch by #1577 by chx, boombatower, Bèr Kessels, kkaefer: made SSL support a bit easier by providing two cookies and ... hook_goto_alter.
parent 2f957104
...@@ -509,7 +509,7 @@ function drupal_settings_initialize() { ...@@ -509,7 +509,7 @@ function drupal_settings_initialize() {
global $base_url, $base_path, $base_root; global $base_url, $base_path, $base_root;
// Export the following settings.php variables to the global namespace // 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(); $conf = array();
if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) { if (file_exists(DRUPAL_ROOT . '/' . conf_path() . '/settings.php')) {
...@@ -519,6 +519,7 @@ function drupal_settings_initialize() { ...@@ -519,6 +519,7 @@ function drupal_settings_initialize() {
if (isset($base_url)) { if (isset($base_url)) {
// Parse fixed base URL from settings.php. // Parse fixed base URL from settings.php.
$parts = parse_url($base_url); $parts = parse_url($base_url);
$http_protocol = $parts['scheme'];
if (!isset($parts['path'])) { if (!isset($parts['path'])) {
$parts['path'] = ''; $parts['path'] = '';
} }
...@@ -528,9 +529,10 @@ function drupal_settings_initialize() { ...@@ -528,9 +529,10 @@ function drupal_settings_initialize() {
} }
else { else {
// Create base URL // 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 // $_SERVER['SCRIPT_NAME'] can, in contrast to $_SERVER['PHP_SELF'], not
// be modified by a visitor. // be modified by a visitor.
...@@ -543,6 +545,9 @@ function drupal_settings_initialize() { ...@@ -543,6 +545,9 @@ function drupal_settings_initialize() {
$base_path = '/'; $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 ($cookie_domain) {
// If the user specifies the cookie domain, also use it for session name. // If the user specifies the cookie domain, also use it for session name.
...@@ -557,15 +562,6 @@ function drupal_settings_initialize() { ...@@ -557,15 +562,6 @@ function drupal_settings_initialize() {
$cookie_domain = check_plain($_SERVER['HTTP_HOST']); $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. // Strip leading periods, www., and port numbers from cookie domain.
$cookie_domain = ltrim($cookie_domain, '.'); $cookie_domain = ltrim($cookie_domain, '.');
if (strpos($cookie_domain, 'www.') === 0) { if (strpos($cookie_domain, 'www.') === 0) {
...@@ -578,7 +574,17 @@ function drupal_settings_initialize() { ...@@ -578,7 +574,17 @@ function drupal_settings_initialize() {
if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) { if (count(explode('.', $cookie_domain)) > 2 && !is_numeric(str_replace('.', '', $cookie_domain))) {
ini_set('session.cookie_domain', $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));
} }
/** /**
......
...@@ -423,6 +423,14 @@ function drupal_goto($path = '', $query = NULL, $fragment = NULL, $http_response ...@@ -423,6 +423,14 @@ function drupal_goto($path = '', $query = NULL, $fragment = NULL, $http_response
extract(parse_url(urldecode($_REQUEST['destination']))); extract(parse_url(urldecode($_REQUEST['destination'])));
} }
$args = array(
'path' => &$path,
'query' => &$query,
'fragment' => &$fragment,
'http_response_code' => &$http_response_code,
);
drupal_alter('drupal_goto', $args);
$url = url($path, array('query' => $query, 'fragment' => $fragment, 'absolute' => TRUE)); $url = url($path, array('query' => $query, 'fragment' => $fragment, 'absolute' => TRUE));
// Allow modules to react to the end of the page request before redirecting. // Allow modules to react to the end of the page request before redirecting.
...@@ -2147,6 +2155,11 @@ function _format_date_callback(array $matches = NULL, $new_langcode = NULL) { ...@@ -2147,6 +2155,11 @@ function _format_date_callback(array $matches = NULL, $new_langcode = NULL) {
* - 'language' * - 'language'
* An optional language object. Used to build the URL to link to and * An optional language object. Used to build the URL to link to and
* look up the proper alias for the link. * look up the proper alias for the link.
* - 'https'
* 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, but HTTPS
* can only be enforced when the variable 'https' is set to TRUE.
* - 'base_url' * - 'base_url'
* Only used internally, to modify the base URL when a language dependent * Only used internally, to modify the base URL when a language dependent
* URL requires so. * URL requires so.
...@@ -2166,6 +2179,7 @@ function url($path = NULL, array $options = array()) { ...@@ -2166,6 +2179,7 @@ function url($path = NULL, array $options = array()) {
'query' => '', 'query' => '',
'absolute' => FALSE, 'absolute' => FALSE,
'alias' => FALSE, 'alias' => FALSE,
'https' => FALSE,
'prefix' => '' 'prefix' => ''
); );
if (!isset($options['external'])) { if (!isset($options['external'])) {
...@@ -2203,7 +2217,7 @@ function url($path = NULL, array $options = array()) { ...@@ -2203,7 +2217,7 @@ function url($path = NULL, array $options = array()) {
return $path . $options['fragment']; return $path . $options['fragment'];
} }
global $base_url; global $base_url, $base_secure_url, $base_insecure_url;
$script = &drupal_static(__FUNCTION__); $script = &drupal_static(__FUNCTION__);
if (!isset($script)) { if (!isset($script)) {
...@@ -2213,10 +2227,22 @@ function url($path = NULL, array $options = array()) { ...@@ -2213,10 +2227,22 @@ function url($path = NULL, array $options = array()) {
$script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === FALSE) ? 'index.php' : ''; $script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === FALSE) ? 'index.php' : '';
} }
if (!isset($options['base_url'])) {
// The base_url might be rewritten from the language rewrite in domain mode. // The base_url might be rewritten from the language rewrite in domain mode.
if (!isset($options['base_url'])) {
if (isset($options['https']) && variable_get('https', FALSE)) {
if ($options['https'] === TRUE) {
$options['base_url'] = $base_secure_url;
$options['absolute'] = TRUE;
}
elseif ($options['https'] === FALSE) {
$options['base_url'] = $base_insecure_url;
$options['absolute'] = TRUE;
}
}
else {
$options['base_url'] = $base_url; $options['base_url'] = $base_url;
} }
}
// Preserve the original path before aliasing. // Preserve the original path before aliasing.
$original_path = $path; $original_path = $path;
......
...@@ -979,6 +979,14 @@ function form_builder($form_id, $element, &$form_state) { ...@@ -979,6 +979,14 @@ function form_builder($form_id, $element, &$form_state) {
// Special handling if we're on the top level form element. // Special handling if we're on the top level form element.
if (isset($element['#type']) && $element['#type'] == 'form') { if (isset($element['#type']) && $element['#type'] == 'form') {
if (!empty($element['#https']) && variable_get('https', FALSE) &&
!menu_path_is_external($element['#action'])) {
global $base_root;
// Not an external URL so ensure that it is secure.
$element['#action'] = str_replace('http://', 'https://', $base_root) . $element['#action'];
}
// Store a complete copy of the form in form_state prior to building the form. // Store a complete copy of the form in form_state prior to building the form.
$form_state['complete form'] = $element; $form_state['complete form'] = $element;
// Set a flag if we have a correct form submission. This is always TRUE for // Set a flag if we have a correct form submission. This is always TRUE for
......
...@@ -66,7 +66,7 @@ function _drupal_session_close() { ...@@ -66,7 +66,7 @@ function _drupal_session_close() {
* was found or the user is anonymous. * was found or the user is anonymous.
*/ */
function _drupal_session_read($sid) { function _drupal_session_read($sid) {
global $user; global $user, $is_https;
// Write and Close handlers are called after destructing objects // Write and Close handlers are called after destructing objects
// since PHP 5.0.5. // since PHP 5.0.5.
...@@ -76,14 +76,29 @@ function _drupal_session_read($sid) { ...@@ -76,14 +76,29 @@ function _drupal_session_read($sid) {
// Handle the case of first time visitors and clients that don't store // Handle the case of first time visitors and clients that don't store
// cookies (eg. web crawlers). // 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(); $user = drupal_anonymous_user();
return ''; return '';
} }
// Otherwise, if the session is still active, we have a record of the // Otherwise, if the session is still active, we have a record of the
// client's session in the database. // 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(); $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. // We found the client's session record and they are an authenticated user.
if ($user && $user->uid > 0) { if ($user && $user->uid > 0) {
...@@ -122,22 +137,27 @@ function _drupal_session_read($sid) { ...@@ -122,22 +137,27 @@ function _drupal_session_read($sid) {
* This function will always return TRUE. * This function will always return TRUE.
*/ */
function _drupal_session_write($sid, $value) { function _drupal_session_write($sid, $value) {
global $user; global $user, $is_https;
if (!drupal_save_session()) { if (!drupal_save_session()) {
// We don't have anything to do if we are not allowed to save the session. // We don't have anything to do if we are not allowed to save the session.
return; return;
} }
db_merge('sessions') $fields = array(
->key(array('sid' => $sid))
->fields(array(
'uid' => $user->uid, 'uid' => $user->uid,
'cache' => isset($user->cache) ? $user->cache : 0, 'cache' => isset($user->cache) ? $user->cache : 0,
'hostname' => ip_address(), 'hostname' => ip_address(),
'session' => $value, 'session' => $value,
'timestamp' => REQUEST_TIME, '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($is_https ? 'ssid' : 'sid' => $sid))
->fields($fields)
->execute(); ->execute();
// Last access time is updated no more frequently than once every 180 seconds. // Last access time is updated no more frequently than once every 180 seconds.
...@@ -246,7 +266,14 @@ function drupal_session_started($set = NULL) { ...@@ -246,7 +266,14 @@ function drupal_session_started($set = NULL) {
* Called when an anonymous user becomes authenticated or vice-versa. * Called when an anonymous user becomes authenticated or vice-versa.
*/ */
function drupal_session_regenerate() { 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()) { if (drupal_session_started()) {
$old_session_id = session_id(); $old_session_id = session_id();
...@@ -264,7 +291,7 @@ function drupal_session_regenerate() { ...@@ -264,7 +291,7 @@ function drupal_session_regenerate() {
if (isset($old_session_id)) { if (isset($old_session_id)) {
db_update('sessions') db_update('sessions')
->fields(array( ->fields(array(
'sid' => session_id() $is_https ? 'ssid' : 'sid' => session_id()
)) ))
->condition('sid', $old_session_id) ->condition('sid', $old_session_id)
->execute(); ->execute();
...@@ -304,11 +331,11 @@ function drupal_session_count($timestamp = 0, $anonymous = TRUE) { ...@@ -304,11 +331,11 @@ function drupal_session_count($timestamp = 0, $anonymous = TRUE) {
* Session ID. * Session ID.
*/ */
function _drupal_session_destroy($sid) { function _drupal_session_destroy($sid) {
global $user; global $user, $is_https;
// Delete session data. // Delete session data.
db_delete('sessions') db_delete('sessions')
->condition('sid', $sid) ->condition($is_https ? 'ssid' : 'sid', $sid)
->execute(); ->execute();
// Reset $_SESSION and $user to prevent a new session from being started // Reset $_SESSION and $user to prevent a new session from being started
...@@ -316,11 +343,26 @@ function _drupal_session_destroy($sid) { ...@@ -316,11 +343,26 @@ function _drupal_session_destroy($sid) {
$_SESSION = array(); $_SESSION = array();
$user = drupal_anonymous_user(); $user = drupal_anonymous_user();
// Unset the session cookie. // Unset the session cookies.
if (isset($_COOKIE[session_name()])) { _drupal_session_delete_cookie(session_name());
if ($is_https) {
_drupal_session_delete_cookie(substr(session_name(), 1), TRUE);
}
}
/**
* Deletes the session cookie.
*
* @param $name
* Name of session cookie to delete.
* @param $force_insecure
* Fornce cookie to be insecure.
*/
function _drupal_session_delete_cookie($name, $force_insecure = FALSE) {
if (isset($_COOKIE[$name])) {
$params = session_get_cookie_params(); $params = session_get_cookie_params();
setcookie(session_name(), '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], !$force_insecure && $params['secure'], $params['httponly']);
unset($_COOKIE[session_name()]); unset($_COOKIE[$name]);
} }
} }
......
...@@ -1310,15 +1310,21 @@ protected function curlHeaderCallback($curlHandler, $header) { ...@@ -1310,15 +1310,21 @@ protected function curlHeaderCallback($curlHandler, $header) {
call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1]))); call_user_func_array(array(&$this, 'error'), unserialize(urldecode($matches[1])));
} }
// Save the session cookie, if set. // Save cookies.
if (preg_match('/^Set-Cookie: ' . preg_quote($this->session_name) . '=([a-z90-9]+)/', $header, $matches)) { if (preg_match('/^Set-Cookie: ([^=]+)=(.+)/', $header, $matches)) {
if ($matches[1] != 'deleted') { $name = $matches[1];
$this->session_id = $matches[1]; $parts = array_map('trim', explode(';', $matches[2]));
$value = array_shift($parts);
$this->cookies[$name] = array('value' => $value, 'secure' => in_array('secure', $parts));
if ($name == $this->session_name) {
if ($value != 'deleted') {
$this->session_id = $value;
} }
else { else {
$this->session_id = NULL; $this->session_id = NULL;
} }
} }
}
// This is required by cURL. // This is required by cURL.
return strlen($header); return strlen($header);
......
...@@ -600,6 +600,43 @@ class DrupalSetContentTestCase extends DrupalWebTestCase { ...@@ -600,6 +600,43 @@ class DrupalSetContentTestCase extends DrupalWebTestCase {
} }
} }
/**
* Testing drupal_goto and hook_drupal_goto_alter().
*/
class DrupalGotoTest extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'Drupal goto',
'description' => 'Performs tests on the drupal_goto function and hook_drupal_goto_alter',
'group' => 'System'
);
}
function setUp() {
parent::setUp('common_test');
}
/**
* Test setting and retrieving content for theme regions.
*/
function testDrupalGoto() {
$this->drupalGet('common-test/drupal_goto/redirect');
$this->assertNoText(t("Drupal goto failed to stop program"), t("Drupal goto stopped program."));
$this->assertText('drupal_goto', t("Drupal goto redirect failed."));
}
/**
* Test setting and retrieving content for theme regions.
*/
function testDrupalGotoAlter() {
$this->drupalGet('common-test/drupal_goto/redirect_fail');
$this->assertNoText(t("Drupal goto failed to stop program"), t("Drupal goto stopped program."));
$this->assertNoText('drupal_goto_fail', t("Drupal goto redirect failed."));
}
}
/** /**
* Tests for the JavaScript system. * Tests for the JavaScript system.
*/ */
......
...@@ -6,6 +6,70 @@ ...@@ -6,6 +6,70 @@
* Helper module for the Common tests. * Helper module for the Common tests.
*/ */
/**
* Implement hook_menu().
*/
function common_test_menu() {
$items = array();
$items['common-test/drupal_goto'] = array(
'title' => 'Drupal Goto',
'page callback' => 'common_test_drupal_goto_land',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
$items['common-test/drupal_goto/fail'] = array(
'title' => 'Drupal Goto',
'page callback' => 'common_test_drupal_goto_land_fail',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
$items['common-test/drupal_goto/redirect'] = array(
'title' => 'Drupal Goto',
'page callback' => 'common_test_drupal_goto_redirect',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
$items['common-test/drupal_goto/redirect_fail'] = array(
'title' => 'Drupal Goto Failure',
'page callback' => 'drupal_goto',
'page arguments' => array('common-test/drupal_goto/fail'),
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Check that drupal_goto() exits once called.
*/
function common_test_drupal_goto_redirect() {
drupal_goto('common-test/drupal_goto');
print t("Drupal goto failed to stop program");
}
/**
* Landing page for drupal_goto().
*/
function common_test_drupal_goto_land() {
print "drupal_goto";
}
/**
* Fail landing page for drupal_goto().
*/
function common_test_drupal_goto_land_fail() {
print "drupal_goto_fail";
}
/**
* Implement hook_drupal_goto_alter().
*/
function common_test_drupal_goto_alter(&$args) {
if ($args['path'] == 'common-test/drupal_goto/fail') {
$args['path'] = 'common-test/drupal_goto/redirect';
}
}
/** /**
* Implement hook_theme(). * Implement hook_theme().
*/ */
......
<?php
// $Id$
/**
* @file
* Fake an https request, for use during testing.
*/
// Negated copy of the condition in _drupal_bootstrap(). If the user agent is
// not from simpletest then disallow access.
if (!(isset($_SERVER['HTTP_USER_AGENT']) && (strpos($_SERVER['HTTP_USER_AGENT'], "simpletest") !== FALSE))) {
exit;
}
// Change to https.
$_SERVER['HTTPS'] = 'on';
// Change to index.php.
chdir('../../..');
foreach ($_SERVER as $key => $value) {
$_SERVER[$key] = str_replace('modules/simpletest/tests/https.php', 'index.php', $value);
$_SERVER[$key] = str_replace('http://', 'https://', $_SERVER[$key]);
}
require_once 'index.php';
...@@ -250,3 +250,95 @@ class SessionTestCase extends DrupalWebTestCase { ...@@ -250,3 +250,95 @@ class SessionTestCase extends DrupalWebTestCase {
} }
} }
} }
/**
* Ensure that when running under https two session cookies are generated.
*/
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('session_test');
}
protected function testHttpsSession() {
global $is_https;
if ($is_https) {
// The functionality does not make sense when running on https.
return;
}
$insecure_session_name = session_name();
$secure_session_name = "S$insecure_session_name";
// Enable secure pages.
variable_set('https', TRUE);
$user = $this->drupalCreateUser(array('access administration pages'));
$this->curlClose();
$this->drupalGet('session-test/set/1');
// Check secure cookie on insecure page.
$this->assertFalse(isset($this->cookies[$secure_session_name]), 'The secure cookie is not sent on insecure pages.');
// Check insecure cookie on insecure page.
$this->assertFalse($this->cookies[$insecure_session_name]['secure'], 'The insecure cookie does not have the secure attribute');
// Check that password request form action is not secure.
$this->drupalGet('user/password');
$form = $this->xpath('//form[@id="user-pass"]');
$this->assertNotEqual(substr($form[0]['action'], 0,