mirror of
https://github.com/oneclickvirt/backtrace.git
synced 2026-07-23 11:20:13 +08:00
feat: extend diagnostics and harden output
Some checks are pending
Sync ASN metadata / sync (push) Waiting to run
Some checks are pending
Sync ASN metadata / sync (push) Waiting to run
This commit is contained in:
parent
46e20f680e
commit
f0ba5fa410
@ -168,13 +168,16 @@ 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, err
|
return nil, errors.New("create ASN metadata request failed")
|
||||||
}
|
}
|
||||||
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 {
|
||||||
return nil, err
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
|
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 {
|
||||||
@ -183,7 +186,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, err
|
return nil, errors.New("ASN metadata response read failed")
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|||||||
@ -53,19 +53,22 @@ 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 "", err
|
return "", errors.New("invalid origin ASN source")
|
||||||
}
|
}
|
||||||
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 "", err
|
return "", errors.New("create origin ASN request failed")
|
||||||
}
|
}
|
||||||
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 {
|
||||||
return "", err
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
|
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 {
|
||||||
@ -76,7 +79,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 "", err
|
return "", errors.New("origin ASN response read failed")
|
||||||
}
|
}
|
||||||
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)
|
||||||
|
|||||||
47
bgptools/privacy_test.go
Normal file
47
bgptools/privacy_test.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package bgptools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type privacyRoundTripper func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (fn privacyRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
return fn(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -60,13 +60,16 @@ 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{}, err
|
return RDAPRecord{}, errors.New("create RDAP request failed")
|
||||||
}
|
}
|
||||||
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 {
|
||||||
return RDAPRecord{}, err
|
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
|
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 {
|
||||||
@ -77,7 +80,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{}, err
|
return RDAPRecord{}, errors.New("RDAP response read failed")
|
||||||
}
|
}
|
||||||
if len(data) > 4<<20 {
|
if len(data) > 4<<20 {
|
||||||
return RDAPRecord{}, ErrRDAPResponseTooLarge
|
return RDAPRecord{}, ErrRDAPResponseTooLarge
|
||||||
|
|||||||
12
cmd/main.go
12
cmd/main.go
@ -96,7 +96,7 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := validateStructuredOptions(options); err != nil {
|
if err := validateStructuredOptions(options); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, sanitizeErrorText(err.Error()))
|
||||||
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: %v\n", err)
|
fmt.Fprintf(os.Stderr, "structured report failed: %s\n", sanitizeErrorText(err.Error()))
|
||||||
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 %v \n", err.Error())
|
fmt.Printf("get ip info err %s \n", sanitizeErrorText(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 %v \n", err.Error())
|
fmt.Printf("json decode err %s \n", sanitizeErrorText(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(results.bgpResult)
|
fmt.Print(indentLegacyOutput(results.bgpResult))
|
||||||
}
|
}
|
||||||
if results.backtraceResult != "" {
|
if results.backtraceResult != "" {
|
||||||
fmt.Printf("%s\n", results.backtraceResult)
|
fmt.Printf("%s\n", indentLegacyOutput(results.backtraceResult))
|
||||||
}
|
}
|
||||||
fmt.Println(Yellow("准确线路自行查看详细路由,本测试结果仅作参考"))
|
fmt.Println(Yellow("准确线路自行查看详细路由,本测试结果仅作参考"))
|
||||||
fmt.Println(Yellow("同一目标地址多个线路时,检测可能已越过汇聚层,除第一个线路外,后续信息可能无效"))
|
fmt.Println(Yellow("同一目标地址多个线路时,检测可能已越过汇聚层,除第一个线路外,后续信息可能无效"))
|
||||||
|
|||||||
27
cmd/output.go
Normal file
27
cmd/output.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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>")
|
||||||
|
}
|
||||||
24
cmd/output_test.go
Normal file
24
cmd/output_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@ package model
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
const BackTraceVersion = "v0.0.15"
|
const BackTraceVersion = "v0.0.16"
|
||||||
|
|
||||||
var EnableLoger = false
|
var EnableLoger = false
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user