Fix access log (#14475)

Fix #14121, #14478.

The `AccessLog` middleware has to be after `Contexter` or `APIContexter` so that we can get `LoginUserName` if possible.
And also there is a **BREAK** change that it removed internal API access log.
This commit is contained in:
Lunny Xiao 2021-01-28 01:46:35 +08:00 committed by GitHub
parent 4c6e029506
commit a51cc6dea4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 129 additions and 72 deletions

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/middlewares"
"code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/timeutil"
) )
@ -121,7 +122,7 @@ func (o *OAuth2) VerifyAuthData(req *http.Request, w http.ResponseWriter, store
return nil return nil
} }
if isInternalPath(req) || !isAPIPath(req) && !isAttachmentDownload(req) { if middlewares.IsInternalPath(req) || !middlewares.IsAPIPath(req) && !isAttachmentDownload(req) {
return nil return nil
} }

View file

@ -94,16 +94,6 @@ func SessionUser(sess SessionStore) *models.User {
return user return user
} }
// isAPIPath returns true if the specified URL is an API path
func isAPIPath(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/api/")
}
// isInternalPath returns true if the specified URL is an internal API path
func isInternalPath(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/api/internal/")
}
// isAttachmentDownload check if request is a file download (GET) with URL to an attachment // isAttachmentDownload check if request is a file download (GET) with URL to an attachment
func isAttachmentDownload(req *http.Request) bool { func isAttachmentDownload(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/attachments/") && req.Method == "GET" return strings.HasPrefix(req.URL.Path, "/attachments/") && req.Method == "GET"

View file

@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/middlewares"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
@ -135,7 +136,7 @@ func (s *SSPI) VerifyAuthData(req *http.Request, w http.ResponseWriter, store Da
} }
// Make sure requests to API paths and PWA resources do not create a new session // Make sure requests to API paths and PWA resources do not create a new session
if !isAPIPath(req) && !isAttachmentDownload(req) { if !middlewares.IsAPIPath(req) && !isAttachmentDownload(req) {
handleSignIn(w, req, sess, user) handleSignIn(w, req, sess, user)
} }
@ -166,9 +167,9 @@ func (s *SSPI) shouldAuthenticate(req *http.Request) (shouldAuth bool) {
} else if req.FormValue("auth_with_sspi") == "1" { } else if req.FormValue("auth_with_sspi") == "1" {
shouldAuth = true shouldAuth = true
} }
} else if isInternalPath(req) { } else if middlewares.IsInternalPath(req) {
shouldAuth = false shouldAuth = false
} else if isAPIPath(req) || isAttachmentDownload(req) { } else if middlewares.IsAPIPath(req) || isAttachmentDownload(req) {
shouldAuth = true shouldAuth = true
} }
return return

View file

@ -0,0 +1,60 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package context
import (
"bytes"
"html/template"
"net/http"
"time"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
type routerLoggerOptions struct {
req *http.Request
Identity *string
Start *time.Time
ResponseWriter http.ResponseWriter
Ctx map[string]interface{}
}
// AccessLogger returns a middleware to log access logger
func AccessLogger() func(http.Handler) http.Handler {
logger := log.GetLogger("access")
logTemplate, _ := template.New("log").Parse(setting.AccessLogTemplate)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
next.ServeHTTP(w, req)
identity := "-"
if val := SignedUserName(req); val != "" {
identity = val
}
rw := w.(ResponseWriter)
buf := bytes.NewBuffer([]byte{})
err := logTemplate.Execute(buf, routerLoggerOptions{
req: req,
Identity: &identity,
Start: &start,
ResponseWriter: rw,
Ctx: map[string]interface{}{
"RemoteAddr": req.RemoteAddr,
"Req": req,
},
})
if err != nil {
log.Error("Could not set up chi access logger: %v", err.Error())
}
err = logger.SendLog(log.INFO, "", "", 0, buf.String(), "")
if err != nil {
log.Error("Could not set up chi access logger: %v", err.Error())
}
})
}
}

View file

