From 604f9fb8764c15bebf7fd1752cd143ec8292fdea Mon Sep 17 00:00:00 2001 From: noobly Date: Sat, 9 Nov 2019 10:07:49 +0800 Subject: [PATCH] first release --- .github/workflows/main.yml | 59 ++++++++++++++++++ .gitignore | 17 ++++++ LICENSE.md | 20 +++++++ README.md | 109 ++++++++++++++++++++++++++++++++- flag.go | 49 +++++++++++++++ go.mod | 17 ++++++ go.sum | 96 +++++++++++++++++++++++++++++ httping/httping.go | 105 ++++++++++++++++++++++++++++++++ log.go | 102 +++++++++++++++++++++++++++++++ main.go | 82 +++++++++++++++++++++++++ mtr/mtr.go | 48 +++++++++++++++ ping/ping.go | 120 +++++++++++++++++++++++++++++++++++++ query.go | 47 +++++++++++++++ tcping/tcping.go | 29 +++++++++ util.go | 50 ++++++++++++++++ 15 files changed, 949 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/main.yml create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 flag.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 httping/httping.go create mode 100644 log.go create mode 100644 main.go create mode 100644 mtr/mtr.go create mode 100644 ping/ping.go create mode 100644 query.go create mode 100644 tcping/tcping.go create mode 100644 util.go diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..0209898 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,59 @@ +name: build + +on: + push: + tags: + - '*' + +jobs: + publish_release: + runs-on: ubuntu-latest + steps: + # checkout code + - uses: actions/checkout@master + # create release + - uses: actions/create-release@v1 + id: create_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + body: | + Changes in this Release + - First Change + - Second Change + draft: false + prerelease: false + # build binaries + - uses: actions/setup-go@v1 + with: + go-version: '1.13.5' + - name: build + run: | + mkdir pingme-linux-amd64 + mkdir pingme-darwin-amd64 + env VERSION=$(echo ${GITHUB_REF:10}) GIN_MODE=release GOOS=linux GOARCH=amd64 bash -c 'go build -ldflags "-X main.VersionString=$VERSION" -o pingme-linux-amd64/pingme *.go' + env VERSION=$(echo ${GITHUB_REF:10}) GIN_MODE=release GOOS=darwin GOARCH=amd64 bash -c 'go build -ldflags "-X main.VersionString=$VERSION" -o pingme-darwin-amd64/pingme *.go' + tar -C $PWD -cvzf pingme-linux-amd64.tar.gz pingme-linux-amd64 + tar -C $PWD -cvzf pingme-darwin-amd64.tar.gz pingme-darwin-amd64 + # upload binaries linux + - uses: actions/upload-release-asset@v1.0.1 + id: upload-release-asset-linux + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./pingme-linux-amd64.tar.gz + asset_name: pingme-linux-amd64.tar.gz + asset_content_type: application/gzip + # upload binaries darwin + - uses: actions/upload-release-asset@v1.0.1 + id: upload-release-asset-darwin + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./pingme-darwin-amd64.tar.gz + asset_name: pingme-darwin-amd64.tar.gz + asset_content_type: application/gzip diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14a1c3e --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Binary Output +pingme +pingme-cli + +# Script +*.sh + +# Log +*.log +*.dat +*.txt + +# Database +*.db + +# Config +config.json diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..900d8c0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2019-2020 Noobly + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index dba8a38..b96531c 100644 --- a/README.md +++ b/README.md @@ -1 +1,108 @@ -# pingme +# Introduction + +Pingme is ping probe command line tool, supporting ICMP, TCP and HTTP protocols. + +You can also use it to query IP information from third-party api provider (currently we use [https://ip-api.com](https://ip-api.com)). + +# Features + +- Support ICMP/TCP/HTTP protocols +- Query basic IP information + +# Installation + +1. Download latest [release](https://github.com/noobly314/pingme/releases/latest) (recommend) + +2. Use go get + +``` +go get -u github.com/noobly314/pingme +``` + +3. Build on your own + +``` +git clone https://github.com/noobly314/pingme.git +cd pingme +go build +``` + +# Usage + +``` + -h string + HTTP Ping + -i string + ICMP Ping + -m string + MTR Trace + -q string + Query ip information + -t string + TCP Ping + -v Version +``` + +# Examples + +``` +$ pingme -h https://www.google.com +Proxy : false +Scheme : https +Host : www.google.com +DNS Lookup: 2.05 ms +TCP : 2.41 ms +TLS : 68.92 ms +Process : 29.28 ms +Transfer : 0.19 ms +Total : 103.06 ms +``` + +``` +$ pingme -i www.google.com +ICMP OPEN 74.125.200.147 2.2 ms +``` + +``` +$ pingme -t www.google.com:443 +TCP OPEN www.google.com:443 +``` + +``` +$ pingme -q www.google.com +IP : 172.217.194.103 +City : Queenstown Estate +Country: Singapore +ISP : Google LLC +AS : AS15169 Google LLC +``` + +# Note + +Root permission is required when running ICMP ping, since it needs to open raw socket. + +You can either use sudo command, or set setuid bit for pingme. + +``` +// Use sudo for one-time ping +$ sudo pingme -i google.com + +// Set setuid bit +$ sudo chown root:root pingme +$ sudo chmod u+s pingme + +``` + +# License + +See the [LICENSE](https://github.com/noobly314/pingme/blob/master/LICENSE.md) file for license rights and limitations (MIT). + +# Acknowledgements + +[https://ip-api.com](https://ip-api.com) + +[lmas/icmp_ping.go](https://gist.github.com/lmas/c13d1c9de3b2224f9c26435eb56e6ef3) + +[sparrc/go-ping](https://github.com/sparrc/go-ping) + +[davecheney/httpstat](https://github.com/davecheney/httpstat) diff --git a/flag.go b/flag.go new file mode 100644 index 0000000..3f1cceb --- /dev/null +++ b/flag.go @@ -0,0 +1,49 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +var Version bool +var PingDst string +var TCPingDst string +var HTTPingDst string +var MtrDst string +var Query string + +var CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) + +var Usage = func() { + fmt.Fprintf(CommandLine.Output(), "Usage:\n") + flag.PrintDefaults() +} + +func init_flag() { + flag.BoolVar(&Version, "v", false, "Version") + flag.StringVar(&PingDst, "i", "", "ICMP Ping") + flag.StringVar(&TCPingDst, "t", "", "TCP Ping") + flag.StringVar(&HTTPingDst, "h", "", "HTTP Ping") + flag.StringVar(&MtrDst, "m", "", "MTR Trace") + flag.StringVar(&Query, "q", "", "Query ip information") + flag.Parse() +} + +func hasFlag() bool { + found := false + flag.Visit(func(f *flag.Flag) { + found = true + }) + return found +} + +func isFlagPassed(name string) bool { + found := false + flag.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + return found +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..986fbad --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/noobly314/pingme + +require ( + github.com/aeden/traceroute v0.0.0-20181124220833-147686d9cb0f + github.com/fatih/color v1.7.0 + github.com/gin-contrib/cors v1.3.0 + github.com/gin-gonic/gin v1.5.0 + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.10 // indirect + github.com/sirupsen/logrus v1.4.2 + github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c + github.com/syndtr/goleveldb v1.0.0 + go.etcd.io/bbolt v1.3.3 + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 +) + +go 1.13 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5582501 --- /dev/null +++ b/go.sum @@ -0,0 +1,96 @@ +github.com/aeden/traceroute v0.0.0-20181124220833-147686d9cb0f h1:LbPChgmEVYTHVg0zmAydDO3YNRgkt31svsI7LJ0crjs= +github.com/aeden/traceroute v0.0.0-20181124220833-147686d9cb0f/go.mod h1:WwE/rUGG8pQ7L4JiBoNPCTZGQGWnciVaM+pXYfQR9ps= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gin-contrib/cors v1.3.0 h1:PolezCc89peu+NgkIWt9OB01Kbzt6IP0J/JvkG6xxlg= +github.com/gin-contrib/cors v1.3.0/go.mod h1:artPvLlhkF7oG06nK8v3U8TNz6IeX+w1uzCSEId5/Vc= +github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= +github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/noobly314/pingme v0.0.0-20191212162823-642d9c7d6afb h1:51sLIkuBVRY1BYlv1lxfG1MfugQ+x+7p68l8EMtiiTU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c h1:gqEdF4VwBu3lTKGHS9rXE9x1/pEaSwCXRLOZRF6qtlw= +github.com/sparrc/go-ping v0.0.0-20190613174326-4e5b6552494c/go.mod h1:eMyUVp6f/5jnzM+3zahzl7q6UXLbgSc3MKg/+ow9QW0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be h1:QAcqgptGM8IQBC9K/RC4o+O9YmqEm0diQn9QmZw/0mU= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/httping/httping.go b/httping/httping.go new file mode 100644 index 0000000..a740dbf --- /dev/null +++ b/httping/httping.go @@ -0,0 +1,105 @@ +package httping + +import ( + "crypto/tls" + "log" + "net/http" + "net/http/httptrace" + "regexp" + "strings" + "time" + + "golang.org/x/net/http/httpproxy" +) + +type Stats struct { + Proxy bool + Scheme string + DNS int64 + TCP int64 + TLS int64 + Process int64 + Transfer int64 + Total int64 +} + +func New(address string) (Stats, error) { + var err error + var stats Stats + var t0, t1, t2, t3, t4, t5, t6, t7 int64 + + req, _ := http.NewRequest("GET", address, nil) + trace := &httptrace.ClientTrace{ + DNSStart: func(info httptrace.DNSStartInfo) { + t0 = time.Now().UnixNano() + }, + DNSDone: func(info httptrace.DNSDoneInfo) { + t1 = time.Now().UnixNano() + if info.Err != nil { + err = info.Err + log.Fatal(info.Err) + } + }, + ConnectStart: func(net, addr string) { + }, + ConnectDone: func(net, addr string, err error) { + if err != nil { + log.Fatalf("unable to connect to host %v: %v", addr, err) + } + t2 = time.Now().UnixNano() + }, + GotConn: func(info httptrace.GotConnInfo) { + t3 = time.Now().UnixNano() + }, + GotFirstResponseByte: func() { + t4 = time.Now().UnixNano() + }, + TLSHandshakeStart: func() { + t5 = time.Now().UnixNano() + }, + TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { + t6 = time.Now().UnixNano() + }, + } + req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace)) + c := &http.Client{ + Timeout: 5 * time.Second, + } + _, err = c.Do(req) + if err != nil { + match, _ := regexp.MatchString("Client.Timeout exceeded", err.Error()) + if match { + log.Fatal("Connection timeout") + } else { + log.Fatal(err) + } + } + + t7 = time.Now().UnixNano() + + if strings.HasPrefix(address, "http://") { + stats.Scheme = "http" + } else if strings.HasPrefix(address, "https://") { + stats.Scheme = "https" + } + + if t0 == 0 { + t0 = t2 + t1 = t2 + } + + stats.DNS = t1 - t0 + stats.TCP = t2 - t1 + stats.Process = t4 - t3 + stats.Transfer = t7 - t4 + stats.TLS = t6 - t5 + stats.Total = t7 - t0 + + // Detect proxies + pc := httpproxy.FromEnvironment() + if pc.HTTPProxy != "" { + stats.Proxy = true + } + + return stats, err +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..dfa47b0 --- /dev/null +++ b/log.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + "net" + "os" + "reflect" + "regexp" + "strconv" + "time" + + "github.com/fatih/color" + "github.com/noobly314/pingme/httping" + "github.com/sirupsen/logrus" +) + +var log = logrus.New() + +var ( + cyan = color.New(color.FgCyan).SprintfFunc() + blue = color.New(color.FgBlue).SprintfFunc() + green = color.New(color.FgGreen).SprintfFunc() + yellow = color.New(color.FgYellow).SprintfFunc() + red = color.New(color.FgRed).SprintfFunc() +) + +func init_log() { + formatter := &logrus.TextFormatter{ + DisableTimestamp: true, + } + log.SetFormatter(formatter) + log.Out = os.Stdout +} + +func logHttping(stats httping.Stats, err error, address string) { + if err == nil { + fmt.Printf("%s: %s\n", cyan("%-10s", "Proxy"), strconv.FormatBool(stats.Proxy)) + fmt.Printf("%s: %s\n", cyan("%-10s", "Scheme"), stats.Scheme) + fmt.Printf("%s: %s\n", cyan("%-10s", "Host"), parseInput(address)) + fmt.Printf("%s: %.2f ms\n", cyan("%-10s", "DNS Lookup"), float64(stats.DNS)/1e6) + fmt.Printf("%s: %.2f ms\n", cyan("%-10s", "TCP"), float64(stats.TCP)/1e6) + if stats.Scheme == "https" { + fmt.Printf("%s: %.2f ms\n", cyan("%-10s", "TLS"), float64(stats.TLS)/1e6) + } + fmt.Printf("%s: %.2f ms\n", cyan("%-10s", "Process"), float64(stats.Process)/1e6) + fmt.Printf("%s: %.2f ms\n", cyan("%-10s", "Transfer"), float64(stats.Transfer)/1e6) + fmt.Printf("%s: %.2f ms\n", cyan("%-10s", "Total"), float64(stats.Total)/1e6) + } +} + +func logTcping(code int, address string) { + if code == 0 { + fmt.Printf("%s%s%s\n", cyan("%-7s", "TCP"), green("%-10s", "OPEN"), address) + } else if code == 1 { + fmt.Printf("%s%s%s\n", cyan("%-7s", "TCP"), yellow("%-10s", "CLOSED"), address) + } else if code == 2 { + fmt.Printf("%s%s%s\n", cyan("%-7s", "TCP"), red("%-10s", "ERROR"), address) + } +} + +func logPing(dst *net.IPAddr, dur time.Duration, err error) { + if err != nil { + match, _ := regexp.MatchString("operation not permitted", err.Error()) + if match { + fmt.Printf("%s%s%s\n", cyan("%-7s", "ICMP"), red("%-10s", "ERROR"), red("No privileges")) + } else { + fmt.Printf("%s%s%s\n", cyan("%-7s", "ICMP"), red("%-10s", "ERROR"), dst.String()) + } + return + } + fmt.Printf("%s%s%s %s ms\n", cyan("%-7s", "ICMP"), green("%-10s", "OPEN"), dst.String(), fmt.Sprintf("%.1f", float64(dur.Microseconds())/1000)) +} + +func logMtr(hops []string, address string) { + for _, h := range hops { + fmt.Printf("%s%s\n", cyan("%-7s", "MTR"), h) + } +} + +func logQuery(info IPInfo) { + v := reflect.ValueOf(info) + names := make([]string, v.NumField()) + values := make([]string, v.NumField()) + for i := 0; i < v.NumField(); i++ { + names[i] = v.Type().Field(i).Name + values[i] = v.Field(i).Interface().(string) + } + l := getMaxNameLength(names) + for i := 0; i < v.NumField(); i++ { + fmt.Printf("%s: %s\n", cyan("%-*s", l, names[i]), values[i]) + } +} + +func getMaxNameLength(names []string) int { + var length int + for _, val := range names { + if len(val) > length { + length = len(val) + } + } + return length +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..971dadb --- /dev/null +++ b/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "flag" + "fmt" + "net" + "strings" + + "github.com/noobly314/pingme/httping" + "github.com/noobly314/pingme/mtr" + "github.com/noobly314/pingme/ping" + "github.com/noobly314/pingme/tcping" +) + +var ( + VersionString string +) + +func init() { + init_log() + init_flag() +} + +func main() { + if !hasFlag() { + switch len(flag.Args()) { + case 0: + flag.PrintDefaults() + case 1: + addr := flag.Args()[0] + + // Query + address := parseInput(addr) + info := queryInfo(address) + logQuery(info) + + // HTTP Ping + if strings.HasPrefix(addr, "http://") || strings.HasPrefix(addr, "https://") { + fmt.Println() + stats, err := httping.New(addr) + logHttping(stats, err, addr) + } + case 2: + addr := flag.Args()[0] + port := flag.Args()[1] + ip := lookupIP(addr) + address := net.JoinHostPort(ip, port) + c := tcping.New(address) + logTcping(c, address) + default: + log.Warn("Too many arguments.") + } + } else { + if isFlagPassed("v") { + // Version + fmt.Println(VersionString) + } else if isFlagPassed("i") { + // ICMP Ping + dst, dur, err := ping.New(PingDst) + logPing(dst, dur, err) + } else if isFlagPassed("t") { + // TCP Ping + c := tcping.New(TCPingDst) + logTcping(c, TCPingDst) + } else if isFlagPassed("h") { + // HTTP Ping + stats, err := httping.New(HTTPingDst) + logHttping(stats, err, HTTPingDst) + } else if isFlagPassed("m") { + // MTR + hops, err := mtr.New(MtrDst) + if err != nil { + log.Fatal(err) + } + logMtr(hops, MtrDst) + } else if isFlagPassed("q") { + address := parseInput(Query) + info := queryInfo(address) + logQuery(info) + } + } +} diff --git a/mtr/mtr.go b/mtr/mtr.go new file mode 100644 index 0000000..07fed87 --- /dev/null +++ b/mtr/mtr.go @@ -0,0 +1,48 @@ +package mtr + +import ( + "fmt" + "os/exec" + "sort" + "strconv" + "strings" +) + +type MtrLine struct { + Name string + Pos int + IP string +} + +func New(s string) ([]string, error) { + _, err := exec.Command("mtr", "-v", s).Output() + if err != nil { + return []string{}, err + } + fmt.Println("Waiting for MTR results...") + out, err := exec.Command("mtr", "--raw", s).Output() + hops := parseOutput(out) + return hops, nil +} + +func parseOutput(b []byte) []string { + var hops []string + var matrix []MtrLine + + raw := strings.Split(string(b), "\n") + for _, rec := range raw { + if len(rec) > 0 && rec[0] == 104 { + tuple := strings.Split(rec, " ") + pos, _ := strconv.Atoi(tuple[1]) + matrix = append(matrix, MtrLine{Name: tuple[0], Pos: pos, IP: tuple[2]}) + } + } + + sort.Slice(matrix, func(i, j int) bool { return matrix[i].Pos < matrix[j].Pos }) + + for _, tuple := range matrix { + hops = append(hops, tuple.IP) + } + + return hops +} diff --git a/ping/ping.go b/ping/ping.go new file mode 100644 index 0000000..a83e739 --- /dev/null +++ b/ping/ping.go @@ -0,0 +1,120 @@ +package ping + +import ( + "fmt" + "net" + "os" + "time" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +type IPType struct { + Type string + ListenAddr string + Network string + ICMPNetwork string + ProtocolNumber int + RequestMessageType icmp.Type + ReplyMessageType icmp.Type +} + +var ( + IPType4 = IPType{ + Type: "4", + ListenAddr: "0.0.0.0", + Network: "ip4", + ICMPNetwork: "ip4:icmp", + ProtocolNumber: 1, + RequestMessageType: ipv4.ICMPTypeEcho, + ReplyMessageType: ipv4.ICMPTypeEchoReply, + } + IPType6 = IPType{ + Type: "6", + ListenAddr: "::", + Network: "ip6", + ICMPNetwork: "ip6:ipv6-icmp", + ProtocolNumber: 58, + RequestMessageType: ipv6.ICMPTypeEchoRequest, + ReplyMessageType: ipv6.ICMPTypeEchoReply, + } +) + +func New(address string) (*net.IPAddr, time.Duration, error) { + // Check ip type + // Resolve address + var err error + var dst *net.IPAddr + var ipType IPType + dst, err = net.ResolveIPAddr("ip4", address) + if err != nil { + dst, err = net.ResolveIPAddr("ip6", address) + if err != nil { + return nil, 0, err + } else { + ipType = IPType6 + } + } else { + ipType = IPType4 + } + + // Start listening for icmp replies + c, err := icmp.ListenPacket(ipType.ICMPNetwork, ipType.ListenAddr) + if err != nil { + return nil, 0, err + } + defer c.Close() + + // Make a new ICMP message + m := icmp.Message{ + Type: ipType.RequestMessageType, + Code: 0, + Body: &icmp.Echo{ + ID: os.Getpid() & 0xffff, Seq: 1, + Data: []byte(""), + }, + } + b, err := m.Marshal(nil) + if err != nil { + return dst, 0, err + } + + // Send it + start := time.Now() + n, err := c.WriteTo(b, dst) + if err != nil { + return dst, 0, err + } else if n != len(b) { + return dst, 0, fmt.Errorf("got %v; want %v", n, len(b)) + } + + // Wait for a reply + reply := make([]byte, 1500) + err = c.SetReadDeadline(time.Now().Add(3 * time.Second)) + if err != nil { + return dst, 0, err + } + n, peer, err := c.ReadFrom(reply) + if err != nil { + return dst, 0, err + } + duration := time.Since(start) + + // Pack it up boys, we're done here + rm, err := icmp.ParseMessage(ipType.ProtocolNumber, reply[:n]) + if err != nil { + return dst, 0, err + } + + //return dst, duration, nil + switch rm.Type { + case ipType.ReplyMessageType: + return dst, duration, nil + default: + return dst, 0, fmt.Errorf("got %+v from %v; want echo reply", rm, peer) + } + + return dst, 0, err +} diff --git a/query.go b/query.go new file mode 100644 index 0000000..8c64cbd --- /dev/null +++ b/query.go @@ -0,0 +1,47 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "regexp" +) + +const ( + API string = "http://ip-api.com/json/" +) + +type IPInfo struct { + IP string `json:"query"` + City string `json:"city"` + Country string `json:"country"` + ISP string `json:"isp"` + AS string `json:"as"` +} + +func queryInfo(address string) IPInfo { + var info IPInfo + + res, err := http.Get(API + address) + if err != nil { + match, _ := regexp.MatchString("connection reset by peer", err.Error()) + if match { + log.Fatal("Oops, your connection was reset by magic power. You may need to set env http_proxy.") + } else { + log.Fatal(err) + } + } + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } + err = json.Unmarshal(body, &info) + if err != nil { + fmt.Println(err) + } + //printInfo(info) + return info +} diff --git a/tcping/tcping.go b/tcping/tcping.go new file mode 100644 index 0000000..4fcdf8a --- /dev/null +++ b/tcping/tcping.go @@ -0,0 +1,29 @@ +package tcping + +import ( + "net" + "regexp" + "time" +) + +func New(address string) int { + d := net.Dialer{Timeout: 3 * time.Second} + _, err := d.Dial("tcp", address) + if err != nil { + match, _ := regexp.MatchString("refused", err.Error()) + if match { + // Closed + return 1 + } + match, _ = regexp.MatchString("timeout", err.Error()) + if match { + // Timeout + return 2 + } + } else { + // Open + return 0 + } + // Default + return 2 +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..78e1d72 --- /dev/null +++ b/util.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "net" + "regexp" +) + +func lookupIP(s string) string { + // is ip + if isValidIP(s) { + return s + } + // is hostname + hostname := parseInput(s) + ips, err := net.LookupIP(hostname) + if err != nil { + fmt.Println(hostname) + fmt.Println(err) + log.Fatal("IP address not found.") + } + return ips[0].String() +} + +func parseInput(s string) string { + re := regexp.MustCompile(`^https?\:\/\/([^\/:?#]+)(?:[\/:?#]|$)`) + results := re.FindStringSubmatch(s) + if len(results) == 0 { + return s + } + if len(results) == 2 { + return results[1] + } + return "" +} + +func isValidIP(s string) bool { + re := regexp.MustCompile(`((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))`) + return re.Match([]byte(s)) +} + +func isPrivateIPv4(s string) bool { + re := regexp.MustCompile(`(^192\.168\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])$)|(^172\.([1][6-9]|[2][0-9]|[3][0-1])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])$)|(^10\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])\.([0-9]|[0-9][0-9]|[0-2][0-5][0-5])$)`) + return re.Match([]byte(s)) +} + +func isPrivateIPv6(s string) bool { + re := regexp.MustCompile(`(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`) + return re.Match([]byte(s)) +}