package main import ( "bufio" // "crypto/tls" "encoding/base64" "errors" "fmt" "io" "log" "net" "net/http" "net/url" "os" "time" tls "github.com/refraction-networking/utls" "golang.org/x/net/http2" ) var ( dialTimeout = time.Duration(15) * time.Second ) // var requestHostname = "crypto.cloudflare.com" // speaks http2 and TLS 1.3 and ECH and PQ // var requestAddr = "crypto.cloudflare.com:443" // var requestPath = "/cdn-cgi/trace" var requestHostname = "tls-ech.dev" // speaks http2 and TLS 1.3 and ECH and PQ var requestAddr = "tls-ech.dev:443" var requestPath = "/" // var requestHostname = "defo.ie" // speaks http2 and TLS 1.3 and ECH and PQ // var requestAddr = "defo.ie:443" // var requestPath = "/ech-check.php" // var requestHostname = "client.tlsfingerprint.io" // speaks http2 and TLS 1.3 and ECH and PQ // var requestAddr = "client.tlsfingerprint.io:443" // var requestPath = "/" func HttpGetCustom(hostname string, addr string) (*http.Response, error) { klw, err := os.OpenFile("./sslkeylogging.log", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return nil, fmt.Errorf("os.OpenFile error: %+v", err) } echConf, err := base64.RawStdEncoding.DecodeString("AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA") if err != nil { return nil, err } config := tls.Config{ ServerName: hostname, KeyLogWriter: klw, EncryptedClientHelloConfigList: echConf, } dialConn, err := net.DialTimeout("tcp", addr, dialTimeout) if err != nil { return nil, fmt.Errorf("net.DialTimeout error: %+v", err) } uTlsConn := tls.UClient(dialConn, &config, tls.HelloGolang) // uTlsConn := tls.Client(dialConn, &config) defer uTlsConn.Close() // do not use this particular spec in production // make sure to generate a separate copy of ClientHelloSpec for every connection // spec, err := tls.UTLSIdToSpec(tls.HelloChrome_120) // // spec, err := tls.UTLSIdToSpec(tls.HelloFirefox_120) // if err != nil { // return nil, fmt.Errorf("tls.UTLSIdToSpec error: %+v", err) // } // err = uTlsConn.ApplyPreset(&spec) // if err != nil { // return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) // } err = uTlsConn.Handshake() if err != nil { return nil, fmt.Errorf("uTlsConn.Handshake() error: %+v", err) } return httpGetOverConn(uTlsConn, uTlsConn.ConnectionState().NegotiatedProtocol) } func httpGetOverConn(conn net.Conn, alpn string) (*http.Response, error) { req := &http.Request{ Method: "GET", URL: &url.URL{Scheme: "https", Host: requestHostname, Path: requestPath}, Header: make(http.Header), Host: requestHostname, } switch alpn { case "h2": log.Println("HTTP/2 enabled") 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", "": log.Println("Using 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 main() { resp, err := HttpGetCustom(requestHostname, requestAddr) if err != nil { panic(err) } fmt.Printf("Response: %+v\n", resp) // read from resp.Body body := make([]byte, 65535) n, err := resp.Body.Read(body) if err != nil && !errors.Is(err, io.EOF) { panic(err) } fmt.Printf("Body: %s\n", body[:n]) }