@@ -6,27 +6,94 @@ package common
6
6
import (
7
7
"net/http"
8
8
"strings"
9
+ "sync/atomic"
10
+ "time"
9
11
10
12
user_model "code.gitea.io/gitea/models/user"
11
13
"code.gitea.io/gitea/modules/reqctx"
12
14
"code.gitea.io/gitea/modules/setting"
15
+ "code.gitea.io/gitea/modules/templates"
16
+ "code.gitea.io/gitea/modules/util"
13
17
"code.gitea.io/gitea/modules/web/middleware"
18
+ "code.gitea.io/gitea/services/context"
14
19
15
20
"github.com/go-chi/chi/v5"
21
+ lru "github.com/hashicorp/golang-lru/v2"
16
22
)
17
23
24
+ const tplStatus503RateLimit templates.TplName = "status/503_ratelimit"
25
+
26
+ type RateLimitToken struct {
27
+ RetryAfter time.Time
28
+ }
29
+
18
30
func BlockExpensive () func (next http.Handler ) http.Handler {
19
- if ! setting .Service .BlockAnonymousAccessExpensive {
31
+ if ! setting .Service .BlockAnonymousAccessExpensive && ! setting . Service . BlockAnonymousAccessOverload {
20
32
return nil
21
33
}
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
+
22
73
return func (next http.Handler ) http.Handler {
74
+ inflightRequestNum := atomic.Int32 {}
23
75
return http .HandlerFunc (func (w http.ResponseWriter , req * http.Request ) {
24
76
ret := determineRequestPriority (reqctx .FromContext (req .Context ()))
25
77
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 )
28
80
return
29
81
}
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
+ }
30
97
}
31
98
next .ServeHTTP (w , req )
32
99
})
@@ -44,6 +111,7 @@ func isRoutePathExpensive(routePattern string) bool {
44
111
"/{username}/{reponame}/blame/" ,
45
112
"/{username}/{reponame}/commit/" ,
46
113
"/{username}/{reponame}/commits/" ,
114
+ "/{username}/{reponame}/compare/" ,
47
115
"/{username}/{reponame}/graph" ,
48
116
"/{username}/{reponame}/media/" ,
49
117
"/{username}/{reponame}/raw/" ,
0 commit comments