Skip to content

Commit

Permalink
Merge pull request #15 from kffl/feat/https
Browse files Browse the repository at this point in the history
Add support for HTTPS targets
  • Loading branch information
kffl authored Nov 10, 2021
2 parents e592beb + a1a9036 commit c94beec
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 23 deletions.
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,22 @@ Flags:
-d, --duration=10s Load test duration
-c, --connections=50 Maximum number of concurrent connections
-t, --timeout=200ms HTTP client timeout
-m, --mode="reqlog" Statistics collection mode: reqlog (logs each request) or
hist (stores histogram of completed requests latencies)
-o, --output=file.csv File to save the request log in CSV format (reqlog mode)
or a text file with raw histogram data (hist mode)
-m, --mode="reqlog" Statistics collection mode: reqlog (logs each request) or hist
(stores histogram of completed requests latencies)
-o, --output=file.csv File to save the request log in CSV format (reqlog mode) or a
text file with raw histogram data (hist mode)
-i, --interval=250ms Interval for statistics calculation (reqlog mode)
--preallocate=1000 Number of requests in req log to preallocate memory for
per connection (reqlog mode)
--preallocate=1000 Number of requests in req log to preallocate memory for per
connection (reqlog mode)
--method=GET The HTTP request method (GET, POST, PUT, PATCH or DELETE)
-b, --body="{data..." HTTP request body
-h, --header="k:v" ... HTTP request header(s). You can set more than one header
by repeating this flag.
-h, --header="k:v" ... HTTP request header(s). You can set more than one header by
repeating this flag.
--trust-all Omit SSL certificate validation
--version Show application version.
Args:
<target> HTTP target URL
<target> HTTP target URL with port (i.e. http://localhost:80/test or https://host:443/x)
```

Below is an example of a load test conducted using gocannon against an Express.js server (notice the performance improvement over time under sustained load):
Expand Down
9 changes: 5 additions & 4 deletions args.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,11 @@ var (
preallocate = kingpin.Flag("preallocate", "Number of requests in req log to preallocate memory for per connection (reqlog mode)").
Default("1000").
Int()
method = kingpin.Flag("method", "The HTTP request method (GET, POST, PUT, PATCH or DELETE)").Default("GET").Enum("GET", "POST", "PUT", "PATCH", "DELETE")
body = parseRequestBody(kingpin.Flag("body", "HTTP request body").Short('b').PlaceHolder("\"{data...\""))
headers = parseRequestHeaders(kingpin.Flag("header", "HTTP request header(s). You can set more than one header by repeating this flag.").Short('h').PlaceHolder("\"k:v\""))
target = kingpin.Arg("target", "HTTP target URL").Required().String()
method = kingpin.Flag("method", "The HTTP request method (GET, POST, PUT, PATCH or DELETE)").Default("GET").Enum("GET", "POST", "PUT", "PATCH", "DELETE")
body = parseRequestBody(kingpin.Flag("body", "HTTP request body").Short('b').PlaceHolder("\"{data...\""))
headers = parseRequestHeaders(kingpin.Flag("header", "HTTP request header(s). You can set more than one header by repeating this flag.").Short('h').PlaceHolder("\"k:v\""))
trustAll = kingpin.Flag("trust-all", "Omit SSL certificate validation").Bool()
target = kingpin.Arg("target", "HTTP target URL with port (i.e. http://localhost:80/test or https://host:443/x)").Required().String()
)

func parseArgs() {
Expand Down
2 changes: 1 addition & 1 deletion gocannon.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func exitWithError(err error) {

func runGocannon() error {

c, err := newHTTPClient(*target, *timeout, *connections)
c, err := newHTTPClient(*target, *timeout, *connections, *trustAll)

if err != nil {
return err
Expand Down
22 changes: 20 additions & 2 deletions http.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package main

import (
"crypto/tls"
"errors"
"net"
"net/url"
"strconv"
"strings"
"time"

"github.com/valyala/fasthttp"
Expand All @@ -12,20 +15,27 @@ import (
var (
ErrWrongTarget = errors.New("wrong target URL")
ErrUnsupportedProtocol = errors.New("unsupported target protocol")
ErrMissingPort = errors.New("missing target port")
)

func newHTTPClient(
target string,
timeout time.Duration,
connections int,
trustAll bool,
) (*fasthttp.HostClient, error) {
u, err := url.ParseRequestURI(target)
if err != nil {
return nil, ErrWrongTarget
}
if u.Scheme != "http" {
if u.Scheme != "http" && u.Scheme != "https" {
return nil, ErrUnsupportedProtocol
}
tokenizedHost := strings.Split(u.Host, ":")
port := tokenizedHost[len(tokenizedHost)-1]
if _, err := strconv.Atoi(port); err != nil {
return nil, ErrMissingPort
}
c := &fasthttp.HostClient{
Addr: u.Host,
MaxConns: int(connections),
Expand All @@ -35,6 +45,10 @@ func newHTTPClient(
Dial: func(addr string) (net.Conn, error) {
return fasthttp.DialTimeout(addr, timeout)
},
IsTLS: u.Scheme == "https",
TLSConfig: &tls.Config{
InsecureSkipVerify: trustAll,
},
}
return c, nil
}
Expand All @@ -45,7 +59,11 @@ func performRequest(c *fasthttp.HostClient, target string, method string, body [
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()

req.URI().SetScheme("http")
if strings.HasPrefix(target, "https") {
req.URI().SetScheme("https")
} else {
req.URI().SetScheme("http")
}
req.Header.SetMethod(method)
req.SetRequestURI(target)

Expand Down
51 changes: 45 additions & 6 deletions http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,43 @@ import (

func TestNewHTTPClientWrongURL(t *testing.T) {

_, err1 := newHTTPClient("XYZthisisawrongurl123", time.Millisecond*200, 10)
_, err2 := newHTTPClient("https://something/", time.Millisecond*200, 10)
_, err1 := newHTTPClient("XYZthisisawrongurl123", time.Millisecond*200, 10, true)
_, err2 := newHTTPClient("ldap://something/", time.Millisecond*200, 10, true)
_, err3 := newHTTPClient("http://localhost/", time.Millisecond*200, 10, true)

assert.ErrorIs(t, err1, ErrWrongTarget, "target URL should be detected as invalid")
assert.ErrorIs(t, err2, ErrUnsupportedProtocol, "target URL should be detected as non-http")
assert.ErrorIs(t, err2, ErrUnsupportedProtocol, "target URL should be detected as unsupported (other than http and https)")
assert.ErrorIs(t, err3, ErrMissingPort, "target URL without port should cause an error")
}

func TestNewHTTPClientCorrectUrl(t *testing.T) {
timeout := time.Millisecond * 200
maxConnections := 123

c, err := newHTTPClient("http://localhost:3000/", timeout, maxConnections)
c, err := newHTTPClient("http://localhost:3000/", timeout, maxConnections, true)
c2, err2 := newHTTPClient("https://localhost:443/", timeout, maxConnections, false)

assert.Nil(t, err, "correct target")
assert.Nil(t, err, "correct http target")
assert.Equal(t, "localhost:3000", c.Addr)
assert.Equal(t, maxConnections, c.MaxConns)
assert.Equal(t, timeout, c.ReadTimeout)
assert.Equal(t, timeout, c.WriteTimeout)
assert.Equal(t, false, c.IsTLS)
assert.Equal(t, true, c.TLSConfig.InsecureSkipVerify)

assert.Nil(t, err2, "correct https target")
assert.Equal(t, "localhost:443", c2.Addr)
assert.Equal(t, maxConnections, c2.MaxConns)
assert.Equal(t, timeout, c2.ReadTimeout)
assert.Equal(t, timeout, c2.WriteTimeout)
assert.Equal(t, true, c2.IsTLS)
assert.Equal(t, false, c2.TLSConfig.InsecureSkipVerify)
}

func TestPerformRequest(t *testing.T) {
timeout := time.Millisecond * 100

c, _ := newHTTPClient("http://localhost:3000/", timeout, 10)
c, _ := newHTTPClient("http://localhost:3000/", timeout, 10, true)
r := requestHeaders{}
customHeader := requestHeaders{requestHeader{"Custom-Header", "gocannon"}}

Expand All @@ -52,3 +65,29 @@ func TestPerformRequest(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, codeMissingHeader)
assert.Equal(t, 200, codeWithHeader)
}

func TestPerformRequestHTTPS(t *testing.T) {
timeout := time.Second * 3

c, _ := newHTTPClient("https://dev.kuffel.io:443/", timeout, 1, false)
r := requestHeaders{}

codeOk, _, _ := performRequest(c, "https://dev.kuffel.io:443/", "GET", []byte(""), r)

assert.Equal(t, 200, codeOk)
}

func TestPerformRequestHTTPSInvalidCert(t *testing.T) {
timeout := time.Second * 3
targetBadCert := "https://self-signed.badssl.com:443/"

trustingClient, _ := newHTTPClient(targetBadCert, timeout, 1, true)
regularClient, _ := newHTTPClient(targetBadCert, timeout, 1, false)
r := requestHeaders{}

codeTrusting, _, _ := performRequest(trustingClient, targetBadCert, "GET", []byte(""), r)
codeRegular, _, _ := performRequest(regularClient, targetBadCert, "GET", []byte(""), r)

assert.Equal(t, 200, codeTrusting)
assert.Equal(t, 0, codeRegular)
}
2 changes: 1 addition & 1 deletion integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestGocannon(t *testing.T) {
conns := 50
body := []byte("")

c, err := newHTTPClient(target, timeout, conns)
c, err := newHTTPClient(target, timeout, conns, true)

var ops int32
var wg sync.WaitGroup
Expand Down

0 comments on commit c94beec

Please sign in to comment.