Skip to content

Instantly share code, notes, and snippets.

@SCP002
Last active December 1, 2023 19:56
Show Gist options
  • Save SCP002/409d62dd4bc8de21d0080942d1b03d4f to your computer and use it in GitHub Desktop.
Save SCP002/409d62dd4bc8de21d0080942d1b03d4f to your computer and use it in GitHub Desktop.
Golang: Run both HTTP and HTTPS server with dummy TLS certificates for testing. Network error classifier included.
package main
import (
"errors"
"net"
"net/url"
"syscall"
)
// ErrType represents network error type
type ErrType string
const (
Nil ErrType = "Nil"
// no such host
NoSuchHost ErrType = "No such host"
// http: server gave HTTP response to HTTPS client
HTTPSClientHTTPServer ErrType = "HTTP response to HTTPS client"
// No connection could be made beerr the target machine actively Refused it
Refused ErrType = "Connection refused"
// context deadline exceeded (Client.timeout exceeded while awaiting headers)
Timeout ErrType = "Timeout"
Unknown ErrType = "Unknown"
)
// GetErrType returns network error type.
//
// Inspired by https://stackoverflow.com/a/67647035
func GetErrType(err error) ErrType {
if err == nil {
return Nil
}
dnsErr := &net.DNSError{}
if ok := errors.As(err, &dnsErr); ok && dnsErr.Err == "no such host" {
return NoSuchHost
}
urlErr := &url.Error{}
if ok := errors.As(err, &urlErr); ok && urlErr.Err.Error() == "http: server gave HTTP response to HTTPS client" {
return HTTPSClientHTTPServer
}
if errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.Errno(10061)) {
return Refused
}
netErr := err.(net.Error)
if ok := errors.As(err, &netErr); ok && netErr.Timeout() {
return Timeout
}
return Unknown
}
package main
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/http/httptest"
"strings"
"time"
)
// NewHttpServer starts http server at <httpPort> and https server at <httpsPort> with request handlers from <mux>,
// which can be *http.ServeMux or http.Handler.
//
// Accepts <onErr> callback for errors, with <srvType> argument being either "HTTP" or "HTTPS".
//
// Returns both running servers, <httpSrv> and <httpsSrv>.
func NewHttpServer(mux http.Handler, httpPort int, httpsPort int, onErr func(srvType string, err error)) (
httpSrv, httpsSrv *http.Server) {
httpSrv = &http.Server{
Addr: fmt.Sprintf(":%v", httpPort),
Handler: mux,
}
// Get certificates from httptest TLS server
certSrv := httptest.NewTLSServer(nil)
certs := certSrv.TLS.Certificates
certSrv.Close()
httpsSrv = &http.Server{
Addr: fmt.Sprintf(":%v", httpsPort),
Handler: mux,
TLSConfig: &tls.Config{
Certificates: certs,
},
}
go func() {
httpErr := httpSrv.ListenAndServe()
onErr("HTTP", httpErr)
}()
go func() {
httpsErr := httpsSrv.ListenAndServeTLS("", "")
onErr("HTTPS", httpsErr)
}()
return
}
// NewHttpClient returns new HTTP client.
//
// If <fake> is true, make connections to localhost regardless of target host specified.
//
// <timeout> is a time limit for requests made by returned client.
func NewHttpClient(fake bool, timeout time.Duration) *http.Client {
tlsCfg := &tls.Config{
InsecureSkipVerify: true,
}
client := &http.Client{
Timeout: timeout,
Transport: &http.Transport{
TLSClientConfig: tlsCfg,
},
}
if fake {
client.Transport = &http.Transport{
TLSClientConfig: tlsCfg,
Dial: func(network, addr string) (net.Conn, error) {
// Add "no such host" error if hostname is named "dead" (optional)
reqAddrNoPort := strings.Split(addr, ":")[0]
if reqAddrNoPort == "dead" {
return nil, &net.DNSError{Err: "no such host", Name: reqAddrNoPort, IsNotFound: true}
}
// Replace request destination host with localhost:port (same as :port)
port := strings.Split(addr, ":")[1]
return net.Dial(network, fmt.Sprintf(":%v", port))
},
}
}
return client
}
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
// Normal http client which make actual network connections with 10 seconds timeout.
client := NewHttpClient(false, time.Second * 10)
url := "https://google.com"
status, err := MakeRequest(url, client)
fmt.Printf("Request to %v with default client: status: %v, error: %v\n", url, status, err)
}
// Sample function to use and test. Returns status code.
func MakeRequest(url string, httpClient *http.Client) (int, error) {
resp, err := httpClient.Get(url)
if err != nil {
return -1, err
}
return resp.StatusCode, nil
}
package main
import (
"errors"
"log"
"net/http"
"testing"
"time"
)
// Test for the MakeRequest sample function
func TestMakeRequest(t *testing.T) {
// Create handlers
mux := http.NewServeMux()
mux.HandleFunc("/ok/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
})
mux.HandleFunc("/not_found/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
})
mux.HandleFunc("/timeout/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second * 5)
})
// Run http & https servers as subset of current test to be able to fail it from another goroutines (servers) if any
// server returns error
var httpSrv, httpsSrv *http.Server
t.Run("http_server", func(t *testing.T) {
httpSrv, httpsSrv = NewHttpServer(mux, 80, 443, func(srvType string, err error) {
if !errors.Is(err, http.ErrServerClosed) {
// Using log or fmt + FailNow or else message will not be displayed
log.Default().Printf("Test %v server stopped with non-standard error %v", srvType, err)
t.FailNow()
}
})
})
defer httpSrv.Close()
defer httpsSrv.Close()
// Create http client
fakeClient := NewHttpClient(true, time.Second * 3)
// Verify results
statusCode, _ := MakeRequest("https://google.com/ok/1", fakeClient)
if statusCode != 200 {
t.Errorf("Expected status code 200, got %v", statusCode)
}
statusCode, _ = MakeRequest("https://google.com/not_found/1", fakeClient)
if statusCode != 404 {
t.Errorf("Expected status code 404, got %v", statusCode)
}
_, err := MakeRequest("http://google.com/timeout/1", fakeClient)
if GetNetErrType(err) != Timeout {
t.Error("Expected timeout")
}
_, err = MakeRequest("https://dead/no_such_host/1", fakeClient)
if GetNetErrType(err) != NoSuchHost {
t.Error("Dead URL's should return DNS lookup error")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment