Compare commits

..

No commits in common. "main" and "v0.0.15" have entirely different histories.

14 changed files with 34 additions and 246 deletions

View File

@ -168,16 +168,13 @@ func validateASNMetadataManifest(data, snapshot []byte, count int, generatedAt t
func fetchASNMetadata(ctx context.Context, client *http.Client, snapshotURL string) ([]byte, error) { func fetchASNMetadata(ctx context.Context, client *http.Client, snapshotURL string) ([]byte, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, snapshotURL, nil) request, err := http.NewRequestWithContext(ctx, http.MethodGet, snapshotURL, nil)
if err != nil { if err != nil {
return nil, errors.New("create ASN metadata request failed") return nil, err
} }
request.Header.Set("Accept", "application/json") request.Header.Set("Accept", "application/json")
request.Header.Set("User-Agent", "oneclickvirt-backtrace-asn-metadata/1") request.Header.Set("User-Agent", "oneclickvirt-backtrace-asn-metadata/1")
response, err := client.Do(request) response, err := client.Do(request)
if err != nil { if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil { return nil, err
return nil, ctxErr
}
return nil, errors.New("ASN metadata request failed")
} }
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode != http.StatusOK { if response.StatusCode != http.StatusOK {
@ -186,7 +183,7 @@ func fetchASNMetadata(ctx context.Context, client *http.Client, snapshotURL stri
const maximumSize = 4 << 20 const maximumSize = 4 << 20
data, err := io.ReadAll(io.LimitReader(response.Body, maximumSize+1)) data, err := io.ReadAll(io.LimitReader(response.Body, maximumSize+1))
if err != nil { if err != nil {
return nil, errors.New("ASN metadata response read failed") return nil, err
} }
if len(data) > maximumSize { if len(data) > maximumSize {
return nil, fmt.Errorf("ASN metadata exceeds %d bytes", maximumSize) return nil, fmt.Errorf("ASN metadata exceeds %d bytes", maximumSize)

View File

@ -53,22 +53,19 @@ func ResolveOriginASNWithConfig(parent context.Context, ip string, config Origin
} }
requestURL, err := addQuery(config.BaseURL, "resource", parsed.String()) requestURL, err := addQuery(config.BaseURL, "resource", parsed.String())
if err != nil { if err != nil {
return "", errors.New("invalid origin ASN source") return "", err
} }
ctx, cancel := context.WithTimeout(parent, config.Timeout) ctx, cancel := context.WithTimeout(parent, config.Timeout)
defer cancel() defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
if err != nil { if err != nil {
return "", errors.New("create origin ASN request failed") return "", err
} }
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "oneclickvirt-backtrace-origin-asn/1") req.Header.Set("User-Agent", "oneclickvirt-backtrace-origin-asn/1")
response, err := config.Client.Do(req) response, err := config.Client.Do(req)
if err != nil { if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil { return "", err
return "", ctxErr
}
return "", errors.New("origin ASN request failed")
} }
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode == http.StatusTooManyRequests { if response.StatusCode == http.StatusTooManyRequests {
@ -79,7 +76,7 @@ func ResolveOriginASNWithConfig(parent context.Context, ip string, config Origin
} }
body, err := io.ReadAll(io.LimitReader(response.Body, config.MaxResponseSize+1)) body, err := io.ReadAll(io.LimitReader(response.Body, config.MaxResponseSize+1))
if err != nil { if err != nil {
return "", errors.New("origin ASN response read failed") return "", err
} }
if int64(len(body)) > config.MaxResponseSize { if int64(len(body)) > config.MaxResponseSize {
return "", fmt.Errorf("origin ASN response exceeds %d bytes", config.MaxResponseSize) return "", fmt.Errorf("origin ASN response exceeds %d bytes", config.MaxResponseSize)

View File

@ -1,97 +0,0 @@
package bgptools
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
type privacyRoundTripper func(*http.Request) (*http.Response, error)
func (fn privacyRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
return fn(request)
}
func TestStructuredReportErrorsDoNotExposeRemoteURLs(t *testing.T) {
var rdap *httptest.Server
rdap = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = fmt.Fprintf(w, `{"handle":"TEST","links":[{"rel":"geofeed","href":"https://private.example/geofeed?token=secret"}]}`)
}))
defer rdap.Close()
failing := &http.Client{Transport: privacyRoundTripper(func(request *http.Request) (*http.Response, error) {
return nil, errors.New("dial " + request.URL.String())
})}
report, err := QueryIPBGPReport(context.Background(), "192.0.2.1", IPBGPReportConfig{
RDAPClient: rdap.Client(),
RDAPBaseURL: rdap.URL,
FetchGeofeed: true,
GeofeedClient: failing,
ResolveASN: func(context.Context, string) (string, error) {
return "", errors.New("resolver https://private.example/asn?key=secret")
},
})
if err != nil {
t.Fatal(err)
}
for _, source := range report.Sources {
for _, field := range []string{source.Source, source.Error} {
for _, forbidden := range []string{"private.example", "token=", "key=", "secret"} {
if strings.Contains(field, forbidden) {
t.Fatalf("structured source leaked %q: %+v", forbidden, source)
}
}
}
}
if len(report.Geofeeds) != 1 || report.Geofeeds[0].Error != "request_failed" {
t.Fatalf("geofeed error was not stabilized: %+v", report.Geofeeds)
}
encoded, err := json.Marshal(report)
if err != nil {
t.Fatal(err)
}
for _, forbidden := range []string{"token=", "key=", "secret", "userinfo@"} {
if strings.Contains(string(encoded), forbidden) {
t.Fatalf("structured report leaked %q: %s", forbidden, encoded)
}
}
if report.RDAP == nil || len(report.RDAP.GeofeedURLs) != 1 || report.RDAP.GeofeedURLs[0] != "https://private.example/geofeed" || report.Geofeeds[0].URL != "https://private.example/geofeed" {
t.Fatalf("geofeed URL was not sanitized consistently: %+v", report)
}
}
func TestRemoteFetchErrorsDoNotExposeSourceURL(t *testing.T) {
client := &http.Client{Transport: privacyRoundTripper(func(request *http.Request) (*http.Response, error) {
return nil, errors.New("dial " + request.URL.String())
})}
tests := []struct {
name string
run func() error
}{
{name: "ASN metadata", run: func() error {
_, err := fetchASNMetadata(context.Background(), client, "https://private.example/asn?token=secret")
return err
}},
{name: "RDAP", run: func() error {
_, err := QueryRDAP(context.Background(), "192.0.2.1", client, "https://private.example/rdap?token=secret")
return err
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := test.run()
if err == nil {
t.Fatal("expected request failure")
}
for _, forbidden := range []string{"private.example", "token=", "secret"} {
if strings.Contains(err.Error(), forbidden) {
t.Fatalf("error leaked %q: %v", forbidden, err)
}
}
})
}
}

View File

@ -60,16 +60,13 @@ func QueryRDAP(ctx context.Context, ip string, client *http.Client, baseURL stri
requestURL := strings.TrimRight(baseURL, "/") + "/" + parsed.String() requestURL := strings.TrimRight(baseURL, "/") + "/" + parsed.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
if err != nil { if err != nil {
return RDAPRecord{}, errors.New("create RDAP request failed") return RDAPRecord{}, err
} }
req.Header.Set("Accept", "application/rdap+json, application/json") req.Header.Set("Accept", "application/rdap+json, application/json")
req.Header.Set("User-Agent", "oneclickvirt-backtrace-rdap/1") req.Header.Set("User-Agent", "oneclickvirt-backtrace-rdap/1")
response, err := client.Do(req) response, err := client.Do(req)
if err != nil { if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil { return RDAPRecord{}, err
return RDAPRecord{}, ctxErr
}
return RDAPRecord{}, errors.New("RDAP request failed")
} }
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode == http.StatusTooManyRequests { if response.StatusCode == http.StatusTooManyRequests {
@ -80,7 +77,7 @@ func QueryRDAP(ctx context.Context, ip string, client *http.Client, baseURL stri
} }
data, err := io.ReadAll(io.LimitReader(response.Body, (4<<20)+1)) data, err := io.ReadAll(io.LimitReader(response.Body, (4<<20)+1))
if err != nil { if err != nil {
return RDAPRecord{}, errors.New("RDAP response read failed") return RDAPRecord{}, err
} }
if len(data) > 4<<20 { if len(data) > 4<<20 {
return RDAPRecord{}, ErrRDAPResponseTooLarge return RDAPRecord{}, ErrRDAPResponseTooLarge
@ -128,7 +125,7 @@ func parseRDAPRecord(ip, source string, data []byte) (RDAPRecord, error) {
record := RDAPRecord{ record := RDAPRecord{
IP: ip, Handle: payload.Handle, Name: payload.Name, Type: payload.Type, IP: ip, Handle: payload.Handle, Name: payload.Name, Type: payload.Type,
Country: payload.Country, StartAddress: payload.StartAddress, EndAddress: payload.EndAddress, Country: payload.Country, StartAddress: payload.StartAddress, EndAddress: payload.EndAddress,
ParentHandle: payload.ParentHandle, Port43: payload.Port43, Status: payload.Status, Source: "rdap", ParentHandle: payload.ParentHandle, Port43: payload.Port43, Status: payload.Status, Source: source,
} }
for _, event := range payload.Events { for _, event := range payload.Events {
switch strings.ToLower(event.Action) { switch strings.ToLower(event.Action) {

View File

@ -131,7 +131,7 @@ func QueryASNRelationships(ctx context.Context, asn string, cfg RelationshipConf
report := &ASNRelationshipReport{TargetASN: normalized} report := &ASNRelationshipReport{TargetASN: normalized}
ripeURL, err := addQuery(cfg.RIPEstatURL, "resource", normalized) ripeURL, err := addQuery(cfg.RIPEstatURL, "resource", normalized)
if err != nil { if err != nil {
return nil, errors.New("invalid relationship source configuration") return nil, err
} }
ripeRaw, ripeStatus, ripeErr := fetchRelationshipJSON(ctx, cfg, "ripestat", ripeURL) ripeRaw, ripeStatus, ripeErr := fetchRelationshipJSON(ctx, cfg, "ripestat", ripeURL)
report.SourceStatuses = append(report.SourceStatuses, sourceStatus("ripestat", ripeStatus, ripeErr)) report.SourceStatuses = append(report.SourceStatuses, sourceStatus("ripestat", ripeStatus, ripeErr))
@ -153,7 +153,7 @@ func QueryASNRelationships(ctx context.Context, asn string, cfg RelationshipConf
peeringURL, err := addQuery(cfg.PeeringDBURL, "asn", normalized) peeringURL, err := addQuery(cfg.PeeringDBURL, "asn", normalized)
if err != nil { if err != nil {
return nil, errors.New("invalid relationship source configuration") return nil, err
} }
peeringRaw, peeringStatus, peeringErr := fetchRelationshipJSON(ctx, cfg, "peeringdb", peeringURL) peeringRaw, peeringStatus, peeringErr := fetchRelationshipJSON(ctx, cfg, "peeringdb", peeringURL)
report.SourceStatuses = append(report.SourceStatuses, sourceStatus("peeringdb", peeringStatus, peeringErr)) report.SourceStatuses = append(report.SourceStatuses, sourceStatus("peeringdb", peeringStatus, peeringErr))
@ -255,16 +255,7 @@ func sourceStatus(source string, status RelationshipStatus, err error) Relations
Timeout: status == RelationshipTimeout, Timeout: status == RelationshipTimeout,
} }
if err != nil { if err != nil {
switch status { item.Error = err.Error()
case RelationshipRateLimited:
item.Error = "rate_limited"
case RelationshipTimeout:
item.Error = "timeout"
case RelationshipMissingFields:
item.Error = "missing_fields"
default:
item.Error = "request_failed"
}
} }
return item return item
} }

View File

@ -217,12 +217,6 @@ func QueryIPBGPReport(parent context.Context, ip string, cfg IPBGPReportConfig)
geofeedURLs = append(geofeedURLs, report.WHOIS.GeofeedURLs...) geofeedURLs = append(geofeedURLs, report.WHOIS.GeofeedURLs...)
} }
geofeedURLs = uniqueSortedStrings(geofeedURLs) geofeedURLs = uniqueSortedStrings(geofeedURLs)
if report.RDAP != nil {
report.RDAP.GeofeedURLs = sanitizeReportURLs(report.RDAP.GeofeedURLs)
}
if report.WHOIS != nil {
report.WHOIS.GeofeedURLs = sanitizeReportURLs(report.WHOIS.GeofeedURLs)
}
for _, geofeedURL := range geofeedURLs { for _, geofeedURL := range geofeedURLs {
if !cfg.FetchGeofeed { if !cfg.FetchGeofeed {
report.Geofeeds = append(report.Geofeeds, GeofeedResult{URL: geofeedURL, Status: ReportUnsupported, Error: "fetch disabled"}) report.Geofeeds = append(report.Geofeeds, GeofeedResult{URL: geofeedURL, Status: ReportUnsupported, Error: "fetch disabled"})
@ -230,7 +224,7 @@ func QueryIPBGPReport(parent context.Context, ip string, cfg IPBGPReportConfig)
} }
result := fetchGeofeed(ctx, geofeedURL, cfg) result := fetchGeofeed(ctx, geofeedURL, cfg)
report.Geofeeds = append(report.Geofeeds, result) report.Geofeeds = append(report.Geofeeds, result)
report.Sources = append(report.Sources, IPBGPSourceStatus{Source: "geofeed", Status: result.Status, Error: result.Error}) report.Sources = append(report.Sources, IPBGPSourceStatus{Source: "geofeed:" + geofeedURL, Status: result.Status, Error: result.Error})
} }
asn := strings.TrimSpace(cfg.ASN) asn := strings.TrimSpace(cfg.ASN)
@ -292,7 +286,7 @@ func inferRIR(record RDAPRecord, available bool) RIRInfo {
} }
func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig) GeofeedResult { func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig) GeofeedResult {
result := GeofeedResult{URL: sanitizeReportURL(rawURL)} result := GeofeedResult{URL: rawURL}
u, err := url.Parse(rawURL) u, err := url.Parse(rawURL)
if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") { if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https") {
result.Status = ReportUnsupported result.Status = ReportUnsupported
@ -316,7 +310,7 @@ func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil { if err != nil {
result.Status = ReportError result.Status = ReportError
result.Error = stableReportError(result.Status, err) result.Error = err.Error()
return result return result
} }
req.Header.Set("Accept", "text/csv, text/plain, */*") req.Header.Set("Accept", "text/csv, text/plain, */*")
@ -324,7 +318,7 @@ func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig)
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
result.Status = reportStatusForError(err) result.Status = reportStatusForError(err)
result.Error = stableReportError(result.Status, err) result.Error = err.Error()
return result return result
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -347,7 +341,7 @@ func fetchGeofeed(parent context.Context, rawURL string, cfg IPBGPReportConfig)
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1)) body, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
if err != nil { if err != nil {
result.Status = reportStatusForError(err) result.Status = reportStatusForError(err)
result.Error = stableReportError(result.Status, err) result.Error = err.Error()
return result return result
} }
if int64(len(body)) > maxBytes { if int64(len(body)) > maxBytes {
@ -523,51 +517,11 @@ func rirNameFromText(value string) string {
func sourceStatusForError(source string, status ReportStatus, err error) IPBGPSourceStatus { func sourceStatusForError(source string, status ReportStatus, err error) IPBGPSourceStatus {
item := IPBGPSourceStatus{Source: source, Status: status} item := IPBGPSourceStatus{Source: source, Status: status}
if err != nil { if err != nil {
item.Error = stableReportError(status, err) item.Error = err.Error()
} }
return item return item
} }
func stableReportError(status ReportStatus, err error) string {
if errors.Is(err, context.Canceled) {
return "canceled"
}
switch status {
case ReportRateLimited:
return "rate_limited"
case ReportTimeout:
return "timeout"
case ReportUnsupported:
return "unsupported"
case ReportMissingFields:
return "missing_fields"
default:
return "request_failed"
}
}
func sanitizeReportURL(raw string) string {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return ""
}
parsed.User = nil
parsed.RawQuery = ""
parsed.ForceQuery = false
parsed.Fragment = ""
return parsed.String()
}
func sanitizeReportURLs(values []string) []string {
result := make([]string, 0, len(values))
for _, value := range values {
if sanitized := sanitizeReportURL(value); sanitized != "" {
result = append(result, sanitized)
}
}
return uniqueSortedStrings(result)
}
func reportStatusForError(err error) ReportStatus { func reportStatusForError(err error) ReportStatus {
if err == nil { if err == nil {
return ReportAvailable return ReportAvailable

View File

@ -210,7 +210,6 @@
240e:4b:8000::/33 240e:4b:8000::/33
240e:4b::/33 240e:4b::/33
240e:4c:4006::/48 240e:4c:4006::/48
240e:4c:5000::/48
240e:4c:8000::/33 240e:4c:8000::/33
240e:4c::/33 240e:4c::/33
240e:4d:50ff::/48 240e:4d:50ff::/48

View File

@ -1,7 +1,7 @@
{ {
"schema": "backtrace.asn-prefixes/v1", "schema": "backtrace.asn-prefixes/v1",
"file": "as4134.txt", "file": "as4134.txt",
"count": 629, "count": 628,
"sha256": "00941f76c022566166b926670ee9358084c53465cbaaf75a3da7b73d997d1947", "sha256": "9e032d30479db6c5bd167681048922765c23d1a35b01ed9e6968b2099b8b916f",
"generated_at": "2026-07-23T02:56:31Z" "generated_at": "2026-07-20T19:12:53Z"
} }

View File

@ -6639,6 +6639,7 @@
2409:8c7c::/32 2409:8c7c::/32
2409:8c7e:9301::/48 2409:8c7e:9301::/48
2409:8c7e::/32 2409:8c7e::/32
2409:8c85:1000::/48
2409:8c85:1001::/48 2409:8c85:1001::/48
2409:8c85:1::/48 2409:8c85:1::/48
2409:8c85:200::/48 2409:8c85:200::/48

View File

@ -1,7 +1,7 @@
{ {
"schema": "backtrace.asn-prefixes/v1", "schema": "backtrace.asn-prefixes/v1",
"file": "as9808.txt", "file": "as9808.txt",
"count": 7170, "count": 7171,
"sha256": "7532e5bba93ed6b7d5cf010b22d9f5c87606392673fd9877ce28417f2baadd5d", "sha256": "c362e447b515ca510b10fd37ba50189c3551a35dede88399190bc869b6fbe66b",
"generated_at": "2026-07-23T02:56:31Z" "generated_at": "2026-07-20T19:12:54Z"
} }

View File

@ -96,7 +96,7 @@ func main() {
return return
} }
if err := validateStructuredOptions(options); err != nil { if err := validateStructuredOptions(options); err != nil {
fmt.Fprintln(os.Stderr, sanitizeErrorText(err.Error())) fmt.Fprintln(os.Stderr, err)
os.Exit(2) os.Exit(2)
} }
if options.jsonOutput { if options.jsonOutput {
@ -109,7 +109,7 @@ func main() {
}, },
} }
if err := writeStructuredReport(context.Background(), os.Stdout, options.specifiedIP, config, bgptools.QueryIPBGPReport); err != nil { if err := writeStructuredReport(context.Background(), os.Stdout, options.specifiedIP, config, bgptools.QueryIPBGPReport); err != nil {
fmt.Fprintf(os.Stderr, "structured report failed: %s\n", sanitizeErrorText(err.Error())) fmt.Fprintf(os.Stderr, "structured report failed: %v\n", err)
os.Exit(1) os.Exit(1)
} }
return return
@ -118,12 +118,12 @@ func main() {
if options.showIPInfo { if options.showIPInfo {
rsp, err := http.Get("http://ipinfo.io") rsp, err := http.Get("http://ipinfo.io")
if err != nil { if err != nil {
fmt.Printf("get ip info err %s \n", sanitizeErrorText(err.Error())) fmt.Printf("get ip info err %v \n", err.Error())
} else { } else {
defer rsp.Body.Close() defer rsp.Body.Close()
err = json.NewDecoder(rsp.Body).Decode(&info) err = json.NewDecoder(rsp.Body).Decode(&info)
if err != nil { if err != nil {
fmt.Printf("json decode err %s \n", sanitizeErrorText(err.Error())) fmt.Printf("json decode err %v \n", err.Error())
} else { } else {
fmt.Println(Green("国家: ") + White(info.Country) + Green(" 城市: ") + White(info.City) + fmt.Println(Green("国家: ") + White(info.Country) + Green(" 城市: ") + White(info.City) +
Green(" 服务商: ") + Blue(info.Org)) Green(" 服务商: ") + Blue(info.Org))
@ -186,10 +186,10 @@ func main() {
}) })
wg.Wait() wg.Wait()
if results.bgpResult != "" { if results.bgpResult != "" {
fmt.Print(indentLegacyOutput(results.bgpResult)) fmt.Print(results.bgpResult)
} }
if results.backtraceResult != "" { if results.backtraceResult != "" {
fmt.Printf("%s\n", indentLegacyOutput(results.backtraceResult)) fmt.Printf("%s\n", results.backtraceResult)
} }
fmt.Println(Yellow("准确线路自行查看详细路由,本测试结果仅作参考")) fmt.Println(Yellow("准确线路自行查看详细路由,本测试结果仅作参考"))
fmt.Println(Yellow("同一目标地址多个线路时,检测可能已越过汇聚层,除第一个线路外,后续信息可能无效")) fmt.Println(Yellow("同一目标地址多个线路时,检测可能已越过汇聚层,除第一个线路外,后续信息可能无效"))

View File

@ -1,27 +0,0 @@
package main
import (
"regexp"
"strings"
)
var privateErrorPattern = regexp.MustCompile("(?i)(https?://)[^\\s]+")
var privateTokenPattern = regexp.MustCompile("(?i)(^|[?&\\s])(token|api[_-]?key|secret|authorization|password|key)=([^&\\s]+)")
var privatePathPattern = regexp.MustCompile("(?:[A-Za-z]:\\\\|/)[^\\s]+")
func indentLegacyOutput(value string) string {
lines := strings.Split(value, "\n")
for index, line := range lines {
if line != "" && line[0] != ' ' && line[0] != '\t' {
lines[index] = " " + line
}
}
return strings.Join(lines, "\n")
}
// sanitizeErrorText keeps remote-source and credential details out of user-visible errors.
func sanitizeErrorText(value string) string {
value = privateErrorPattern.ReplaceAllString(value, "<remote>")
value = privateTokenPattern.ReplaceAllString(value, "$1$2=<redacted>")
return privatePathPattern.ReplaceAllString(value, "<path>")
}

View File

@ -1,24 +0,0 @@
package main
import (
"strings"
"testing"
)
func TestIndentLegacyOutputLeavesOneLeadingCell(t *testing.T) {
got := indentLegacyOutput("alpha\n beta\n\tgamma\n")
want := " alpha\n beta\n\tgamma\n"
if got != want {
t.Fatalf("indentLegacyOutput() = %q, want %q", got, want)
}
}
func TestSanitizeErrorTextRedactsRemoteSecretsAndPaths(t *testing.T) {
input := "load https://private.example/repo/data?token=secret from /private/cache failed: api_key=hidden"
got := sanitizeErrorText(input)
for _, forbidden := range []string{"private.example", "secret", "hidden", "/private/cache"} {
if strings.Contains(got, forbidden) {
t.Fatalf("sanitizeErrorText() leaked %q in %q", forbidden, got)
}
}
}

View File

@ -2,7 +2,7 @@ package model
import "time" import "time"
const BackTraceVersion = "v0.0.17" const BackTraceVersion = "v0.0.15"
var EnableLoger = false var EnableLoger = false