mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
--debug replaced every query value with REDACTED, so it could not show which filters were sent. Redact only parameters that can authenticate a request (tokens, passwords, secrets, signatures, storage credentials, OAuth codes and the OAuth state), and log the sanitized server message of failed responses. A 404 now names the request instead of blaming the instance, and API errors recorded from a request keep its instance and credential so a caller can look up why it failed. Network failures keep the server they could not reach. Signed-off-by: bircni <bircni@icloud.com>
103 lines
2.9 KiB
Go
103 lines
2.9 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package transport
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type debugTransport struct {
|
|
base http.RoundTripper
|
|
out io.Writer
|
|
}
|
|
|
|
func (t debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
start := time.Now()
|
|
resp, err := t.base.RoundTrip(req)
|
|
var outcome, message string
|
|
if err != nil {
|
|
outcome = "error: " + err.Error()
|
|
} else {
|
|
outcome = resp.Status
|
|
message = failureMessage(req, resp)
|
|
}
|
|
line := fmt.Sprintf("[debug] %s %s -> %s (%s)", req.Method, redactURL(req.URL), outcome, time.Since(start).Round(time.Millisecond))
|
|
if message != "" {
|
|
line += ": " + message
|
|
}
|
|
_, _ = fmt.Fprintln(t.out, line)
|
|
return resp, err
|
|
}
|
|
|
|
// failureMessage returns the sanitized server message of a failed response and keeps its body
|
|
// readable, so --debug shows what the server said when tea explains the failure in its own words.
|
|
func failureMessage(req *http.Request, resp *http.Response) string {
|
|
if resp.StatusCode < 400 {
|
|
return ""
|
|
}
|
|
data, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrorBody))
|
|
resp.Body = struct {
|
|
io.Reader
|
|
io.Closer
|
|
}{io.MultiReader(bytes.NewReader(data), resp.Body), resp.Body}
|
|
token := strings.TrimPrefix(req.Header.Get("Authorization"), "token ")
|
|
return newAPIError(resp.StatusCode, req.Method, req.URL.Path, token, data).Message
|
|
}
|
|
|
|
// redactURL drops userinfo and the values of query parameters that can carry credentials or
|
|
// signatures, keeping filters such as limit, page and state readable.
|
|
func redactURL(u *url.URL) string {
|
|
clean := *u
|
|
clean.User = nil
|
|
if clean.RawQuery != "" {
|
|
query := clean.Query()
|
|
for key := range query {
|
|
if sensitiveQuery(clean.Path, key) {
|
|
query[key] = []string{"REDACTED"}
|
|
}
|
|
}
|
|
clean.RawQuery = query.Encode()
|
|
}
|
|
return clean.String()
|
|
}
|
|
|
|
// sensitiveQuery reports query parameters whose values can authenticate a request: tokens,
|
|
// passwords, secrets, signatures and credentials of signed storage URLs, and OAuth codes and state.
|
|
func sensitiveQuery(path, key string) bool {
|
|
key = strings.ToLower(key)
|
|
switch {
|
|
case key == "code" || key == "key" || strings.HasPrefix(key, "sig"):
|
|
return true
|
|
case key == "state":
|
|
return strings.Contains(path, "/login/oauth/")
|
|
}
|
|
for _, word := range []string{"token", "password", "secret", "signature", "credential"} {
|
|
if strings.Contains(key, word) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Debug returns a copy of c that logs the method, redacted URL, status, and duration of each
|
|
// request to w, plus the sanitized server message of a failed response. Headers and bodies are
|
|
// never logged.
|
|
func (c *Client) Debug(w io.Writer) *Client {
|
|
clone := *c
|
|
httpClient := *c.HTTP
|
|
base := httpClient.Transport
|
|
if base == nil {
|
|
base = http.DefaultTransport
|
|
}
|
|
httpClient.Transport = debugTransport{base: base, out: w}
|
|
clone.HTTP = &httpClient
|
|
return &clone
|
|
}
|