@ -485,6 +485,31 @@ func GetContext(req *http.Request) *Context {
return req.Context().Value(contextKey).(*Context) return req.Context().Value(contextKey).(*Context)
} }
// SignedUserName returns signed user's name via context
func SignedUserName(req *http.Request) string {
if middlewares.IsInternalPath(req) {
return ""
}
if middlewares.IsAPIPath(req) {
ctx, ok := req.Context().Value(apiContextKey).(*APIContext)
if ok {
v := ctx.Data["SignedUserName"]
if res, ok := v.(string); ok {
return res
}
}
} else {
ctx, ok := req.Context().Value(contextKey).(*Context)
if ok {
v := ctx.Data["SignedUserName"]
if res, ok := v.(string); ok {
return res
}
}
}
return ""
}
func getCsrfOpts() CsrfOptions { func getCsrfOpts() CsrfOptions {
return CsrfOptions{ return CsrfOptions{
Secret: setting.SecretKey, Secret: setting.SecretKey,

View file

@ -12,6 +12,7 @@ type ResponseWriter interface {
Flush() Flush()
Status() int Status() int
Before(func(ResponseWriter)) Before(func(ResponseWriter))
Size() int
} }
var ( var (
@ -21,11 +22,17 @@ var (
// Response represents a response // Response represents a response
type Response struct { type Response struct {
http.ResponseWriter http.ResponseWriter
written int
status int status int
befores []func(ResponseWriter) befores []func(ResponseWriter)
beforeExecuted bool beforeExecuted bool
} }
// Size return written size
func (r *Response) Size() int {
return r.written
}
// Write writes bytes to HTTP endpoint // Write writes bytes to HTTP endpoint
func (r *Response) Write(bs []byte) (int, error) { func (r *Response) Write(bs []byte) (int, error) {
if !r.beforeExecuted { if !r.beforeExecuted {
@ -35,8 +42,9 @@ func (r *Response) Write(bs []byte) (int, error) {
r.beforeExecuted = true r.beforeExecuted = true
} }
size, err := r.ResponseWriter.Write(bs) size, err := r.ResponseWriter.Write(bs)
r.written += size
if err != nil { if err != nil {
return 0, err return size, err
} }
if r.status == 0 { if r.status == 0 {
r.WriteHeader(200) r.WriteHeader(200)

View file

@ -0,0 +1,20 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package middlewares
import (
"net/http"
"strings"
)
// IsAPIPath returns true if the specified URL is an API path
func IsAPIPath(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/api/")
}
// IsInternalPath returns true if the specified URL is an internal API path
func IsInternalPath(req *http.Request) bool {
return strings.HasPrefix(req.URL.Path, "/api/internal/")
}

View file

@ -553,6 +553,11 @@ func Routes() *web.Route {
})) }))
} }
m.Use(context.APIContexter()) m.Use(context.APIContexter())
if setting.EnableAccessLog {
m.Use(context.AccessLogger())
}
m.Use(context.ToggleAPI(&context.ToggleOptions{ m.Use(context.ToggleAPI(&context.ToggleOptions{
SignInRequired: setting.Service.RequireSignInView, SignInRequired: setting.Service.RequireSignInView,
})) }))

View file

@ -5,7 +5,6 @@
package routes package routes
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -13,7 +12,6 @@ import (
"os" "os"
"path" "path"
"strings" "strings"
"text/template"
"time" "time"
"code.gitea.io/gitea/modules/auth/sso" "code.gitea.io/gitea/modules/auth/sso"
@ -28,57 +26,6 @@ import (
"gitea.com/go-chi/session" "gitea.com/go-chi/session"
) )
type routerLoggerOptions struct {
req *http.Request
Identity *string
Start *time.Time
ResponseWriter http.ResponseWriter
}
// SignedUserName returns signed user's name via context
func SignedUserName(req *http.Request) string {
ctx := context.GetContext(req)
if ctx != nil {
v := ctx.Data["SignedUserName"]
if res, ok := v.(string); ok {
return res
}
}
return ""
}
func accessLogger() func(http.Handler) http.Handler {
logger := log.GetLogger("access")
logTemplate, _ := template.New("log").Parse(setting.AccessLogTemplate)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
start := time.Now()
next.ServeHTTP(w, req)
identity := "-"
if val := SignedUserName(req); val != "" {
identity = val
}
rw := w
buf := bytes.NewBuffer([]byte{})
err := logTemplate.Execute(buf, routerLoggerOptions{
req: req,
Identity: &identity,
Start: &start,
ResponseWriter: rw,
})
if err != nil {
log.Error("Could not set up chi access logger: %v", err.Error())
}
err = logger.SendLog(log.INFO, "", "", 0, buf.String(), "")
if err != nil {
log.Error("Could not set up chi access logger: %v", err.Error())
}
})
}
}
// LoggerHandler is a handler that will log the routing to the default gitea log // LoggerHandler is a handler that will log the routing to the default gitea log
func LoggerHandler(level log.Level) func(next http.Handler) http.Handler { func LoggerHandler(level log.Level) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {

View file

@ -88,10 +88,6 @@ func commonMiddlewares() []func(http.Handler) http.Handler {
next.ServeHTTP(resp, req) next.ServeHTTP(resp, req)
}) })
}) })
if setting.EnableAccessLog {
handlers = append(handlers, accessLogger())
}
return handlers return handlers
} }
@ -168,6 +164,10 @@ func WebRoutes() *web.Route {
r.Use(context.Contexter()) r.Use(context.Contexter())
// Removed: SetAutoHead allow a get request redirect to head if get method is not exist // Removed: SetAutoHead allow a get request redirect to head if get method is not exist
if setting.EnableAccessLog {
r.Use(context.AccessLogger())
}
r.Use(user.GetNotificationCount) r.Use(user.GetNotificationCount)
r.Use(repo.GetActiveStopwatch) r.Use(repo.GetActiveStopwatch)
r.Use(func(ctx *context.Context) { r.Use(func(ctx *context.Context) {