Skip to content

Commit

Permalink
Merge pull request #84 from lukaszbudnik/ping-with-timeout
Browse files Browse the repository at this point in the history
implemented support for custom timeout parameter for ping/reachability analyzer
  • Loading branch information
lukaszbudnik committed Nov 17, 2023
2 parents bfb0a19 + 5651f72 commit 906be51
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 17 deletions.
47 changes: 36 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,16 @@ yosoy responds to all requests with a JSON containing the information about:

Check [Sample JSON response](#sample-json-response) to see how you can use yosoy for stubbing/prototyping/troubleshooting distributed applications.

Check [ping/reachability analyzer](#pingreachability-analyzer) to see how you can use yosoy for troubleshooting network connectivity.
## ping/reachability analyzer

yosoy includes a simple ping/reachability analyzer. You can use this functionality when prototyping distributed systems to validate whether a given component can reach a specific endpoint. yosoy exposes a dedicated `/_/yosoy/ping` endpoint which accepts the following 4 query parameters:

* `h` - required - hostname of the endpoint
* `p` - required - port of the endpoint
* `n` - optional - network, all valid Go networks are supported (including the most popular ones like `tcp`, `udp`, IPv4, IPV6, etc.). If `n` parameter is not provided, it defaults to `tcp`. If `n` parameter is set to unknown network, an error will be returned.
* `t` - optional - timeout in seconds. If `t` parameter is not provided, it defaults to `10`. If `t` contains invalid integer literal, an error will be returned.

Check [Sample ping/reachability analyzer responses](#sample-pingreachability-analyzer-responses) to see how you can use yosoy for troubleshooting network connectivity.

## Docker image

Expand Down Expand Up @@ -132,24 +141,40 @@ A sample yosoy JSON response to a request made from a single page application (S
}
```

## ping/reachability analyzer

yosoy includes a simple ping/reachability analyzer. You can use this functionality when prototyping distributed systems to validate whether a given component can reach a specific endpoint. yosoy exposes a dedicated `/_/yosoy/ping` endpoint which accepts the following 3 query parameters:

* `h` - required - hostname of the endpoint
* `p` - required - port of the endpoint
* `n` - optional - network, all valid Go networks are supported (including the most popular ones like `tcp`, `udp`, IPv4, IPV6, etc.). If `n` parameter is not provided, it defaults to `tcp`. Go will throw an error if `n` parameter will be set to unknown network.
## Sample ping/reachability analyzer responses

For example, to test if yosoy can connect to `google.com` on port `443` using default `tcp` network use the following command:
To test if yosoy can connect to `google.com` on port `443` using default `tcp` network use the following command:

```bash
curl "$URL/_/yosoy/ping?h=google.com&p=443"
curl -v "http://localhost/_/yosoy/ping?h=google.com&p=443"
> GET /_/yosoy/ping?h=google.com&p=443 HTTP/1.1
> Host: localhost
> User-Agent: curl/7.86.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 17 Nov 2023 05:54:36 GMT
< Content-Length: 29
< Content-Type: text/plain; charset=utf-8
<
{"message":"ping succeeded"}
```

To see an unsuccessful response you may use localhost with some random port number:

```bash
curl "$URL/_/yosoy/ping?h=127.0.0.1&p=12345"
curl -v "http://localhost/_/yosoy/ping?h=127.0.0.1&p=12345"
> GET /_/yosoy/ping?h=127.0.0.1&p=12345 HTTP/1.1
> Host: localhost
> User-Agent: curl/7.86.0
> Accept: */*
>
< HTTP/1.1 500 Internal Server Error
< Date: Fri, 17 Nov 2023 05:53:48 GMT
< Content-Length: 66
< Content-Type: text/plain; charset=utf-8
<
{"error":"dial tcp 127.0.0.1:12345: connect: connection refused"}
```

## Building and testing locally
Expand Down
35 changes: 29 additions & 6 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ import (
"net"
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)

const PING_DEFAULT_TIMEOUT = 10
const PING_DEFAULT_NETWORK = "tcp"

type response struct {
Host string `json:"host"`
Proto string `json:"proto"`
Expand Down Expand Up @@ -95,10 +100,12 @@ func handler(w http.ResponseWriter, req *http.Request) {
}

func ping(w http.ResponseWriter, req *http.Request) {
// get h, p, t parameters from query string
// get h, p, n, t parameters from query string
hostname := req.URL.Query().Get("h")
port := req.URL.Query().Get("p")
network := req.URL.Query().Get("n")
timeoutString := req.URL.Query().Get("t")
var timeout int64 = PING_DEFAULT_TIMEOUT

// return HTTP BadRequest when hostname is empty
if hostname == "" {
Expand All @@ -114,12 +121,25 @@ func ping(w http.ResponseWriter, req *http.Request) {
json.NewEncoder(w).Encode(&errorResponse{"port is empty"})
return
}
// check if timeoutString is a valid int64, return HTTP BadRequest when invalid, otherwise set timeout value
if timeoutString != "" {
timeoutInt, err := strconv.ParseInt(timeoutString, 10, 64)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Header().Add("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(&errorResponse{"timeout is invalid"})
return
}
timeout = timeoutInt
}

// if network is empty set default to tcp
if network == "" {
network = "tcp"
network = PING_DEFAULT_NETWORK
}
// ping the hostname and port by opening a socket
err := pingHost(hostname, port, network)

// ping the hostname and port
err := pingHost(hostname, port, network, timeout)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Header().Add("Content-Type", "application/json; charset=utf-8")
Expand All @@ -132,9 +152,12 @@ func ping(w http.ResponseWriter, req *http.Request) {
json.NewEncoder(w).Encode(&successResponse{"ping succeeded"})
}

func pingHost(hostname, port, network string) error {
func pingHost(hostname, port, network string, timeout int64) error {
// create timeoutDuration variable of Duration type using timeout as the value in seconds
timeoutDuration := time.Duration(timeout) * time.Second

// open a socket to hostname and port
conn, err := net.Dial(network, hostname+":"+port)
conn, err := net.DialTimeout(network, hostname+":"+port, timeoutDuration)
if err != nil {
return err
}
Expand Down
68 changes: 68 additions & 0 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,71 @@ func TestHandlerPreflight(t *testing.T) {
assert.Equal(t, "600", result.Header.Get("Access-Control-Max-Age"))
assert.Equal(t, "*", result.Header.Get("Access-Control-Expose-Headers"))
}

// write test for request /_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=5, the request should return 200 ok and return JSON with message "ping succeeded"
func TestHandlerPingWithHostnameAndPortAndNetworkAndTimeout(t *testing.T) {

// create tcp process to listen on port 8123
listener, err := net.Listen("tcp", "localhost:8123")
if err != nil {
t.Fatal(err)
}
defer listener.Close()

req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=5", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Accept", "*/*")

rr := httptest.NewRecorder()
handler := http.HandlerFunc(ping)

handler.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}

result := rr.Result()
buf := new(bytes.Buffer)
buf.ReadFrom(result.Body)
var response successResponse
json.Unmarshal(buf.Bytes(), &response)
// test response
assert.Equal(t, "ping succeeded", response.Message)
}

// write test for request /_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=invalid, the request should return 400 bad request and return JSON with error "timeout is invalid"
func TestHandlerPingWithHostnameAndPortAndNetworkAndTimeoutInvalid(t *testing.T) {

// create tcp process to listen on port 8123
listener, err := net.Listen("tcp", "localhost:8123")
if err != nil {
t.Fatal(err)
}
defer listener.Close()

req, err := http.NewRequest("GET", "https://example.org/_/yosoy/ping?h=127.0.0.1&p=8123&n=tcp&t=invalid", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Accept", "*/*")

rr := httptest.NewRecorder()
handler := http.HandlerFunc(ping)

handler.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusBadRequest {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusBadRequest)
}

result := rr.Result()
buf := new(bytes.Buffer)
buf.ReadFrom(result.Body)
var response errorResponse
json.Unmarshal(buf.Bytes(), &response)
// test response
assert.Equal(t, "timeout is invalid", response.Error)
}

0 comments on commit 906be51

Please sign in to comment.