├── .gitignore ├── README.md ├── client.go ├── client_test.go ├── connect.go ├── go.mod ├── go.sum └── roundtripper.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # IntelliJ IDE Artifacts 18 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CClient 2 | 3 | Fixes TLS and stuff. Uses the yawning utls fork instead of the original refraction-networking one. 4 | 5 | # Example 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "log" 12 | 13 | tls "github.com/Carcraftz/utls" 14 | "github.com/Carcraftz/cclient" 15 | ) 16 | 17 | func main() { 18 | client, err := cclient.NewClient(tls.HelloChrome_Auto,"",true,6) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | resp, err := client.Get("https://www.google.com/") 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | resp.Body.Close() 28 | 29 | log.Println(resp.Status) 30 | } 31 | ``` 32 | 33 | # Notes 34 | 35 | If you experience any issues with the gitlab.com/yawning/utls import during installation, please try: `go get gitlab.com/yawning/utls`. Some path issue with go and gitlab ¯\\\_(ツ)\_/¯ 36 | 37 | The go.mod issue with git etc. was fixed by using my fork of the project with a change to the go.mod file instead of yawning's fork 38 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package cclient 2 | 3 | import ( 4 | "time" 5 | 6 | http "github.com/Carcraftz/fhttp" 7 | "github.com/Carcraftz/fhttp/cookiejar" 8 | 9 | "golang.org/x/net/proxy" 10 | 11 | utls "github.com/Carcraftz/utls" 12 | ) 13 | 14 | func NewClient(clientHello utls.ClientHelloID, proxyUrl string, allowRedirect bool, timeout time.Duration) (http.Client, error) { 15 | if len(proxyUrl) > 0 { 16 | dialer, err := newConnectDialer(proxyUrl) 17 | if err != nil { 18 | if allowRedirect { 19 | cJar, _ := cookiejar.New(nil) 20 | return http.Client{ 21 | Jar: cJar, 22 | Timeout: time.Second * timeout, 23 | }, err 24 | } 25 | cJar, _ := cookiejar.New(nil) 26 | return http.Client{ 27 | Jar: cJar, 28 | Timeout: time.Second * timeout, 29 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 30 | return http.ErrUseLastResponse 31 | }, 32 | }, err 33 | } 34 | if allowRedirect { 35 | cJar, _ := cookiejar.New(nil) 36 | return http.Client{ 37 | Jar: cJar, 38 | Transport: newRoundTripper(clientHello, dialer), 39 | Timeout: time.Second * timeout, 40 | }, nil 41 | } 42 | cJar, _ := cookiejar.New(nil) 43 | return http.Client{ 44 | Jar: cJar, 45 | Transport: newRoundTripper(clientHello, dialer), 46 | Timeout: time.Second * timeout, 47 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 48 | return http.ErrUseLastResponse 49 | }, 50 | }, nil 51 | } else { 52 | if allowRedirect { 53 | cJar, _ := cookiejar.New(nil) 54 | return http.Client{ 55 | Jar: cJar, 56 | Transport: newRoundTripper(clientHello, proxy.Direct), 57 | Timeout: time.Second * timeout, 58 | }, nil 59 | } 60 | cJar, _ := cookiejar.New(nil) 61 | return http.Client{ 62 | Jar: cJar, 63 | Transport: newRoundTripper(clientHello, proxy.Direct), 64 | Timeout: time.Second * timeout, 65 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 66 | return http.ErrUseLastResponse 67 | }, 68 | }, nil 69 | 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package cclient 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "io/ioutil" 7 | "testing" 8 | 9 | tls "github.com/Carcraftz/utls" 10 | ) 11 | 12 | type JA3Response struct { 13 | JA3Hash string `json:"ja3_hash"` 14 | JA3 string `json:"ja3"` 15 | UserAgent string `json:"User-Agent"` 16 | } 17 | 18 | func readAndClose(r io.ReadCloser) ([]byte, error) { 19 | readBytes, err := ioutil.ReadAll(r) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return readBytes, r.Close() 24 | } 25 | 26 | const Chrome83Hash = "b32309a26951912be7dba376398abc3b" 27 | 28 | var client, _ = NewClient(tls.HelloChrome_83) // cannot throw an error because there is no proxy 29 | 30 | func TestCClient_JA3(t *testing.T) { 31 | resp, err := client.Get("https://ja3er.com/json") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | respBody, err := readAndClose(resp.Body) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | var ja3Response JA3Response 41 | if err := json.Unmarshal(respBody, &ja3Response); err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | if ja3Response.JA3Hash != Chrome83Hash { 46 | t.Error("unexpected JA3 hash; expected:", Chrome83Hash, "| got:", ja3Response.JA3Hash) 47 | } 48 | } 49 | 50 | func TestCClient_HTTP2(t *testing.T) { 51 | resp, err := client.Get("https://www.google.com") 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | _, err = readAndClose(resp.Body) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | if resp.ProtoMajor != 2 || resp.ProtoMinor != 0 { 62 | t.Error("unexpected response proto; expected: HTTP/2.0 | got: ", resp.Proto) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /connect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // stolen from https://github.com/caddyserver/forwardproxy/blob/master/httpclient/httpclient.go 16 | package cclient 17 | 18 | import ( 19 | "bufio" 20 | "context" 21 | "crypto/tls" 22 | "encoding/base64" 23 | "errors" 24 | "io" 25 | "net" 26 | "net/url" 27 | "sync" 28 | 29 | http "github.com/Carcraftz/fhttp" 30 | "golang.org/x/net/proxy" 31 | 32 | "github.com/Carcraftz/fhttp/http2" 33 | ) 34 | 35 | // connectDialer allows to configure one-time use HTTP CONNECT client 36 | type connectDialer struct { 37 | ProxyUrl url.URL 38 | DefaultHeader http.Header 39 | 40 | Dialer net.Dialer // overridden dialer allow to control establishment of TCP connection 41 | 42 | // overridden DialTLS allows user to control establishment of TLS connection 43 | // MUST return connection with completed Handshake, and NegotiatedProtocol 44 | DialTLS func(network string, address string) (net.Conn, string, error) 45 | 46 | EnableH2ConnReuse bool 47 | cacheH2Mu sync.Mutex 48 | cachedH2ClientConn *http2.ClientConn 49 | cachedH2RawConn net.Conn 50 | } 51 | 52 | // newConnectDialer creates a dialer to issue CONNECT requests and tunnel traffic via HTTP/S proxy. 53 | // proxyUrlStr must provide Scheme and Host, may provide credentials and port. 54 | // Example: https://username:password@golang.org:443 55 | func newConnectDialer(proxyUrlStr string) (proxy.ContextDialer, error) { 56 | proxyUrl, err := url.Parse(proxyUrlStr) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | if proxyUrl.Host == "" { 62 | return nil, errors.New("invalid url `" + proxyUrlStr + 63 | "`, make sure to specify full url like https://username:password@hostname.com:443/") 64 | } 65 | 66 | switch proxyUrl.Scheme { 67 | case "http": 68 | if proxyUrl.Port() == "" { 69 | proxyUrl.Host = net.JoinHostPort(proxyUrl.Host, "80") 70 | } 71 | case "https": 72 | if proxyUrl.Port() == "" { 73 | proxyUrl.Host = net.JoinHostPort(proxyUrl.Host, "443") 74 | } 75 | case "": 76 | return nil, errors.New("specify scheme explicitly (https://)") 77 | default: 78 | return nil, errors.New("scheme " + proxyUrl.Scheme + " is not supported") 79 | } 80 | 81 | client := &connectDialer{ 82 | ProxyUrl: *proxyUrl, 83 | DefaultHeader: make(http.Header), 84 | EnableH2ConnReuse: true, 85 | } 86 | 87 | if proxyUrl.User != nil { 88 | if proxyUrl.User.Username() != "" { 89 | password, _ := proxyUrl.User.Password() 90 | client.DefaultHeader.Set("Proxy-Authorization", "Basic "+ 91 | base64.StdEncoding.EncodeToString([]byte(proxyUrl.User.Username()+":"+password))) 92 | } 93 | } 94 | return client, nil 95 | } 96 | 97 | func (c *connectDialer) Dial(network, address string) (net.Conn, error) { 98 | return c.DialContext(context.Background(), network, address) 99 | } 100 | 101 | // Users of context.WithValue should define their own types for keys 102 | type ContextKeyHeader struct{} 103 | 104 | // ctx.Value will be inspected for optional ContextKeyHeader{} key, with `http.Header` value, 105 | // which will be added to outgoing request headers, overriding any colliding c.DefaultHeader 106 | func (c *connectDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { 107 | req := (&http.Request{ 108 | Method: "CONNECT", 109 | URL: &url.URL{Host: address}, 110 | Header: make(http.Header), 111 | Host: address, 112 | }).WithContext(ctx) 113 | for k, v := range c.DefaultHeader { 114 | req.Header[k] = v 115 | } 116 | if ctxHeader, ctxHasHeader := ctx.Value(ContextKeyHeader{}).(http.Header); ctxHasHeader { 117 | for k, v := range ctxHeader { 118 | req.Header[k] = v 119 | } 120 | } 121 | 122 | connectHttp2 := func(rawConn net.Conn, h2clientConn *http2.ClientConn) (net.Conn, error) { 123 | req.Proto = "HTTP/2.0" 124 | req.ProtoMajor = 2 125 | req.ProtoMinor = 0 126 | pr, pw := io.Pipe() 127 | req.Body = pr 128 | 129 | resp, err := h2clientConn.RoundTrip(req) 130 | if err != nil { 131 | _ = rawConn.Close() 132 | return nil, err 133 | } 134 | 135 | if resp.StatusCode != http.StatusOK { 136 | _ = rawConn.Close() 137 | return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) 138 | } 139 | return newHttp2Conn(rawConn, pw, resp.Body), nil 140 | } 141 | 142 | connectHttp1 := func(rawConn net.Conn) (net.Conn, error) { 143 | req.Proto = "HTTP/1.1" 144 | req.ProtoMajor = 1 145 | req.ProtoMinor = 1 146 | 147 | err := req.Write(rawConn) 148 | if err != nil { 149 | _ = rawConn.Close() 150 | return nil, err 151 | } 152 | 153 | resp, err := http.ReadResponse(bufio.NewReader(rawConn), req) 154 | if err != nil { 155 | _ = rawConn.Close() 156 | return nil, err 157 | } 158 | 159 | if resp.StatusCode != http.StatusOK { 160 | _ = rawConn.Close() 161 | return nil, errors.New("Proxy responded with non 200 code: " + resp.Status) 162 | } 163 | return rawConn, nil 164 | } 165 | 166 | if c.EnableH2ConnReuse { 167 | c.cacheH2Mu.Lock() 168 | unlocked := false 169 | if c.cachedH2ClientConn != nil && c.cachedH2RawConn != nil { 170 | if c.cachedH2ClientConn.CanTakeNewRequest() { 171 | rc := c.cachedH2RawConn 172 | cc := c.cachedH2ClientConn 173 | c.cacheH2Mu.Unlock() 174 | unlocked = true 175 | proxyConn, err := connectHttp2(rc, cc) 176 | if err == nil { 177 | return proxyConn, err 178 | } 179 | // else: carry on and try again 180 | } 181 | } 182 | if !unlocked { 183 | c.cacheH2Mu.Unlock() 184 | } 185 | } 186 | 187 | var err error 188 | var rawConn net.Conn 189 | negotiatedProtocol := "" 190 | switch c.ProxyUrl.Scheme { 191 | case "http": 192 | rawConn, err = c.Dialer.DialContext(ctx, network, c.ProxyUrl.Host) 193 | if err != nil { 194 | return nil, err 195 | } 196 | case "https": 197 | if c.DialTLS != nil { 198 | rawConn, negotiatedProtocol, err = c.DialTLS(network, c.ProxyUrl.Host) 199 | if err != nil { 200 | return nil, err 201 | } 202 | } else { 203 | tlsConf := tls.Config{ 204 | NextProtos: []string{"h2", "http/1.1"}, 205 | ServerName: c.ProxyUrl.Hostname(), 206 | } 207 | tlsConn, err := tls.Dial(network, c.ProxyUrl.Host, &tlsConf) 208 | if err != nil { 209 | return nil, err 210 | } 211 | err = tlsConn.Handshake() 212 | if err != nil { 213 | return nil, err 214 | } 215 | negotiatedProtocol = tlsConn.ConnectionState().NegotiatedProtocol 216 | rawConn = tlsConn 217 | } 218 | default: 219 | return nil, errors.New("scheme " + c.ProxyUrl.Scheme + " is not supported") 220 | } 221 | 222 | switch negotiatedProtocol { 223 | case "": 224 | fallthrough 225 | case "http/1.1": 226 | return connectHttp1(rawConn) 227 | case "h2": 228 | t := http2.Transport{} 229 | h2clientConn, err := t.NewClientConn(rawConn) 230 | if err != nil { 231 | _ = rawConn.Close() 232 | return nil, err 233 | } 234 | 235 | proxyConn, err := connectHttp2(rawConn, h2clientConn) 236 | if err != nil { 237 | _ = rawConn.Close() 238 | return nil, err 239 | } 240 | if c.EnableH2ConnReuse { 241 | c.cacheH2Mu.Lock() 242 | c.cachedH2ClientConn = h2clientConn 243 | c.cachedH2RawConn = rawConn 244 | c.cacheH2Mu.Unlock() 245 | } 246 | return proxyConn, err 247 | default: 248 | _ = rawConn.Close() 249 | return nil, errors.New("negotiated unsupported application layer protocol: " + 250 | negotiatedProtocol) 251 | } 252 | } 253 | 254 | func newHttp2Conn(c net.Conn, pipedReqBody *io.PipeWriter, respBody io.ReadCloser) net.Conn { 255 | return &http2Conn{Conn: c, in: pipedReqBody, out: respBody} 256 | } 257 | 258 | type http2Conn struct { 259 | net.Conn 260 | in *io.PipeWriter 261 | out io.ReadCloser 262 | } 263 | 264 | func (h *http2Conn) Read(p []byte) (n int, err error) { 265 | return h.out.Read(p) 266 | } 267 | 268 | func (h *http2Conn) Write(p []byte) (n int, err error) { 269 | return h.in.Write(p) 270 | } 271 | 272 | func (h *http2Conn) Close() error { 273 | var retErr error = nil 274 | if err := h.in.Close(); err != nil { 275 | retErr = err 276 | } 277 | if err := h.out.Close(); err != nil { 278 | retErr = err 279 | } 280 | return retErr 281 | } 282 | 283 | func (h *http2Conn) CloseConn() error { 284 | return h.Conn.Close() 285 | } 286 | 287 | func (h *http2Conn) CloseWrite() error { 288 | return h.in.Close() 289 | } 290 | 291 | func (h *http2Conn) CloseRead() error { 292 | return h.out.Close() 293 | } 294 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Carcraftz/cclient 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Carcraftz/utls v0.0.0-20210907185630-32782f880d54 // indirect 7 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect 8 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777 9 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect 10 | golang.org/x/text v0.3.5 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | git.schwanenlied.me/yawning/bsaes.git v0.0.0-20190320102049-26d1add596b6 h1:zOrl5/RvK48MxMrif6Z+/OpuYyRnvB+ZTrQWEV9VYb0= 2 | git.schwanenlied.me/yawning/bsaes.git v0.0.0-20190320102049-26d1add596b6/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo= 3 | github.com/Carcraftz/utls v0.0.0-20210907185630-32782f880d54 h1:V/84tXJdc3wi5/kBU4iVNCxUU+XC3+wSGKFf/tRG+k4= 4 | github.com/Carcraftz/utls v0.0.0-20210907185630-32782f880d54/go.mod h1:YMKKxFhs/MzFaQP80rFaWsO78e/pYjtjgrlCbu5Rpps= 5 | github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= 6 | github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= 7 | github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= 8 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 9 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 10 | github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= 11 | gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= 12 | gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= 13 | gitlab.com/yawning/utls.git v0.0.11-1 h1:cQLJ4sN+u07Rn9M7RpvCEWUPndtrsQsy6U3ZaEHeDvI= 14 | gitlab.com/yawning/utls.git v0.0.11-1/go.mod h1:eYdrOOCoedNc3xw50kJ/s8JquyxeS5kr3vkFZFPTI9w= 15 | gitlab.com/yawning/utls.git v0.0.12-1 h1:RL6O0MP2YI0KghuEU/uGN6+8b4183eqNWoYgx7CXD0U= 16 | gitlab.com/yawning/utls.git v0.0.12-1/go.mod h1:3ONKiSFR9Im/c3t5RKmMJTVdmZN496FNyk3mjrY1dyo= 17 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 18 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 19 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= 20 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 21 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 22 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 23 | golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 24 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 25 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= 26 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 27 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= 28 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 31 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y= 36 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= 39 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 42 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 43 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 44 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 45 | golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= 46 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 47 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 48 | -------------------------------------------------------------------------------- /roundtripper.go: -------------------------------------------------------------------------------- 1 | package cclient 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strings" 9 | "sync" 10 | 11 | http "github.com/Carcraftz/fhttp" 12 | 13 | "github.com/Carcraftz/fhttp/http2" 14 | "golang.org/x/net/proxy" 15 | 16 | utls "github.com/Carcraftz/utls" 17 | ) 18 | 19 | var errProtocolNegotiated = errors.New("protocol negotiated") 20 | 21 | type roundTripper struct { 22 | sync.Mutex 23 | 24 | clientHelloId utls.ClientHelloID 25 | 26 | cachedConnections map[string]net.Conn 27 | cachedTransports map[string]http.RoundTripper 28 | 29 | dialer proxy.ContextDialer 30 | } 31 | 32 | func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 33 | addr := rt.getDialTLSAddr(req) 34 | if _, ok := rt.cachedTransports[addr]; !ok { 35 | if err := rt.getTransport(req, addr); err != nil { 36 | return nil, err 37 | } 38 | } 39 | return rt.cachedTransports[addr].RoundTrip(req) 40 | } 41 | 42 | func (rt *roundTripper) getTransport(req *http.Request, addr string) error { 43 | switch strings.ToLower(req.URL.Scheme) { 44 | case "http": 45 | rt.cachedTransports[addr] = &http.Transport{DialContext: rt.dialer.DialContext} 46 | return nil 47 | case "https": 48 | default: 49 | return fmt.Errorf("invalid URL scheme: [%v]", req.URL.Scheme) 50 | } 51 | 52 | _, err := rt.dialTLS(context.Background(), "tcp", addr) 53 | switch err { 54 | case errProtocolNegotiated: 55 | case nil: 56 | // Should never happen. 57 | panic("dialTLS returned no error when determining cachedTransports") 58 | default: 59 | return err 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (rt *roundTripper) dialTLS(ctx context.Context, network, addr string) (net.Conn, error) { 66 | rt.Lock() 67 | defer rt.Unlock() 68 | 69 | // If we have the connection from when we determined the HTTPS 70 | // cachedTransports to use, return that. 71 | if conn := rt.cachedConnections[addr]; conn != nil { 72 | delete(rt.cachedConnections, addr) 73 | return conn, nil 74 | } 75 | 76 | rawConn, err := rt.dialer.DialContext(ctx, network, addr) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | var host string 82 | if host, _, err = net.SplitHostPort(addr); err != nil { 83 | host = addr 84 | } 85 | 86 | conn := utls.UClient(rawConn, &utls.Config{ServerName: host}, rt.clientHelloId) 87 | if err = conn.Handshake(); err != nil { 88 | _ = conn.Close() 89 | return nil, err 90 | } 91 | 92 | if rt.cachedTransports[addr] != nil { 93 | return conn, nil 94 | } 95 | 96 | // No http.Transport constructed yet, create one based on the results 97 | // of ALPN. 98 | switch conn.ConnectionState().NegotiatedProtocol { 99 | case http2.NextProtoTLS: 100 | t2 := http2.Transport{DialTLS: rt.dialTLSHTTP2} 101 | t2.Settings = []http2.Setting{ 102 | {ID: http2.SettingMaxConcurrentStreams, Val: 1000}, 103 | {ID: http2.SettingMaxFrameSize, Val: 16384}, 104 | {ID: http2.SettingMaxHeaderListSize, Val: 262144}, 105 | } 106 | t2.InitialWindowSize = 6291456 107 | t2.HeaderTableSize = 65536 108 | t2.PushHandler = &http2.DefaultPushHandler{} 109 | rt.cachedTransports[addr] = &t2 110 | default: 111 | // Assume the remote peer is speaking HTTP 1.x + TLS. 112 | rt.cachedTransports[addr] = &http.Transport{DialTLSContext: rt.dialTLS} 113 | } 114 | 115 | // Stash the connection just established for use servicing the 116 | // actual request (should be near-immediate). 117 | rt.cachedConnections[addr] = conn 118 | 119 | return nil, errProtocolNegotiated 120 | } 121 | 122 | func (rt *roundTripper) dialTLSHTTP2(network, addr string, _ *utls.Config) (net.Conn, error) { 123 | return rt.dialTLS(context.Background(), network, addr) 124 | } 125 | 126 | func (rt *roundTripper) getDialTLSAddr(req *http.Request) string { 127 | host, port, err := net.SplitHostPort(req.URL.Host) 128 | if err == nil { 129 | return net.JoinHostPort(host, port) 130 | } 131 | return net.JoinHostPort(req.URL.Host, "443") // we can assume port is 443 at this point 132 | } 133 | 134 | func newRoundTripper(clientHello utls.ClientHelloID, dialer ...proxy.ContextDialer) http.RoundTripper { 135 | if len(dialer) > 0 { 136 | return &roundTripper{ 137 | dialer: dialer[0], 138 | 139 | clientHelloId: clientHello, 140 | 141 | cachedTransports: make(map[string]http.RoundTripper), 142 | cachedConnections: make(map[string]net.Conn), 143 | } 144 | } else { 145 | return &roundTripper{ 146 | dialer: proxy.Direct, 147 | 148 | clientHelloId: clientHello, 149 | 150 | cachedTransports: make(map[string]http.RoundTripper), 151 | cachedConnections: make(map[string]net.Conn), 152 | } 153 | } 154 | } 155 | --------------------------------------------------------------------------------