From 1d608c6483f589f8da48bdc12e423721aca94c4b Mon Sep 17 00:00:00 2001 From: Leah Neukirchen Date: Sat, 14 Mar 2020 23:45:39 +0100 Subject: initial commit --- Makefile | 24 +++++ README | 72 ++++++++++++++ go.mod | 3 + htping.go | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 422 insertions(+) create mode 100644 Makefile create mode 100644 README create mode 100644 go.mod create mode 100644 htping.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..11ebf16 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +ALL=htping + +DESTDIR= +PREFIX=/usr/local +BINDIR=$(PREFIX)/bin +MANDIR=$(PREFIX)/share/man + +all: $(ALL) + +htping: + go build + +clean: FRC + rm -f $(ALL) + +install: FRC all + mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 + install -m0755 $(ALL) $(DESTDIR)$(BINDIR) + install -m0644 $(ALL:=.1) $(DESTDIR)$(MANDIR)/man1 + +README: htping.1 + mandoc -Tutf8 $< | col -bx >$@ + +FRC: diff --git a/README b/README new file mode 100644 index 0000000..e012ed6 --- /dev/null +++ b/README @@ -0,0 +1,72 @@ +HTPING(1) General Commands Manual HTPING(1) + +NAME + htping – periodically send HTTP requests + +SYNOPSIS + htping [-4] [-6] [-H field:value] [-X method] [-c count] [-i interval] + [-f] [-k] [--http1.1] [--keepalive] host + +DESCRIPTION + The htping utility periodically sends HTTP requests to host, prints the + results and computes some statistics at exit. Use Ctrl-C to quit htping. + + The options are as follows: + + -4 Use IPv4 only. + + -6 Use IPv6 only. + + -H field:value + Add an additional HTTP header to the requests. + + -X method + Send a different HTTP method than the default ‘HEAD’. + + -c count + Stop after sending count requests. By default, htping loops + indefinitely. + + -i interval + Perform one HTTP request every interval (default: ‘1s’). + + -f Flood mode: perform requests back-to-back without waiting. + + -k Turn TLS verification errors into warnings. + + --http1.1 + Disable HTTP/2 requests. + + --keepalive + Enable keepalive resp. use persistent connections. + +EXIT STATUS + The htping utility exits 0 on success, and >0 if an error occurs. + +EXAMPLES + Example output: + + HEAD http://example.com + 0 bytes from 93.184.216.34:80: HTTP/1.1 200 seq=0 time=0.211 ms + 0 bytes from 93.184.216.34:80: HTTP/1.1 200 seq=1 time=0.222 ms + 0 bytes from 93.184.216.34:80: HTTP/1.1 200 seq=2 time=0.221 ms + 0 bytes from 93.184.216.34:80: HTTP/1.1 200 seq=3 time=0.222 ms + ^C + 4 requests sent, 4 responses, 100% successful, time 4000ms + rtt min/avg/max/mdev = 0.211/0.219/0.222/0.005 ms + +SEE ALSO + curl(1), httping(1) + +AUTHORS + Leah Neukirchen + +LICENSE + htping is in the public domain. + + To the extent possible under law, the creator of this work has waived all + copyright and related or neighboring rights to this work. + + http://creativecommons.org/publicdomain/zero/1.0/ + +Void Linux March 14, 2020 Void Linux diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d6b499c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/leahneukirchen/htping + +go 1.14 diff --git a/htping.go b/htping.go new file mode 100644 index 0000000..94c9689 --- /dev/null +++ b/htping.go @@ -0,0 +1,323 @@ +package main + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "math" + "net" + "net/http" + "net/http/httptrace" + "net/url" + "os" + "os/signal" + "strings" + "sync/atomic" + "time" +) + +var ntotal int32 + +var flag4 bool +var flag6 bool +var myHeaders headers +var method string + +var kflag bool + +var http11 bool +var keepalive bool + +type transport struct { + rtp http.RoundTripper + msg string + addr string +} + +func newTransport() *transport { + tr := &transport{} + + tlsconfig := &tls.Config{ + InsecureSkipVerify: kflag, + } + + tlsconfig.VerifyPeerCertificate = + func(certificates [][]byte, _ [][]*x509.Certificate) error { + certs := make([]*x509.Certificate, len(certificates)) + for i, asn1Data := range certificates { + cert, err := x509.ParseCertificate(asn1Data) + if err != nil { + return errors.New("tls: failed to parse certificate from server: " + err.Error()) + } + certs[i] = cert + } + + opts := x509.VerifyOptions{ + Roots: tlsconfig.RootCAs, + DNSName: tlsconfig.ServerName, + Intermediates: x509.NewCertPool(), + } + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + + _, err := certs[0].Verify(opts) + if err != nil { + tr.msg = err.Error() + } + + // succeed + return nil + } + + dialer := &net.Dialer{ + Timeout: 5 * time.Second, + DualStack: true, + } + + tr.rtp = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSHandshakeTimeout: 5 * time.Second, + DisableKeepAlives: !keepalive, + TLSClientConfig: tlsconfig, + // we set TLSClientConfig, so http2 is off by default anyway + ForceAttemptHTTP2: !http11, + + DialContext: func(ctx context.Context, _, addr string) (net.Conn, error) { + if flag4 { + return dialer.DialContext(ctx, "tcp4", addr) + } else if flag6 { + return dialer.DialContext(ctx, "tcp6", addr) + } else { + return dialer.DialContext(ctx, "tcp", addr) + } + }, + } + + return tr +} + +func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { + return t.rtp.RoundTrip(req) +} + +func (t *transport) GotConn(info httptrace.GotConnInfo) { + t.addr = info.Conn.RemoteAddr().String() +} + +type result struct { + dur float64 + code int +} + +func ping(url string, seq int, durs chan result) { + start := time.Now() + + atomic.AddInt32(&ntotal, 1) + + t := newTransport() + + req, err := http.NewRequest(method, url, nil) + if err != nil { + fmt.Printf("error=%v\n", err) + durs <- result{0, -1} + return + } + + for _, e := range myHeaders { + req.Header.Set(e.key, e.value) + } + + trace := &httptrace.ClientTrace{ + GotConn: t.GotConn, + } + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + + client := &http.Client{ + Transport: t, + Timeout: 10 * time.Second, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + res, err := client.Do(req) + if err != nil { + fmt.Printf("error=%v\n", err) + durs <- result{0, -1} + return + } + + written, _ := io.Copy(ioutil.Discard, res.Body) + res.Body.Close() + client.CloseIdleConnections() + + stop := time.Now() + + dur := float64(stop.Sub(start)) / float64(time.Second) + + if len(t.msg) > 0 { + fmt.Printf("%v\n", t.msg) + } + + fmt.Printf("%d bytes from %v: %s %d seq=%d time=%.3f ms\n", + written, + t.addr, + res.Proto, + res.StatusCode, + seq, dur) + + durs <- result{dur, res.StatusCode} +} + +func stats(results chan result, done chan bool) { + var min, max, sum, sum2 float64 + min = math.Inf(1) + nrecv := 0 + nsucc := 0 + + start := time.Now() + + for { + select { + case r := <-results: + if r.code > 0 { + if r.dur < min { + min = r.dur + } + if r.dur > max { + max = r.dur + } + sum += r.dur + sum2 += r.dur * r.dur + nrecv++ + if r.code <= 400 { + nsucc++ + } + } + + case <-done: + stop := time.Now() + fmt.Printf("\n%d requests sent, %d (%d%%) responses, %d (%d%%) successful, time %dms\n", + ntotal, + nrecv, + (100*nrecv)/int(ntotal), + nsucc, + (100*nsucc)/int(ntotal), + int64(stop.Sub(start)/time.Millisecond)) + if nrecv > 0 { + mdev := math.Sqrt(sum2/float64(nrecv) - + sum/float64(nrecv)*sum/float64(nrecv)) + fmt.Printf("rtt min/avg/max/mdev = %.3f/%.3f/%.3f/%.3f ms\n", + min, sum/float64(nrecv), max, mdev) + } + + done <- true + } + } +} + +type header struct { + key, value string +} +type headers []header + +func (i *headers) String() string { + return "" +} + +func (i *headers) Set(value string) error { + parts := strings.SplitN(value, ":", 2) + if len(parts) != 2 { + return errors.New("header does not contain field and value") + } + *i = append(*i, header{parts[0], parts[1]}) + return nil +} + +func main() { + flag.BoolVar(&flag4, "4", false, "resolve IPv4 only") + flag.BoolVar(&flag6, "6", false, "resolve IPv6 only") + flag.Var(&myHeaders, "H", "set custom headers") + flag.StringVar(&method, "X", "HEAD", "HTTP method") + + maxCount := flag.Int("c", -1, "count") + flood := flag.Bool("f", false, "flood ping") + sleep := flag.Duration("i", 1*time.Second, "interval") + flag.BoolVar(&kflag, "k", false, "turn TLS errors into warnings") + + flag.BoolVar(&http11, "http1.1", false, "force HTTP/1.1") + flag.BoolVar(&keepalive, "keepalive", false, + "enable keepalive/use persistent connections") + + flag.Parse() + + args := flag.Args() + if len(args) != 1 { + fmt.Fprintf(os.Stderr, "Usage: %s [FLAGS...] URL\n", os.Args[0]) + flag.PrintDefaults() + os.Exit(1) + } + u := args[0] + + u2, err := url.ParseRequestURI(u) + if (err != nil && strings.HasSuffix(err.Error(), + "invalid URI for request")) || + (u2.Scheme != "http" && u2.Scheme != "https") { + u = "http://" + u + } + + _, err = url.ParseRequestURI(u) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } + + fmt.Printf("%s %s\n", method, u) + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + + results := make(chan result) + done := make(chan bool) + go stats(results, done) + + count := 0 + + if *flood { + flood_loop: + for { + select { + default: + ping(u, count, results) + count++ + case <-interrupt: + break flood_loop + } + } + } else { + pingTicker := time.NewTicker(*sleep) + go ping(u, count, results) + count++ + ping_loop: + for { + if *maxCount > 0 && count > *maxCount { + break + } + select { + case <-pingTicker.C: + go ping(u, count, results) + count++ + case <-interrupt: + break ping_loop + } + } + } + + done <- true + <-done +} -- cgit 1.4.1