mode "755"
end
+directory "/srv/tile.openstreetmap.org/conf" do
+ owner "tile"
+ group "tile"
+ mode "755"
+end
+
+file "/srv/tile.openstreetmap.org/conf/ip.map" do
+ owner "tile"
+ group "adm"
+ mode "644"
+end
+
package "renderd"
systemd_service "renderd" do
python3-pyproj
]
+gem_package "apachelogregex"
+gem_package "file-tail"
+gem_package "lru_redux"
+
remote_directory "/usr/local/bin" do
source "bin"
owner "root"
files_mode "755"
end
+template "/usr/local/bin/tile-ratelimit" do
+ source "tile-ratelimit.erb"
+ owner "root"
+ group "root"
+ mode "755"
+end
+
+systemd_service "tile-ratelimit" do
+ description "Monitor tile requests and enforce rate limits"
+ after "apache2.service"
+ user "tile"
+ group "adm"
+ exec_start "/usr/local/bin/tile-ratelimit"
+ private_tmp true
+ private_devices true
+ private_network true
+ protect_system "full"
+ protect_home true
+ read_write_paths "/srv/tile.openstreetmap.org/conf"
+ no_new_privileges true
+ restart "on-failure"
+end
+
+service "tile-ratelimit" do
+ action [:enable, :start]
+ subscribes :restart, "file[/usr/local/bin/time-ratelimit]"
+ subscribes :restart, "systemd_service[tile-ratelimit]"
+end
+
template "/usr/local/bin/expire-tiles" do
source "expire-tiles.erb"
owner "root"
# Enable the rewrite engine
RewriteEngine on
+ # Enforce rate limits
+ RewriteMap ipmap txt:/srv/tile.openstreetmap.org/conf/ip.map
+ RewriteCond ${ipmap:%{REMOTE_ADDR}} ^.+$
+ RewriteRule ^.*$ /${ipmap:%{REMOTE_ADDR}} [PT]
+
# Rewrite tile requests to the default style
RewriteRule ^/(\d+)/(\d+)/(\d+)\.png$ /default/$1/$2/$3.png [PT,T=image/png,L]
RewriteRule ^/(\d+)/(\d+)/(\d+)\.png/status/?$ /default/$1/$2/$3.png/status [PT,T=text/plain,L]
# Redirect ACME certificate challenges
RedirectPermanent /.well-known/acme-challenge/ http://acme.openstreetmap.org/.well-known/acme-challenge/
+
+ # Internal endpoint for blocked users
+ <Location /blocked>
+ Header always set Cache-Control private
+ Redirect 429
+ </Location>
</VirtualHost>
<VirtualHost *:80>
--- /dev/null
+#!/usr/bin/ruby
+
+require "apache_log_regex"
+require "date"
+require "file-tail"
+require "gdbm"
+require "lru_redux"
+
+REQUESTS_PER_SECOND = <%= node[:tile][:ratelimit][:requests_per_second] %>
+BLOCK_AT = <%= node[:tile][:ratelimit][:maximum_backlog] %>
+UNBLOCK_AT = BLOCK_AT / 2
+
+parser = ApacheLogRegex.new('%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"')
+clients = LruRedux::Cache.new(1000000)
+
+def decay_count(client, time)
+ decay = (time.to_i - client[:last_update]) * REQUESTS_PER_SECOND
+
+ client[:request_count] = [client[:request_count] - decay, 0].max
+ client[:last_update] = time.to_i
+end
+
+def write_blocked_ips(clients)
+ time = Time.now
+
+ File.open("/srv/tile.openstreetmap.org/conf/ip.map.new", "w") do |file|
+ clients.each do |address, client|
+ decay_count(client, time)
+
+ if client[:request_count] >= UNBLOCK_AT
+ file.puts "#{address} blocked"
+ elsif client.has_key?(:blocked_at)
+ puts "Unblocked #{address}"
+
+ client.delete(:blocked_at)
+ end
+ end
+ end
+
+ File.rename("/srv/tile.openstreetmap.org/conf/ip.map.new",
+ "/srv/tile.openstreetmap.org/conf/ip.map")
+
+ time + 900
+end
+
+next_check = write_blocked_ips(clients)
+
+File::Tail::Logfile.tail("/var/log/apache2/access.log") do |line|
+ begin
+ hash = parser.parse!(line)
+
+ address = hash["%a"]
+
+ next if address == "127.0.0.1" || address == "::1"
+
+ time = Time.now
+
+ client = clients.getset(address) do
+ { :request_count => 0, :last_update => 0 }
+ end
+
+ decay_count(client, time)
+
+ client[:request_count] = client[:request_count] + 1
+
+ if client[:request_count] > BLOCK_AT && !client.has_key?(:blocked_at)
+ puts "Blocked #{address}"
+
+ client[:blocked_at] = time
+
+ next_check = time
+ elsif client[:request_count] < UNBLOCK_AT && client.has_key?(:blocked_at)
+ puts "Unblocked #{address}"
+
+ client.delete(:blocked_at)
+
+ next_check = time
+ end
+
+ if time >= next_check
+ next_check = write_blocked_ips(clients)
+ end
+ rescue ApacheLogRegex::ParseError
+ # nil
+ end
+end