Ticket #12208: names.go

File names.go, 7.9 KB (added by dcf, 15 months ago)

Demo allowing you to set the URL, Host, SNI, and verify names independently.

Line 
1package main
2
3import (
4        "crypto/tls"
5        "crypto/x509"
6        "errors"
7        "flag"
8        "fmt"
9        "net/http"
10        "net/url"
11        "os"
12        "reflect"
13        "sync"
14        "time"
15
16        "golang.org/x/net/http2"
17)
18
19// Copy the public fields (fields for which CanSet is true) from src to dst.
20// src and dst must be pointers to the same type.
21func copyPublicFields(dst, src interface{}) {
22        if reflect.TypeOf(dst) != reflect.TypeOf(src) {
23                panic("unequal types")
24        }
25        dstValue := reflect.ValueOf(dst).Elem()
26        srcValue := reflect.ValueOf(src).Elem()
27        for i := 0; i < dstValue.NumField(); i++ {
28                if dstValue.Field(i).CanSet() {
29                        dstValue.Field(i).Set(srcValue.Field(i))
30                }
31        }
32}
33
34// Returns a pointer to a new http.Transport that takes default settings from
35// http.DefaultTransport and is set up for HTTP/2 support (including a non-nil
36// TLSClientConfig field).
37func newHTTPTransport() *http.Transport {
38        transport := new(http.Transport)
39        // http.DefaultTransport has nice default values for fields like
40        // MaxIdleConns, so we want to use it as a template for each new
41        // http.Transport we create. The standard library doesn't provide a way
42        // to clone http.Transport, and we cannot simply use struct assigment:
43        //   *transport = *http.DefaultTransport.(*http.Transport)
44        // because http.Transport contains private mutexes ("go vet" complains).
45        // This idea of using reflection to copy only the public fields comes
46        // from a post by Nick Craig-Wood:
47        // https://groups.google.com/d/msg/Golang-Nuts/SDiGYNVE8iY/89hRKTF4BAAJ
48        copyPublicFields(transport, http.DefaultTransport.(*http.Transport))
49        // Set up HTTP/2 support. This has the side effect of setting up
50        // TLSClientConfig properly; if we were to just create our own
51        // TLSClientConfig (with e.g. custom ServerName), without doing
52        // everything else that http2.ConfigureTransport does, we wouldn't get
53        // HTTP/2.
54        err := http2.ConfigureTransport(transport)
55        if err != nil {
56                panic(err)
57        }
58        return transport
59}
60
61func verifyCertificateForName(rawCerts [][]byte, config *tls.Config, dnsName string) error {
62        // This is cribbed from doFullHandshake in
63        // crypto/tls/handshake_client.go from the Go standard library. The
64        // important difference is that we set VerifyOptions.DNSName to the
65        // provided dnsName, rather than config.ServerName.
66        certs := make([]*x509.Certificate, len(rawCerts))
67        for i, asn1Data := range rawCerts {
68                cert, err := x509.ParseCertificate(asn1Data)
69                if err != nil {
70                        return errors.New("tls: failed to parse certificate from server: " + err.Error())
71                }
72                certs[i] = cert
73        }
74
75        var currentTime time.Time
76        if config.Time != nil {
77                currentTime = config.Time()
78        }
79        opts := x509.VerifyOptions{
80                Roots:         config.RootCAs,
81                CurrentTime:   currentTime,
82                DNSName:       dnsName,
83                Intermediates: x509.NewCertPool(),
84        }
85
86        for i, cert := range certs {
87                if i == 0 {
88                        continue
89                }
90                opts.Intermediates.AddCert(cert)
91        }
92        _, err := certs[0].Verify(opts)
93        return err
94}
95
96// Mediates access to the httpTransportForSameNames and httpTransports.
97var httpTransportsLock sync.Mutex
98
99// We use this *http.Transport when the name in the URL, the SNI name, and the
100// name we verify against are all equal. (This is independent of domain
101// fronting; the HTTP Host header is not part of this logic.) It uses the
102// default internal logic of the net/http package when
103// TLSClientConfig.ServerName is nil: take the SNI name and the verification
104// name from the name in the URL.
105var httpTransportForSameNames *http.Transport
106
107type httpTransportsCacheKey struct {
108        sniName    string
109        verifyName string
110}
111
112// This is a map from an (SNI name, verify name) pair to a *http.Transport set
113// up to use the given names.
114var httpTransportsCache map[httpTransportsCacheKey]*http.Transport
115
116// Get (creating, if necessary) a *http.Transport appropriate for connecting to
117// the given URL hostname, sending sniName in the SNI extension, and verifying
118// the server certificate against verifyName.
119func getHTTPTransportForNames(urlName, sniName, verifyName string) *http.Transport {
120        httpTransportsLock.Lock()
121        defer httpTransportsLock.Unlock()
122
123        if urlName == sniName && urlName == verifyName {
124                // This is the case where the domain name in the URL (the
125                // address we connect to), the name in the SNI, and the name we
126                // verify against are all the same. This matches the built-in
127                // logic of the net/http library and we don't have to do
128                // anything special. A single httpTransportForSameNames is
129                // shared between all the callers that provided three equal
130                // names.
131                if httpTransportForSameNames == nil {
132                        httpTransportForSameNames = newHTTPTransport()
133                }
134                return httpTransportForSameNames
135        }
136
137        // If the provided names are not all equal, that means we are doing
138        // something special: using an SNI name that is different from the URL
139        // name that we are connecting to, verifying against and different name
140        // than appears in the SNI, or some other combination. Both the SNI name
141        // and the verify name are properties of the *tls.Config inside a
142        // http.Transport. Because they are properties of an http.Transport, not
143        // of a single request, we cannot share http.Transports between callers
144        // that have different (sniName, verifyName) combinations—we have to
145        // make a new one for each new combination. urlName is no longer used
146        // after this point.
147
148        // First, check if we've already generated and cached a *http.Transport
149        // for this (sniName, verifyName) combination.
150        key := httpTransportsCacheKey{
151                sniName:    sniName,
152                verifyName: verifyName,
153        }
154        if transport, ok := httpTransportsCache[key]; ok {
155                return transport
156        }
157
158        // Not cached; need to make a new one.
159        transport := newHTTPTransport()
160        // Set the SNI name.
161        if sniName == "" {
162                // An IP address causes tls.Dial to omit the server_name
163                // extension; the value is not otherwise used when
164                // InsecureSkipVerify is also set.
165                transport.TLSClientConfig.ServerName = "0.0.0.0"
166        } else {
167                transport.TLSClientConfig.ServerName = sniName
168        }
169        // Set the name to verify against.
170        transport.TLSClientConfig.InsecureSkipVerify = true
171        transport.TLSClientConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
172                return verifyCertificateForName(rawCerts, transport.TLSClientConfig, verifyName)
173        }
174
175        // Insert our new *http.Transport into the cache.
176        if httpTransportsCache == nil {
177                httpTransportsCache = make(map[httpTransportsCacheKey]*http.Transport)
178        }
179        httpTransportsCache[key] = transport
180
181        return transport
182}
183
184func get(url *url.URL, hostName, sniName, verifyName string) (*http.Response, error) {
185        req, err := http.NewRequest("GET", url.String(), nil)
186        if err != nil {
187                return nil, err
188        }
189
190        // Here we set the Host header inside the HTTP request.
191        req.Host = hostName
192
193        // Now we fetch a *http.Transport that is set up to send our chosen
194        // sniName in the client hello, and verify the server certificate
195        // against verifyName.
196        transport := getHTTPTransportForNames(url.Hostname(), sniName, verifyName)
197
198        return transport.RoundTrip(req)
199}
200
201func main() {
202        var hostName, sniName, verifyName string
203
204        flag.StringVar(&hostName, "host", "", "name for Host header")
205        flag.StringVar(&sniName, "sni", "", "name for SNI extension")
206        flag.StringVar(&verifyName, "verify", "", "name for vertification verification")
207        flag.Parse()
208
209        if flag.NArg() != 1 {
210                fmt.Fprintln(os.Stderr, "usage: verify [-host NAME] [-sni NAME] [-verify NAME] URL")
211                os.Exit(1)
212        }
213        url, err := url.Parse(flag.Arg(0))
214        if err != nil {
215                fmt.Fprintln(os.Stderr, err)
216                os.Exit(1)
217        }
218
219        // Supply defaults for hostName and verifyName. sniName is blank by
220        // default (meaning no SNI).
221        if hostName == "" {
222                hostName = url.Host
223        }
224        if verifyName == "" {
225                verifyName = url.Hostname()
226        }
227
228        fmt.Printf("urlName:    %q\n", url.Host)
229        fmt.Printf("hostName:   %q\n", hostName)
230        fmt.Printf("sniName:    %q\n", sniName)
231        fmt.Printf("verifyName: %q\n", verifyName)
232
233        resp, err := get(url, hostName, sniName, verifyName)
234        if err != nil {
235                fmt.Fprintf(os.Stderr, "error: %s\n", err)
236                os.Exit(1)
237        }
238
239        fmt.Printf("%s %s\n", resp.Proto, resp.Status)
240}