mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
130 lines
3.4 KiB
Go
130 lines
3.4 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"bytes"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
"unicode"
|
|
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
"github.com/hanzoai/git/modules/web/middleware"
|
|
)
|
|
|
|
type accessLoggerTmplData struct {
|
|
Identity *string
|
|
Start *time.Time
|
|
ResponseWriter struct {
|
|
Status, Size int
|
|
}
|
|
Ctx map[string]any
|
|
RequestID *string
|
|
}
|
|
|
|
const keyOfRequestIDInTemplate = ".RequestID"
|
|
|
|
// According to:
|
|
// TraceId: A valid trace identifier is a 16-byte array with at least one non-zero byte
|
|
// MD5 output is 16 or 32 bytes: md5-bytes is 16, md5-hex is 32
|
|
// SHA1: similar, SHA1-bytes is 20, SHA1-hex is 40.
|
|
// UUID is 128-bit, 32 hex chars, 36 ASCII chars with 4 dashes
|
|
// 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 {
|
|
if req.Header.Get(key) != "" {
|
|
requestID = req.Header.Get(key)
|
|
break
|
|
}
|
|
}
|
|
if !isSafeRequestID(requestID) {
|
|
return "-"
|
|
}
|
|
if len(requestID) > maxRequestIDByteLength {
|
|
requestID = requestID[:maxRequestIDByteLength] + "..."
|
|
}
|
|
return requestID
|
|
}
|
|
|
|
type accessLogRecorder struct {
|
|
logger log.BaseLogger
|
|
logTemplate *template.Template
|
|
needRequestID bool
|
|
}
|
|
|
|
func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) {
|
|
var requestID string
|
|
if lr.needRequestID {
|
|
requestID = parseRequestIDFromRequestHeader(req)
|
|
}
|
|
|
|
reqHost, _, err := net.SplitHostPort(req.RemoteAddr)
|
|
if err != nil {
|
|
reqHost = req.RemoteAddr
|
|
}
|
|
|
|
identity := "-"
|
|
data := middleware.GetContextData(req.Context())
|
|
if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok {
|
|
identity = signedUser.Name
|
|
}
|
|
buf := bytes.NewBuffer([]byte{})
|
|
tmplData := accessLoggerTmplData{
|
|
Identity: &identity,
|
|
Start: &start,
|
|
Ctx: map[string]any{
|
|
"RemoteAddr": req.RemoteAddr,
|
|
"RemoteHost": reqHost,
|
|
"Req": req,
|
|
},
|
|
RequestID: &requestID,
|
|
}
|
|
tmplData.ResponseWriter.Status = respWriter.WrittenStatus()
|
|
tmplData.ResponseWriter.Size = respWriter.WrittenSize()
|
|
err = lr.logTemplate.Execute(buf, tmplData)
|
|
if err != nil {
|
|
log.Error("Could not execute access logger template: %v", err.Error())
|
|
}
|
|
|
|
lr.logger.Log(1, &log.Event{Level: log.INFO}, "%s", buf.String())
|
|
}
|
|
|
|
func newAccessLogRecorder() *accessLogRecorder {
|
|
return &accessLogRecorder{
|
|
logger: log.GetLogger("access"),
|
|
logTemplate: template.Must(template.New("log").Parse(setting.Log.AccessLogTemplate)),
|
|
needRequestID: len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate),
|
|
}
|
|
}
|
|
|
|
// AccessLogger returns a middleware to log access logger
|
|
func AccessLogger() func(http.Handler) http.Handler {
|
|
recorder := newAccessLogRecorder()
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
start := time.Now()
|
|
next.ServeHTTP(w, req)
|
|
recorder.record(start, w.(ResponseWriter), req)
|
|
})
|
|
}
|
|
}
|