Introduce $wgForceHTTPS

Add $wgForceHTTPS. When set to true:

* It makes the HTTP to HTTPS redirect unconditional and suppresses the
  forceHTTPS cookie.
* It makes session cookies be secure.
* In the Action API, it triggers the existing deprecation warning and
  avoids more expensive user/session checks.
* In login and signup, it suppresses the old hidden form fields for
  protocol switching.
* It hides the prefershttps user preference.

Other changes:

* Factor out the HTTPS redirect in MediaWiki::main() into
  maybeDoHttpsRedirect() and shouldDoHttpRedirect(). Improve
  documentation.
* User::requiresHTTPS() reflects $wgForceHTTPS whereas the Session
  concept of "force HTTPS" does not. The documentation of
  User::requiresHTTPS() says that it includes configuration, and
  retaining this definition was beneficial for some callers. Whereas
  Session::shouldForceHTTPS() was used fairly narrowly as the value
  of the forceHTTPS cookie, and injecting configuration into it is not
  so easy or beneficial, so I left it as it was, except for clarifying
  the documentation.
* Deprecate the following hooks: BeforeHttpsRedirect, UserRequiresHTTPS,
  CanIPUseHTTPS. No known extension uses them, and they're not compatible
  with the long-term goal of ending support for mixed-protocol wikis.
  BeforeHttpsRedirect was documented as unstable from its inception.
  CanIPUseHTTPS was a WMF config hack now superseded by GFOC's SNI
  sniffing.
* For tests which failed with $wgForceHTTPS=true, I mostly split the
  tests, testing each configuration value separately.
* Add ArrayUtils::cartesianProduct() as a helper for generating
  combinations of boolean options in the session tests.

Bug: T256095

Change-Id: Iefb5ba55af35350dfc7c050f9fb8f4e8a79751cb
This commit is contained in:
Tim Starling 2020-06-24 10:56:46 +10:00
parent 01bf0a5786
commit c75eef91bf
22 changed files with 399 additions and 110 deletions

View file

@ -73,6 +73,9 @@ For notes on 1.34.x and older releases, see HISTORY.
* Added $wgCdnMaxageStale, which controls the Cache-Control s-maxage header
for page views when PoolCounter lock contention indicates that a stale cache
entry should be sent.
* Added $wgForceHTTPS, which makes the HTTP to HTTPS redirect be unconditional
and suppresses various hacks needed to support mixed HTTP/HTTPS wikis. We
recommend this be set to true on pure HTTPS wikis.
* …
==== Changed configuration ====
@ -1270,6 +1273,9 @@ because of Phabricator reports.
deprecated. Use OutputPageBodyAttributes hook instead.
* Installer::getDBTypes has been hard deprecated in favor of
InstallerDBSupport::getDatabases
* The hooks BeforeHttpsRedirect, CanIPUseHTTPS and UserRequiresHTTPS were
deprecated, as part of a long-term plan to remove support for mixed
HTTP/HTTPS wikis.
* …
=== Other changes in 1.35 ===

View file

@ -145,6 +145,26 @@ $wgAssumeProxiesUseDefaultProtocolPorts = true;
*/
$wgHttpsPort = 443;
/**
* If this is true, when an insecure HTTP request is received, always redirect
* to HTTPS. This overrides and disables the preferhttps user preference, and it
* overrides $wgSecureLogin and the CanIPUseHTTPS hook.
*
* $wgServer may be either https or protocol-relative. If $wgServer starts with
* "http://", an exception will be thrown.
*
* If a reverse proxy or CDN is used to forward requests from HTTPS to HTTP,
* the request header "X-Forwarded-Proto: https" should be sent to suppress
* the redirect.
*
* In addition to setting this to true, for optimal security, the web server
* should also be configured to send Strict-Transport-Security response headers.
*
* @var bool
* @since 1.35
*/
$wgForceHTTPS = false;
/**
* The path we should point to.
* It might be a virtual path in case with use apache mod_rewrite for example.
@ -6431,7 +6451,11 @@ $wgCookiePath = '/';
* Whether the "secure" flag should be set on the cookie. This can be:
* - true: Set secure flag
* - false: Don't set secure flag
* - "detect": Set the secure flag if $wgServer is set to an HTTPS URL
* - "detect": Set the secure flag if $wgServer is set to an HTTPS URL,
* or if $wgForceHTTPS is true.
*
* If $wgForceHTTPS is true, session cookies will be secure regardless of this
* setting. However, other cookies will still be affected.
*/
$wgCookieSecure = 'detect';

