@@ -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
@@ -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);
new file mode 100644
@@ -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);
+---
@@ -96,6 +96,11 @@  public function handle($http_data) {
 
 		$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';
@@ -130,6 +135,87 @@  public function handle($http_data) {
 		}
 	}
 
+	/*
+	 * 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.
 	 *
 
  
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> --- v2: - Fix column name scheme - Support sqlite - Simplify deletion of old limits - Put DDL SQL in schema - Allow to disable limit by setting to 0 in config conf/config.proto | 4 +++ schema/aur-schema.sql | 10 ++++++ upgrading/4.7.0.txt | 11 ++++++ web/lib/aurjson.class.php | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 upgrading/4.7.0.txt