diff --git a/net/ping/ping.go b/net/ping/ping.go new file mode 100644 index 0000000..11c7faa --- /dev/null +++ b/net/ping/ping.go @@ -0,0 +1,389 @@ +package ping + +import ( + "log" + "strconv" + "strings" +) + +type PingOptions struct { + Retry int + Interval int + Deadline int +} + +type Response struct { + TTL int + Time float32 +} + +type Summary struct { + SendCount int + ReceiveCount int + LossPercent float32 + MinTime float32 + MaxTime float32 + AvgTime float32 +} + +type PingResult struct { + Responses map[int]*Response + Summary *Summary +} + +func (o *PingOptions) Validate() { + if 0 >= o.Retry { + o.Retry = 1 + } + if 0 >= o.Interval { + o.Interval = 1 + } + if 0 >= o.Deadline { + o.Deadline = 0 + } +} + +// $ ping 192.168.1.1 -c 7 -i 1 -w 0.3 +// PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data. +// 64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=0.325 ms +// 64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=0.344 ms +// 64 bytes from 192.168.1.1: icmp_seq=3 ttl=64 time=0.163 ms +// 64 bytes from 192.168.1.1: icmp_seq=7 ttl=64 time=0.180 ms + +// --- 192.168.1.1 ping statistics --- +// 7 packets transmitted, 4 received, 42% packet loss, time 6010ms +// rtt min/avg/max/mdev = 0.163/0.253/0.344/0.082 ms +func parseLinuxPing(output []byte) (*PingResult, error) { + result := &PingResult{ + Responses: make(map[int]*Response, 0), + Summary: &Summary{}, + } + lines := strings.Split(string(output), "\n") + +LOOP: + for _, line := range lines { + log.Print(line) + fields := strings.Fields(line) + switch len(fields) { + case 5: + if "rtt" != fields[0] { + continue LOOP + } + + times := strings.Split(fields[3], "/") + + minTime, err := strconv.ParseFloat(times[0], 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.MinTime = float32(minTime) + + maxTime, err := strconv.ParseFloat(times[2], 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.MaxTime = float32(maxTime) + + avgTime, err := strconv.ParseFloat(times[1], 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.AvgTime = float32(avgTime) + + case 8: + seqs := strings.Split(fields[4], "=") + ttls := strings.Split(fields[5], "=") + times := strings.Split(fields[6], "=") + seq, err := strconv.Atoi(seqs[1]) + if nil != err { + log.Print(err) + continue LOOP + } + ttl, err := strconv.Atoi(ttls[1]) + if nil != err { + log.Print(err) + continue LOOP + } + _time, err := strconv.ParseFloat(times[1], 32) + if nil != err { + log.Print(err) + continue LOOP + } + + result.Responses[seq] = &Response{ + TTL: ttl, + Time: float32(_time), + } + + case 10: + sendCount, err := strconv.Atoi(fields[0]) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.SendCount = sendCount + + receiveCount, err := strconv.Atoi(fields[3]) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.ReceiveCount = receiveCount + + lossPercent, err := strconv.ParseFloat(strings.Replace(fields[5], "%", "", -1), 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.LossPercent = float32(lossPercent) + } + } + + return result, nil +} + +// Windows 10 +// Active code page: 437 + +// Pinging 192.168.1.1 with 32 bytes of data: +// Reply from 192.168.1.1: bytes=32 time<1ms TTL=64 +// Reply from 192.168.1.1: bytes=32 time<1ms TTL=64 +// Reply from 192.168.1.1: bytes=32 time<1ms TTL=64 +// Reply from 192.168.1.1: bytes=32 time<1ms TTL=64 +// Reply from 192.168.1.1: bytes=32 time<1ms TTL=64 + +// Ping statistics for 192.168.1.1: +// Packets: Sent = 5, Received = 5, Lost = 0 (0% loss), +// Approximate round trip times in milli-seconds: +// Minimum = 0ms, Maximum = 0ms, Average = 0ms + +// Active code page: 437 + +// Pinging www.google.com [216.58.221.164] with 32 bytes of data: +// Reply from 216.58.221.164: bytes=32 time=37ms TTL=51 +// Request timed out. +// Reply from 216.58.221.164: bytes=32 time=38ms TTL=51 +// Reply from 216.58.221.164: bytes=32 time=37ms TTL=51 +// Reply from 216.58.221.164: bytes=32 time=37ms TTL=51 + +// Ping statistics for 216.58.221.164: +// Packets: Sent = 5, Received = 4, Lost = 1 (20% loss), +// Approximate round trip times in milli-seconds: +// Minimum = 37ms, Maximum = 38ms, Average = 37ms + +func parseWindowsPing(output []byte) (*PingResult, error) { + result := &PingResult{ + Responses: make(map[int]*Response, 0), + Summary: &Summary{}, + } + lines := strings.Split(string(output), "\n") + + seq := 1 +LOOP: + for _, line := range lines { + log.Print(line) + fields := strings.Fields(line) + switch len(fields) { + case 3: + log.Print(fields) + if "Request timed out." != line { + continue LOOP + } + result.Responses[seq] = nil + seq = seq + 1 + case 6: + if "Reply" != fields[0] || "from" != fields[1] { + continue LOOP + } + times := strings.Replace(fields[4], "time", "", -1) + times = strings.Replace(times, "<", "", -1) + times = strings.Replace(times, "=", "", -1) + times = strings.Replace(times, "ms", "", -1) + + ttls := strings.Split(fields[5], "=") + + ttl, err := strconv.Atoi(ttls[1]) + if nil != err { + log.Print(err) + continue LOOP + } + _time, err := strconv.ParseFloat(times, 32) + if nil != err { + log.Print(err) + continue LOOP + } + + result.Responses[seq] = &Response{ + TTL: ttl, + Time: float32(_time), + } + seq = seq + 1 + case 9: + log.Print(fields) + if "Minimum" != fields[0] { + continue LOOP + } + + minTimes := strings.Replace(fields[2], "ms", "", -1) + minTimes = strings.Replace(minTimes, ",", "", -1) + minTime, err := strconv.ParseFloat(minTimes, 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.MinTime = float32(minTime) + + maxTimes := strings.Replace(fields[5], "ms", "", -1) + maxTimes = strings.Replace(maxTimes, ",", "", -1) + maxTime, err := strconv.ParseFloat(maxTimes, 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.MaxTime = float32(maxTime) + + avgTimes := strings.Replace(fields[8], "ms", "", -1) + avgTime, err := strconv.ParseFloat(avgTimes, 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.AvgTime = float32(avgTime) + + case 12: + if "Packets:" != fields[0] { + continue LOOP + } + sendCount, err := strconv.Atoi(strings.Replace(fields[3], ",", "", -1)) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.SendCount = sendCount + + receiveCount, err := strconv.Atoi(strings.Replace(fields[6], ",", "", -1)) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.ReceiveCount = receiveCount + + lossPercents := strings.Replace(fields[10], "(", "", -1) + lossPercents = strings.Replace(lossPercents, "%", "", -1) + lossPercent, err := strconv.ParseFloat(lossPercents, 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.LossPercent = float32(lossPercent) + } + } + + return result, nil +} + +// $ ping 192.168.1.1 -c 5 -i 1 +// PING 192.168.1.1 (192.168.1.1): 56 data bytes +// 64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=1.664 ms +// 64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=0.971 ms +// 64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=3.934 ms +// 64 bytes from 192.168.1.1: icmp_seq=3 ttl=64 time=3.539 ms +// 64 bytes from 192.168.1.1: icmp_seq=4 ttl=64 time=3.690 ms + +// --- 192.168.1.1 ping statistics --- +// 5 packets transmitted, 5 packets received, 0.0% packet loss +// round-trip min/avg/max/stddev = 0.971/2.760/3.934/1.204 ms +func parseDarwinPing(output []byte) (*PingResult, error) { + result := &PingResult{ + Responses: make(map[int]*Response, 0), + Summary: &Summary{}, + } + lines := strings.Split(string(output), "\n") + +LOOP: + for _, line := range lines { + log.Print(line) + fields := strings.Fields(line) + switch len(fields) { + case 5: + if "round-trip" != fields[0] { + continue LOOP + } + + times := strings.Split(fields[3], "/") + + minTime, err := strconv.ParseFloat(times[0], 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.MinTime = float32(minTime) + + maxTime, err := strconv.ParseFloat(times[2], 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.MaxTime = float32(maxTime) + + avgTime, err := strconv.ParseFloat(times[1], 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.AvgTime = float32(avgTime) + + case 8: + seqs := strings.Split(fields[4], "=") + ttls := strings.Split(fields[5], "=") + times := strings.Split(fields[6], "=") + seq, err := strconv.Atoi(seqs[1]) + if nil != err { + log.Print(err) + continue LOOP + } + ttl, err := strconv.Atoi(ttls[1]) + if nil != err { + log.Print(err) + continue LOOP + } + _time, err := strconv.ParseFloat(times[1], 32) + if nil != err { + log.Print(err) + continue LOOP + } + + result.Responses[seq] = &Response{ + TTL: ttl, + Time: float32(_time), + } + + case 9: + sendCount, err := strconv.Atoi(fields[0]) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.SendCount = sendCount + + receiveCount, err := strconv.Atoi(fields[3]) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.ReceiveCount = receiveCount + + lossPercent, err := strconv.ParseFloat(strings.Replace(fields[6], "%", "", -1), 32) + if nil != err { + log.Print(err) + continue LOOP + } + result.Summary.LossPercent = float32(lossPercent) + } + } + + return result, nil +} diff --git a/net/ping/ping_darwin.go b/net/ping/ping_darwin.go new file mode 100644 index 0000000..1d90f44 --- /dev/null +++ b/net/ping/ping_darwin.go @@ -0,0 +1 @@ +package ping diff --git a/net/ping/ping_linux.go b/net/ping/ping_linux.go new file mode 100644 index 0000000..1578270 --- /dev/null +++ b/net/ping/ping_linux.go @@ -0,0 +1,24 @@ +package ping + +import ( + "fmt" + "os/exec" +) + +func Ping(destination string, options *PingOptions) (*PingResult, error) { + options.Validate() + + params := make([]string, 0) + params = append(params, destination) + params = append(params, fmt.Sprintf("-c %d", options.Retry)) + params = append(params, fmt.Sprintf("-i %d", options.Interval)) + params = append(params, fmt.Sprintf("-w %d", options.Deadline)) + + pCmd := exec.Command("ping", params...) + output, err := pCmd.CombinedOutput() + if err != nil { + return nil, err + } + + return parseLinuxPing(output) +} diff --git a/net/ping/ping_linux_test.go b/net/ping/ping_linux_test.go new file mode 100644 index 0000000..4ea5306 --- /dev/null +++ b/net/ping/ping_linux_test.go @@ -0,0 +1,38 @@ +package ping + +import "testing" + +func TestPing(t *testing.T) { + type args struct { + destination string + options *PingOptions + } + tests := []struct { + name string + args args + want *PingResult + wantErr bool + }{ + { + name: "192.168.1.1", + args: args{ + destination: "192.168.1.1", + options: &PingOptions{ + Retry: 4, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Ping(tt.args.destination, tt.args.options) + if (err != nil) != tt.wantErr { + t.Errorf("Ping() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Ping() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/net/ping/ping_test.go b/net/ping/ping_test.go new file mode 100644 index 0000000..366c70b --- /dev/null +++ b/net/ping/ping_test.go @@ -0,0 +1,145 @@ +package ping + +import ( + "reflect" + "testing" +) + +func TestPingOptions_Validate(t *testing.T) { + type fields struct { + Retry int + Interval int + Deadline int + } + tests := []struct { + name string + fields fields + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &PingOptions{ + Retry: tt.fields.Retry, + Interval: tt.fields.Interval, + Deadline: tt.fields.Deadline, + } + o.Validate() + }) + } +} + +func Test_parseLinuxPing(t *testing.T) { + type args struct { + output []byte + } + tests := []struct { + name string + args args + want *PingResult + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseLinuxPing(tt.args.output) + if (err != nil) != tt.wantErr { + t.Errorf("parseLinuxPing() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseLinuxPing() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseWindowsPing(t *testing.T) { + type args struct { + output []byte + } + tests := []struct { + name string + args args + want *PingResult + wantErr bool + }{ + { + name: "test", + args: args{ + output: []byte(` +Active code page: 437 + +Pinging www.google.com [216.58.221.164] with 32 bytes of data: +Reply from 216.58.221.164: bytes=32 time=37ms TTL=51 +Request timed out. +Reply from 216.58.221.164: bytes=32 time=38ms TTL=51 +Reply from 216.58.221.164: bytes=32 time=37ms TTL=51 +Reply from 216.58.221.164: bytes=32 time=37ms TTL=51 + +Ping statistics for 216.58.221.164: + Packets: Sent = 5, Received = 4, Lost = 1 (20% loss), +Approximate round trip times in milli-seconds: + Minimum = 37ms, Maximum = 38ms, Average = 37ms + + `), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseWindowsPing(tt.args.output) + if (err != nil) != tt.wantErr { + t.Errorf("parseWindowsPing() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseWindowsPing() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseDarwinPing(t *testing.T) { + type args struct { + output []byte + } + tests := []struct { + name string + args args + want *PingResult + wantErr bool + }{ + { + name: "test", + args: args{ + output: []byte(` +PING 192.168.1.1 (192.168.1.1): 56 data bytes +64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=1.664 ms +64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=0.971 ms +64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=3.934 ms +64 bytes from 192.168.1.1: icmp_seq=3 ttl=64 time=3.539 ms +64 bytes from 192.168.1.1: icmp_seq=4 ttl=64 time=3.690 ms + +--- 192.168.1.1 ping statistics --- +5 packets transmitted, 5 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = 0.971/2.760/3.934/1.204 ms + + `), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseDarwinPing(tt.args.output) + if (err != nil) != tt.wantErr { + t.Errorf("parseDarwinPing() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseDarwinPing() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/net/ping/ping_windows.go b/net/ping/ping_windows.go new file mode 100644 index 0000000..46bdd14 --- /dev/null +++ b/net/ping/ping_windows.go @@ -0,0 +1,23 @@ +package ping + +import ( + "fmt" + "os/exec" +) + +func Ping(destination string, options *PingOptions) (string, error) { + options.Validate() + + params := make([]string, 0) + params = append(params, destination) + params = append(params, fmt.Sprintf("-n %d", options.Retry)) + params = append(params, fmt.Sprintf("-w %d", options.Deadline*1000)) + + pCmd := exec.Command("cmd.exe", "/C", "chcp 437", "&&", "ping", params...) + output, err := pCmd.CombinedOutput() + if err != nil { + return "", err + } + + return parseLinuxPing(output) +} diff --git a/net/ping/ping_windows_test.go b/net/ping/ping_windows_test.go new file mode 100644 index 0000000..a30ebb9 --- /dev/null +++ b/net/ping/ping_windows_test.go @@ -0,0 +1,38 @@ +package ping + +import "testing" + +func TestPing(t *testing.T) { + type args struct { + destination string + options *PingOptions + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "192.168.1.1", + args: args{ + destination: "192.168.1.1", + options: &PingOptions{ + Retry: 4, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Ping(tt.args.destination, tt.args.options) + if (err != nil) != tt.wantErr { + t.Errorf("Ping() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Ping() = %v, want %v", got, tt.want) + } + }) + } +}