View file

@ -5,7 +5,7 @@ namespace MediaWiki\Hook;
use IContextSource;
/**
* @stable for implementation
* @deprecated since 1.35
* @ingroup Hooks
*/
interface BeforeHttpsRedirectHook {

View file

@ -3,7 +3,7 @@
namespace MediaWiki\Hook;
/**
* @stable for implementation
* @deprecated since 1.35
* @ingroup Hooks
*/
interface CanIPUseHTTPSHook {
@ -11,6 +11,8 @@ interface CanIPUseHTTPSHook {
* Use this hook to determine whether the client at a given source IP is likely
* to be able to access the wiki via HTTPS.
*
* @deprecated since 1.35 This feature will be removed. All clients should use HTTPS.
*
* @since 1.35
*
* @param string $ip IP address in human-readable form

View file

@ -46,7 +46,9 @@ class DeprecatedHooks {
'ArticleRollbackComplete' => [ 'deprecatedVersion' => '1.35' ],
'BaseTemplateAfterPortlet' => [ 'deprecatedVersion' => '1.35', 'silent' => true ],
'BaseTemplateToolbox' => [ 'deprecatedVersion' => '1.35' ],
'BeforeHttpsRedirect' => [ 'deprecatedVersion' => '1.35' ],
'BeforeParserrenderImageGallery' => [ 'deprecatedVersion' => '1.35' ],
'CanIPUseHTTPS' => [ 'deprecatedVersion' => '1.35' ],
'DiffRevisionTools' => [ 'deprecatedVersion => 1.35' ],
'DiffViewHeader' => [ 'deprecatedVersion' => '1.35' ],
'HistoryRevisionTools' => [ 'deprecatedVersion => 1.35' ],
@ -75,6 +77,7 @@ class DeprecatedHooks {
'TitleMoveComplete' => [ 'deprecatedVersion' => '1.35', 'silent' => true ],
'TitleMoveCompleting' => [ 'deprecatedVersion' => '1.35' ],
'UndeleteShowRevision' => [ 'deprecatedVersion' => '1.35' ],
'UserRequiresHTTPS' => [ 'deprecatedVersion' => '1.35' ],
'UserRetrieveNewTalks' => [ 'deprecatedVersion' => '1.35' ],
'UserSetCookies' => [ 'deprecatedVersion' => '1.27' ],
'WikiPageDeletionUpdates' => [ 'deprecatedVersion' => '1.32', 'silent' => true ],

View file

@ -913,54 +913,8 @@ class MediaWiki {
$trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ );
}
// If the user has forceHTTPS set to true, or if the user
// is in a group requiring HTTPS, or if they have the HTTPS
// preference set, redirect them to HTTPS.
// Note: Do this after $wgTitle is setup, otherwise the hooks run from
// isLoggedIn() will do all sorts of weird stuff.
if (
$request->getProtocol() == 'http' &&
// switch to HTTPS only when supported by the server
preg_match( '#^https://#', wfExpandUrl( $request->getRequestURL(), PROTO_HTTPS ) ) &&
(
$request->getSession()->shouldForceHTTPS() ||
// Check the cookie manually, for paranoia
$request->getCookie( 'forceHTTPS', '' ) ||
// check for prefixed version that was used for a time in older MW versions
$request->getCookie( 'forceHTTPS' ) ||
// Avoid checking the user and groups unless it's enabled.
(
$this->context->getUser()->isLoggedIn()
&& $this->context->getUser()->requiresHTTPS()
)
)
) {
$oldUrl = $request->getFullRequestURL();
$redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
// ATTENTION: This hook is likely to be removed soon due to overall design of the system.
if ( $this->getHookRunner()->onBeforeHttpsRedirect( $this->context, $redirUrl ) ) {
if ( $request->wasPosted() ) {
// This is weird and we'd hope it almost never happens. This
// means that a POST came in via HTTP and policy requires us
// redirecting to HTTPS. It's likely such a request is going
// to fail due to post data being lost, but let's try anyway
// and just log the instance.
// @todo FIXME: See if we could issue a 307 or 308 here, need
// to see how clients (automated & browser) behave when we do
wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
}
// Setup dummy Title, otherwise OutputPage::redirect will fail
$title = Title::newFromText( 'REDIR', NS_MAIN );
$this->context->setTitle( $title );
// Since we only do this redir to change proto, always send a vary header
$output->addVaryHeader( 'X-Forwarded-Proto' );
$output->redirect( $redirUrl );
$output->output();
return;
}
if ( $this->maybeDoHttpsRedirect() ) {
return;
}
if ( $title->canExist() && HTMLFileCache::useFileCache( $this->context ) ) {
@ -1006,6 +960,93 @@ class MediaWiki {
$this->outputResponsePayload( $outputWork() );
}
/**
* Check if an HTTP->HTTPS redirect should be done. It may still be aborted
* by a hook, so this is not the final word.
*
* @return bool
*/
private function shouldDoHttpRedirect() {
$request = $this->context->getRequest();
// Don't redirect if we're already on HTTPS
if ( $request->getProtocol() !== 'http' ) {
return false;
}
$force = $this->config->get( 'ForceHTTPS' );
// Don't redirect if $wgServer is explicitly HTTP. We test for this here
// by checking whether wfExpandUrl() is able to force HTTPS.
if ( !preg_match( '#^https://#', wfExpandUrl( $request->getRequestURL(), PROTO_HTTPS ) ) ) {
if ( $force ) {
throw new RuntimeException( '$wgForceHTTPS is true but the server is not HTTPS' );
}
return false;
}
// Configured $wgForceHTTPS overrides the remaining conditions
if ( $force ) {
return true;
}
// Check if HTTPS is required by the session or user preferences
return $request->getSession()->shouldForceHTTPS() ||
// Check the cookie manually, for paranoia
$request->getCookie( 'forceHTTPS', '' ) ||
// Avoid checking the user and groups unless it's enabled.
(
$this->context->getUser()->isLoggedIn()
&& $this->context->getUser()->requiresHTTPS()
);
}
/**
* If the stars are suitably aligned, do an HTTP->HTTPS redirect
*
* Note: Do this after $wgTitle is setup, otherwise the hooks run from
* isLoggedIn() will do all sorts of weird stuff.
*
* @return bool True if the redirect was done. Handling of the request
* should be aborted. False if no redirect was done.
*/
private function maybeDoHttpsRedirect() {
if ( !$this->shouldDoHttpRedirect() ) {
return false;
}
$request = $this->context->getRequest();
$oldUrl = $request->getFullRequestURL();
$redirUrl = preg_replace( '#^http://#', 'https://', $oldUrl );
// ATTENTION: This hook is likely to be removed soon due to overall design of the system.
if ( !$this->getHookRunner()->onBeforeHttpsRedirect( $this->context, $redirUrl ) ) {
return false;
}
if ( $request->wasPosted() ) {
// This is weird and we'd hope it almost never happens. This
// means that a POST came in via HTTP and policy requires us
// redirecting to HTTPS. It's likely such a request is going
// to fail due to post data being lost, but let's try anyway
// and just log the instance.
// @todo FIXME: See if we could issue a 307 or 308 here, need
// to see how clients (automated & browser) behave when we do
wfDebugLog( 'RedirectedPosts', "Redirected from HTTP to HTTPS: $oldUrl" );
}
// Setup dummy Title, otherwise OutputPage::redirect will fail
$title = Title::newFromText( 'REDIR', NS_MAIN );
$this->context->setTitle( $title );
// Since we only do this redir to change proto, always send a vary header
$output = $this->context->getOutput();
$output->addVaryHeader( 'X-Forwarded-Proto' );
$output->redirect( $redirUrl );
$output->output();
return true;
}
/**
* Set the actual output and attempt to flush it to the client if necessary
*

View file

@ -503,7 +503,7 @@ if ( $wgPageLanguageUseDB ) {
}
if ( $wgCookieSecure === 'detect' ) {
$wgCookieSecure = ( WebRequest::detectProtocol() === 'https' );
$wgCookieSecure = $wgForceHTTPS || ( WebRequest::detectProtocol() === 'https' );
}
// Backwards compatibility with old password limits

View file

@ -1542,11 +1542,14 @@ class ApiMain extends ApiBase {
$this->mPrinter = $this->createPrinterByName( $params['format'] );
}
if ( $request->getProtocol() === 'http' && (
$request->getSession()->shouldForceHTTPS() ||
( $this->getUser()->isLoggedIn() &&
$this->getUser()->requiresHTTPS() )
) ) {
if ( $request->getProtocol() === 'http' &&
(
$this->getConfig()->get( 'ForceHTTPS' ) ||
$request->getSession()->shouldForceHTTPS() ||
( $this->getUser()->isLoggedIn() &&
$this->getUser()->requiresHTTPS() )
)
) {
$this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
}
}

View file

@ -182,4 +182,72 @@ class ArrayUtils {
return $ret;
}
/**
* Make an array consisting of every combination of the elements of the
* input arrays. Each element of the output array is an array with a number
* of elements equal to the number of input parameters.
*
* In mathematical terms, this is an n-ary Cartesian product.
*
* For example, ArrayUtils::cartesianProduct( [ 1, 2 ], [ 'a', 'b' ] )
* produces [ [ 1, 'a' ], [ 1, 'b' ], [ 2, 'a' ], [ 2, 'b' ] ]
*
* If any of the input arrays is empty, the result is the empty array [].
* This is in keeping with the mathematical definition.
*
* If no parameters are given, the result is also the empty array.
*
* The array keys are ignored. This implementation uses the internal
* pointers of the input arrays to keep track of the current position
* without referring to the keys.
*
* @since 1.35
*
* @param array ...$inputArrays
* @return array
*/
public static function cartesianProduct( ...$inputArrays ) {
$numInputs = count( $inputArrays );
if ( $numInputs === 0 ) {
return [];
}
// Reset the internal pointers
foreach ( $inputArrays as &$inputArray ) {
if ( !count( $inputArray ) ) {
return [];
}
reset( $inputArray );
}
unset( $inputArray );
$outputArrays = [];
$done = false;
while ( !$done ) {
// Construct the output array element
$element = [];
foreach ( $inputArrays as $paramIndex => $inputArray ) {
$element[] = current( $inputArray );
}
$outputArrays[] = $element;
// Increment the pointers starting from the least significant.
// If the least significant rolls over back to the start of the
// array, continue with the next most significant, and so on until
// that stops happening. If all pointers roll over, we are done.
$done = true;
for ( $paramIndex = $numInputs - 1; $paramIndex >= 0; $paramIndex-- ) {
next( $inputArrays[$paramIndex] );
if ( key( $inputArrays[$paramIndex] ) === null ) {
reset( $inputArrays[$paramIndex] );
// continue
} else {
$done = false;
break;
}
}
}
return $outputArrays;
}
}

View file

@ -112,6 +112,7 @@ class DefaultPreferencesFactory implements PreferencesFactory {
'EnotifRevealEditorAddress',
'EnotifUserTalk',
'EnotifWatchlist',
'ForceHTTPS',
'HiddenPrefs',
'ImageLimits',
'LanguageCode',
@ -457,7 +458,10 @@ class DefaultPreferencesFactory implements PreferencesFactory {
];
}
// Only show prefershttps if secure login is turned on
if ( $this->options->get( 'SecureLogin' ) && $canIPUseHTTPS ) {
if ( !$this->options->get( 'ForceHTTPS' )
&& $this->options->get( 'SecureLogin' )
&& $canIPUseHTTPS
) {
$defaultPreferences['prefershttps'] = [
'type' => 'toggle',
'label-message' => 'tog-prefershttps',

View file

@ -100,7 +100,7 @@ class CookieSessionProvider extends SessionProvider {
'prefix' => $config->get( 'CookiePrefix' ),
'path' => $config->get( 'CookiePath' ),
'domain' => $config->get( 'CookieDomain' ),
'secure' => $config->get( 'CookieSecure' ),
'secure' => $config->get( 'CookieSecure' ) || $this->config->get( 'ForceHTTPS' ),
'httpOnly' => $config->get( 'CookieHttpOnly' ),
];
}
@ -210,10 +210,8 @@ class CookieSessionProvider extends SessionProvider {
$forceHTTPS = $session->shouldForceHTTPS() || $user->requiresHTTPS();
if ( $forceHTTPS ) {
// Don't set the secure flag if the request came in
// over "http", for backwards compat.
// @todo Break that backwards compat properly.
$options['secure'] = $this->config->get( 'CookieSecure' );
$options['secure'] = $this->config->get( 'CookieSecure' )
|| $this->config->get( 'ForceHTTPS' );
}
$response->setCookie( $this->params['sessionName'], $session->getId(), null,
@ -263,12 +261,17 @@ class CookieSessionProvider extends SessionProvider {
}
/**
* Set the "forceHTTPS" cookie
* Set the "forceHTTPS" cookie, unless $wgForceHTTPS prevents it.
*
* @param bool $set Whether the cookie should be set or not
* @param SessionBackend|null $backend
* @param WebRequest $request
*/
protected function setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request ) {
if ( $this->config->get( 'ForceHTTPS' ) ) {
// No need to send a cookie if the wiki is always HTTPS (T256095)
return;
}
$response = $request->response();
if ( $set ) {
if ( $backend->shouldRememberUser() ) {

View file

@ -112,8 +112,11 @@ abstract class ImmutableSessionProviderWithCookie extends SessionProvider {
$options = $this->sessionCookieOptions;
if ( $session->shouldForceHTTPS() || $session->getUser()->requiresHTTPS() ) {
$response->setCookie( 'forceHTTPS', 'true', null,
[ 'prefix' => '', 'secure' => false ] + $options );
// Send a cookie unless $wgForceHTTPS is set (T256095)
if ( !$this->config->get( 'ForceHTTPS' ) ) {
$response->setCookie( 'forceHTTPS', 'true', null,
[ 'prefix' => '', 'secure' => false ] + $options );
}
$options['secure'] = true;
}

View file

@ -209,7 +209,10 @@ class Session implements \Countable, \Iterator, \ArrayAccess {
}
/**
* Whether HTTPS should be forced
* Get the expected value of the forceHTTPS cookie. This reflects whether
* session cookies were sent with the Secure attribute. If $wgForceHTTPS
* is true, the forceHTTPS cookie is not sent and this value is ignored.
*
* @return bool
*/
public function shouldForceHTTPS() {
@ -217,7 +220,10 @@ class Session implements \Countable, \Iterator, \ArrayAccess {
}
/**
* Set whether HTTPS should be forced
* Set the value of the forceHTTPS cookie. This reflects whether session
* cookies were sent with the Secure attribute. If $wgForceHTTPS is true,
* the forceHTTPS cookie is not sent, and this value is ignored.
*
* @param bool $force
*/
public function setForceHTTPS( $force ) {

View file

@ -80,7 +80,8 @@ class SessionInfo {
* - persisted: (bool) Whether this session was persisted
* - remembered: (bool) Whether the verified user was remembered.
* Defaults to true.
* - forceHTTPS: (bool) Whether to force HTTPS for this session
* - forceHTTPS: (bool) Whether to force HTTPS for this session. This is
* ignored if $wgForceHTTPS is true.
* - metadata: (array) Provider metadata, to be returned by
* Session::getProviderMetadata(). See SessionProvider::mergeMetadata()
* and SessionProvider::refreshSessionInfo().
@ -271,7 +272,9 @@ class SessionInfo {
}
/**
* Whether this session should only be used over HTTPS
* Whether this session should only be used over HTTPS. This should be
* ignored if $wgForceHTTPS is true.
*
* @return bool
*/
final public function forceHTTPS() {

View file

@ -108,7 +108,8 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
$this->mAction = $request->getVal( 'action' );
$this->mFromHTTP = $request->getBool( 'fromhttp', false )
|| $request->getBool( 'wpFromhttp', false );
$this->mStickHTTPS = ( !$this->mFromHTTP && $request->getProtocol() === 'https' )
$this->mStickHTTPS = $this->getConfig()->get( 'ForceHTTPS' )
|| ( !$this->mFromHTTP && $request->getProtocol() === 'https' )
|| $request->getBool( 'wpForceHttps', false );
$this->mLanguage = $request->getText( 'uselang' );
$this->mReturnTo = $request->getVal( 'returnto', '' );
@ -650,7 +651,6 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
* @return HTMLForm
*/
protected function getAuthForm( array $requests, $action, $msg = '', $msgType = 'error' ) {
global $wgSecureLogin;
// FIXME merge this with parent
if ( isset( $this->authForm ) ) {
@ -679,7 +679,8 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
}
$form->addHiddenField( 'force', $this->securityLevel );
$form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
if ( $wgSecureLogin ) {
$config = $this->getConfig();
if ( $config->get( 'SecureLogin' ) && !$config->get( 'ForceHTTPS' ) ) {
// If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved
if ( !$this->isSignup() ) {
$form->addHiddenField( 'wpForceHttps', (int)$this->mStickHTTPS );

View file

@ -63,7 +63,8 @@ class LoginHelper extends ContextSource {
* - successredirect: send an HTTP redirect using $wgRedirectOnLogin if needed
* @param string $returnTo
* @param array|string $returnToQuery
* @param bool $stickHTTPS Keep redirect link on HTTPS
* @param bool $stickHTTPS Keep redirect link on HTTPS. Ignored (treated as
* true) if $wgForceHTTPS is true.
*/
public function showReturnToPage(
$type, $returnTo = '', $returnToQuery = '', $stickHTTPS = false
@ -81,12 +82,14 @@ class LoginHelper extends ContextSource {
$returnToTitle = Title::newFromText( $returnTo ) ?: Title::newMainPage();
if ( $config->get( 'SecureLogin' ) && !$stickHTTPS ) {
$options = [ 'http' ];
$proto = PROTO_HTTP;
} elseif ( $config->get( 'SecureLogin' ) ) {
if ( $config->get( 'ForceHTTPS' )
|| ( $config->get( 'SecureLogin' ) && $stickHTTPS )
) {
$options = [ 'https' ];
$proto = PROTO_HTTPS;
} elseif ( $config->get( 'SecureLogin' ) && !$stickHTTPS ) {
$options = [ 'http' ];
$proto = PROTO_HTTP;
} else {
$options = [];
$proto = PROTO_RELATIVE;

View file

@ -5,13 +5,16 @@ namespace MediaWiki\User\Hook;
use User;
/**
* @stable for implementation
* @deprecated since 1.35
* @ingroup Hooks
*/
interface UserRequiresHTTPSHook {
/**
* This hook is called to determine whether a user needs to be switched to HTTPS.
*
* Deprecated since 1.35 as part of a drive towards deprecation of
* mixed-protocol wikis.
*
* @since 1.35
*
* @param User $user User in question.

View file

@ -2891,11 +2891,13 @@ class User implements IDBAccessObject, UserIdentity {
* @return bool
*/
public function requiresHTTPS() {
global $wgSecureLogin;
global $wgForceHTTPS, $wgSecureLogin;
if ( $wgForceHTTPS ) {
return true;
}
if ( !$wgSecureLogin ) {
return false;
}
$https = $this->getBoolOption( 'prefershttps' );
$this->getHookRunner()->onUserRequiresHTTPS( $this, $https );
if ( $https ) {

View file

@ -26,6 +26,7 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
'SessionName' => false,
'CookieExpiration' => 100,
'ExtendedLoginCookieExpiration' => 200,
'ForceHTTPS' => false,
] );
}
@ -37,6 +38,16 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
return MediaWikiServices::getInstance()->getHookContainer();
}
/**
* Provider for testing both values of $wgForceHTTPS
*/
public static function provideForceHTTPS() {
return [
[ false ],
[ true ]
];
}
public function testConstructor() {
try {
new CookieSessionProvider();
@ -400,7 +411,8 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
$this->assertEquals( 'Example', $provider->suggestLoginUsername( $request ) );
}
public function testPersistSession() {
/** @dataProvider provideForceHTTPS */
public function testPersistSession( $forceHTTPS ) {
$provider = new CookieSessionProvider( [
'priority' => 1,
'sessionName' => 'MySessionName',
@ -408,6 +420,7 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
'cookieOptions' => [ 'prefix' => 'x' ],
] );
$config = $this->getConfig();
$config->set( 'ForceHTTPS', $forceHTTPS );
$hookContainer = $this->getHookContainer();
$provider->setLogger( new \TestLogger() );
$provider->setConfig( $config );
@ -416,6 +429,10 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$store = new TestBagOStuff();
// For User::requiresHTTPS
$this->setMwGlobals( [ 'wgForceHTTPS' => $forceHTTPS ] );
$user = static::getTestSysop()->getUser();
$anon = new User;
@ -450,7 +467,11 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
$this->assertSame( '', $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( null, $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
$this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) );
if ( $forceHTTPS ) {
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
} else {
$this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) );
}
$this->assertSame( [], $backend->getData() );
// Logged-in user, no remember
@ -463,7 +484,11 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( '', $request->response()->getCookie( 'xToken' ) );
$this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) );
if ( $forceHTTPS ) {
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
} else {
$this->assertSame( '', $request->response()->getCookie( 'forceHTTPS' ) );
}
$this->assertSame( [], $backend->getData() );
// Logged-in user, remember
@ -477,7 +502,11 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
$this->assertSame( (string)$user->getId(), $request->response()->getCookie( 'xUserID' ) );
$this->assertSame( $user->getName(), $request->response()->getCookie( 'xUserName' ) );
$this->assertSame( $user->getToken(), $request->response()->getCookie( 'xToken' ) );
$this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) );
if ( $forceHTTPS ) {
$this->assertSame( null, $request->response()->getCookie( 'forceHTTPS' ) );
} else {
$this->assertSame( 'true', $request->response()->getCookie( 'forceHTTPS' ) );
}
$this->assertSame( [], $backend->getData() );
}
@ -485,10 +514,12 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
* @dataProvider provideCookieData
* @param bool $secure
* @param bool $remember
* @param bool $forceHTTPS
*/
public function testCookieData( $secure, $remember ) {
public function testCookieData( $secure, $remember, $forceHTTPS ) {
$this->setMwGlobals( [
'wgSecureLogin' => false,
'wgForceHTTPS' => $forceHTTPS,
] );
$provider = new CookieSessionProvider( [
@ -499,6 +530,7 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
] );
$config = $this->getConfig();
$config->set( 'CookieSecure', $secure );
$config->set( 'ForceHTTPS', $forceHTTPS );
$hookContainer = $this->getHookContainer();
$provider->setLogger( new \TestLogger() );
$provider->setConfig( $config );
@ -507,7 +539,7 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$user = static::getTestSysop()->getUser();
$this->assertFalse( $user->requiresHTTPS(), 'sanity check' );
$this->assertSame( $user->requiresHTTPS(), $forceHTTPS, 'sanity check' );
$backend = new SessionBackend(
new SessionId( $sessionId ),
@ -534,7 +566,7 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
'expire' => (int)100,
'path' => $config->get( 'CookiePath' ),
'domain' => $config->get( 'CookieDomain' ),
'secure' => $secure,
'secure' => $secure || $forceHTTPS,
'httpOnly' => $config->get( 'CookieHttpOnly' ),
'raw' => false,
];
@ -558,13 +590,15 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
'xToken' => [
'value' => $remember ? $user->getToken() : '',
'expire' => $remember ? $extendedExpiry : -31536000,
] + $defaults,
'forceHTTPS' => [
] + $defaults
];
if ( !$forceHTTPS ) {
$expect['forceHTTPS'] = [
'value' => $secure ? 'true' : '',
'secure' => false,
'expire' => $secure ? ( $remember ? $defaults['expire'] : 0 ) : -31536000,
] + $defaults,
];
] + $defaults;
}
foreach ( $expect as $key => $value ) {
$actual = $request->response()->getCookieData( $key );
if ( $actual && $actual['expire'] > 0 ) {
@ -576,12 +610,11 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
}
public static function provideCookieData() {
return [
[ false, false ],
[ false, true ],
[ true, false ],
[ true, true ],
];
return \ArrayUtils::cartesianProduct(
[ false, true ], // $secure
[ false, true ], // $remember
[ false, true ] // $forceHTTPS
);
}
protected function getSentRequest() {
@ -612,6 +645,9 @@ class CookieSessionProviderTest extends MediaWikiTestCase {
$provider->setManager( SessionManager::singleton() );
$provider->setHookContainer( $hookContainer );
// For User::requiresHTTPS
$this->setMwGlobals( [ 'wgForceHTTPS' => false ] );
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$store = new TestBagOStuff();
$user = static::getTestSysop()->getUser();

View file

@ -13,9 +13,10 @@ use Wikimedia\TestingAccessWrapper;
*/
class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase {
private function getProvider( $name, $prefix = null ) {
private function getProvider( $name, $prefix = null, $forceHTTPS = false ) {
$config = new \HashConfig();
$config->set( 'CookiePrefix', 'wgCookiePrefix' );
$config->set( 'ForceHTTPS', $forceHTTPS );
$params = [
'sessionCookieName' => $name,
@ -178,14 +179,16 @@ class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase {
* @dataProvider providePersistSession
* @param bool $secure
* @param bool $remember
* @param bool $forceHTTPS
*/
public function testPersistSession( $secure, $remember ) {
public function testPersistSession( $secure, $remember, $forceHTTPS ) {
$this->setMwGlobals( [
'wgCookieExpiration' => 100,
'wgSecureLogin' => false,
'wgForceHTTPS' => $forceHTTPS,
] );
$provider = $this->getProvider( 'session' );
$provider = $this->getProvider( 'session', null, $forceHTTPS );
$provider->setLogger( new \Psr\Log\NullLogger() );
$priv = TestingAccessWrapper::newFromObject( $provider );
$priv->sessionCookieOptions = [
@ -198,7 +201,7 @@ class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase {
$sessionId = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$user = User::newFromName( 'UTSysop' );
$this->assertFalse( $user->requiresHTTPS(), 'sanity check' );
$this->assertSame( $forceHTTPS, $user->requiresHTTPS(), 'sanity check' );
$backend = new SessionBackend(
new SessionId( $sessionId ),
@ -241,13 +244,13 @@ class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase {
'expire' => null,
'path' => 'CookiePath',
'domain' => 'CookieDomain',
'secure' => $secure,
'secure' => $secure || $forceHTTPS,
'httpOnly' => true,
'raw' => false,
], $cookie );
$cookie = $request->response()->getCookieData( 'forceHTTPS' );
if ( $secure ) {
if ( $secure && !$forceHTTPS ) {
$this->assertIsArray( $cookie );
if ( isset( $cookie['expire'] ) && $cookie['expire'] > 0 ) {
// Round expiry so we don't randomly fail if the seconds ticked during the test.
@ -273,12 +276,11 @@ class ImmutableSessionProviderWithCookieTest extends MediaWikiTestCase {
}
public static function providePersistSession() {
return [
[ false, false ],
[ false, true ],
[ true, false ],
[ true, true ],
];
return \ArrayUtils::cartesianProduct(
[ false, true ], // $secure
[ false, true ], // $remember
[ false, true ] // $forceHTTPS
);
}
public function testUnpersistSession() {

View file

@ -2569,16 +2569,19 @@ class UserTest extends MediaWikiTestCase {
public function testRequiresHTTPS( $preference, $hook1, $hook2, bool $expected ) {
$this->setMwGlobals( [
'wgSecureLogin' => true,
'wgForceHTTPS' => false,
] );
$user = User::newFromName( 'UserWhoMayRequireHTTPS' );
$user->setOption( 'prefershttps', $preference );
$user->saveSettings();
$this->filterDeprecated( '/UserRequiresHTTPS hook/' );
$this->setTemporaryHook( 'UserRequiresHTTPS', function ( $user, &$https ) use ( $hook1 ) {
$https = $hook1;
return false;
} );
$this->filterDeprecated( '/CanIPUseHTTPS hook/' );
$this->setTemporaryHook( 'CanIPUseHTTPS', function ( $ip, &$canDo ) use ( $hook2 ) {
if ( $hook2 === 'notcalled' ) {
$this->fail( 'CanIPUseHTTPS hook should not have been called' );
@ -2608,6 +2611,7 @@ class UserTest extends MediaWikiTestCase {
public function testRequiresHTTPS_disabled() {
$this->setMwGlobals( [
'wgSecureLogin' => false,
'wgForceHTTPS' => false,
] );
$user = User::newFromName( 'UserWhoMayRequireHTTP' );
@ -2621,6 +2625,26 @@ class UserTest extends MediaWikiTestCase {
);
}
/**
* @covers User::requiresHTTPS
*/
public function testRequiresHTTPS_forced() {
$this->setMwGlobals( [
'wgSecureLogin' => true,
'wgForceHTTPS' => true,
] );
$user = User::newFromName( 'UserWhoMayRequireHTTP' );
$user->setOption( 'prefershttps', false );
$user->saveSettings();
$user = User::newFromName( $user->getName() );
$this->assertTrue(
$user->requiresHTTPS(),
'User preference ignored if wgForceHTTPS is true'
);
}
/**
* @covers User::isCreatableName
*/

View file

@ -303,4 +303,56 @@ class ArrayUtilsTest extends PHPUnit\Framework\TestCase {
],
];
}
/**
* @dataProvider provideCartesianProduct
* @covers ArrayUtils::cartesianProduct
*/
public function testCartesianProduct( $inputs, $expected ) {
$result = ArrayUtils::cartesianProduct( ...$inputs );
$this->assertSame( $expected, $result );
}
public function provideCartesianProduct() {
$ab = [ 'a', 'b' ];
$cd = [ 'c', 'd' ];
$ac = [ 'a', 'c' ];
$ad = [ 'a', 'd' ];
$bc = [ 'b', 'c' ];
$bd = [ 'b', 'd' ];
return [
'no inputs' => [
[],
[]
],
'one empty input' => [
[ [] ],
[]
],
'one non-empty input' => [
[ $ab ],
[ [ 'a' ], [ 'b' ] ]
],
'non-empty times empty' => [
[ $ab, [] ],
[]
],
'empty times non-empty' => [
[ [], $ab ],
[]
],
'ab x cd' => [
[ $ab, $cd ],
[ $ac, $ad, $bc, $bd ]
],
'keys are ignored' => [
[
[ 99 => 'a', 98 => 'b' ],
[ 97 => 'c', 96 => 'd' ],
],
[ $ac, $ad, $bc, $bd ]
],
];
}
}