Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implemented support for custom timeout parameter for ping/reachability analyzer #84

Merged
merged 1 commit into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}