RegisterThrottlePlugin.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <?php
  2. // This file is part of GNU social - https://www.gnu.org/software/social
  3. //
  4. // GNU social is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // GNU social is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Throttle registration by IP address
  18. *
  19. * @category Spam
  20. * @package GNUsocial
  21. * @author Evan Prodromou <evan@status.net>
  22. * @copyright 2010 StatusNet, Inc.
  23. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  24. */
  25. defined('GNUSOCIAL') || die();
  26. /**
  27. * Throttle registration by IP address
  28. *
  29. * We a) record IP address of registrants and b) throttle registrations.
  30. *
  31. * @category Spam
  32. * @package GNUsocial
  33. * @author Evan Prodromou <evan@status.net>
  34. * @copyright 2010 StatusNet, Inc.
  35. * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
  36. */
  37. class RegisterThrottlePlugin extends Plugin
  38. {
  39. const PLUGIN_VERSION = '2.0.0';
  40. /**
  41. * Array of time spans in seconds to limits.
  42. *
  43. * Default is 3 registrations per hour, 5 per day, 10 per week.
  44. */
  45. public $regLimits = array(604800 => 10, // per week
  46. 86400 => 5, // per day
  47. 3600 => 3); // per hour
  48. /**
  49. * Disallow registration if a silenced user has registered from
  50. * this IP address.
  51. */
  52. public $silenced = true;
  53. /**
  54. * Auto-silence all other users from the same registration_ip
  55. * as the one being silenced. Caution: Many users may come from
  56. * the same IP (even entire countries) without having any sort
  57. * of relevant connection for moderation.
  58. */
  59. public $auto_silence_by_ip = false;
  60. /**
  61. * Whether we're enabled; prevents recursion.
  62. */
  63. private static $enabled = true;
  64. /**
  65. * Database schema setup
  66. *
  67. * We store user registrations in a table registration_ip.
  68. *
  69. * @return boolean hook value; true means continue processing, false means stop.
  70. */
  71. public function onCheckSchema()
  72. {
  73. $schema = Schema::get();
  74. // For storing user-submitted flags on profiles
  75. $schema->ensureTable('registration_ip', Registration_ip::schemaDef());
  76. return true;
  77. }
  78. public function onRouterInitialized(URLMapper $m)
  79. {
  80. $m->connect(
  81. 'main/ipregistrations/:ipaddress',
  82. ['action' => 'ipregistrations'],
  83. ['ipaddress' => '[0-9a-f\.\:]+']
  84. );
  85. }
  86. /**
  87. * Called when someone tries to register.
  88. *
  89. * We check the IP here to determine if it goes over any of our
  90. * configured limits.
  91. *
  92. * @param Action $action Action that is being executed
  93. *
  94. * @return boolean hook value
  95. */
  96. public function onStartRegistrationTry($action)
  97. {
  98. $ipaddress = $this->_getIpAddress();
  99. if (empty($ipaddress)) {
  100. // TRANS: Server exception thrown when no IP address can be found for a registation attempt.
  101. throw new ServerException(_m('Cannot find IP address.'));
  102. }
  103. foreach ($this->regLimits as $seconds => $limit) {
  104. $this->debug("Checking $seconds ($limit)");
  105. $reg = $this->_getNthReg($ipaddress, $limit);
  106. if (!empty($reg)) {
  107. $this->debug("Got a {$limit}th registration.");
  108. $regtime = strtotime($reg->created);
  109. $now = time();
  110. $this->debug("Comparing {$regtime} to {$now}");
  111. if ($now - $regtime < $seconds) {
  112. // TRANS: Exception thrown when too many user have registered from one IP address within a given time frame.
  113. throw new Exception(_m('Too many registrations. Take a break and try again later.'));
  114. }
  115. }
  116. }
  117. // Check for silenced users
  118. if ($this->silenced) {
  119. $ids = Registration_ip::usersByIP($ipaddress);
  120. foreach ($ids as $id) {
  121. $profile = Profile::getKV('id', $id);
  122. if ($profile && $profile->isSilenced()) {
  123. // TRANS: Exception thrown when attempting to register from an IP address from which silenced users have registered.
  124. throw new Exception(_m('A banned user has registered from this address.'));
  125. }
  126. }
  127. }
  128. return true;
  129. }
  130. public function onEndShowSections(Action $action)
  131. {
  132. if (!$action instanceof ShowstreamAction) {
  133. // early return for actions we're not interested in
  134. return true;
  135. }
  136. $target = $action->getTarget();
  137. if (!$target->isSilenced()) {
  138. // Only show the IP of users who are not silenced.
  139. return true;
  140. }
  141. $scoped = $action->getScoped();
  142. if (!$scoped->hasRight(Right::SILENCEUSER)) {
  143. // only show registration IP if we have the right to silence users
  144. return true;
  145. }
  146. $ri = Registration_ip::getKV('user_id', $target->getID());
  147. $ipaddress = null;
  148. if ($ri instanceof Registration_ip) {
  149. $ipaddress = $ri->ipaddress;
  150. unset($ri);
  151. }
  152. $action->elementStart('div', array('id' => 'entity_mod_log',
  153. 'class' => 'section'));
  154. $action->element('h2', null, _('Registration IP'));
  155. // TRANS: Label for the information about which IP a users registered from.
  156. $action->element('strong', null, _('Registered from:'));
  157. $el = 'span';
  158. $attrs = ['class'=>'ipaddress'];
  159. if (!is_null($ipaddress)) {
  160. $el = 'a';
  161. $attrs['href'] = common_local_url(
  162. 'ipregistrations',
  163. ['ipaddress' => $ipaddress]
  164. );
  165. }
  166. $action->element(
  167. $el,
  168. $attrs,
  169. // TRANS: Unknown IP address.
  170. $ipaddress ?? _('unknown')
  171. );
  172. $action->elementEnd('div');
  173. }
  174. /**
  175. * Called after someone registers, by any means.
  176. *
  177. * We record the successful registration and IP address.
  178. *
  179. * @param Profile $profile new user's profile
  180. *
  181. * @return boolean hook value
  182. */
  183. public function onEndUserRegister(Profile $profile)
  184. {
  185. $ipaddress = $this->_getIpAddress();
  186. if (empty($ipaddress)) {
  187. // User registration can happen from command-line scripts etc.
  188. return true;
  189. }
  190. $reg = new Registration_ip();
  191. $reg->user_id = $profile->getID();
  192. $reg->ipaddress = mb_strtolower($ipaddress);
  193. $reg->created = common_sql_now();
  194. $result = $reg->insert();
  195. if ($result === false) {
  196. common_log_db_error($reg, 'INSERT', __FILE__);
  197. // @todo throw an exception?
  198. }
  199. return true;
  200. }
  201. /**
  202. * Check the version of the plugin.
  203. *
  204. * @param array &$versions Version array.
  205. *
  206. * @return boolean hook value
  207. */
  208. public function onPluginVersion(array &$versions): bool
  209. {
  210. $versions[] = array('name' => 'RegisterThrottle',
  211. 'version' => self::PLUGIN_VERSION,
  212. 'author' => 'Evan Prodromou',
  213. 'homepage' => GNUSOCIAL_ENGINE_REPO_URL . 'tree/master/plugins/RegisterThrottle',
  214. 'description' =>
  215. // TRANS: Plugin description.
  216. _m('Throttles excessive registration from a single IP address.'));
  217. return true;
  218. }
  219. /**
  220. * Gets the current IP address.
  221. *
  222. * @return string IP address or null if not found.
  223. */
  224. private function _getIpAddress()
  225. {
  226. $keys = array('HTTP_X_FORWARDED_FOR',
  227. 'HTTP_X_CLIENT',
  228. 'CLIENT-IP',
  229. 'REMOTE_ADDR');
  230. foreach ($keys as $k) {
  231. if (!empty($_SERVER[$k])) {
  232. return $_SERVER[$k];
  233. }
  234. }
  235. return null;
  236. }
  237. /**
  238. * Gets the Nth registration with the given IP address.
  239. *
  240. * @param string $ipaddress Address to key on
  241. * @param integer $n Nth address
  242. *
  243. * @return Registration_ip nth registration or null if not found.
  244. */
  245. private function _getNthReg($ipaddress, $n)
  246. {
  247. $reg = new Registration_ip();
  248. $reg->ipaddress = $ipaddress;
  249. $reg->orderBy('created DESC, user_id DESC');
  250. $reg->limit($n - 1, 1);
  251. if ($reg->find(true)) {
  252. return $reg;
  253. } else {
  254. return null;
  255. }
  256. }
  257. /**
  258. * When silencing a user, silence all other users registered from that IP
  259. * address.
  260. *
  261. * @param Profile $profile Person getting a new role
  262. * @param string $role Role being assigned like 'moderator' or 'silenced'
  263. *
  264. * @return boolean hook value
  265. */
  266. public function onEndGrantRole($profile, $role)
  267. {
  268. if (!self::$enabled) {
  269. return true;
  270. }
  271. if ($role !== Profile_role::SILENCED) {
  272. return true;
  273. }
  274. if (!$this->auto_silence_by_ip) {
  275. return true;
  276. }
  277. $ri = Registration_ip::getKV('user_id', $profile->getID());
  278. if (empty($ri)) {
  279. return true;
  280. }
  281. $ids = Registration_ip::usersByIP($ri->ipaddress);
  282. foreach ($ids as $id) {
  283. if ($id == $profile->getID()) {
  284. continue;
  285. }
  286. try {
  287. $other = Profile::getByID($id);
  288. } catch (NoResultException $e) {
  289. continue;
  290. }
  291. if ($other->isSilenced()) {
  292. continue;
  293. }
  294. // 'enabled' here is used to prevent recursion, since
  295. // we'll end up in this function again on ->silence()
  296. // though I actually think it doesn't matter since we
  297. // do this in onEndGrantRole and that means the above
  298. // $other->isSilenced() test should've 'continue'd...
  299. $old = self::$enabled;
  300. self::$enabled = false;
  301. $other->silence();
  302. self::$enabled = $old;
  303. }
  304. }
  305. }