diff --git a/README.md b/README.md index 435f476..cbf1ecd 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # h2cli -Simple http 2.0 (h2) client in go +Simple http 2.0 (h2) client in go including h2c (http2 over clear text, not requiring TLS) ## Installation @@ -33,12 +33,12 @@ flags: Standard ``` -$ go run . +$ go run . -url https://debug.fortio.org 12:50:58 I GET on https://debug.fortio.org 12:50:58 I Response code 200, proto HTTP/2.0, size 366 Φορτίο version 1.50.1 h1:5FSttAHQsyAsi3dzxDmSByfzDYByrWY/yw53bqOg+Kc= go1.19.6 amd64 linux (in fortio.org/proxy 1.10.0) Debug server on ol1 up for 15h38m51.4s -Request from [2600:1700:1151:b24f:41e5:48a:2b96:711d]:56772 https TLS_AES_128_GCM_SHA256 +Request from [2600:1700:xxx]:56772 https TLS_AES_128_GCM_SHA256 GET / HTTP/2.0 @@ -97,3 +97,42 @@ User-Agent: Go-http-client/2.0 body: ``` + +### H2C examples + +h2c against debug.fortio.org is the new default for `go run .` +``` +% go run . +15:28:59 I h2c GET on http://debug.fortio.org +15:29:00 I Response code 200, proto HTTP/2.0, size 334 +Φορτίο version 1.52.0 h1:xHVOXkR3k5V5DvVM7/byfIHff3ia613qunnm+7O0EuQ= go1.19.6 arm64 linux (in fortio.org/proxy 1.11.1) +Debug server on a1 up for 19h4m25s +Request from [2600:1700:xxx]:52833 + +GET / HTTP/2.0 + +headers: + +Host: debug.fortio.org +Accept-Encoding: gzip +User-Agent: Go-http-client/2.0 + +body: +``` + +``` +% go run . -url localhost:8080/debug +15:31:16 I h2c GET on http://localhost:8080/debug +15:31:16 I Response code 200, proto HTTP/2.0, size 245 +Φορτίο version dev go1.19.6 arm64 darwin echo debug server up for 26m39.3s on MacBook-Air.local - request from [::1]:53010 + +GET /debug HTTP/2.0 + +headers: + +Host: localhost:8080 +Accept-Encoding: gzip +User-Agent: Go-http-client/2.0 + +body: +``` diff --git a/go.mod b/go.mod index 2ee4c11..3de6064 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,12 @@ module github.com/fortio/h2cli go 1.19 require ( - fortio.org/cli v1.0.0 - fortio.org/log v1.2.2 + fortio.org/cli v1.1.0 + fortio.org/log v1.3.0 + golang.org/x/net v0.7.0 ) -require fortio.org/version v1.0.2 // indirect +require ( + fortio.org/version v1.0.2 // indirect + golang.org/x/text v0.7.0 // indirect +) diff --git a/go.sum b/go.sum index 2fe19a5..b9e857b 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ -fortio.org/cli v1.0.0 h1:qUyHr1DiyxFoq5RBPFJ6Hw30NcV/0KYSxmptdiLiW5I= -fortio.org/cli v1.0.0/go.mod h1:O3nVImKwJSvHKbMYWkqMbEagAXCS1xvSv1YbHwkKJjY= -fortio.org/log v1.2.2 h1:vs42JjNwiqbMbacittZjJE9+oi72Za6aekML9gKmILg= -fortio.org/log v1.2.2/go.mod h1:u/8/2lyczXq52aT5Nw6reD+3cR6m/EbS2jBiIYhgiTU= +fortio.org/cli v1.1.0 h1:ATIxi7DgA7WAexUCF8p5a0qlGYk48ZgkwSEDrvwXeN4= +fortio.org/cli v1.1.0/go.mod h1:O3nVImKwJSvHKbMYWkqMbEagAXCS1xvSv1YbHwkKJjY= +fortio.org/log v1.3.0 h1:bESPvuQGKejw7rrx41Sg3GoF+tsrB7oC08PxBs5/AM0= +fortio.org/log v1.3.0/go.mod h1:u/8/2lyczXq52aT5Nw6reD+3cR6m/EbS2jBiIYhgiTU= fortio.org/version v1.0.2 h1:8NwxdX58aoeKx7T5xAPO0xlUu1Hpk42nRz5s6e6eKZ0= fortio.org/version v1.0.2/go.mod h1:2JQp9Ax+tm6QKiGuzR5nJY63kFeANcgrZ0osoQFDVm0= +golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= diff --git a/h2cli.go b/h2cli.go index b50e76e..6c10fee 100644 --- a/h2cli.go +++ b/h2cli.go @@ -6,18 +6,22 @@ import ( "crypto/x509" "flag" "io" + "net" "net/http" + "net/url" "os" + "strings" "fortio.org/cli" "fortio.org/log" + "golang.org/x/net/http2" ) var ( - h2 = flag.Bool("h2", true, "use HTTP2") - url = flag.String("url", "https://debug.fortio.org", "URL to fetch") - method = flag.String("method", "GET", "HTTP method to use") - caCert = flag.String("cacert", "", + h2 = flag.Bool("h2", true, "use HTTP2") + urlFlag = flag.String("url", "http://debug.fortio.org", "URL to fetch") + method = flag.String("method", "GET", "HTTP method to use") + caCert = flag.String("cacert", "", "`Path` to a custom CA certificate file instead standard internet/system CAs") ) @@ -36,20 +40,45 @@ func main() { caCertPool.AppendCertsFromPEM(ca) tlsConfig.RootCAs = caCertPool } - - client.Transport = &http.Transport{ - TLSClientConfig: tlsConfig, - ForceAttemptHTTP2: *h2, // could also use &http2.Transport{TLSClientConfig: tlsConfig} but that's not necessary to get h2 + lu := strings.ToLower(*urlFlag) + if !strings.HasPrefix(lu, "https://") && !strings.HasPrefix(lu, "http://") { + // be nice and add http:// if missing + log.LogVf("Adding http:// to url %q", *urlFlag) + *urlFlag = "http://" + *urlFlag + } + u, err := url.Parse(*urlFlag) + if err != nil { + log.Fatalf("Failed to parse url %q: %v", *urlFlag, err) + } + h2c := "" + if u.Scheme == "https" || !*h2 { + // This will do h2 over tls if the server supports it but not h2c + // but with TLS all is good in the standard package, with ForceAttemptHTTP2 + client.Transport = &http.Transport{ + TLSClientConfig: tlsConfig, // only used if url is https + ForceAttemptHTTP2: *h2, // *h2 for h2 over tls + } + } else { + // h2c + h2c = "h2c " + client.Transport = &http2.Transport{ + AllowHTTP: true, // to get h2c + // Trick to get h2c without TLS: + // thanks to https://github.com/thrawn01/h2c-golang-example/blob/master/README.md + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + } } - log.Infof("%s on %s", *method, *url) + log.Infof("%s%s on %s", h2c, *method, *urlFlag) // Perform the request - req, err := http.NewRequestWithContext(context.Background(), *method, *url, nil) + req, err := http.NewRequestWithContext(context.Background(), *method, *urlFlag, nil) if err != nil { - log.Fatalf("Request method %q url %q error: %v", *method, *url, err) + log.Fatalf("Request method %q url %q error: %v", *method, *urlFlag, err) } resp, err := client.Do(req) if err != nil { - log.Fatalf("Failed %q %q - error: %v", *method, *url, err) + log.Fatalf("Failed %q %q - error: %v", *method, *urlFlag, err) } body, err := io.ReadAll(resp.Body) resp.Body.Close()