feat: support monitoring WebSocket endpoints (#511)

* feat: support monitoring WebSocket endpoints

WebSocket endpoints are automatically identified by the URL protocol
specification: `wss://` or `ws://`. The request body is used as the
"message" written to the server, and the answer is stored in the
`[BODY]`.

Optionally, the user can set the `jsonrpc` flag to automatically wrap
the request body in a JSON RPC 2.0 method call.

* core:websocket: close connection after using it

* test: add tests related to WebSocket support

- test we can identify the endpoint type for WebSockets based on the URL
  supplied: `wss://` (with SSL/TLS) and `ws://` (plain text).
- test we can generate a JsonRPC 2.0 message via the new endpoint flag
  `JsonRPC`.

* core:endpoint: fix name of jsonrpc parameter

See https://en.wikipedia.org/wiki/JSON-RPC#Version_2.0

* core:websocket: fix dangling open connection on error

Move the `defer ws.Close()` to after opening the connection, so the
socket is closed also in case of errors.

* remove jsonrpc flag

* core:websocket: fix nil pointer dereference

The connection should only be closed if successfully opened.

* Move websocket function to client

* update go.mod

* Fix build errors

* Fix errors

* Update client/client.go

---------

Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
Heitor 2023-08-08 23:12:37 -03:00 committed by GitHub
parent 34313bec7e
commit 5f69351b6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 116 additions and 49 deletions

View File

@ -87,6 +87,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
- [Monitoring a TCP endpoint](#monitoring-a-tcp-endpoint)
- [Monitoring a UDP endpoint](#monitoring-a-udp-endpoint)
- [Monitoring a SCTP endpoint](#monitoring-a-sctp-endpoint)
- [Monitoring a WebSocket endpoint](#monitoring-a-websocket-endpoint)
- [Monitoring an endpoint using ICMP](#monitoring-an-endpoint-using-icmp)
- [Monitoring an endpoint using DNS queries](#monitoring-an-endpoint-using-dns-queries)
- [Monitoring an endpoint using STARTTLS](#monitoring-an-endpoint-using-starttls)
@ -1531,6 +1532,22 @@ Placeholders `[STATUS]` and `[BODY]` as well as the fields `endpoints[].body`, `
This works for SCTP based application.
### Monitoring a WebSocket endpoint
By prefixing `endpoints[].url` with `ws://` or `wss://`, you can monitor WebSocket endpoints at a very basic level:
```yaml
endpoints:
- name: example
url: "wss://example.com/"
body: "status"
conditions:
- "[CONNECTED] == true"
- "[BODY].result >= 0"
```
The `[BODY]` placeholder contains the output of the query, and `[CONNECTED]`
shows whether the connected was successfully established.
### Monitoring an endpoint using ICMP
By prefixing `endpoints[].url` with `icmp:\\`, you can monitor endpoints at a very basic level using ICMP, or more
commonly known as "ping" or "echo":

View File

@ -5,6 +5,7 @@ import (
"crypto/x509"
"errors"
"fmt"
"golang.org/x/net/websocket"
"net"
"net/http"
"net/smtp"
@ -184,6 +185,37 @@ func Ping(address string, config *Config) (bool, time.Duration) {
return true, 0
}
// Open a websocket connection, write `body` and return a message from the server
func QueryWebSocket(address string, config *Config, body string) (bool, []byte, error) {
const (
Origin = "http://localhost/"
MaximumMessageSize = 1024 // in bytes
)
wsConfig, err := websocket.NewConfig(address, Origin)
if err != nil {
return false, nil, fmt.Errorf("error configuring websocket connection: %w", err)
}
// Dial URL
ws, err := websocket.DialConfig(wsConfig)
if err != nil {
return false, nil, fmt.Errorf("error dialing websocket: %w", err)
}
defer ws.Close()
connected := true
// Write message
if _, err := ws.Write([]byte(body)); err != nil {
return false, nil, fmt.Errorf("error writing websocket body: %w", err)
}
// Read message
var n int
msg := make([]byte, MaximumMessageSize)
if n, err = ws.Read(msg); err != nil {
return false, nil, fmt.Errorf("error reading websocket message: %w", err)
}
return connected, msg[:n], nil
}
// InjectHTTPClient is used to inject a custom HTTP client for testing purposes
func InjectHTTPClient(httpClient *http.Client) {
injectedHTTPClient = httpClient

View File

@ -42,6 +42,7 @@ const (
EndpointTypeSTARTTLS EndpointType = "STARTTLS"
EndpointTypeTLS EndpointType = "TLS"
EndpointTypeHTTP EndpointType = "HTTP"
EndpointTypeWS EndpointType = "WEBSOCKET"
EndpointTypeUNKNOWN EndpointType = "UNKNOWN"
)
@ -149,6 +150,8 @@ func (endpoint Endpoint) Type() EndpointType {
return EndpointTypeTLS
case strings.HasPrefix(endpoint.URL, "http://") || strings.HasPrefix(endpoint.URL, "https://"):
return EndpointTypeHTTP
case strings.HasPrefix(endpoint.URL, "ws://") || strings.HasPrefix(endpoint.URL, "wss://"):
return EndpointTypeWS
default:
return EndpointTypeUNKNOWN
}
@ -340,6 +343,9 @@ func (endpoint *Endpoint) call(result *Result) {
result.Duration = time.Since(startTime)
} else if endpointType == EndpointTypeICMP {
result.Connected, result.Duration = client.Ping(strings.TrimPrefix(endpoint.URL, "icmp://"), endpoint.ClientConfig)
} else if endpointType == EndpointTypeWS {
result.Connected, result.Body, err = client.QueryWebSocket(endpoint.URL, endpoint.ClientConfig, endpoint.Body)
result.Duration = time.Since(startTime)
} else {
response, err = client.GetHTTPClient(endpoint.ClientConfig).Do(request)
result.Duration = time.Since(startTime)

View File

@ -313,6 +313,18 @@ func TestEndpoint_Type(t *testing.T) {
},
want: EndpointTypeHTTP,
},
{
args: args{
URL: "wss://example.com/",
},
want: EndpointTypeWS,
},
{
args: args{
URL: "ws://example.com/",
},
want: EndpointTypeWS,
},
{
args: args{
URL: "invalid://example.org",

2
go.mod
View File

@ -20,6 +20,7 @@ require (
github.com/valyala/fasthttp v1.48.0
github.com/wcharczuk/go-chart/v2 v2.1.0
golang.org/x/crypto v0.11.0
golang.org/x/net v0.11.0
golang.org/x/oauth2 v0.8.0
gopkg.in/mail.v2 v2.3.1
gopkg.in/yaml.v3 v3.0.1
@ -57,7 +58,6 @@ require (
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/image v0.5.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/tools v0.4.0 // indirect