Skip to content

Commit a0a6535

Browse files
authored
Merge pull request #220 from newrelic/http_client_invalid_hostname
http client accepting invalid hostnames
2 parents 5cf9f89 + 7d01691 commit a0a6535

File tree

4 files changed

+404
-111
lines changed

4 files changed

+404
-111
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
language: go
22

33
go:
4-
- 1.8.x
54
- 1.9.x
65
- 1.10.x
76
- 1.11.x

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
9+
### Next
10+
11+
### Added
12+
13+
- Package `http` can now create a client that validate certificates but also
14+
accepts invalid hostnames via `NewAcceptInvalidHostname`.
15+
816
### 3.6.3
917

1018
### Fixed

http/client.go

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"io/ioutil"
1010
"net/http"
11+
"os"
1112
"path/filepath"
1213
"strings"
1314
"time"
@@ -16,37 +17,89 @@ import (
1617
// New creates a new http.Client with a custom certificate, which can be loaded from the passed CA Bundle file and/or
1718
// directory. If both CABundleFile and CABundleDir are empty arguments, it creates an unsecure HTTP client.
1819
func New(CABundleFile, CABundleDir string, httpTimeout time.Duration) (*http.Client, error) {
20+
return _new(CABundleFile, CABundleDir, httpTimeout, "")
21+
}
22+
23+
// NewAcceptInvalidHostname new http.Client with ability to accept HTTPS certificates that don't
24+
// match the hostname of the server they are connecting to.
25+
func NewAcceptInvalidHostname(CABundleFile, CABundleDir string, httpTimeout time.Duration, hostname string) (*http.Client, error) {
26+
return _new(CABundleFile, CABundleDir, httpTimeout, hostname)
27+
}
28+
29+
func _new(CABundleFile, CABundleDir string, httpTimeout time.Duration, acceptInvalidHostname string) (*http.Client, error) {
1930
// go default http transport settings
20-
transport := &http.Transport{}
31+
t := &http.Transport{}
2132

2233
if CABundleFile != "" || CABundleDir != "" {
2334
certs, err := getCertPool(CABundleFile, CABundleDir)
2435
if err != nil {
2536
return nil, err
2637
}
27-
transport.TLSClientConfig = &tls.Config{RootCAs: certs}
38+
39+
t.TLSClientConfig = &tls.Config{
40+
RootCAs: certs,
41+
}
42+
43+
if acceptInvalidHostname != "" {
44+
// Default validation is replaced with VerifyPeerCertificate.
45+
// Note that when InsecureSkipVerify and VerifyPeerCertificate are in use,
46+
// ConnectionState.VerifiedChains will be nil.
47+
t.TLSClientConfig.InsecureSkipVerify = true
48+
// While packages like net/http will implicitly set ServerName, the
49+
// VerifyPeerCertificate callback can't access that value, so it has to be set
50+
// explicitly here or in VerifyPeerCertificate on the client side. If in
51+
// an http.Transport DialTLS callback, this can be obtained by passing
52+
// the addr argument to net.SplitHostPort.
53+
t.TLSClientConfig.ServerName = acceptInvalidHostname
54+
// Approximately equivalent to what crypto/tls does normally:
55+
// https://github.com/golang/go/commit/29cfb4d3c3a97b6f426d1b899234da905be699aa
56+
t.TLSClientConfig.VerifyPeerCertificate = func(certificates [][]byte, _ [][]*x509.Certificate) error {
57+
certs := make([]*x509.Certificate, len(certificates))
58+
for i, asn1Data := range certificates {
59+
cert, err := x509.ParseCertificate(asn1Data)
60+
if err != nil {
61+
return errors.New("tls: failed to parse certificate from server: " + err.Error())
62+
}
63+
certs[i] = cert
64+
}
65+
66+
opts := x509.VerifyOptions{
67+
Roots: t.TLSClientConfig.RootCAs, // On the server side, use config.ClientCAs.
68+
DNSName: acceptInvalidHostname,
69+
Intermediates: x509.NewCertPool(),
70+
// On the server side, set KeyUsages to ExtKeyUsageClientAuth. The
71+
// default value is appropriate for clients side verification.
72+
// KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
73+
}
74+
for _, cert := range certs[1:] {
75+
opts.Intermediates.AddCert(cert)
76+
}
77+
_, err := certs[0].Verify(opts)
78+
return err
79+
}
80+
81+
}
2882
}
2983

3084
return &http.Client{
3185
Timeout: httpTimeout,
32-
Transport: transport,
86+
Transport: t,
3387
}, nil
3488
}
3589

3690
func getCertPool(certFile string, certDirectory string) (*x509.CertPool, error) {
3791
caCertPool := x509.NewCertPool()
38-
if certFile != "" {
39-
caCert, err := ioutil.ReadFile(certFile)
40-
if err != nil {
41-
return nil, err
42-
}
4392

44-
ok := caCertPool.AppendCertsFromPEM(caCert)
45-
if !ok {
46-
return nil, errors.New("can't parse certificate")
93+
if certFile != "" {
94+
if err := addCert(filepath.Join(certDirectory, certFile), caCertPool); err != nil {
95+
if os.IsNotExist(err) {
96+
if err = addCert(certFile, caCertPool); err != nil {
97+
return nil, err
98+
}
99+
}
47100
}
48-
49101
}
102+
50103
if certDirectory != "" {
51104
files, err := ioutil.ReadDir(certDirectory)
52105
if err != nil {
@@ -55,17 +108,25 @@ func getCertPool(certFile string, certDirectory string) (*x509.CertPool, error)
55108

56109
for _, f := range files {
57110
if strings.Contains(f.Name(), ".pem") {
58-
caCertFilePath := filepath.Join(certDirectory + "/" + f.Name())
59-
caCert, err := ioutil.ReadFile(caCertFilePath)
60-
if err != nil {
111+
caCertFilePath := filepath.Join(certDirectory, f.Name())
112+
if err := addCert(caCertFilePath, caCertPool); err != nil {
61113
return nil, err
62114
}
63-
ok := caCertPool.AppendCertsFromPEM(caCert)
64-
if !ok {
65-
return nil, errors.New("can't parse certificate")
66-
}
67115
}
68116
}
69117
}
70118
return caCertPool, nil
71119
}
120+
121+
func addCert(certFile string, caCertPool *x509.CertPool) error {
122+
caCert, err := ioutil.ReadFile(certFile)
123+
if err != nil {
124+
return err
125+
}
126+
127+
ok := caCertPool.AppendCertsFromPEM(caCert)
128+
if !ok {
129+
return errors.New("can't parse certificate")
130+
}
131+
return nil
132+
}

0 commit comments

Comments
 (0)