summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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