diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 5b1128b6..6ee78e52 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,7 @@ - fix: optimize websocket step initialization - fix: reuse plugin instance if already initialized - fix: deep copy api step to avoid data racing +- feat: support ping/dns/traceroute for dial test ## v4.1.6 (2022-07-04) diff --git a/go.mod b/go.mod index c175a55c..51da4a79 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/getsentry/sentry-go v0.13.0 github.com/go-errors/errors v1.0.1 github.com/go-openapi/spec v0.20.6 + github.com/go-ping/ping v1.1.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.4.1 @@ -17,6 +18,7 @@ require ( github.com/jmespath/go-jmespath v0.4.0 github.com/json-iterator/go v1.1.12 github.com/maja42/goval v1.2.1 + github.com/miekg/dns v1.0.14 github.com/mitchellh/mapstructure v1.4.1 github.com/olekukonko/tablewriter v0.0.5 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 600942d3..09453f97 100644 --- a/go.sum +++ b/go.sum @@ -145,6 +145,8 @@ github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-ping/ping v1.1.0 h1:3MCGhVX4fyEUuhsfwPrsEdQw6xspHkv5zHsiSoDFZYw= +github.com/go-ping/ping v1.1.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -222,6 +224,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -343,6 +346,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -510,6 +514,7 @@ golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e h1:1SzTfNOXwIS2oWiMF+6qu0OUDKb0dauo6MoDUQyu+yU= golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -615,6 +620,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/hrp/cmd/dial.go b/hrp/cmd/dial.go new file mode 100644 index 00000000..df14f3b1 --- /dev/null +++ b/hrp/cmd/dial.go @@ -0,0 +1,96 @@ +package cmd + +import ( + "runtime" + "time" + + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + + "github.com/httprunner/httprunner/v4/hrp/internal/dial" +) + +var ( + pingOptions dial.PingOptions + dnsOptions dial.DnsOptions + traceRouteOptions dial.TraceRouteOptions +) + +var pingCmd = &cobra.Command{ + Use: "ping $url", + Short: "run integrated ping command", + Args: cobra.ExactArgs(1), + PreRun: func(cmd *cobra.Command, args []string) { + setLogLevel(logLevel) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return dial.DoPing(&pingOptions, args) + }, +} + +var dnsCmd = &cobra.Command{ + Use: "dns $url", + Short: "DNS resolution for different source and record types", + Args: cobra.ExactArgs(1), + PreRun: func(cmd *cobra.Command, args []string) { + setLogLevel(logLevel) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if dnsOptions.DnsSourceType != dial.DnsSourceTypeLocal && dnsOptions.DnsServer != "" { + log.Warn().Msg("DNS server not supported for non-local DNS source, ignored") + } + if dnsOptions.DnsSourceType == dial.DnsSourceTypeHttp && dnsOptions.DnsRecordType == dial.DnsRecordTypeCNAME { + log.Warn().Msg("CNAME record not supported for http DNS source, using default record type(A)") + } + return dial.DoDns(&dnsOptions, args) + }, +} + +var traceRouteCmd = &cobra.Command{ + Use: "traceroute $url", + Short: "run integrated traceroute command", + Args: cobra.ExactArgs(1), + PreRun: func(cmd *cobra.Command, args []string) { + setLogLevel(logLevel) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if runtime.GOOS == "windows" { + log.Info().Msg("using default probe number (3) on Windows") + } + return dial.DoTraceRoute(&traceRouteOptions, args) + }, +} + +var curlCmd = &cobra.Command{ + Use: "curl $url", + Short: "run integrated curl command", + Args: cobra.MinimumNArgs(1), + DisableFlagParsing: true, + PreRun: func(cmd *cobra.Command, args []string) { + setLogLevel(logLevel) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return dial.DoCurl(args) + }, +} + +func init() { + rootCmd.AddCommand(pingCmd) + pingCmd.Flags().IntVarP(&pingOptions.Count, "count", "c", 10, "Stop after sending (and receiving) N packets") + pingCmd.Flags().DurationVarP(&pingOptions.Timeout, "timeout", "t", 20*time.Second, "Ping exits after N seconds") + pingCmd.Flags().DurationVarP(&pingOptions.Interval, "interval", "i", 1*time.Second, "Wait N seconds between sending each packet") + pingCmd.Flags().BoolVar(&pingOptions.SaveTests, "save-tests", false, "Save ping result as json") + + rootCmd.AddCommand(dnsCmd) + dnsCmd.Flags().IntVar(&dnsOptions.DnsSourceType, "dns-source", 0, "DNS source type\n0: local DNS\n1: http DNS\n2: google DNS") + dnsCmd.Flags().IntVar(&dnsOptions.DnsRecordType, "dns-record", 1, "DNS record type\n1: A\n28: AAAA\n5: CNAME") + dnsCmd.Flags().StringVar(&dnsOptions.DnsServer, "dns-server", "", "DNS server, only available for local DNS source") + dnsCmd.Flags().BoolVar(&dnsOptions.SaveTests, "save-tests", false, "Save DNS resolution result as json") + + rootCmd.AddCommand(traceRouteCmd) + traceRouteCmd.Flags().IntVarP(&traceRouteOptions.MaxTTL, "max-hops", "m", 30, "Set the max number of hops (max TTL to be reached)") + traceRouteCmd.Flags().IntVarP(&traceRouteOptions.Queries, "queries", "q", 1, "Set the number of probes per each hop") + traceRouteCmd.Flags().BoolVar(&traceRouteOptions.SaveTests, "save-tests", false, "Save traceroute result as json") + + rootCmd.AddCommand(curlCmd) +} diff --git a/hrp/internal/dial/curl.go b/hrp/internal/dial/curl.go new file mode 100644 index 00000000..8cd3436a --- /dev/null +++ b/hrp/internal/dial/curl.go @@ -0,0 +1,72 @@ +package dial + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" +) + +const ( + normalResult = "STDOUT" + errorResult = "STDERR" + failedResult = "FAILED" +) + +type CurlResult struct { + Result string `json:"result"` + ErrorMsg string `json:"errorMsg"` + ResultType string `json:"resultType"` +} + +func DoCurl(args []string) (err error) { + var saveTests bool + for i, arg := range args { + if arg == "--save-tests" { + args = append(args[:i], args[i+1:]...) + saveTests = true + } + } + var curlResult CurlResult + defer func() { + if saveTests { + dir, _ := os.Getwd() + curlResultName := fmt.Sprintf("curl_result_%v.json", time.Now().Format("20060102150405")) + curlResultPath := filepath.Join(dir, curlResultName) + err = builtin.Dump2JSON(curlResult, curlResultPath) + if err != nil { + log.Error().Err(err).Msg("save dns resolution result failed") + } + } + }() + + cmd := exec.Command("curl", args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err = cmd.Run() + if err != nil { + log.Error().Err(err).Msgf("fail to run curl command") + curlResult.ErrorMsg = err.Error() + curlResult.Result = stderr.String() + curlResult.ResultType = errorResult + return + } + if stdout.String() != "" { + fmt.Printf(stdout.String()) + curlResult.Result = stdout.String() + curlResult.ResultType = normalResult + } else if stderr.String() != "" { + fmt.Printf(stderr.String()) + curlResult.ErrorMsg = stderr.String() + curlResult.ResultType = errorResult + } + return +} diff --git a/hrp/internal/dial/dns.go b/hrp/internal/dial/dns.go new file mode 100644 index 00000000..7f1e7e03 --- /dev/null +++ b/hrp/internal/dial/dns.go @@ -0,0 +1,251 @@ +package dial + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/miekg/dns" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" +) + +const ( + httpDnsUrl = "https://dig.bdurl.net/q" + googleDnsUrl = "https://dns.google/resolve" +) + +const ( + DnsSourceTypeLocal = iota + DnsSourceTypeHttp + DnsSourceTypeGoogle +) + +const ( + DnsRecordTypeA = 1 + DnsRecordTypeAAAA = 28 + DnsRecordTypeCNAME = 5 +) + +var dnsHttpClient = &http.Client{ + Timeout: 5 * time.Minute, +} + +type DnsOptions struct { + DnsSourceType int + DnsRecordType int + DnsServer string + SaveTests bool +} + +type DnsResult struct { + DnsList []string `json:"dnsList"` + DnsSource int `json:"dnsType"` + DnsRecordType int `json:"dnsRecordType"` + DnsServer string `json:"dnsServer,omitempty"` + Ttl int `json:"ttl"` + Suc bool `json:"suc"` + ErrMsg string `json:"errMsg"` +} + +type googleDnsResp struct { + Answer []googleDnsAnswer `json:"Answer"` +} + +type httpDnsResp struct { + Ips []string `json:"ips"` + Ttl int `json:"ttl"` +} + +type googleDnsAnswer struct { + Name string `json:"name"` + Type int `json:"type"` + TTL int `json:"TTL"` + Data string `json:"data"` +} + +func ParseIP(s string) (net.IP, int) { + ip := net.ParseIP(s) + if ip == nil { + return nil, 0 + } + for i := 0; i < len(s); i++ { + switch s[i] { + case '.': + return ip, 4 + case ':': + return ip, 6 + } + } + return nil, 0 +} + +func localDns(src string, dnsRecordType int, dnsServer string) (dnsResult DnsResult, err error) { + dnsResult.DnsSource = DnsSourceTypeLocal + dnsResult.DnsRecordType = dnsRecordType + + if dnsServer == "" { + config, _ := dns.ClientConfigFromFile("/etc/resolv.conf") + dnsServer = config.Servers[0] + } else { + dnsResult.DnsServer = dnsServer + } + + _, ipType := ParseIP(dnsServer) + if ipType == 4 { + dnsServer += ":53" + } + + c := dns.Client{ + Timeout: 5 * time.Second, + } + m := dns.Msg{} + + m.SetQuestion(src+".", uint16(dnsRecordType)) + r, _, err := c.Exchange(&m, dnsServer) + if err != nil { + return + } + for _, ans := range r.Answer { + switch dnsRecordType { + case DnsRecordTypeA: + record, isType := ans.(*dns.A) + if isType { + dnsResult.Ttl = int(record.Hdr.Ttl) + dnsResult.DnsList = append(dnsResult.DnsList, record.A.String()) + } + case DnsRecordTypeAAAA: + record, isType := ans.(*dns.AAAA) + if isType { + dnsResult.Ttl = int(record.Hdr.Ttl) + dnsResult.DnsList = append(dnsResult.DnsList, record.AAAA.String()) + } + case DnsRecordTypeCNAME: + record, isType := ans.(*dns.CNAME) + if isType { + dnsResult.Ttl = int(record.Hdr.Ttl) + dnsResult.DnsList = append(dnsResult.DnsList, record.Target) + } + } + } + return +} + +func httpDns(url string, dnsRecordType int) (dnsResult DnsResult, err error) { + target := httpDnsUrl + "?host=" + url + if dnsRecordType == DnsRecordTypeAAAA { + target += "&aid=13&f=2" + } + resp, err := dnsHttpClient.Get(target) + + dnsResult.DnsSource = DnsSourceTypeHttp + dnsResult.DnsRecordType = dnsRecordType + + if err != nil { + return + } + defer resp.Body.Close() + var buf []byte + buf, err = ioutil.ReadAll(resp.Body) + if err != nil { + return + } + var result httpDnsResp + err = json.Unmarshal(buf, &result) + if err != nil { + return + } + dnsResult.DnsList = result.Ips + dnsResult.Ttl = result.Ttl + return +} + +func googleDns(url string, dnsRecordType int) (dnsResult DnsResult, err error) { + resp, err := dnsHttpClient.Get(googleDnsUrl + "?name=" + url + "&type=" + strconv.Itoa(dnsRecordType)) + + dnsResult.DnsSource = DnsSourceTypeGoogle + dnsResult.DnsRecordType = dnsRecordType + + if err != nil { + return + } + defer resp.Body.Close() + var buf []byte + buf, err = ioutil.ReadAll(resp.Body) + if err != nil { + return + } + var result googleDnsResp + err = json.Unmarshal(buf, &result) + if err != nil { + return + } + if len(result.Answer) == 0 { + return + } + for _, answer := range result.Answer { + if answer.Type == dnsRecordType { + dnsResult.Ttl = answer.TTL + dnsResult.DnsList = append(dnsResult.DnsList, answer.Data) + } + } + return +} + +func DoDns(dnsOptions *DnsOptions, args []string) (err error) { + if len(args) != 1 { + return errors.New("there should be one argument") + } + + var dnsResult DnsResult + defer func() { + if dnsOptions.SaveTests { + dir, _ := os.Getwd() + dnsResultName := fmt.Sprintf("dns_result_%v.json", time.Now().Format("20060102150405")) + dnsResultPath := filepath.Join(dir, dnsResultName) + err = builtin.Dump2JSON(dnsResult, dnsResultPath) + if err != nil { + log.Error().Err(err).Msg("save dns resolution result failed") + } + } + }() + + dnsTarget := args[0] + + parsedURL, err := url.Parse(dnsTarget) + if err == nil && parsedURL.Host != "" { + log.Info().Msgf("parse input url %v and extract host %v", dnsTarget, parsedURL.Host) + dnsTarget = strings.Split(parsedURL.Host, ":")[0] + } + log.Info().Msgf("resolve DNS for %v", dnsTarget) + dnsRecordType := dnsOptions.DnsRecordType + dnsServer := dnsOptions.DnsServer + switch dnsOptions.DnsSourceType { + case DnsSourceTypeLocal: + dnsResult, err = localDns(dnsTarget, dnsRecordType, dnsServer) + case DnsSourceTypeHttp: + dnsResult, err = httpDns(dnsTarget, dnsRecordType) + case DnsSourceTypeGoogle: + dnsResult, err = googleDns(dnsTarget, dnsRecordType) + } + if err != nil { + dnsResult.Suc = false + dnsResult.ErrMsg = err.Error() + log.Error().Err(err).Msgf("fail to do DNS for %s", dnsTarget) + } else { + dnsResult.Suc = true + dnsResult.ErrMsg = "" + fmt.Printf("\nDNS resolution done, result IP list: %v\n", dnsResult.DnsList) + } + return +} diff --git a/hrp/internal/dial/ping.go b/hrp/internal/dial/ping.go new file mode 100644 index 00000000..29c24295 --- /dev/null +++ b/hrp/internal/dial/ping.go @@ -0,0 +1,116 @@ +package dial + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/go-ping/ping" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" +) + +type PingOptions struct { + Count int + Timeout time.Duration + Interval time.Duration + SaveTests bool +} + +type PingResult struct { + Suc bool `json:"suc"` + ErrMsg string `json:"errMsg"` + Ip string `json:"ip"` + AvgCost int `json:"avgCost"` + MaxCost int `json:"maxCost"` + MinCost int `json:"minCost"` + Lost int `json:"lost"` + PingCount int `json:"pingCount"` + PacketSize int `json:"packetSize"` + ReceivePacketCount int `json:"receivePacketCount"` + SendPacketCount int `json:"sendPacketCount"` + SuccessCount int `json:"successCount"` + DebugLog string `json:"debugLog"` +} + +func DoPing(pingOptions *PingOptions, args []string) (err error) { + if len(args) != 1 { + return errors.New("there should be one argument") + } + + var pingResult PingResult + defer func() { + if pingOptions.SaveTests { + dir, _ := os.Getwd() + pingResultName := fmt.Sprintf("ping_result_%v.json", time.Now().Format("20060102150405")) + pingResultPath := filepath.Join(dir, pingResultName) + err = builtin.Dump2JSON(pingResult, pingResultPath) + if err != nil { + log.Error().Err(err).Msg("save ping result failed") + } + } + }() + + pingTarget := args[0] + + parsedURL, err := url.Parse(pingTarget) + if err == nil && parsedURL.Host != "" { + log.Info().Msgf("parse input url %v and extract host %v", pingTarget, parsedURL.Host) + pingTarget = strings.Split(parsedURL.Host, ":")[0] + } + + log.Info().Msgf("ping host %v", pingTarget) + pinger, err := ping.NewPinger(pingTarget) + if err != nil { + log.Error().Err(err).Msgf("fail to get pinger for %s", pingTarget) + pingResult.Suc = false + pingResult.ErrMsg = err.Error() + pingResult.DebugLog = err.Error() + return + } + pinger.Count = pingOptions.Count + pinger.Timeout = pingOptions.Timeout + pinger.Interval = pingOptions.Interval + + pinger.OnRecv = func(pkt *ping.Packet) { + pingResult.DebugLog += fmt.Sprintf("%d bytes from %s: icmp_seq=%d time=%v\n", + pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt) + } + pinger.OnFinish = func(stats *ping.Statistics) { + pingResult.DebugLog += fmt.Sprintf("\n--- %s ping statistics ---\n", stats.Addr) + pingResult.DebugLog += fmt.Sprintf("%d packets transmitted, %d packets received, %v%% packet loss\n", + stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss) + pingResult.DebugLog += fmt.Sprintf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n", + stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) + } + pingResult.DebugLog += fmt.Sprintf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr()) + + err = pinger.Run() // blocks until finished + if err != nil { + log.Error().Err(err).Msgf("fail to run ping for %s", parsedURL) + pingResult.Suc = false + pingResult.ErrMsg = err.Error() + pingResult.DebugLog = err.Error() + return + } + fmt.Print(pingResult.DebugLog) + stats := pinger.Statistics() // get send/receive/rtt stats + pingResult.Ip = pinger.IPAddr().String() + pingResult.AvgCost = int(stats.AvgRtt / time.Millisecond) + pingResult.MaxCost = int(stats.MaxRtt / time.Millisecond) + pingResult.MinCost = int(stats.MinRtt / time.Millisecond) + pingResult.Lost = int(stats.PacketLoss) + pingResult.PingCount = pingOptions.Count + pingResult.PacketSize = pinger.Size + pingResult.ReceivePacketCount = stats.PacketsRecv + pingResult.SendPacketCount = stats.PacketsSent + pingResult.SuccessCount = stats.PacketsRecv + pingResult.Suc = true + pingResult.ErrMsg = "" + return +} diff --git a/hrp/internal/dial/traceroute.go b/hrp/internal/dial/traceroute.go new file mode 100644 index 00000000..d20e5f1b --- /dev/null +++ b/hrp/internal/dial/traceroute.go @@ -0,0 +1,20 @@ +package dial + +type TraceRouteOptions struct { + MaxTTL int + Queries int + SaveTests bool +} + +type TraceRouteResult struct { + IP string `json:"ip"` + Details []TraceRouteResultNode `json:"details"` + Suc bool `json:"suc"` + ErrMsg string `json:"errMsg"` +} + +type TraceRouteResultNode struct { + Id int `json:"id"` + Ip string `json:"ip"` + Time string `json:"time"` +} diff --git a/hrp/internal/dial/traceroute_unix.go b/hrp/internal/dial/traceroute_unix.go new file mode 100644 index 00000000..b6621592 --- /dev/null +++ b/hrp/internal/dial/traceroute_unix.go @@ -0,0 +1,106 @@ +//go:build darwin || linux +// +build darwin linux + +package dial + +import ( + "bufio" + "fmt" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" +) + +var ( + regexIPAddr = regexp.MustCompile(`([\d.]+)`) + regexElapsedTime = regexp.MustCompile(`(\d+\.\d+)`) + regexTraceroutePass = regexp.MustCompile(fmt.Sprintf(`(\d+)[\s*]+(\S+)\s+\(%s\)\s+%s\s+ms`, regexIPAddr, regexElapsedTime)) + regexTracerouteFailure = regexp.MustCompile(`(\d+)[\s*]+$`) +) + +func DoTraceRoute(traceRouteOptions *TraceRouteOptions, args []string) (err error) { + if len(args) != 1 { + return errors.New("there should be one argument") + } + var traceRouteResult TraceRouteResult + defer func() { + if traceRouteOptions.SaveTests { + dir, _ := os.Getwd() + traceRouteResultName := fmt.Sprintf("traceroute_result_%v.json", time.Now().Format("20060102150405")) + traceRouteResultPath := filepath.Join(dir, traceRouteResultName) + err = builtin.Dump2JSON(traceRouteResult, traceRouteResultPath) + if err != nil { + log.Error().Err(err).Msg("save traceroute result failed") + } + } + }() + + traceRouteTarget := args[0] + parsedURL, err := url.Parse(traceRouteTarget) + if err == nil && parsedURL.Host != "" { + log.Info().Msgf("parse input url %v and extract host %v", traceRouteTarget, parsedURL.Host) + traceRouteTarget = strings.Split(parsedURL.Host, ":")[0] + } + + cmd := exec.Command("traceroute", "-m", strconv.Itoa(traceRouteOptions.MaxTTL), + "-q", strconv.Itoa(traceRouteOptions.Queries), traceRouteTarget) + stdout, _ := cmd.StdoutPipe() + + startT := time.Now() + defer func() { + log.Info().Msgf("for target %s, traceroute costs %v", traceRouteTarget, time.Since(startT)) + }() + + log.Info().Msgf("start to traceroute %v", traceRouteTarget) + err = cmd.Start() + if err != nil { + traceRouteResult.Suc = false + traceRouteResult.ErrMsg = "execute traceroute failed" + log.Error().Err(err).Msg("start command failed") + return + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + hopLine := scanner.Text() + fmt.Println(hopLine) + failureLine := regexTracerouteFailure.FindStringSubmatch(hopLine) + if len(failureLine) == 2 { + hopID, _ := strconv.Atoi(failureLine[1]) + traceRouteResult.Details = append(traceRouteResult.Details, TraceRouteResultNode{ + Id: hopID, + }) + continue + } + passLine := regexTraceroutePass.FindStringSubmatch(hopLine) + if len(passLine) == 5 { + hopID, _ := strconv.Atoi(passLine[1]) + traceRouteResult.Details = append(traceRouteResult.Details, TraceRouteResultNode{ + Id: hopID, + Ip: passLine[3], + Time: passLine[4], + }) + traceRouteResult.Suc = true + } + } + hopCount := len(traceRouteResult.Details) + traceRouteResult.IP = traceRouteResult.Details[hopCount-1].Ip + err = cmd.Wait() + if err != nil { + traceRouteResult.Suc = false + traceRouteResult.ErrMsg = "wait traceroute finish failed" + log.Error().Err(err).Msg("wait command failed") + return + } + return +} diff --git a/hrp/internal/dial/traceroute_windows.go b/hrp/internal/dial/traceroute_windows.go new file mode 100644 index 00000000..a1b4b37b --- /dev/null +++ b/hrp/internal/dial/traceroute_windows.go @@ -0,0 +1,105 @@ +//go:build windows +// +build windows + +package dial + +import ( + "bufio" + "fmt" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" +) + +var ( + regexTracertPass = regexp.MustCompile(`(\d+)[\s*<]+(\d+)\s+ms`) + regexTracertFailure = regexp.MustCompile(`(\d+)[\s*]+Request timed out`) +) + +func DoTraceRoute(traceRouteOptions *TraceRouteOptions, args []string) (err error) { + if len(args) != 1 { + return errors.New("there should be one argument") + } + var traceRouteResult TraceRouteResult + defer func() { + if traceRouteOptions.SaveTests { + dir, _ := os.Getwd() + traceRouteResultName := fmt.Sprintf("traceroute_result_%v.json", time.Now().Format("20060102150405")) + traceRouteResultPath := filepath.Join(dir, traceRouteResultName) + err = builtin.Dump2JSON(traceRouteResult, traceRouteResultPath) + if err != nil { + log.Error().Err(err).Msg("save traceroute result failed") + } + } + }() + + traceRouteTarget := args[0] + parsedURL, err := url.Parse(traceRouteTarget) + if err == nil && parsedURL.Host != "" { + log.Info().Msgf("parse input url %v and extract host %v", traceRouteTarget, parsedURL.Host) + traceRouteTarget = strings.Split(parsedURL.Host, ":")[0] + } + + cmd := exec.Command("tracert", "-h", strconv.Itoa(traceRouteOptions.MaxTTL), traceRouteTarget) + stdout, _ := cmd.StdoutPipe() + + startT := time.Now() + defer func() { + log.Info().Msgf("for target %s, traceroute costs %v", traceRouteTarget, time.Since(startT)) + }() + + log.Info().Msgf("start to traceroute %v", traceRouteTarget) + err = cmd.Start() + if err != nil { + traceRouteResult.Suc = false + traceRouteResult.ErrMsg = "execute traceroute failed" + log.Error().Err(err).Msg("start command failed") + return + } + + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + hopLine := scanner.Text() + fmt.Println(hopLine) + failureLine := regexTracertFailure.FindStringSubmatch(hopLine) + if len(failureLine) == 2 { + hopID, _ := strconv.Atoi(failureLine[1]) + traceRouteResult.Details = append(traceRouteResult.Details, TraceRouteResultNode{ + Id: hopID, + }) + continue + } + passLine := regexTracertPass.FindStringSubmatch(hopLine) + if len(passLine) == 3 { + hopID, _ := strconv.Atoi(passLine[1]) + fields := strings.Fields(hopLine) + hopIP := strings.Trim(fields[len(fields)-1], "[]") + traceRouteResult.Details = append(traceRouteResult.Details, TraceRouteResultNode{ + Id: hopID, + Ip: hopIP, + Time: passLine[2], + }) + traceRouteResult.Suc = true + } + } + hopCount := len(traceRouteResult.Details) + traceRouteResult.IP = traceRouteResult.Details[hopCount-1].Ip + err = cmd.Wait() + if err != nil { + traceRouteResult.Suc = false + traceRouteResult.ErrMsg = "wait traceroute finish failed" + log.Error().Err(err).Msg("wait command failed") + return + } + return +}