MediaWiki  master
Throttler.php
Go to the documentation of this file.
1 <?php
22 namespace MediaWiki\Auth;
23 
24 use BagOStuff;
30 
37 class Throttler implements LoggerAwareInterface {
39  protected $type;
46  protected $conditions;
48  protected $cache;
50  protected $logger;
52  protected $warningLimit;
53 
63  public function __construct( array $conditions = null, array $params = [] ) {
64  $invalidParams = array_diff_key( $params,
65  array_fill_keys( [ 'type', 'cache', 'warningLimit' ], true ) );
66  if ( $invalidParams ) {
67  throw new \InvalidArgumentException( 'unrecognized parameters: '
68  . implode( ', ', array_keys( $invalidParams ) ) );
69  }
70 
71  if ( $conditions === null ) {
72  $config = MediaWikiServices::getInstance()->getMainConfig();
73  $conditions = $config->get( 'PasswordAttemptThrottle' );
74  $params += [
75  'type' => 'password',
77  'warningLimit' => 50,
78  ];
79  } else {
80  $params += [
81  'type' => 'custom',
83  'warningLimit' => INF,
84  ];
85  }
86 
87  $this->type = $params['type'];
88  $this->conditions = static::normalizeThrottleConditions( $conditions );
89  $this->cache = $params['cache'];
90  $this->warningLimit = $params['warningLimit'];
91 
92  $this->setLogger( LoggerFactory::getInstance( 'throttler' ) );
93  }
94 
95  public function setLogger( LoggerInterface $logger ) {
96  $this->logger = $logger;
97  }
98 
113  public function increase( $username = null, $ip = null, $caller = null ) {
114  if ( $username === null && $ip === null ) {
115  throw new \InvalidArgumentException( 'Either username or IP must be set for throttling' );
116  }
117 
118  $userKey = $username ? md5( $username ) : null;
119  foreach ( $this->conditions as $index => $throttleCondition ) {
120  $ipKey = isset( $throttleCondition['allIPs'] ) ? null : $ip;
121  $count = $throttleCondition['count'];
122  $expiry = $throttleCondition['seconds'];
123 
124  // a limit of 0 is used as a disable flag in some throttling configuration settings
125  // throttling the whole world is probably a bad idea
126  if ( !$count || $userKey === null && $ipKey === null ) {
127  continue;
128  }
129 
130  $throttleKey = $this->cache->makeGlobalKey(
131  'throttler',
132  $this->type,
133  $index,
134  $ipKey,
135  $userKey
136  );
137  $throttleCount = $this->cache->get( $throttleKey );
138  if ( $throttleCount && $throttleCount >= $count ) {
139  // Throttle limited reached
140  $this->logRejection( [
141  'throttle' => $this->type,
142  'index' => $index,
143  'ipKey' => $ipKey,
144  'username' => $username,
145  'count' => $count,
146  'expiry' => $expiry,
147  // @codeCoverageIgnoreStart
148  'method' => $caller ?: __METHOD__,
149  // @codeCoverageIgnoreEnd
150  ] );
151 
152  return [ 'throttleIndex' => $index, 'count' => $count, 'wait' => $expiry ];
153  } else {
154  $this->cache->incrWithInit( $throttleKey, $expiry, 1 );
155  }
156  }
157 
158  return false;
159  }
160 
170  public function clear( $username = null, $ip = null ) {
171  $userKey = $username ? md5( $username ) : null;
172  foreach ( $this->conditions as $index => $specificThrottle ) {
173  $ipKey = isset( $specificThrottle['allIPs'] ) ? null : $ip;
174  $throttleKey = $this->cache->makeGlobalKey( 'throttler', $this->type, $index, $ipKey, $userKey );
175  $this->cache->delete( $throttleKey );
176  }
177  }
178 
185  protected static function normalizeThrottleConditions( $throttleConditions ) {
186  if ( !is_array( $throttleConditions ) ) {
187  return [];
188  }
189  if ( isset( $throttleConditions['count'] ) ) { // old style
190  $throttleConditions = [ $throttleConditions ];
191  }
192  return $throttleConditions;
193  }
194 
195  protected function logRejection( array $context ) {
196  $logMsg = 'Throttle {throttle} hit, throttled for {expiry} seconds due to {count} attempts '
197  . 'from username {username} and IP {ipKey}';
198 
199  // If we are hitting a throttle for >= warningLimit attempts, it is much more likely to be
200  // an attack than someone simply forgetting their password, so log it at a higher level.
201  $level = $context['count'] >= $this->warningLimit ? LogLevel::WARNING : LogLevel::INFO;
202 
203  // It should be noted that once the throttle is hit, every attempt to login will
204  // generate the log message until the throttle expires, not just the attempt that
205  // puts the throttle over the top.
206  $this->logger->log( $level, $logMsg, $context );
207  }
208 
209 }
Config $config
Definition: MediaWiki.php:43
static getLocalClusterInstance()
Get the main cluster-local cache object.
static getInstance( $channel)
Get a named logger instance from the currently configured logger factory.
static getInstance()
Returns the global default instance of the top level service locator.
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
LoggerInterface $logger
Definition: Throttler.php:50
logRejection(array $context)
Definition: Throttler.php:195
IContextSource $context
Definition: MediaWiki.php:38
__construct(array $conditions=null, array $params=[])
Definition: Throttler.php:63
static normalizeThrottleConditions( $throttleConditions)
Handles B/C for $wgPasswordAttemptThrottle.
Definition: Throttler.php:185
clear( $username=null, $ip=null)
Clear the throttle counter.
Definition: Throttler.php:170
increase( $username=null, $ip=null, $caller=null)
Increase the throttle counter and return whether the attempt should be throttled. ...
Definition: Throttler.php:113
setLogger(LoggerInterface $logger)
Definition: Throttler.php:95
array [] $conditions
See documentation of $wgPasswordAttemptThrottle for format.
Definition: Throttler.php:46