mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
fix(transport): keep harmless query values visible in --debug output
--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>
This commit is contained in:
7 files changed
+225
-17
No files matched your search
@@ -4,10 +4,12 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -19,32 +21,74 @@ type debugTransport struct {
|
||||
func (t debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
start := time.Now()
|
||||
resp, err := t.base.RoundTrip(req)
|
||||
var outcome string
|
||||
var outcome, message string
|
||||
if err != nil {
|
||||
outcome = "error: " + err.Error()
|
||||
} else {
|
||||
outcome = resp.Status
|
||||
message = failureMessage(req, resp)
|
||||
}
|
||||
_, _ = fmt.Fprintf(t.out, "[debug] %s %s -> %s (%s)\n", req.Method, redactURL(req.URL), outcome, time.Since(start).Round(time.Millisecond))
|
||||
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
|
||||
}
|
||||
|
||||
// redactURL drops userinfo and query values, which can carry tokens or signatures.
|
||||
// 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 {
|
||||
query[key] = []string{"REDACTED"}
|
||||
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. Headers and bodies are never logged.
|
||||
// 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
|
||||
|
||||
Reference in new issue
Block a user