mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-17 12:46:59 +00:00
* Add GitHub Actions workflow for codespell on master * Add rudimentary codespell config * Tune codespell config: skip generated code, ignore camelCase, whitelist domain terms Add camelCase/PascalCase regex to ignore common Go/Rust/JS identifiers like allLocations, publishErr, ReadInside, FlushInterval. Also skip templ-generated *_templ.go files, and whitelist a handful of short/domain-specific words (visibles, fo, te, ser, bject, unparseable, keep-alives, tread, anc, ue) that show up as false positives across the tree. Co-Authored-By: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix ambiguous typos and protect false positives Fixes typos that codespell reports with multiple candidate suggestions (so `codespell -w` cannot auto-apply them), plus one inline pragma and one config entry to protect legitimate identifiers. Manual fixes (single correct answer chosen from context): - pattens -> patterns (5x) in filer/upload/shell flag help strings - finded -> found (2x) in tarantool storage.lua comment - spacify -> specify (2x) in helm chart values.yaml comment - wether -> whether in skiplist.go docstring - simpe -> simple in mq schema test case name False-positive protection: - Add `//codespell:ignore` next to `source GET's` (possessive of HTTP verb) in s3api_object_handlers_copy_stream.go - Whitelist `auther` in .codespellrc — it's a local variable meaning "authenticator" in weed/security/tls.go, not a typo of "author". Co-Authored-By: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Extend codespell ignore list: .git-meta path and thirdparty groupId Also skip `.git-meta` (scratch dir for commit messages that may contain typo words verbatim) and whitelist `thirdparty` — it appears as the literal Maven groupId `org.apache.hadoop.thirdparty` in hdfs3 poms and cannot be renamed. Co-Authored-By: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [DATALAD RUNCMD] Fix non-ambiguous typos with codespell -w Auto-applied fixes to the 44 remaining single-suggestion typos across docs, comments, log messages, tests, config, and one Java pom. === Do not change lines below === { "chain": [], "cmd": "uvx codespell -w", "exit": 0, "extra_inputs": [], "inputs": [], "outputs": [], "pwd": "." } ^^^ Do not change lines above ^^^ * Revert breaking codespell fixes; whitelist unknwon and atleast Two of the auto-applied `codespell -w` fixes were false positives that would break the build/tests: - go.mod: `github.com/unknwon/goconfig` is a real Go module path — the upstream author's GitHub handle is literally `unknwon`. Renaming to `unknown` would fail dependency resolution. - test/benchmark/fuse_db/bin/{sqlite_verify.py,run_mysql.sh,run_sqlite.sh}: `atleast` is a literal CLI mode value (a string constant compared and passed as a positional argument). Rewriting to `at least` splits it into two arguments and breaks the mode check. Reverted those files and whitelisted both words in .codespellrc so future runs won't re-suggest the same broken fixes. Co-Authored-By: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com>
257 lines
8.2 KiB
Go
257 lines
8.2 KiB
Go
package s3err
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/fluent/fluent-logger-golang/fluent"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/util/request_id"
|
|
)
|
|
|
|
type AccessLogExtend struct {
|
|
AccessLog
|
|
AccessLogHTTP
|
|
}
|
|
|
|
type AccessLog struct {
|
|
Bucket string `msg:"bucket" json:"bucket"` // awsexamplebucket1
|
|
Time int64 `msg:"time" json:"time"` // [06/Feb/2019:00:00:38 +0000]
|
|
RemoteIP string `msg:"remote_ip" json:"remote_ip,omitempty"` // 192.0.2.3
|
|
Requester string `msg:"requester" json:"requester,omitempty"` // IAM user id
|
|
RequestID string `msg:"request_id" json:"request_id,omitempty"` // 3E57427F33A59F07
|
|
Operation string `msg:"operation" json:"operation,omitempty"` // REST.HTTP_method.resource_type REST.PUT.OBJECT
|
|
Key string `msg:"key" json:"key,omitempty"` // /photos/2019/08/puppy.jpg
|
|
ErrorCode string `msg:"error_code" json:"error_code,omitempty"`
|
|
HostId string `msg:"host_id" json:"host_id,omitempty"`
|
|
HostHeader string `msg:"host_header" json:"host_header,omitempty"` // s3.us-west-2.amazonaws.com
|
|
UserAgent string `msg:"user_agent" json:"user_agent,omitempty"`
|
|
HTTPStatus int `msg:"status" json:"status,omitempty"`
|
|
SignatureVersion string `msg:"signature_version" json:"signature_version,omitempty"`
|
|
}
|
|
|
|
type AccessLogHTTP struct {
|
|
RequestURI string `json:"request_uri,omitempty"` // "GET /awsexamplebucket1/photos/2019/08/puppy.jpg?x-foo=bar HTTP/1.1"
|
|
BytesSent string `json:"bytes_sent,omitempty"`
|
|
ObjectSize string `json:"object_size,omitempty"`
|
|
TotalTime int `json:"total_time,omitempty"`
|
|
TurnAroundTime int `json:"turn_around_time,omitempty"`
|
|
Referer string `json:"Referer,omitempty"`
|
|
VersionId string `json:"version_id,omitempty"`
|
|
CipherSuite string `json:"cipher_suite,omitempty"`
|
|
AuthenticationType string `json:"auth_type,omitempty"`
|
|
TLSVersion string `json:"TLS_version,omitempty"`
|
|
}
|
|
|
|
const tag = "s3.access"
|
|
|
|
var (
|
|
Logger *fluent.Fluent
|
|
hostname = os.Getenv("HOSTNAME")
|
|
environment = os.Getenv("ENVIRONMENT")
|
|
)
|
|
|
|
func InitAuditLog(config string) {
|
|
configContent, readErr := os.ReadFile(config)
|
|
if readErr != nil {
|
|
glog.Errorf("fail to read fluent config %s : %v", config, readErr)
|
|
return
|
|
}
|
|
fluentConfig := &fluent.Config{}
|
|
if err := json.Unmarshal(configContent, fluentConfig); err != nil {
|
|
glog.Errorf("fail to parse fluent config %s : %v", string(configContent), err)
|
|
return
|
|
}
|
|
if len(fluentConfig.TagPrefix) == 0 && len(environment) > 0 {
|
|
fluentConfig.TagPrefix = environment
|
|
}
|
|
fluentConfig.Async = true
|
|
fluentConfig.AsyncResultCallback = func(data []byte, err error) {
|
|
if err != nil {
|
|
glog.Warning("Error while posting log: ", err)
|
|
}
|
|
}
|
|
var err error
|
|
Logger, err = fluent.New(*fluentConfig)
|
|
if err != nil {
|
|
glog.Errorf("fail to load fluent config: %v", err)
|
|
}
|
|
}
|
|
|
|
// getRemoteIP returns the client IP for the audit log, honoring forwarding
|
|
// headers set by reverse proxies. Preference order: X-Forwarded-For (first
|
|
// non-empty entry), X-Real-IP, then r.RemoteAddr. Headers are trusted as-is;
|
|
// operators who expose the S3 endpoint directly to untrusted networks should
|
|
// strip these headers at the proxy boundary so clients cannot spoof them.
|
|
func getRemoteIP(r *http.Request) string {
|
|
if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
|
|
for _, entry := range strings.Split(forwardedFor, ",") {
|
|
if ip := strings.TrimSpace(entry); ip != "" {
|
|
return ip
|
|
}
|
|
}
|
|
}
|
|
if realIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); realIP != "" {
|
|
return realIP
|
|
}
|
|
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
|
return host
|
|
}
|
|
return r.RemoteAddr
|
|
}
|
|
|
|
func getREST(httpMetod string, resourceType string) string {
|
|
return fmt.Sprintf("REST.%s.%s", httpMetod, resourceType)
|
|
}
|
|
|
|
func getResourceType(object string, query_key string, method string) (string, bool) {
|
|
if object == "/" {
|
|
switch query_key {
|
|
case "delete":
|
|
return "BATCH.DELETE.OBJECT", true
|
|
case "tagging":
|
|
return getREST(method, "OBJECTTAGGING"), true
|
|
case "lifecycle":
|
|
return getREST(method, "LIFECYCLECONFIGURATION"), true
|
|
case "acl":
|
|
return getREST(method, "ACCESSCONTROLPOLICY"), true
|
|
case "policy":
|
|
return getREST(method, "BUCKETPOLICY"), true
|
|
default:
|
|
return getREST(method, "BUCKET"), false
|
|
}
|
|
} else {
|
|
switch query_key {
|
|
case "tagging":
|
|
return getREST(method, "OBJECTTAGGING"), true
|
|
default:
|
|
return getREST(method, "OBJECT"), false
|
|
}
|
|
}
|
|
}
|
|
|
|
func getOperation(object string, r *http.Request) string {
|
|
queries := r.URL.Query()
|
|
var operation string
|
|
var queryFound bool
|
|
for key, _ := range queries {
|
|
operation, queryFound = getResourceType(object, key, r.Method)
|
|
if queryFound {
|
|
return operation
|
|
}
|
|
}
|
|
if len(queries) == 0 {
|
|
operation, _ = getResourceType(object, "", r.Method)
|
|
}
|
|
return operation
|
|
}
|
|
|
|
func GetAccessLog(r *http.Request, HTTPStatusCode int, s3errCode ErrorCode) *AccessLog {
|
|
bucket, key := s3_constants.GetBucketAndObject(r)
|
|
var errorCode string
|
|
if s3errCode != ErrNone {
|
|
errorCode = GetAPIError(s3errCode).Code
|
|
}
|
|
remoteIP := getRemoteIP(r)
|
|
hostHeader := r.Header.Get("X-Forwarded-Host")
|
|
if len(hostHeader) == 0 {
|
|
hostHeader = r.Host
|
|
}
|
|
return &AccessLog{
|
|
HostHeader: hostHeader,
|
|
RequestID: request_id.GetFromRequest(r),
|
|
RemoteIP: remoteIP,
|
|
Requester: s3_constants.GetIdentityNameFromContext(r), // Get from context, not header (secure)
|
|
SignatureVersion: r.Header.Get(s3_constants.AmzAuthType),
|
|
UserAgent: r.Header.Get("user-agent"),
|
|
HostId: hostname,
|
|
Bucket: bucket,
|
|
HTTPStatus: HTTPStatusCode,
|
|
Time: time.Now().Unix(),
|
|
Key: key,
|
|
Operation: getOperation(key, r),
|
|
ErrorCode: errorCode,
|
|
}
|
|
}
|
|
|
|
func PostLog(r *http.Request, HTTPStatusCode int, errorCode ErrorCode) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
// Mark before the Logger nil-check so the middleware fallback in track()
|
|
// still sees that audit was handled by the caller in deployments that
|
|
// haven't configured fluent — keeps the flag behavior consistent and
|
|
// makes wiring testable without standing up a fluent server.
|
|
markAuditLogged(r)
|
|
if Logger == nil {
|
|
return
|
|
}
|
|
if err := Logger.Post(tag, *GetAccessLog(r, HTTPStatusCode, errorCode)); err != nil {
|
|
glog.Warning("Error while posting log: ", err)
|
|
}
|
|
}
|
|
|
|
func PostAccessLog(log AccessLog) {
|
|
if Logger == nil || len(log.Key) == 0 {
|
|
return
|
|
}
|
|
if err := Logger.Post(tag, log); err != nil {
|
|
glog.Warning("Error while posting log: ", err)
|
|
}
|
|
}
|
|
|
|
// auditLogCtxKey identifies the per-request flag used to detect whether an
|
|
// audit entry has already been emitted, so middleware can supply a fallback
|
|
// log for handlers that don't call PostLog themselves without double-logging
|
|
// the ones that do.
|
|
type auditLogCtxKey struct{}
|
|
|
|
// EnsureAuditTracking attaches an audit-tracking flag to the request context
|
|
// if one is not already present. Safe to call when no fluent logger is
|
|
// configured; the flag is harmless in that case.
|
|
func EnsureAuditTracking(r *http.Request) *http.Request {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
if v, ok := r.Context().Value(auditLogCtxKey{}).(*atomic.Bool); ok && v != nil {
|
|
return r
|
|
}
|
|
flag := new(atomic.Bool)
|
|
return r.WithContext(context.WithValue(r.Context(), auditLogCtxKey{}, flag))
|
|
}
|
|
|
|
// MarkAuditLogged signals that an audit entry has already been emitted for r.
|
|
// Callers that emit logs via paths other than PostLog (e.g. PostAccessLog in
|
|
// batch delete) should invoke this so the middleware fallback skips r.
|
|
func MarkAuditLogged(r *http.Request) {
|
|
markAuditLogged(r)
|
|
}
|
|
|
|
func markAuditLogged(r *http.Request) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
if v, ok := r.Context().Value(auditLogCtxKey{}).(*atomic.Bool); ok && v != nil {
|
|
v.Store(true)
|
|
}
|
|
}
|
|
|
|
// AuditAlreadyLogged reports whether PostLog (or MarkAuditLogged) has run for r.
|
|
func AuditAlreadyLogged(r *http.Request) bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
if v, ok := r.Context().Value(auditLogCtxKey{}).(*atomic.Bool); ok && v != nil {
|
|
return v.Load()
|
|
}
|
|
return false
|
|
}
|