@@ -6,27 +6,94 @@ package common
66import (
77 "net/http"
88 "strings"
9+ "sync/atomic"
10+ "time"
911
1012 user_model "code.gitea.io/gitea/models/user"
1113 "code.gitea.io/gitea/modules/reqctx"
1214 "code.gitea.io/gitea/modules/setting"
15+ "code.gitea.io/gitea/modules/templates"
16+ "code.gitea.io/gitea/modules/util"
1317 "code.gitea.io/gitea/modules/web/middleware"
18+ "code.gitea.io/gitea/services/context"
1419
1520 "github.com/go-chi/chi/v5"
21+ lru "github.com/hashicorp/golang-lru/v2"
1622)
1723
24+ const tplStatus503RateLimit templates.TplName = "status/503_ratelimit"
25+
26+ type RateLimitToken struct {
27+ RetryAfter time.Time
28+ }
29+
1830func BlockExpensive () func (next http.Handler ) http.Handler {
19- if ! setting .Service .BlockAnonymousAccessExpensive {
31+ if ! setting .Service .BlockAnonymousAccessExpensive && ! setting . Service . BlockAnonymousAccessOverload {
2032 return nil
2133 }
34+
35+ tokenCache , _ := lru.New [string , RateLimitToken ](10000 )
36+
37+ deferAnonymousRateLimitAccess := func (w http.ResponseWriter , req * http.Request ) bool {
38+ // * For a crawler: if it sees 503 error, it would retry later (they have their own queue), there is still a chance for them to read all pages
39+ // * For a real anonymous user: allocate a token, and let them wait for a while by browser JS (queue the request by browser)
40+
41+ const tokenCookieName = "gitea_arlt" // gitea anonymous rate limit token
42+ cookieToken , _ := req .Cookie (tokenCookieName )
43+ if cookieToken != nil && cookieToken .Value != "" {
44+ token , exist := tokenCache .Get (cookieToken .Value )
45+ if exist {
46+ if time .Now ().After (token .RetryAfter ) {
47+ // still valid
48+ tokenCache .Remove (cookieToken .Value )
49+ return false
50+ }
51+ // not reach RetryAfter time, so either remove the old one and allocate a new one, or keep using the old one
52+ // TODO: in the future, we could do better to allow more accesses for the same token, or extend the expiration time if the access seems well-behaved
53+ tokenCache .Remove (cookieToken .Value )
54+ }
55+ }
56+
57+ // TODO: merge the code with RenderPanicErrorPage
58+ tmplCtx := context.TemplateContext {}
59+ tmplCtx ["Locale" ] = middleware .Locale (w , req )
60+ ctxData := middleware .GetContextData (req .Context ())
61+
62+ tokenKey , _ := util .CryptoRandomString (32 )
63+ retryAfterDuration := 1 * time .Second
64+ token := RateLimitToken {RetryAfter : time .Now ().Add (retryAfterDuration )}
65+ tokenCache .Add (tokenKey , token )
66+ ctxData ["RateLimitTokenKey" ] = tokenKey
67+ ctxData ["RateLimitCookieName" ] = tokenCookieName
68+ ctxData ["RateLimitRetryAfterMs" ] = retryAfterDuration .Milliseconds () + 100
69+ _ = templates .HTMLRenderer ().HTML (w , http .StatusServiceUnavailable , tplStatus503RateLimit , ctxData , tmplCtx )
70+ return true
71+ }
72+
2273 return func (next http.Handler ) http.Handler {
74+ inflightRequestNum := atomic.Int32 {}
2375 return http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
2476 ret := determineRequestPriority (reqctx .FromContext (req .Context ()))
2577 if ! ret .SignedIn {
26- if ret .Expensive || ret . LongPolling {
27- http .Redirect (w , req , setting . AppSubURL + "/user/login" , http .StatusSeeOther )
78+ if ret .LongPolling {
79+ http .Error (w , "Long polling is not allowed for anonymous users" , http .StatusForbidden )
2880 return
2981 }
82+ if ret .Expensive {
83+ inflightNum := inflightRequestNum .Add (1 )
84+ defer inflightRequestNum .Add (- 1 )
85+
86+ if setting .Service .BlockAnonymousAccessExpensive {
87+ // strictly block the anonymous accesses to expensive pages, to save CPU
88+ http .Redirect (w , req , setting .AppSubURL + "/user/login" , http .StatusSeeOther )
89+ return
90+ } else if int (inflightNum ) > setting .Service .OverloadInflightAnonymousRequests {
91+ // be friendly to anonymous access (crawler, real anonymous user) to expensive pages, but limit the inflight requests
92+ if deferAnonymousRateLimitAccess (w , req ) {
93+ return
94+ }
95+ }
96+ }
3097 }
3198 next .ServeHTTP (w , req )
3299 })
@@ -44,6 +111,7 @@ func isRoutePathExpensive(routePattern string) bool {
44111 "/{username}/{reponame}/blame/" ,
45112 "/{username}/{reponame}/commit/" ,
46113 "/{username}/{reponame}/commits/" ,
114+ "/{username}/{reponame}/compare/" ,
47115 "/{username}/{reponame}/graph" ,
48116 "/{username}/{reponame}/media/" ,
49117 "/{username}/{reponame}/raw/" ,
0 commit comments