feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
@@ -10,6 +10,7 @@ import (
"strings"
"text/template"
"time"
"unicode"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
@@ -37,6 +38,16 @@ const keyOfRequestIDInTemplate = ".RequestID"
// So, we accept a Request ID with a maximum character length of 40
const maxRequestIDByteLength = 40
func isSafeRequestID(id string) bool {
for _, r := range id {
safe := unicode.IsPrint(r)
if !safe {
return false
}
}
return true
}
func parseRequestIDFromRequestHeader(req *http.Request) string {
requestID := "-"
for _, key := range setting.Log.RequestIDHeaders {
@@ -45,6 +56,9 @@ func parseRequestIDFromRequestHeader(req *http.Request) string {
break
}
}
if !isSafeRequestID(requestID) {
return "-"
}
if len(requestID) > maxRequestIDByteLength {
requestID = requestID[:maxRequestIDByteLength] + "..."
}
@@ -69,3 +69,8 @@ func TestAccessLogger(t *testing.T) {
recorder.record(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), &testAccessLoggerResponseWriterMock{}, req)
assert.Equal(t, []string{`remote-addr - - [02/Jan/2000:03:04:05 +0000] "GET /path https" 200 123123 "referer" "user-agent"`}, mockLogger.logs)
}
func TestAccessLoggerRequestID(t *testing.T) {
assert.False(t, isSafeRequestID("\x00"))
assert.True(t, isSafeRequestID("a b-c"))
}
+12 -6
View File
@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
@@ -47,6 +48,12 @@ type APIContext struct {
PublicOnly bool // Whether the request is for a public endpoint
}
// TokenCanAccessRepo reports whether the current API token is allowed to access the repository.
// A public-only token cannot reach a private repo; any other token is unrestricted by this check.
func (ctx *APIContext) TokenCanAccessRepo(repo *repo_model.Repository) bool {
return repo == nil || !ctx.PublicOnly || !repo.IsPrivate
}
func init() {
web.RegisterResponseStatusProvider[*APIContext](func(req *http.Request) web_types.ResponseStatusProvider {
return req.Context().Value(apiContextKey).(*APIContext)
@@ -163,7 +170,7 @@ func GetAPIContext(req *http.Request) *APIContext {
return req.Context().Value(apiContextKey).(*APIContext)
}
func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string {
func genAPILinks(curURL *url.URL, total int64, pageSize, curPage int) []string {
page := NewPagination(total, pageSize, curPage, 0)
paginater := page.Paginater
links := make([]string, 0, 4)
@@ -204,7 +211,8 @@ func genAPILinks(curURL *url.URL, total, pageSize, curPage int) []string {
}
// SetLinkHeader sets pagination link header by given total number and page size.
func (ctx *APIContext) SetLinkHeader(total, pageSize int) {
// "count" is usually from database result "count int64", so it also uses int64,
func (ctx *APIContext) SetLinkHeader(total int64, pageSize int) {
links := genAPILinks(ctx.Req.URL, total, pageSize, ctx.FormInt("page"))
if len(links) > 0 {
@@ -221,13 +229,13 @@ func APIContexter() func(http.Handler) http.Handler {
ctx := &APIContext{
Base: base,
Cache: cache.GetCache(),
Repo: &Repository{PullRequest: &PullRequest{}},
Repo: &Repository{},
Org: &APIOrganization{},
}
ctx.SetContextValue(apiContextKey, ctx)
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
// FIXME: GLOBAL-PARSE-FORM: see more details in another FIXME comment
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
if !ctx.ParseMultipartForm() {
return
@@ -235,8 +243,6 @@ func APIContexter() func(http.Handler) http.Handler {
}
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
next.ServeHTTP(ctx.Resp, ctx.Req)
})
}
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
)
@@ -147,10 +148,7 @@ func (b *Base) PlainText(status int, text string) {
// Redirect redirects the request
func (b *Base) Redirect(location string, status ...int) {
code := http.StatusSeeOther
if len(status) == 1 {
code = status[0]
}
code := util.OptionalArg(status, http.StatusSeeOther)
if !httplib.IsRelativeURL(location) {
// Some browsers (Safari) have buggy behavior for Cookie + Cache + External Redirection, eg: /my-path => https://other/path
@@ -172,15 +170,15 @@ func (b *Base) Redirect(location string, status ...int) {
http.Redirect(b.Resp, b.Req, location, code)
}
type ServeHeaderOptions httplib.ServeHeaderOptions
type ServeHeaderOptions = httplib.ServeHeaderOptions
func (b *Base) SetServeHeaders(opt *ServeHeaderOptions) {
httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opt))
func (b *Base) SetServeHeaders(opts ServeHeaderOptions) {
httplib.ServeSetHeaders(b.Resp, opts)
}
// ServeContent serves content to http request
func (b *Base) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) {
httplib.ServeSetHeaders(b.Resp, (*httplib.ServeHeaderOptions)(opts))
func (b *Base) ServeContent(r io.ReadSeeker, opts ServeHeaderOptions) {
httplib.ServeSetHeaders(b.Resp, opts)
http.ServeContent(b.Resp, b.Req, opts.Filename, opts.LastModified, r)
}
@@ -37,11 +37,16 @@ func (b *Base) PathParamInt64(p string) int64 {
return v
}
func (b *Base) PathParamInt(p string) int {
v, _ := strconv.Atoi(b.PathParam(p))
return v
}
// SetPathParam set request path params into routes
func (b *Base) SetPathParam(name, value string) {
if strings.HasPrefix(name, ":") {
setting.PanicInDevOrTesting("path param should not start with ':'")
name = name[1:]
}
chi.RouteContext(b).URLParams.Add(name, url.PathEscape(value))
}
func (b *Base) SetPathParamRaw(name, value string) {
chi.RouteContext(b).URLParams.Add(name, value)
}
@@ -5,6 +5,7 @@ package context
import (
"fmt"
"image/color"
"sync"
"code.gitea.io/gitea/modules/cache"
@@ -29,32 +30,25 @@ func GetImageCaptcha() *captcha.Captcha {
imageCaptchaOnce.Do(func() {
cpt = captcha.NewCaptcha(captcha.Options{
SubURL: setting.AppSubURL,
// Use a color palette with high contrast colors suitable for both light and dark modes
// These colors provide good visibility and readability in both themes
ColorPalette: color.Palette{
color.RGBA{R: 234, G: 67, B: 53, A: 255}, // Bright red
color.RGBA{R: 66, G: 133, B: 244, A: 255}, // Medium blue
color.RGBA{R: 52, G: 168, B: 83, A: 255}, // Green
color.RGBA{R: 251, G: 188, B: 5, A: 255}, // Yellow/gold
color.RGBA{R: 171, G: 71, B: 188, A: 255}, // Purple
},
})
cpt.Store = cache.GetCache().ChiCache()
})
return cpt
}
// SetCaptchaData sets common captcha data
func SetCaptchaData(ctx *Context) {
if !setting.Service.EnableCaptcha {
return
}
ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha
ctx.Data["RecaptchaURL"] = setting.Service.RecaptchaURL
ctx.Data["Captcha"] = GetImageCaptcha()
ctx.Data["CaptchaType"] = setting.Service.CaptchaType
ctx.Data["RecaptchaSitekey"] = setting.Service.RecaptchaSitekey
ctx.Data["HcaptchaSitekey"] = setting.Service.HcaptchaSitekey
ctx.Data["McaptchaSitekey"] = setting.Service.McaptchaSitekey
ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL
ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey
}
const (
gRecaptchaResponseField = "g-recaptcha-response"
hCaptchaResponseField = "h-captcha-response"
mCaptchaResponseField = "m-captcha-response"
mCaptchaResponseField = "mcaptcha__token" // this form key is hard-coded in the mcaptcha frontend library
cfTurnstileResponseField = "cf-turnstile-response"
)
@@ -88,6 +82,6 @@ func VerifyCaptcha(ctx *Context, tpl templates.TplName, form any) {
if !valid {
ctx.Data["Err_Captcha"] = true
ctx.RenderWithErr(ctx.Tr("form.captcha_incorrect"), tpl, form)
ctx.RenderWithErrDeprecated(ctx.Tr("form.captcha_incorrect"), tpl, form)
}
}
@@ -6,19 +6,18 @@ package context
import (
"context"
"encoding/hex"
"fmt"
"html/template"
"io"
"net/http"
"net/url"
"strings"
"time"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/httpcache"
"code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/session"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates"
@@ -48,7 +47,6 @@ type Context struct {
PageData map[string]any // data used by JavaScript modules in one page, it's `window.config.pageData`
Cache cache.StringCache
Csrf CSRFProtector
Flash *middleware.Flash
Session session.Store
@@ -102,12 +100,13 @@ func GetValidateContext(req *http.Request) (ctx *ValidateContext) {
return ctx
}
func NewTemplateContextForWeb(ctx *Context) TemplateContext {
tmplCtx := NewTemplateContext(ctx)
tmplCtx["Locale"] = ctx.Base.Locale
func NewTemplateContextForWeb(ctx reqctx.RequestContext, req *http.Request, locale translation.Locale) TemplateContext {
tmplCtx := NewTemplateContext(ctx, req)
tmplCtx["Locale"] = locale
tmplCtx["AvatarUtils"] = templates.NewAvatarUtils(ctx)
tmplCtx["RenderUtils"] = templates.NewRenderUtils(ctx)
tmplCtx["RootData"] = ctx.Data
tmplCtx["MiscUtils"] = templates.NewMiscUtils(ctx)
tmplCtx["RootData"] = ctx.GetData()
tmplCtx["Consts"] = map[string]any{
"RepoUnitTypeCode": unit.TypeCode,
"RepoUnitTypeIssues": unit.TypeIssues,
@@ -131,30 +130,36 @@ func NewWebContext(base *Base, render Render, session session.Store) *Context {
Cache: cache.GetCache(),
Link: setting.AppSubURL + strings.TrimSuffix(base.Req.URL.EscapedPath(), "/"),
Repo: &Repository{PullRequest: &PullRequest{}},
Repo: &Repository{},
Org: &Organization{},
}
ctx.TemplateContext = NewTemplateContextForWeb(ctx)
ctx.TemplateContext = NewTemplateContextForWeb(ctx, ctx.Base.Req, ctx.Base.Locale)
ctx.Flash = &middleware.Flash{DataStore: ctx, Values: url.Values{}}
ctx.SetContextValue(WebContextKey, ctx)
return ctx
}
func ContexterInstallPage(data map[string]any) func(next http.Handler) http.Handler {
rnd := templates.PageRenderer()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base := NewBaseContext(resp, req)
ctx := NewWebContext(base, rnd, session.GetContextSession(req))
ctx.Data.MergeFrom(middleware.CommonTemplateContextData())
ctx.Data.MergeFrom(reqctx.ContextData{
"Title": ctx.Locale.Tr("install.install"),
"PageIsInstall": true,
"AllLangs": translation.AllLangs(),
})
ctx.Data.MergeFrom(data)
next.ServeHTTP(resp, ctx.Req)
})
}
}
// Contexter initializes a classic context for a request.
func Contexter() func(next http.Handler) http.Handler {
rnd := templates.HTMLRenderer()
csrfOpts := CsrfOptions{
Secret: hex.EncodeToString(setting.GetGeneralTokenSigningSecret()),
Cookie: setting.CSRFCookieName,
Secure: setting.SessionConfig.Secure,
CookieHTTPOnly: setting.CSRFCookieHTTPOnly,
CookieDomain: setting.SessionConfig.Domain,
CookiePath: setting.SessionConfig.CookiePath,
SameSite: setting.SessionConfig.SameSite,
}
if !setting.IsProd {
CsrfTokenRegenerationInterval = 5 * time.Second // in dev, re-generate the tokens more aggressively for debug purpose
}
rnd := templates.PageRenderer()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base := NewBaseContext(resp, req)
@@ -167,8 +172,6 @@ func Contexter() func(next http.Handler) http.Handler {
ctx.PageData = map[string]any{}
ctx.Data["PageData"] = ctx.PageData
ctx.Csrf = NewCSRFProtector(csrfOpts)
// get the last flash message from cookie
lastFlashCookie, lastFlashMsg := middleware.GetSiteCookieFlashMessage(ctx, ctx.Req, CookieNameFlash)
if vals, _ := url.ParseQuery(lastFlashCookie); len(vals) > 0 {
@@ -184,7 +187,10 @@ func Contexter() func(next http.Handler) http.Handler {
}
})
// If request sends files, parse them here otherwise the Query() can't be parsed and the CsrfToken will be invalid.
// FIXME: GLOBAL-PARSE-FORM: this ParseMultipartForm was used for parsing the csrf token from multipart/form-data
// We have dropped the csrf token, so ideally this global ParseMultipartForm should be removed.
// When removing this, we need to avoid regressions in the handler functions because Golang's http framework is quite fragile
// and developers sometimes need to manually parse the form before accessing some values.
if ctx.Req.Method == http.MethodPost && strings.Contains(ctx.Req.Header.Get("Content-Type"), "multipart/form-data") {
if !ctx.ParseMultipartForm() {
return
@@ -192,7 +198,10 @@ func Contexter() func(next http.Handler) http.Handler {
}
httpcache.SetCacheControlInHeader(ctx.Resp.Header(), &httpcache.CacheControlOptions{NoTransform: true})
ctx.Resp.Header().Set(`X-Frame-Options`, setting.CORSConfig.XFrameOptions)
if setting.Security.XFrameOptions != "unset" {
ctx.Resp.Header().Set(`X-Frame-Options`, setting.Security.XFrameOptions)
}
ctx.Data["SystemConfig"] = setting.Config()
@@ -25,13 +25,11 @@ func removeSessionCookieHeader(w http.ResponseWriter) {
}
// SetSiteCookie convenience function to set most cookies consistently
// CSRF and a few others are the exception here
func (ctx *Context) SetSiteCookie(name, value string, maxAge int) {
middleware.SetSiteCookie(ctx.Resp, name, value, maxAge)
}
// DeleteSiteCookie convenience function to delete most cookies consistently
// CSRF and a few others are the exception here
func (ctx *Context) DeleteSiteCookie(name string) {
middleware.SetSiteCookie(ctx.Resp, name, "", -1)
}
@@ -22,6 +22,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web/middleware"
)
@@ -124,8 +125,12 @@ func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTM
return template.HTML(buf.String()), err
}
// RenderWithErr used for page has form validation but need to prompt error to users.
func (ctx *Context) RenderWithErr(msg any, tpl templates.TplName, form any) {
// RenderWithErrDeprecated render the page with form validation when it needs to prompt error to users.
// Deprecated: use "form-fetch-action" and JSON response instead.
// WARNING: in many cases, this function is not able to render the page or recover the form fields correctly.
// And it is very difficult to test the page rendered by this function.
// DO NOT USE IT ANYMORE.
func (ctx *Context) RenderWithErrDeprecated(msg any, tpl templates.TplName, form any) {
if form != nil {
middleware.AssignForm(form, ctx.Data)
}
@@ -139,11 +144,9 @@ func (ctx *Context) NotFound(logErr error) {
}
func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
// TODO: it's safe to show the error message to end users if the error is fully controlled by our error system
if logErr != nil {
log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr)
if !setting.IsProd {
ctx.Data["ErrorMsg"] = logErr
}
}
// response simple message if Accept isn't text/html
@@ -162,11 +165,17 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
ctx.Data["Title"] = "Page Not Found"
ctx.Data["ErrorMsg"] = "" // FIXME: the template never renders this message, need to fix in the future (and show safe messages to end users)
ctx.HTML(http.StatusNotFound, "status/404")
}
// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
// If the error is controlled by our error system, a related 404 page can be displayed instead.
func (ctx *Context) ServerError(logMsg string, logErr error) {
if errors.Is(logErr, util.ErrNotExist) {
ctx.notFoundInternal(logMsg, logErr)
return
}
ctx.serverErrorInternal(logMsg, logErr)
}
@@ -5,13 +5,26 @@ package context
import (
"context"
"html/template"
"net/http"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/webtheme"
)
var _ context.Context = TemplateContext(nil)
func NewTemplateContext(ctx context.Context) TemplateContext {
return TemplateContext{"_ctx": ctx}
func NewTemplateContext(ctx context.Context, req *http.Request) TemplateContext {
return TemplateContext{"_ctx": ctx, "_req": req}
}
func (c TemplateContext) req() *http.Request {
return c["_req"].(*http.Request)
}
func (c TemplateContext) parentContext() context.Context {
@@ -33,3 +46,40 @@ func (c TemplateContext) Err() error {
func (c TemplateContext) Value(key any) any {
return c.parentContext().Value(key)
}
func (c TemplateContext) CurrentWebTheme() *webtheme.ThemeMetaInfo {
var themeName string
if webCtx := GetWebContext(c); webCtx != nil {
if webCtx.Doer != nil {
themeName = webCtx.Doer.Theme
}
}
if themeName == "" {
themeName = middleware.GetSiteCookie(c.req(), middleware.CookieTheme)
}
return webtheme.GuaranteeGetThemeMetaInfo(themeName)
}
func (c TemplateContext) CurrentWebBanner() *setting.WebBannerType {
// Using revision as a simple approach to determine if the banner has been changed after the user dismissed it.
// There could be some false-positives because revision can be changed even if the banner isn't.
// While it should be still good enough (no admin would keep changing the settings) and doesn't really harm end users (just a few more times to see the banner)
// So it doesn't need to make it more complicated by allocating unique IDs or using hashes.
dismissedBannerRevision, _ := strconv.Atoi(middleware.GetSiteCookie(c.req(), middleware.CookieWebBannerDismissed))
banner, revision, _ := setting.Config().Instance.WebBanner.ValueRevision(c)
if banner.ShouldDisplay() && dismissedBannerRevision != revision {
return &banner
}
return nil
}
// AppFullLink returns a full URL link with AppSubURL for the given app link (no AppSubURL)
// If no link is given, it returns the current app full URL with sub-path but without trailing slash (that's why it is not named as AppURL)
func (c TemplateContext) AppFullLink(link ...string) template.URL {
s := httplib.GuessCurrentAppURL(c.parentContext())
s = strings.TrimSuffix(s, "/")
if len(link) == 0 {
return template.URL(s)
}
return template.URL(s + "/" + strings.TrimPrefix(link[0], "/"))
}
@@ -49,3 +49,17 @@ func TestRedirectToCurrentSite(t *testing.T) {
})
}
}
func TestAppFullLink(t *testing.T) {
setting.IsInTesting = true
defer test.MockVariableValue(&setting.AppURL, "https://gitea.example.com/sub/")()
defer test.MockVariableValue(&setting.AppSubURL, "/sub")()
defer test.MockVariableValue(&setting.PublicURLDetection, setting.PublicURLNever)()
req := httptest.NewRequest(http.MethodGet, "https://gitea.example.com/sub/", nil)
tmplCtx := NewTemplateContext(req.Context(), req)
assert.Equal(t, "https://gitea.example.com/sub", string(tmplCtx.AppFullLink()))
assert.Equal(t, "https://gitea.example.com/sub/user/repo", string(tmplCtx.AppFullLink("user/repo")))
assert.Equal(t, "https://gitea.example.com/sub/user/repo", string(tmplCtx.AppFullLink("/user/repo")))
}
@@ -1,168 +0,0 @@
// Copyright 2013 Martini Authors
// Copyright 2014 The Macaron Authors
// Copyright 2021 The Gitea Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// SPDX-License-Identifier: Apache-2.0
// a middleware that generates and validates CSRF tokens.
package context
import (
"html/template"
"net/http"
"strconv"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/util"
)
const (
CsrfHeaderName = "X-Csrf-Token"
CsrfFormName = "_csrf"
)
// CSRFProtector represents a CSRF protector and is used to get the current token and validate the token.
type CSRFProtector interface {
// PrepareForSessionUser prepares the csrf protector for the current session user.
PrepareForSessionUser(ctx *Context)
// Validate validates the csrf token in http context.
Validate(ctx *Context)
// DeleteCookie deletes the csrf cookie
DeleteCookie(ctx *Context)
}
type csrfProtector struct {
opt CsrfOptions
// id must be unique per user.
id string
// token is the valid one which will be used by end user and passed via header, cookie, or hidden form value.
token string
}
// CsrfOptions maintains options to manage behavior of Generate.
type CsrfOptions struct {
// The global secret value used to generate Tokens.
Secret string
// Cookie value used to set and get token.
Cookie string
// Cookie domain.
CookieDomain string
// Cookie path.
CookiePath string
CookieHTTPOnly bool
// SameSite set the cookie SameSite type
SameSite http.SameSite
// Set the Secure flag to true on the cookie.
Secure bool
// sessionKey is the key used for getting the unique ID per user.
sessionKey string
// oldSessionKey saves old value corresponding to sessionKey.
oldSessionKey string
}
func newCsrfCookie(opt *CsrfOptions, value string) *http.Cookie {
return &http.Cookie{
Name: opt.Cookie,
Value: value,
Path: opt.CookiePath,
Domain: opt.CookieDomain,
MaxAge: int(CsrfTokenTimeout.Seconds()),
Secure: opt.Secure,
HttpOnly: opt.CookieHTTPOnly,
SameSite: opt.SameSite,
}
}
func NewCSRFProtector(opt CsrfOptions) CSRFProtector {
if opt.Secret == "" {
panic("CSRF secret is empty but it must be set") // it shouldn't happen because it is always set in code
}
opt.Cookie = util.IfZero(opt.Cookie, "_csrf")
opt.CookiePath = util.IfZero(opt.CookiePath, "/")
opt.sessionKey = "uid"
opt.oldSessionKey = "_old_" + opt.sessionKey
return &csrfProtector{opt: opt}
}
func (c *csrfProtector) PrepareForSessionUser(ctx *Context) {
c.id = "0"
if uidAny := ctx.Session.Get(c.opt.sessionKey); uidAny != nil {
switch uidVal := uidAny.(type) {
case string:
c.id = uidVal
case int64:
c.id = strconv.FormatInt(uidVal, 10)
default:
log.Error("invalid uid type in session: %T", uidAny)
}
}
oldUID := ctx.Session.Get(c.opt.oldSessionKey)
uidChanged := oldUID == nil || oldUID.(string) != c.id
cookieToken := ctx.GetSiteCookie(c.opt.Cookie)
needsNew := true
if uidChanged {
_ = ctx.Session.Set(c.opt.oldSessionKey, c.id)
} else if cookieToken != "" {
// If cookie token present, re-use existing unexpired token, else generate a new one.
if issueTime, ok := ParseCsrfToken(cookieToken); ok {
dur := time.Since(issueTime) // issueTime is not a monotonic-clock, the server time may change a lot to an early time.
if dur >= -CsrfTokenRegenerationInterval && dur <= CsrfTokenRegenerationInterval {
c.token = cookieToken
needsNew = false
}
}
}
if needsNew {
c.token = GenerateCsrfToken(c.opt.Secret, c.id, "POST", time.Now())
ctx.Resp.Header().Add("Set-Cookie", newCsrfCookie(&c.opt, c.token).String())
}
ctx.Data["CsrfToken"] = c.token
ctx.Data["CsrfTokenHtml"] = template.HTML(`<input type="hidden" name="_csrf" value="` + template.HTMLEscapeString(c.token) + `">`)
}
func (c *csrfProtector) validateToken(ctx *Context, token string) {
if !ValidCsrfToken(token, c.opt.Secret, c.id, "POST", time.Now()) {
c.DeleteCookie(ctx)
// currently, there should be no access to the APIPath with CSRF token. because templates shouldn't use the `/api/` endpoints.
// FIXME: distinguish what the response is for: HTML (web page) or JSON (fetch)
http.Error(ctx.Resp, "Invalid CSRF token.", http.StatusBadRequest)
}
}
// Validate should be used as a per route middleware. It attempts to get a token from an "X-Csrf-Token"
// HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated.
// If this validation fails, http.StatusBadRequest is sent.
func (c *csrfProtector) Validate(ctx *Context) {
if token := ctx.Req.Header.Get(CsrfHeaderName); token != "" {
c.validateToken(ctx, token)
return
}
if token := ctx.Req.FormValue(CsrfFormName); token != "" {
c.validateToken(ctx, token)
return
}
c.validateToken(ctx, "") // no csrf token, use an empty token to respond error
}
func (c *csrfProtector) DeleteCookie(ctx *Context) {
cookie := newCsrfCookie(&c.opt, "")
cookie.MaxAge = -1
ctx.Resp.Header().Add("Set-Cookie", cookie.String())
}
+6 -4
View File
@@ -132,10 +132,12 @@ func OrgAssignment(orgAssignmentOpts OrgAssignmentOptions) func(ctx *Context) {
ctx.ServerError("IsOrgMember", err)
return
}
ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("CanCreateOrgRepo", err)
return
if ctx.Org.IsMember {
ctx.Org.CanCreateOrgRepo, err = org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("CanCreateOrgRepo", err)
return
}
}
}
} else {
@@ -4,6 +4,7 @@
package context
import (
"errors"
"fmt"
"net/http"
@@ -58,23 +59,28 @@ func PackageAssignmentAPI() func(ctx *APIContext) {
}
func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package {
pkg := &Package{
Owner: ctx.ContextUser,
}
var err error
pkg.AccessMode, err = determineAccessMode(ctx.Base, pkg, ctx.Doer)
pkgOwner := ctx.ContextUser
accessMode, err := determineAccessMode(ctx.Base, pkgOwner, ctx.Doer)
if err != nil {
errCb(http.StatusInternalServerError, fmt.Errorf("determineAccessMode: %w", err))
return nil
}
pkg := &Package{
Owner: pkgOwner,
AccessMode: accessMode,
}
packageType := ctx.PathParam("type")
name := ctx.PathParam("name")
if packageType == "" || name == "" {
return pkg
}
packageType := ctx.PathParam("type")
name := ctx.PathParam("name")
version := ctx.PathParam("version")
if packageType != "" && name != "" && version != "" {
if version != "" {
pv, err := packages_model.GetVersionByNameAndVersion(ctx, pkg.Owner.ID, packages_model.Type(packageType), name, version)
if err != nil {
if err == packages_model.ErrPackageNotExist {
if errors.Is(err, packages_model.ErrPackageNotExist) {
errCb(http.StatusNotFound, fmt.Errorf("GetVersionByNameAndVersion: %w", err))
} else {
errCb(http.StatusInternalServerError, fmt.Errorf("GetVersionByNameAndVersion: %w", err))
@@ -87,12 +93,27 @@ func packageAssignment(ctx *packageAssignmentCtx, errCb func(int, any)) *Package
errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageDescriptor: %w", err))
return pkg
}
} else {
p, err := packages_model.GetPackageByName(ctx, pkg.Owner.ID, packages_model.Type(packageType), name)
if err != nil {
if errors.Is(err, packages_model.ErrPackageNotExist) {
errCb(http.StatusNotFound, fmt.Errorf("GetPackageByName: %w", err))
} else {
errCb(http.StatusInternalServerError, fmt.Errorf("GetPackageByName: %w", err))
}
return pkg
}
pkg.Descriptor = &packages_model.PackageDescriptor{
Package: p,
Owner: pkg.Owner,
}
}
return pkg
}
func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.AccessMode, error) {
func determineAccessMode(ctx *Base, pkgOwner, doer *user_model.User) (perm.AccessMode, error) {
if setting.Service.RequireSignInViewStrict && (doer == nil || doer.IsGhost()) {
return perm.AccessModeNone, nil
}
@@ -103,8 +124,8 @@ func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.A
// TODO: ActionUser permission check
accessMode := perm.AccessModeNone
if pkg.Owner.IsOrganization() {
org := organization.OrgFromUser(pkg.Owner)
if pkgOwner.IsOrganization() {
org := organization.OrgFromUser(pkgOwner)
if doer != nil && !doer.IsGhost() {
// 1. If user is logged in, check all team packages permissions
@@ -128,19 +149,19 @@ func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.A
}
}
}
if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, pkg.Owner, doer) {
if accessMode == perm.AccessModeNone && organization.HasOrgOrUserVisible(ctx, pkgOwner, doer) {
// 2. If user is unauthorized or no org member, check if org is visible
accessMode = perm.AccessModeRead
}
} else {
if doer != nil && !doer.IsGhost() {
// 1. Check if user is package owner
if doer.ID == pkg.Owner.ID {
if doer.ID == pkgOwner.ID {
accessMode = perm.AccessModeOwner
} else if pkg.Owner.Visibility == structs.VisibleTypePublic || pkg.Owner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited
} else if pkgOwner.Visibility == structs.VisibleTypePublic || pkgOwner.Visibility == structs.VisibleTypeLimited { // 2. Check if package owner is public or limited
accessMode = perm.AccessModeRead
}
} else if pkg.Owner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public
} else if pkgOwner.Visibility == structs.VisibleTypePublic { // 3. Check if package owner is public
accessMode = perm.AccessModeRead
}
}
@@ -150,7 +171,7 @@ func determineAccessMode(ctx *Base, pkg *Package, doer *user_model.User) (perm.A
// PackageContexter initializes a package context for a request.
func PackageContexter() func(next http.Handler) http.Handler {
renderer := templates.HTMLRenderer()
renderer := templates.PageRenderer()
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
base := NewBaseContext(resp, req)
@@ -6,6 +6,7 @@ package context
import (
"fmt"
"html/template"
"math"
"net/http"
"net/url"
"slices"
@@ -22,11 +23,13 @@ type Pagination struct {
}
// NewPagination creates a new instance of the Pagination struct.
// "total" is usually from database result "count int64", so it also uses int64
// "pagingNum" is "page size" or "limit", "current" is "page"
// total=-1 means only showing prev/next
func NewPagination(total, pagingNum, current, numPages int) *Pagination {
func NewPagination(total int64, pagingNum, current, numPages int) *Pagination {
totalInt := int(min(total, int64(math.MaxInt)))
p := &Pagination{}
p.Paginater = paginator.New(total, pagingNum, current, numPages)
p.Paginater = paginator.New(totalInt, pagingNum, current, numPages)
return p
}
@@ -12,6 +12,39 @@ import (
"code.gitea.io/gitea/models/unit"
)
// CheckTokenScopes checks whether the authenticated API token contains any of the given scopes.
func CheckTokenScopes(ctx *Context, repo *repo_model.Repository, scopes ...auth_model.AccessTokenScope) {
if ctx.Data["IsApiToken"] != true {
return
}
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if !ok {
return
}
publicOnly, err := scope.PublicOnly()
if err != nil {
ctx.ServerError("PublicOnly", err)
return
}
if publicOnly && repo != nil && repo.IsPrivate {
ctx.HTTPError(http.StatusForbidden)
return
}
scopeMatched, err := scope.HasAnyScope(scopes...)
if err != nil {
ctx.ServerError("HasAnyScope", err)
return
}
if !scopeMatched {
ctx.HTTPError(http.StatusForbidden)
}
}
// RequireRepoAdmin returns a middleware for requiring repository admin permission
func RequireRepoAdmin() func(ctx *Context) {
return func(ctx *Context) {
@@ -57,39 +90,7 @@ func RequireUnitReader(unitTypes ...unit.Type) func(ctx *Context) {
}
}
// CheckRepoScopedToken check whether personal access token has repo scope
// CheckRepoScopedToken checks whether the authenticated API token has repo scope.
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
return
}
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
if ok { // it's a personal access token but not oauth2 token
var scopeMatched bool
requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)
// check if scope only applies to public resources
publicOnly, err := scope.PublicOnly()
if err != nil {
ctx.ServerError("HasScope", err)
return
}
if publicOnly && repo.IsPrivate {
ctx.HTTPError(http.StatusForbidden)
return
}
scopeMatched, err = scope.HasScope(requiredScopes...)
if err != nil {
ctx.ServerError("HasScope", err)
return
}
if !scopeMatched {
ctx.HTTPError(http.StatusForbidden)
return
}
}
CheckTokenScopes(ctx, repo, auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)...)
}
+83 -38
View File
@@ -37,11 +37,46 @@ import (
"github.com/editorconfig/editorconfig-core-go/v2"
)
// PullRequest contains information to make a pull request
type PullRequest struct {
BaseRepo *repo_model.Repository
Allowed bool // it only used by the web tmpl: "PullRequestCtx.Allowed"
SameRepo bool // it only used by the web tmpl: "PullRequestCtx.SameRepo"
// PullRequestContext contains context information for making a new pull request
type PullRequestContext struct {
ctx *Context
baseRepo, headRepo *repo_model.Repository
canCreateNewPull *bool
defaultTargetBranch *string
}
func (prc *PullRequestContext) SameRepo() bool {
return prc.baseRepo != nil && prc.headRepo != nil && prc.baseRepo.ID == prc.headRepo.ID
}
func (prc *PullRequestContext) CanCreateNewPull() bool {
if prc.canCreateNewPull != nil {
return *prc.canCreateNewPull
}
ctx := prc.ctx
// People who have push access or have forked repository can propose a new pull request.
can := prc.baseRepo.CanContentChange() &&
(ctx.Repo.CanWrite(unit_model.TypeCode) || (ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)))
prc.canCreateNewPull = &can
return can
}
func (prc *PullRequestContext) MakeDefaultCompareLink(headBranch string) string {
return prc.baseRepo.Link() + "/compare/" +
util.PathEscapeSegments(prc.DefaultTargetBranch()) + "..." +
util.Iif(prc.SameRepo(), "", util.PathEscapeSegments(prc.headRepo.OwnerName)+":") +
util.PathEscapeSegments(headBranch)
}
func (prc *PullRequestContext) DefaultTargetBranch() string {
if prc.defaultTargetBranch != nil {
return *prc.defaultTargetBranch
}
branchName := prc.baseRepo.GetPullRequestTargetBranch(prc.ctx)
prc.defaultTargetBranch = &branchName
return branchName
}
// Repository contains information to operate a repository
@@ -64,7 +99,7 @@ type Repository struct {
CommitID string
CommitsCount int64
PullRequest *PullRequest
PullRequestCtx *PullRequestContext
}
// CanWriteToBranch checks if the branch is writable by the user
@@ -138,9 +173,25 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r
protectedBranch.Repo = targetRepo
canPushWithProtection = protectedBranch.CanUserPush(ctx, doer)
protectionRequireSigned = protectedBranch.RequireSignedCommits
// If branch-wide push is restricted, allow direct commit when the
// URL-derived tree path matches an unprotected file pattern. The
// pre-receive hook re-checks every path the commit actually touches
// (e.g. rename source and destination).
if !canPushWithProtection && ctx.Repo.TreePath != "" && protectedBranch.UnprotectedFilePatterns != "" {
globs := protectedBranch.GetUnprotectedFilePatterns()
if protectedBranch.IsUnprotectedFile(globs, ctx.Repo.TreePath) {
canPushWithProtection = true
}
}
}
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String())
targetGitRepo, closer, err := gitrepo.RepositoryFromContextOrOpen(ctx, targetRepo)
if err != nil {
return nil, err
}
defer closer.Close()
willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, doer, targetGitRepo, refName.String())
wontSignReason := ""
if asymkey_service.IsErrWontSign(err) {
wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
@@ -202,14 +253,14 @@ func (r *Repository) CanCreateIssueDependencies(ctx context.Context, user *user_
}
// GetCommitsCount returns cached commit count for current view
func (r *Repository) GetCommitsCount() (int64, error) {
func (r *Repository) GetCommitsCount(ctx context.Context) (int64, error) {
if r.Commit == nil {
return 0, nil
}
contextName := r.RefFullName.ShortName()
isRef := r.RefFullName.IsBranch() || r.RefFullName.IsTag()
return cache.GetInt64(r.Repository.GetCommitsCountCacheKey(contextName, isRef), func() (int64, error) {
return r.Commit.CommitsCount()
return gitrepo.CommitsCountOfCommit(ctx, r.Repository, r.Commit.ID.String())
})
}
@@ -219,11 +270,10 @@ func (r *Repository) GetCommitGraphsCount(ctx context.Context, hidePRRefs bool,
return cache.GetInt64(cacheKey, func() (int64, error) {
if len(branches) == 0 {
return git.AllCommitsCount(ctx, r.Repository.RepoPath(), hidePRRefs, files...)
return gitrepo.AllCommitsCount(ctx, r.Repository, hidePRRefs, files...)
}
return git.CommitsCount(ctx,
git.CommitsCountOptions{
RepoPath: r.Repository.RepoPath(),
return gitrepo.CommitsCount(ctx, r.Repository,
gitrepo.CommitsCountOptions{
Revision: branches,
RelPath: files,
})
@@ -304,9 +354,9 @@ func RetrieveTemplateRepo(ctx *Context, repo *repo_model.Repository) {
return
}
perm, err := access_model.GetUserRepoPermission(ctx, templateRepo, ctx.Doer)
perm, err := access_model.GetDoerRepoPermission(ctx, templateRepo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
ctx.ServerError("GetDoerRepoPermission", err)
return
}
@@ -381,9 +431,9 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
if ctx.DoerNeedTwoFactorAuth() {
ctx.Repo.Permission = access_model.PermissionNoAccess()
} else {
ctx.Repo.Permission, err = access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
ctx.Repo.Permission, err = access_model.GetDoerRepoPermission(ctx, repo, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserRepoPermission", err)
ctx.ServerError("GetDoerRepoPermission", err)
return
}
}
@@ -413,6 +463,12 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
}
func InitRepoPullRequestCtx(ctx *Context, base, head *repo_model.Repository) {
ctx.Repo.PullRequestCtx = &PullRequestContext{ctx: ctx}
ctx.Repo.PullRequestCtx.baseRepo, ctx.Repo.PullRequestCtx.headRepo = base, head
ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequestCtx
}
// RepoAssignment returns a middleware to handle repository assignment
func RepoAssignment(ctx *Context) {
if ctx.Data["Repository"] != nil {
@@ -622,7 +678,7 @@ func RepoAssignment(ctx *Context) {
ctx.Repo.GitRepo, err = gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
if strings.Contains(err.Error(), "repository does not exist") || strings.Contains(err.Error(), "no such file or directory") {
log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RepoPath(), err)
log.Error("Repository %-v has a broken repository on the file system: %s Error: %v", ctx.Repo.Repository, ctx.Repo.Repository.RelativePath(), err)
ctx.Repo.Repository.MarkAsBrokenEmpty()
// Only allow access to base of repo or settings
if !isHomeOrSettings {
@@ -661,28 +717,16 @@ func RepoAssignment(ctx *Context) {
ctx.Data["BranchesCount"] = branchesTotal
// People who have push access or have forked repository can propose a new pull request.
canPush := ctx.Repo.CanWrite(unit_model.TypeCode) ||
(ctx.IsSigned && repo_model.HasForkedRepo(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID))
canCompare := false
// Pull request is allowed if this is a fork repository
// and base repository accepts pull requests.
// Pull request is allowed if this is a fork repository, and base repository accepts pull requests.
if repo.BaseRepo != nil && repo.BaseRepo.AllowsPulls(ctx) {
canCompare = true
// TODO: this (and below) "BaseRepo" var is not clear and should be removed in the future
ctx.Data["BaseRepo"] = repo.BaseRepo
ctx.Repo.PullRequest.BaseRepo = repo.BaseRepo
ctx.Repo.PullRequest.Allowed = canPush
InitRepoPullRequestCtx(ctx, repo.BaseRepo, repo)
} else if repo.AllowsPulls(ctx) {
// Or, this is repository accepts pull requests between branches.
canCompare = true
ctx.Data["BaseRepo"] = repo
ctx.Repo.PullRequest.BaseRepo = repo
ctx.Repo.PullRequest.Allowed = canPush
ctx.Repo.PullRequest.SameRepo = true
InitRepoPullRequestCtx(ctx, repo, repo)
}
ctx.Data["CanCompareOrPull"] = canCompare
ctx.Data["PullRequestCtx"] = ctx.Repo.PullRequest
if ctx.Repo.Repository.Status == repo_model.RepositoryPendingTransfer {
repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
@@ -821,7 +865,7 @@ func RepoRefByDefaultBranch() func(*Context) {
ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch)
ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch
ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName)
ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount()
ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount(ctx)
ctx.Data["RefFullName"] = ctx.Repo.RefFullName
ctx.Data["BranchName"] = ctx.Repo.BranchName
ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount
@@ -858,7 +902,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
if err == nil && len(brs) != 0 {
refShortName = brs[0]
} else if len(brs) == 0 {
log.Error("No branches in non-empty repository %s", ctx.Repo.GitRepo.Path)
log.Error("No branches in non-empty repository %s", ctx.Repo.Repository.RelativePath())
} else {
log.Error("GetBranches error: %v", err)
}
@@ -927,7 +971,8 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
}
// If short commit ID add canonical link header
if len(refShortName) < ctx.Repo.GetObjectFormat().FullLength() {
canonicalURL := util.URLJoin(httplib.GuessCurrentAppURL(ctx), strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refShortName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))
// FIXME: the dirty hack of "strings.Replace" should be fixed
canonicalURL := strings.TrimSuffix(httplib.GuessCurrentAppURL(ctx), "/") + strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refShortName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1)
ctx.RespHeader().Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, canonicalURL))
}
} else {
@@ -970,7 +1015,7 @@ func RepoRefByType(detectRefType git.RefType) func(*Context) {
ctx.Data["CanCreateBranch"] = ctx.Repo.CanCreateBranch() // only used by the branch selector dropdown: AllowCreateNewRef
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount()
ctx.Repo.CommitsCount, err = ctx.Repo.GetCommitsCount(ctx)
if err != nil {
ctx.ServerError("GetCommitsCount", err)
return
@@ -4,6 +4,8 @@
package context
import (
"bufio"
"net"
"net/http"
web_types "code.gitea.io/gitea/modules/web/types"
@@ -67,6 +69,15 @@ func (r *Response) WriteHeader(statusCode int) {
}
}
// Hijack implements http.Hijacker, delegating to the underlying ResponseWriter.
// This is needed for WebSocket upgrades through reverse proxies.
func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := r.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, http.ErrNotSupported
}
// Flush flushes cached data
func (r *Response) Flush() {
if f, ok := r.ResponseWriter.(http.Flusher); ok {
@@ -95,8 +95,8 @@ func AddUploadContext(ctx *context.Context, uploadType string) {
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/releases/attachments/remove"
ctx.Data["UploadLinkUrl"] = ctx.Repo.RepoLink + "/releases/attachments"
ctx.Data["UploadAccepts"] = strings.ReplaceAll(setting.Repository.Release.AllowedTypes, "|", ",")
ctx.Data["UploadMaxFiles"] = setting.Attachment.MaxFiles
ctx.Data["UploadMaxSize"] = setting.Attachment.MaxSize
ctx.Data["UploadMaxFiles"] = setting.Repository.Release.MaxFiles
ctx.Data["UploadMaxSize"] = setting.Repository.Release.FileMaxSize
case "comment":
ctx.Data["UploadUrl"] = ctx.Repo.RepoLink + "/issues/attachments"
ctx.Data["UploadRemoveUrl"] = ctx.Repo.RepoLink + "/issues/attachments/remove"
@@ -1,99 +0,0 @@
// Copyright 2012 Google Inc. All Rights Reserved.
// Copyright 2014 The Macaron Authors
// Copyright 2020 The Gitea Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// SPDX-License-Identifier: Apache-2.0
package context
import (
"bytes"
"crypto/hmac"
"crypto/sha1"
"crypto/subtle"
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
)
// CsrfTokenTimeout represents the duration that XSRF tokens are valid.
// It is exported so clients may set cookie timeouts that match generated tokens.
const CsrfTokenTimeout = 24 * time.Hour
// CsrfTokenRegenerationInterval is the interval between token generations, old tokens are still valid before CsrfTokenTimeout
var CsrfTokenRegenerationInterval = 10 * time.Minute
var csrfTokenSep = []byte(":")
// GenerateCsrfToken returns a URL-safe secure XSRF token that expires in CsrfTokenTimeout hours.
// key is a secret key for your application.
// userID is a unique identifier for the user.
// actionID is the action the user is taking (e.g. POSTing to a particular path).
func GenerateCsrfToken(key, userID, actionID string, now time.Time) string {
nowUnixNano := now.UnixNano()
nowUnixNanoStr := strconv.FormatInt(nowUnixNano, 10)
h := hmac.New(sha1.New, []byte(key))
h.Write([]byte(strings.ReplaceAll(userID, ":", "_")))
h.Write(csrfTokenSep)
h.Write([]byte(strings.ReplaceAll(actionID, ":", "_")))
h.Write(csrfTokenSep)
h.Write([]byte(nowUnixNanoStr))
tok := fmt.Sprintf("%s:%s", h.Sum(nil), nowUnixNanoStr)
return base64.RawURLEncoding.EncodeToString([]byte(tok))
}
func ParseCsrfToken(token string) (issueTime time.Time, ok bool) {
data, err := base64.RawURLEncoding.DecodeString(token)
if err != nil {
return time.Time{}, false
}
pos := bytes.LastIndex(data, csrfTokenSep)
if pos == -1 {
return time.Time{}, false
}
nanos, err := strconv.ParseInt(string(data[pos+1:]), 10, 64)
if err != nil {
return time.Time{}, false
}
return time.Unix(0, nanos), true
}
// ValidCsrfToken returns true if token is a valid and unexpired token returned by Generate.
func ValidCsrfToken(token, key, userID, actionID string, now time.Time) bool {
issueTime, ok := ParseCsrfToken(token)
if !ok {
return false
}
// Check that the token is not expired.
if now.Sub(issueTime) >= CsrfTokenTimeout {
return false
}
// Check that the token is not from the future.
// Allow 1-minute grace period in case the token is being verified on a
// machine whose clock is behind the machine that issued the token.
if issueTime.After(now.Add(1 * time.Minute)) {
return false
}
expected := GenerateCsrfToken(key, userID, actionID, issueTime)
// Check that the token matches the expected value.
// Use constant time comparison to avoid timing attacks.
return subtle.ConstantTimeCompare([]byte(token), []byte(expected)) == 1
}
@@ -1,91 +0,0 @@
// Copyright 2012 Google Inc. All Rights Reserved.
// Copyright 2014 The Macaron Authors
// Copyright 2020 The Gitea Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// SPDX-License-Identifier: Apache-2.0
package context
import (
"encoding/base64"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
const (
key = "quay"
userID = "12345678"
actionID = "POST /form"
)
var (
now = time.Now()
oneMinuteFromNow = now.Add(1 * time.Minute)
)
func Test_ValidToken(t *testing.T) {
t.Run("Validate token", func(t *testing.T) {
tok := GenerateCsrfToken(key, userID, actionID, now)
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, oneMinuteFromNow))
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(CsrfTokenTimeout-1*time.Nanosecond)))
assert.True(t, ValidCsrfToken(tok, key, userID, actionID, now.Add(-1*time.Minute)))
})
}
// Test_SeparatorReplacement tests that separators are being correctly substituted
func Test_SeparatorReplacement(t *testing.T) {
t.Run("Test two separator replacements", func(t *testing.T) {
assert.NotEqual(t, GenerateCsrfToken("foo:bar", "baz", "wah", now),
GenerateCsrfToken("foo", "bar:baz", "wah", now))
})
}
func Test_InvalidToken(t *testing.T) {
t.Run("Test invalid tokens", func(t *testing.T) {
invalidTokenTests := []struct {
name, key, userID, actionID string
t time.Time
}{
{"Bad key", "foobar", userID, actionID, oneMinuteFromNow},
{"Bad userID", key, "foobar", actionID, oneMinuteFromNow},
{"Bad actionID", key, userID, "foobar", oneMinuteFromNow},
{"Expired", key, userID, actionID, now.Add(CsrfTokenTimeout)},
{"More than 1 minute from the future", key, userID, actionID, now.Add(-1*time.Nanosecond - 1*time.Minute)},
}
tok := GenerateCsrfToken(key, userID, actionID, now)
for _, itt := range invalidTokenTests {
assert.False(t, ValidCsrfToken(tok, itt.key, itt.userID, itt.actionID, itt.t))
}
})
}
// Test_ValidateBadData primarily tests that no unexpected panics are triggered during parsing
func Test_ValidateBadData(t *testing.T) {
t.Run("Validate bad data", func(t *testing.T) {
badDataTests := []struct {
name, tok string
}{
{"Invalid Base64", "ASDab24(@)$*=="},
{"No delimiter", base64.URLEncoding.EncodeToString([]byte("foobar12345678"))},
{"Invalid time", base64.URLEncoding.EncodeToString([]byte("foobar:foobar"))},
}
for _, bdt := range badDataTests {
assert.False(t, ValidCsrfToken(bdt.tok, key, userID, actionID, oneMinuteFromNow))
}
})
}