Last active
December 1, 2023 19:56
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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