diff --git a/examples/examples.go b/examples/examples.go index 0c75d874..63f8eaa8 100644 --- a/examples/examples.go +++ b/examples/examples.go @@ -1,15 +1,16 @@ package main import ( + "bufio" "encoding/hex" "fmt" "net" "net/http" "net/http/httputil" - "strings" + "net/url" "time" - tls "github.com/refraction-networking/utls" + "github.com/refraction-networking/utls" "golang.org/x/net/http2" ) @@ -17,51 +18,44 @@ var ( dialTimeout = time.Duration(15) * time.Second sessionTicket = []uint8(`Here goes phony session ticket: phony enough to get into ASCII range Ticket could be of any length, but for camouflage purposes it's better to use uniformly random contents -and standard length such as 228`) +and common length. See https://tlsfingerprint.io/session-tickets`) ) -func HttpGetDefault(hostname string, addr string) (string, error) { +var requestHostname = "facebook.com" // speaks http2 and TLS 1.3 +var requestAddr = "31.13.72.36:443" + +func HttpGetDefault(hostname string, addr string) (*http.Response, error) { config := tls.Config{ServerName: hostname} dialConn, err := net.DialTimeout("tcp", addr, dialTimeout) if err != nil { - return "", fmt.Errorf("net.DialTimeout error: %+v", err) + return nil, fmt.Errorf("net.DialTimeout error: %+v", err) } tlsConn := tls.Client(dialConn, &config) defer tlsConn.Close() - - err = tlsConn.Handshake() - if err != nil { - return "", fmt.Errorf("tlsConn.Handshake() error: %+v", err) - } - tlsConn.Write([]byte("GET / HTTP/1.1\r\nHost: " + hostname + "\r\n\r\n")) - buf := make([]byte, 14096) - tlsConn.Read(buf) - return string(buf), nil + return httpGetOverConn(tlsConn, tlsConn.ConnectionState().NegotiatedProtocol) } -func HttpGetByHelloID(hostname string, addr string, helloID tls.ClientHelloID) (string, error) { +func HttpGetByHelloID(hostname string, addr string, helloID tls.ClientHelloID) (*http.Response, error) { config := tls.Config{ServerName: hostname} dialConn, err := net.DialTimeout("tcp", addr, dialTimeout) if err != nil { - return "", fmt.Errorf("net.DialTimeout error: %+v", err) + return nil, fmt.Errorf("net.DialTimeout error: %+v", err) } uTlsConn := tls.UClient(dialConn, &config, helloID) defer uTlsConn.Close() err = uTlsConn.Handshake() if err != nil { - return "", fmt.Errorf("uTlsConn.Handshake() error: %+v", err) + return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } - uTlsConn.Write([]byte("GET / HTTP/1.1\r\nHost: " + hostname + "\r\n\r\n")) - buf := make([]byte, 14096) - uTlsConn.Read(buf) - return string(buf), nil + + return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) } -func HttpGetExplicitRandom(hostname string, addr string) (string, error) { +func HttpGetExplicitRandom(hostname string, addr string) (*http.Response, error) { dialConn, err := net.DialTimeout("tcp", addr, dialTimeout) if err != nil { - return "", fmt.Errorf("net.DialTimeout error: %+v", err) + return nil, fmt.Errorf("net.DialTimeout error: %+v", err) } uTlsConn := tls.UClient(dialConn, nil, tls.HelloGolang) defer uTlsConn.Close() @@ -70,7 +64,7 @@ func HttpGetExplicitRandom(hostname string, addr string) (string, error) { err = uTlsConn.BuildHandshakeState() if err != nil { // have to call BuildHandshakeState() first, when using default UClient, to avoid settings' overwriting - return "", fmt.Errorf("uTlsConn.BuildHandshakeState() error: %+v", err) + return nil, fmt.Errorf("uTlsConn.BuildHandshakeState() error: %+v", err) } cRandom := []byte{100, 101, 102, 103, 104, 105, 106, 107, 108, 109, @@ -80,25 +74,22 @@ func HttpGetExplicitRandom(hostname string, addr string) (string, error) { uTlsConn.SetClientRandom(cRandom) err = uTlsConn.Handshake() if err != nil { - return "", fmt.Errorf("uTlsConn.Handshake() error: %+v", err) + return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } // These fields are accessible regardless of setting client hello explicitly fmt.Printf("#> MasterSecret:\n%s", hex.Dump(uTlsConn.HandshakeState.MasterSecret)) fmt.Printf("#> ClientHello Random:\n%s", hex.Dump(uTlsConn.HandshakeState.Hello.Random)) fmt.Printf("#> ServerHello Random:\n%s", hex.Dump(uTlsConn.HandshakeState.ServerHello.Random)) - uTlsConn.Write([]byte("GET / HTTP/1.1\r\nHost: " + hostname + "\r\n\r\n")) - buf := make([]byte, 14096) - uTlsConn.Read(buf) - return string(buf), nil + return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) } // Note that the server will reject the fake ticket(unless you set up your server to accept them) and do full handshake -func HttpGetTicket(hostname string, addr string) (string, error) { +func HttpGetTicket(hostname string, addr string) (*http.Response, error) { config := tls.Config{ServerName: hostname} dialConn, err := net.DialTimeout("tcp", addr, dialTimeout) if err != nil { - return "", fmt.Errorf("net.DialTimeout error: %+v", err) + return nil, fmt.Errorf("net.DialTimeout error: %+v", err) } uTlsConn := tls.UClient(dialConn, &config, tls.HelloGolang) defer uTlsConn.Close() @@ -106,7 +97,7 @@ func HttpGetTicket(hostname string, addr string) (string, error) { err = uTlsConn.BuildHandshakeState() if err != nil { // have to call BuildHandshakeState() first, when using default UClient, to avoid settings' overwriting - return "", fmt.Errorf("uTlsConn.BuildHandshakeState() error: %+v", err) + return nil, fmt.Errorf("uTlsConn.BuildHandshakeState() error: %+v", err) } masterSecret := make([]byte, 48) @@ -118,27 +109,27 @@ func HttpGetTicket(hostname string, addr string) (string, error) { masterSecret, nil, nil) - uTlsConn.SetSessionState(sessionState) + err = uTlsConn.SetSessionState(sessionState) + if err != nil { + return nil, err + } err = uTlsConn.Handshake() if err != nil { - return "", fmt.Errorf("uTlsConn.Handshake() error: %+v", err) + return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } fmt.Println("#> This is how client hello with session ticket looked:") fmt.Print(hex.Dump(uTlsConn.HandshakeState.Hello.Raw)) - uTlsConn.Write([]byte("GET / HTTP/1.1\r\nHost: " + hostname + "\r\n\r\n")) - buf := make([]byte, 14096) - uTlsConn.Read(buf) - return string(buf), nil + return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) } // Note that the server will reject the fake ticket(unless you set up your server to accept them) and do full handshake -func HttpGetTicketHelloID(hostname string, addr string, helloID tls.ClientHelloID) (string, error) { +func HttpGetTicketHelloID(hostname string, addr string, helloID tls.ClientHelloID) (*http.Response, error) { config := tls.Config{ServerName: hostname} dialConn, err := net.DialTimeout("tcp", addr, dialTimeout) if err != nil { - return "", fmt.Errorf("net.DialTimeout error: %+v", err) + return nil, fmt.Errorf("net.DialTimeout error: %+v", err) } uTlsConn := tls.UClient(dialConn, &config, helloID) defer uTlsConn.Close() @@ -155,34 +146,36 @@ func HttpGetTicketHelloID(hostname string, addr string, helloID tls.ClientHelloI uTlsConn.SetSessionState(sessionState) err = uTlsConn.Handshake() if err != nil { - return "", fmt.Errorf("uTlsConn.Handshake() error: %+v", err) + return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } fmt.Println("#> This is how client hello with session ticket looked:") fmt.Print(hex.Dump(uTlsConn.HandshakeState.Hello.Raw)) - uTlsConn.Write([]byte("GET / HTTP/1.1\r\nHost: " + hostname + "\r\n\r\n")) - buf := make([]byte, 14096) - uTlsConn.Read(buf) - return string(buf), nil + return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) } -func HttpGetCustom(hostname string, addr string) (string, error) { +func HttpGetCustom(hostname string, addr string) (*http.Response, error) { config := tls.Config{ServerName: hostname} dialConn, err := net.DialTimeout("tcp", addr, dialTimeout) if err != nil { - return "", fmt.Errorf("net.DialTimeout error: %+v", err) + return nil, fmt.Errorf("net.DialTimeout error: %+v", err) } uTlsConn := tls.UClient(dialConn, &config, tls.HelloCustom) defer uTlsConn.Close() + // do not use this particular spec in production + // make sure to generate a separate copy of ClientHelloSpec for every connection spec := tls.ClientHelloSpec{ + TLSVersMax: tls.VersionTLS13, + TLSVersMin: tls.VersionTLS10, CipherSuites: []uint16{ tls.GREASE_PLACEHOLDER, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_AES_128_GCM_SHA256, // tls 1.3 tls.FAKE_TLS_DHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_CBC_SHA, @@ -190,9 +183,9 @@ func HttpGetCustom(hostname string, addr string) (string, error) { Extensions: []tls.TLSExtension{ &tls.SNIExtension{}, &tls.SupportedCurvesExtension{Curves: []tls.CurveID{tls.X25519, tls.CurveP256}}, - &tls.SupportedPointsExtension{SupportedPoints: []byte{0}}, + &tls.SupportedPointsExtension{SupportedPoints: []byte{0}}, // uncompressed &tls.SessionTicketExtension{}, - &tls.ALPNExtension{AlpnProtocols: []string{"myFancyProtocol", "h2", "http/1.1"}}, + &tls.ALPNExtension{AlpnProtocols: []string{"myFancyProtocol", "http/1.1"}}, &tls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []tls.SignatureScheme{ tls.ECDSAWithP256AndSHA256, tls.ECDSAWithP384AndSHA384, @@ -204,146 +197,163 @@ func HttpGetCustom(hostname string, addr string) (string, error) { tls.PKCS1WithSHA384, tls.PKCS1WithSHA512, tls.ECDSAWithSHA1, - tls.PKCS1WithSHA1}, - }, + tls.PKCS1WithSHA1}}, + &tls.KeyShareExtension{[]tls.KeyShare{ + {Group: tls.CurveID(tls.GREASE_PLACEHOLDER), Data: []byte{0}}, + {Group: tls.X25519}, + }}, + &tls.PSKKeyExchangeModesExtension{[]uint8{1}}, // pskModeDHE + &tls.SupportedVersionsExtension{[]uint16{ + tls.VersionTLS13, + tls.VersionTLS12, + tls.VersionTLS11, + tls.VersionTLS10}}, }, GetSessionID: nil, } err = uTlsConn.ApplyPreset(&spec) if err != nil { - return "", fmt.Errorf("uTlsConn.Handshake() error: %+v", err) + return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } err = uTlsConn.Handshake() if err != nil { - return "", fmt.Errorf("uTlsConn.Handshake() error: %+v", err) + return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } - uTlsConn.Write([]byte("GET / HTTP/1.1\r\nHost: " + hostname + "\r\n\r\n")) - buf := make([]byte, 14096) - uTlsConn.Read(buf) - return string(buf), nil + + return httpGetOverConn(uTlsConn, uTlsConn.HandshakeState.ServerHello.AlpnProtocol) } var roller *tls.Roller -func HttpGetGoogleWithRoller() (string, error) { +// this example creates a new roller for each function call, +// however it is advised to reuse the Roller +func HttpGetGoogleWithRoller() (*http.Response, error) { var err error - hostname := "www.google.com" if roller == nil { roller, err = tls.NewRoller() if err != nil { - return "", err + return nil, err } } // As of 2018-07-24 this tries to connect with Chrome, fails due to ChannelID extension // being selected by Google, but not supported by utls, and seamlessly moves on to either // Firefox or iOS fingerprints, which work. - c, err := roller.Dial("tcp4", hostname+":443", hostname) + c, err := roller.Dial("tcp4", requestHostname+":443", requestHostname) if err != nil { - return "", err - } - if c.ConnectionState().NegotiatedProtocol == "h2" { - t := http2.Transport{} - h2c, err := t.NewClientConn(c) - if err != nil { - return "", err - } - req, err := http.NewRequest("GET", "/", nil) - if err != nil { - return "", err - } - resp, err := h2c.RoundTrip(req) - if err != nil { - return "", err - } - respbytes, err := httputil.DumpResponse(resp, true) - if err != nil { - return "", err - } - return string(respbytes), nil - } else { - c.Write([]byte("GET / HTTP/1.1\r\nHost: " + hostname + "\r\n\r\n")) - buf := make([]byte, 14096) - c.Read(buf) - return string(buf), nil + return nil, err } + + return httpGetOverConn(c, c.HandshakeState.ServerHello.AlpnProtocol) } func main() { - var response string + var response *http.Response var err error - requestHostname := "tlsfingerprint.io" - requestAddr := "54.145.209.94:443" - response, err = HttpGetDefault(requestHostname, requestAddr) if err != nil { fmt.Printf("#> HttpGetDefault failed: %+v\n", err) } else { - fmt.Printf("#> HttpGetDefault response: %+s\n", getFirstLine(response)) + fmt.Printf("#> HttpGetDefault response: %+s\n", dumpResponseNoBody(response)) } response, err = HttpGetByHelloID(requestHostname, requestAddr, tls.HelloChrome_62) if err != nil { fmt.Printf("#> HttpGetByHelloID(HelloChrome_62) failed: %+v\n", err) } else { - fmt.Printf("#> HttpGetByHelloID(HelloChrome_62) response: %+s\n", getFirstLine(response)) + fmt.Printf("#> HttpGetByHelloID(HelloChrome_62) response: %+s\n", dumpResponseNoBody(response)) } response, err = HttpGetByHelloID(requestHostname, requestAddr, tls.HelloRandomizedNoALPN) if err != nil { fmt.Printf("#> HttpGetByHelloID(Randomized) failed: %+v\n", err) } else { - fmt.Printf("#> HttpGetByHelloID(Randomized) response: %+s\n", getFirstLine(response)) + fmt.Printf("#> HttpGetByHelloID(Randomized) response: %+s\n", dumpResponseNoBody(response)) } response, err = HttpGetExplicitRandom(requestHostname, requestAddr) if err != nil { fmt.Printf("#> HttpGetExplicitRandom failed: %+v\n", err) } else { - fmt.Printf("#> HttpGetExplicitRandom response: %+s\n", getFirstLine(response)) + fmt.Printf("#> HttpGetExplicitRandom response: %+s\n", dumpResponseNoBody(response)) } response, err = HttpGetTicket(requestHostname, requestAddr) if err != nil { fmt.Printf("#> HttpGetTicket failed: %+v\n", err) } else { - fmt.Printf("#> HttpGetTicket response: %+s\n", getFirstLine(response)) + fmt.Printf("#> HttpGetTicket response: %+s\n", dumpResponseNoBody(response)) } response, err = HttpGetTicketHelloID(requestHostname, requestAddr, tls.HelloFirefox_56) if err != nil { fmt.Printf("#> HttpGetTicketHelloID(HelloFirefox_56) failed: %+v\n", err) } else { - fmt.Printf("#> HttpGetTicketHelloID(HelloFirefox_56) response: %+s\n", getFirstLine(response)) + fmt.Printf("#> HttpGetTicketHelloID(HelloFirefox_56) response: %+s\n", dumpResponseNoBody(response)) } response, err = HttpGetCustom(requestHostname, requestAddr) if err != nil { fmt.Printf("#> HttpGetCustom() failed: %+v\n", err) } else { - fmt.Printf("#> HttpGetCustom() response: %+s\n", getFirstLine(response)) + fmt.Printf("#> HttpGetCustom() response: %+s\n", dumpResponseNoBody(response)) } for i := 0; i < 5; i++ { response, err = HttpGetGoogleWithRoller() if err != nil { - fmt.Printf("#> HttpGetGoogleWithRoller() failed: %+v\n", err) + fmt.Printf("#> HttpGetGoogleWithRoller() #%v failed: %+v\n", i, err) } else { - fmt.Printf("#> HttpGetGoogleWithRoller() response: %+s\n", getFirstLine(response)) + fmt.Printf("#> HttpGetGoogleWithRoller() #%v response: %+s\n", + i, dumpResponseNoBody(response)) } } return } -func getFirstLine(s string) string { - ss := strings.Split(s, "\r\n") - if len(ss) == 0 { - return "" - } else { - return ss[0] +func httpGetOverConn(conn net.Conn, alpn string) (*http.Response, error) { + req := &http.Request{ + Method: "GET", + URL: &url.URL{Host: "www." + requestHostname + "/"}, + Header: make(http.Header), + Host: "www." + requestHostname, + } + + switch alpn { + case "h2": + req.Proto = "HTTP/2.0" + req.ProtoMajor = 2 + req.ProtoMinor = 0 + + tr := http2.Transport{} + cConn, err := tr.NewClientConn(conn) + if err != nil { + return nil, err + } + return cConn.RoundTrip(req) + case "http/1.1", "": + req.Proto = "HTTP/1.1" + req.ProtoMajor = 1 + req.ProtoMinor = 1 + + err := req.Write(conn) + if err != nil { + return nil, err + } + return http.ReadResponse(bufio.NewReader(conn), req) + default: + return nil, fmt.Errorf("unsupported ALPN: %v", alpn) + } +} + +func dumpResponseNoBody(response *http.Response) string { + resp, err := httputil.DumpResponse(response, false) + if err != nil { + return fmt.Sprintf("failed to dump response: %v", err) } + return string(resp) }