<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom"><id>https://blog.tusooa.xyz</id><title>forgejo - 何事西风不待人</title><link href="https://blog.tusooa.xyz"/><subtitle>迷糊萝莉</subtitle><updated>2026-07-09T19:00:00.000Z</updated><entry><id>https://blog.tusooa.xyz/2026/07/09/Stop-crawling-my-code-repositories/</id><title>Stop crawling my code repositories</title><link rel="alternate" href="https://blog.tusooa.xyz/2026/07/09/Stop-crawling-my-code-repositories/"/><published>2026-07-09T19:00:00.000Z</published><content type="html">&lt;p&gt;I don&apos;t know if it&apos;s related to the prevalence of language-model-based automation tools, but for the past few years, I have been seeing logs of page loads of blame pages and file-at-commit pages for my code repositories at &lt;a href=&quot;https://r.lily-is.land&quot;&gt;Forgejo&lt;/a&gt; and &lt;a href=&quot;https://iron.lily-is.land&quot;&gt;Phorge&lt;/a&gt;. Their IP was randomly distributed, and the rate goes up to 10 requests per second. This does not look normal for our small site, and it made the load average of the server hike up to 40 (!).&lt;/p&gt;
&lt;a&gt;&lt;/a&gt;
&lt;p&gt;For Forgejo, the main problem is it invokes git every time it receives such request, and that git command is very costly. For Phorge, the main cost is invoking the pygmentize code highlighter.&lt;/p&gt;
&lt;p&gt;So I decided to rate-limit the requests. I started with 5 requests per second limit, and paired that with fail2ban. The load went down, but it wasn&apos;t enough for us to maintain the stability of the server. Normal requests still took unreasonable time and it could time out at any moment. So I set the rate-limit to a more aggressive 1 request per second. Good enough for some time, but still problematic, as the rate-limit often unnecessarily hurts the experience of legit users like myself, and I have seen myself banned by fail2ban because I was the only one requested twice in a row with the same IP. Crawlers, on the other hand, always use hundreds of thousands of different IPs.&lt;/p&gt;
&lt;p&gt;So this needs to end. One way to do it is to let logged-in users view the pages as usual while keeping out non-logged-in crawlers.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You probably want to interject for a moment, saying &quot;Why not proof of work?&quot;
And the answer is, I really hate the idea of using a machine to check whether
someone is human. So things like Anubis or Go Away is none of interest for me.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And I let the shit generator to generate such a config for me. The idea is to use the session cookie: ask the reverse proxy to access a page that requires authentication, but is much less costly than rendering the file-at-commit and blame pages.&lt;/p&gt;
&lt;p&gt;For Forgejo, the page could be &lt;code&gt;/user/settings&lt;/code&gt;. It will return 200 if the user is logged in, but non-logged-in users will get a &lt;code&gt;303&lt;/code&gt; redirect to the login page. The result is the nginx configuration below:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;  location = /_auth {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    internal;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_method HEAD;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_pass http://forgejo/user/settings;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_pass_request_body off;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header Content-Length &quot;&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header Host r.lily-is.land;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header Cookie $http_cookie;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_intercept_errors on;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    error_page 301 302 303 307 =401 /_auth_fail;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  location @login_redirect {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    add_header Content-Type text/html always;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return 401 &quot;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Unauthorized&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Due to spam, we have limited file at commit and blame pages to logged-in users. Please &amp;lt;a href=&apos;https://r.lily-is.land/user/login&apos;&amp;gt;login&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  location ~ ^/[^/]+/[^/]+/(src|commits|raw|blame)/commit {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    auth_request /_auth;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    error_page 401 = @login_redirect;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_pass http://forgejo;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header Connection $http_connection;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header Upgrade $http_upgrade;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header Host $host;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header X-Real-IP $remote_addr;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header X-Forwarded-Proto $scheme;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Phorge, the problem is a little bit more tricky, because I cannot find something that return 2xx if and only if the user is logged in. Quite the opposite, many pages, like the settings page, return 200 if and only if the user is &lt;strong&gt;not&lt;/strong&gt; logged in. So I went on with composing another mini-server that analyze the result and convert the status code to my needs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;#!/usr/bin/env python3&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;import&lt;/span&gt;&lt;span&gt; http.server, urllib.request&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;PHORGE&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &quot;http://......&quot;&lt;/span&gt;&lt;span&gt; # intranet address of phorge instance&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;PHORGE_HOST&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; &quot;iron.lily-is.land&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt; Handler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;server&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    def&lt;/span&gt;&lt;span&gt; do_GET&lt;/span&gt;&lt;span&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        cookie &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; self&lt;/span&gt;&lt;span&gt;.headers.get(&lt;/span&gt;&lt;span&gt;&quot;Cookie&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        req &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; urllib.request.Request(&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;{PHORGE}&lt;/span&gt;&lt;span&gt;/settings/&quot;&lt;/span&gt;&lt;span&gt;, method&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;&quot;HEAD&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        req.add_header(&lt;/span&gt;&lt;span&gt;&quot;Host&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;PHORGE_HOST&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        req.add_header(&lt;/span&gt;&lt;span&gt;&quot;Cookie&quot;&lt;/span&gt;&lt;span&gt;, cookie)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        try&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            resp &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; urllib.request.urlopen(req)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Response:&apos;&lt;/span&gt;&lt;span&gt;, resp)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            self&lt;/span&gt;&lt;span&gt;.send_response(&lt;/span&gt;&lt;span&gt;200&lt;/span&gt;&lt;span&gt; if&lt;/span&gt;&lt;span&gt; resp.url.startswith(&lt;/span&gt;&lt;span&gt;f&lt;/span&gt;&lt;span&gt;&quot;https://&lt;/span&gt;&lt;span&gt;{PHORGE_HOST}&lt;/span&gt;&lt;span&gt;/settings/user/&quot;&lt;/span&gt;&lt;span&gt;) &lt;/span&gt;&lt;span&gt;else&lt;/span&gt;&lt;span&gt; 401&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        except&lt;/span&gt;&lt;span&gt; Exception&lt;/span&gt;&lt;span&gt; as&lt;/span&gt;&lt;span&gt; e:&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            print&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;Exception:&apos;&lt;/span&gt;&lt;span&gt;, e)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;            self&lt;/span&gt;&lt;span&gt;.send_response(&lt;/span&gt;&lt;span&gt;401&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;        self&lt;/span&gt;&lt;span&gt;.end_headers()&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;http.server.HTTPServer((&lt;/span&gt;&lt;span&gt;&quot;0.0.0.0&quot;&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;9090&lt;/span&gt;&lt;span&gt;), Handler).serve_forever()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reasoning is as follows: When accessing &lt;code&gt;HEAD /settings/&lt;/code&gt;, if the session cookie is valid for a logged-in user, it redirects to &lt;code&gt;&amp;lt;Public-Address&amp;gt;/settings/user/&amp;lt;Username&amp;gt;/&lt;/code&gt;, otherwise it, well, it can behave in a couple of ways, like returning 200 (with a login form if it is a GET request) or 500 (why?). It is worth notice that the session cookie has &lt;code&gt;phusr=&amp;lt;Username&amp;gt;&lt;/code&gt;, and I tried requesting the endpoint with a bad cookie like &lt;code&gt;phsid=xxx; phusr=tusooa&lt;/code&gt; (where xxx is a valid session id but with one letter different, because Phorge tends to check length and format before actual checks!), and it will not make the redirect to &lt;code&gt;/settings/user/tusooa/&lt;/code&gt;, so this method is safe.&lt;/p&gt;
&lt;p&gt;And the nginx configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;  location = /_phorge_auth {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    internal;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_pass http://logincheckserver/; # Must have trailing slash, otherwise it requests GET /_phorge_auth instead of GET /&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_pass_request_body off;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header Cookie $http_cookie;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header Content-Length &quot;&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  location ~ ^/diffusion/[^/]+/(history|browse) {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    auth_request /_phorge_auth;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_pass http://phorge;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_redirect off;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header Host $http_host;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header X-Real-IP $remote_addr;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header X-Forwarded-Proto $scheme;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header X-Forwarded-Protocol $scheme;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    proxy_set_header X-Url-Scheme $scheme;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    error_page 401 = @not_logged_in;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  location @not_logged_in {&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    add_header Content-Type text/html always;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;    return 401 &quot;&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;h1&amp;gt;Unauthorized&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Due to spam, we have limited file at commit and blame pages to logged-in users. Please &amp;lt;a href=&apos;/auth/start/&apos;&amp;gt;login&amp;lt;/a&amp;gt;, use the &amp;lt;a href=&apos;https://r.lily-is.land/&apos;&amp;gt;Forgejo instance&amp;lt;/a&amp;gt;, or better yet, clone the repository for your local use.&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hopefully I can get rid of the crawlers soon. Last time I disabled these endpoints completely on Forgejo and returned 403 for them, the rate of crawler requests decreased significantly.&lt;/p&gt;</content></entry></feed>