summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFlorian Pritz <bluewind@xinu.at>2018-02-01 11:55:44 +0100
committerLukas Fleischer <lfleischer@archlinux.org>2018-02-24 14:57:31 +0100
commit27654afadb5088dda4eafd83f07410c2a48fa4b0 (patch)
tree504d5fdb20e8f6be6a757f70054edc1db737b839
parentf51d4c32cd8bed69d2f6b0c50424280613c68496 (diff)
downloadaur-27654afadb5088dda4eafd83f07410c2a48fa4b0.tar.gz
aur-27654afadb5088dda4eafd83f07410c2a48fa4b0.tar.xz
Add rate limit support to API
This allows us to prevent users from hammering the API every few seconds to check if any of their packages were updated. Real world users check as often as every 5 or 10 seconds. Signed-off-by: Florian Pritz <bluewind@xinu.at> Signed-off-by: Lukas Fleischer <lfleischer@archlinux.org>
-rw-r--r--conf/config.proto4
-rw-r--r--schema/aur-schema.sql10
-rw-r--r--upgrading/4.7.0.txt11
-rw-r--r--web/lib/aurjson.class.php86
4 files changed, 111 insertions, 0 deletions
diff --git a/conf/config.proto b/conf/config.proto
index 17509299..934d3697 100644
--- a/conf/config.proto
+++ b/conf/config.proto
@@ -36,6 +36,10 @@ enable-maintenance = 1
maintenance-exceptions = 127.0.0.1
render-comment-cmd = /usr/local/bin/aurweb-rendercomment
+[ratelimit]
+request_limit = 4000
+window_length = 86400
+
[notifications]
notify-cmd = /usr/local/bin/aurweb-notify
sendmail = /usr/bin/sendmail
diff --git a/schema/aur-schema.sql b/schema/aur-schema.sql
index 45272bbe..79de3f27 100644
--- a/schema/aur-schema.sql
+++ b/schema/aur-schema.sql
@@ -399,3 +399,13 @@ CREATE TABLE AcceptedTerms (
FOREIGN KEY (UsersID) REFERENCES Users(ID) ON DELETE CASCADE,
FOREIGN KEY (TermsID) REFERENCES Terms(ID) ON DELETE CASCADE
) ENGINE = InnoDB;
+
+-- Rate limits for API
+--
+CREATE TABLE `ApiRateLimit` (
+ IP VARCHAR(45) NOT NULL,
+ Requests INT(11) NOT NULL,
+ WindowStart BIGINT(20) NOT NULL,
+ PRIMARY KEY (`ip`)
+) ENGINE = InnoDB;
+CREATE INDEX ApiRateLimitWindowStart ON ApiRateLimit (WindowStart);
diff --git a/upgrading/4.7.0.txt b/upgrading/4.7.0.txt
new file mode 100644
index 00000000..820e4540
--- /dev/null
+++ b/upgrading/4.7.0.txt
@@ -0,0 +1,11 @@
+1. Add ApiRateLimit table:
+
+---
+CREATE TABLE `ApiRateLimit` (
+ IP VARCHAR(45) NOT NULL,
+ Requests INT(11) NOT NULL,
+ WindowStart BIGINT(20) NOT NULL,
+ PRIMARY KEY (`ip`)
+) ENGINE = InnoDB;
+CREATE INDEX ApiRateLimitWindowStart ON ApiRateLimit (WindowStart);
+---
diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php
index 9eeaafde..b4cced04 100644
--- a/web/lib/aurjson.class.php
+++ b/web/lib/aurjson.class.php
@@ -96,6 +96,11 @@ class AurJSON {
$this->dbh = DB::connect();
+ if ($this->check_ratelimit($_SERVER['REMOTE_ADDR'])) {
+ header("HTTP/1.1 429 Too Many Requests");
+ return $this->json_error('Rate limit reached');
+ }
+
$type = str_replace('-', '_', $http_data['type']);
if ($type == 'info' && $this->version >= 5) {
$type = 'multiinfo';
@@ -131,6 +136,87 @@ class AurJSON {
}
/*
+ * Check if an IP needs to be rate limited.
+ *
+ * @param $ip IP of the current request
+ *
+ * @return true if IP needs to be rate limited, false otherwise.
+ */
+ private function check_ratelimit($ip) {
+ $limit = config_get("ratelimit", "request_limit");
+ if ($limit == 0) {
+ return false;
+ }
+
+ $window_length = config_get("ratelimit", "window_length");
+ $this->update_ratelimit($ip);
+ $stmt = $this->dbh->prepare("
+ SELECT Requests FROM ApiRateLimit
+ WHERE IP = :ip");
+ $stmt->bindParam(":ip", $ip);
+ $result = $stmt->execute();
+
+ if (!$result) {
+ return false;
+ }
+
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+ if ($row['Requests'] > $limit) {
+ return true;
+ }
+ return false;
+ }
+
+ /*
+ * Update a rate limit for an IP by increasing it's requests value by one.
+ *
+ * @param $ip IP of the current request
+ *
+ * @return void
+ */
+ private function update_ratelimit($ip) {
+ $window_length = config_get("ratelimit", "window_length");
+ $db_backend = config_get("database", "backend");
+ $time = time();
+
+ // Clean up old windows
+ $deletion_time = $time - $window_length;
+ $stmt = $this->dbh->prepare("
+ DELETE FROM ApiRateLimit
+ WHERE WindowStart < :time");
+ $stmt->bindParam(":time", $deletion_time);
+ $stmt->execute();
+
+ if ($db_backend == "mysql") {
+ $stmt = $this->dbh->prepare("
+ INSERT INTO ApiRateLimit
+ (IP, Requests, WindowStart)
+ VALUES (:ip, 1, :window_start)
+ ON DUPLICATE KEY UPDATE Requests=Requests+1");
+ $stmt->bindParam(":ip", $ip);
+ $stmt->bindParam(":window_start", $time);
+ $stmt->execute();
+ } elseif ($db_backend == "sqlite") {
+ $stmt = $this->dbh->prepare("
+ INSERT OR IGNORE INTO ApiRateLimit
+ (IP, Requests, WindowStart)
+ VALUES (:ip, 0, :window_start);");
+ $stmt->bindParam(":ip", $ip);
+ $stmt->bindParam(":window_start", $time);
+ $stmt->execute();
+
+ $stmt = $this->dbh->prepare("
+ UPDATE ApiRateLimit
+ SET Requests = Requests + 1
+ WHERE IP = :ip");
+ $stmt->bindParam(":ip", $ip);
+ $stmt->execute();
+ } else {
+ throw new RuntimeException("Unknown database backend");
+ }
+ }
+
+ /*
* Returns a JSON formatted error string.
*
* @param $msg The error string to return