Closed ldemailly closed 2 years ago
Forgot to mention that this is with
curl 7.83.0 (aarch64-apple-darwin21.3.0) libcurl/7.83.0 (SecureTransport) OpenSSL/1.1.1o zlib/1.2.11 brotli/1.0.9 zstd/1.5.2 libidn2/2.3.2 libssh2/1.10.0 nghttp2/1.47.0 librtmp/2.3 OpenLDAP/2.6.2
Release-Date: 2022-04-27
Protocols: dict file ftp ftps gopher gophers http https imap imaps ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTPS-proxy IDN IPv6 Kerberos Largefile libz MultiSSL NTLM NTLM_WB SPNEGO SSL TLS-SRP UnixSockets zstd
and just rechecked, same with go1.18.2
just the error is only on 2nd write:
H2c Server starting on :8001
Got POST HTTP/1.1 request from 127.0.0.1:57528
read 3 from body
2022/05/12 17:29:16 http: response.Write on hijacked connection from fmt.Fprintf (print.go:205)
Code is also available on https://github.com/fortio/h2-bug
cc @neild
Thanks! Note that the same happens when trying to use ReverseProxy as well (which is how I went down this path:)
curl/7.83.0/bin/curl -v --http2 -d foo "localhost:8001/echo?delay=1s"
Server GODEBUG=http2debug=2 go run .
2022/05/14 14:21:57 http2: Framer 0x1400014e1c0: wrote SETTINGS len=18, settings: MAX_CONCURRENT_STREAMS=100, INITIAL_WINDOW_SIZE=33554432, ENABLE_PUSH=0
2022/05/14 14:21:57 http2: Framer 0x1400014e1c0: wrote HEADERS flags=END_HEADERS stream=1 len=74
web req: /echo?delay=1s
2022/05/14 14:21:58 http: response.WriteHeader on hijacked connection from net/http/httputil.(*ReverseProxy).ServeHTTP (reverseproxy.go:337)
2022/05/14 14:21:58 http: response.Write on hijacked connection from net/http/httputil.(*ReverseProxy).copyBuffer (reverseproxy.go:467)
httpURL, _ := url.Parse("http://localhost:8080/")
httpProxy := httputil.NewSingleHostReverseProxy(httpURL)
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("web req: %v\n", r.URL)
httpProxy.ServeHTTP(w, r)
})
h2s := &http2.Server{}
srv := &http.Server{
Addr: ":8001",
Handler: h2c.NewHandler(mux, h2s),
}
//http2.ConfigureServer(srv, nil)
log.Fatal(srv.ListenAndServe())
curl side logs (failing upgrade:):
/opt/homebrew/Cellar/curl/7.83.0/bin/curl --trace - --trace-time -m 2 --http2 -d foo "localhost:8001/echo?delay=1s"
14:34:15.830214 == Info: Trying 127.0.0.1:8001...
14:34:15.830736 == Info: Connected to localhost (127.0.0.1) port 8001 (#0)
14:34:15.830800 => Send header, 253 bytes (0xfd)
0000: 50 4f 53 54 20 2f 65 63 68 6f 3f 64 65 6c 61 79 POST /echo?delay
0010: 3d 31 73 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f =1s HTTP/1.1..Ho
0020: 73 74 3a 20 6c 6f 63 61 6c 68 6f 73 74 3a 38 30 st: localhost:80
0030: 30 31 0d 0a 55 73 65 72 2d 41 67 65 6e 74 3a 20 01..User-Agent:
0040: 63 75 72 6c 2f 37 2e 38 33 2e 30 0d 0a 41 63 63 curl/7.83.0..Acc
0050: 65 70 74 3a 20 2a 2f 2a 0d 0a 43 6f 6e 6e 65 63 ept: */*..Connec
0060: 74 69 6f 6e 3a 20 55 70 67 72 61 64 65 2c 20 48 tion: Upgrade, H
0070: 54 54 50 32 2d 53 65 74 74 69 6e 67 73 0d 0a 55 TTP2-Settings..U
0080: 70 67 72 61 64 65 3a 20 68 32 63 0d 0a 48 54 54 pgrade: h2c..HTT
0090: 50 32 2d 53 65 74 74 69 6e 67 73 3a 20 41 41 4d P2-Settings: AAM
00a0: 41 41 41 42 6b 41 41 51 43 41 41 41 41 41 41 49 AAABkAAQCAAAAAAI
00b0: 41 41 41 41 41 0d 0a 43 6f 6e 74 65 6e 74 2d 4c AAAAA..Content-L
00c0: 65 6e 67 74 68 3a 20 33 0d 0a 43 6f 6e 74 65 6e ength: 3..Conten
00d0: 74 2d 54 79 70 65 3a 20 61 70 70 6c 69 63 61 74 t-Type: applicat
00e0: 69 6f 6e 2f 78 2d 77 77 77 2d 66 6f 72 6d 2d 75 ion/x-www-form-u
00f0: 72 6c 65 6e 63 6f 64 65 64 0d 0a 0d 0a rlencoded....
14:34:15.830909 => Send data, 3 bytes (0x3)
0000: 66 6f 6f foo
14:34:15.831048 == Info: Mark bundle as not supporting multiuse
14:34:15.831057 <= Recv header, 34 bytes (0x22)
0000: 48 54 54 50 2f 31 2e 31 20 31 30 31 20 53 77 69 HTTP/1.1 101 Swi
0010: 74 63 68 69 6e 67 20 50 72 6f 74 6f 63 6f 6c 73 tching Protocols
0020: 0d 0a ..
14:34:15.831078 <= Recv header, 21 bytes (0x15)
0000: 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 55 70 67 72 Connection: Upgr
0010: 61 64 65 0d 0a ade..
14:34:15.831093 <= Recv header, 14 bytes (0xe)
0000: 55 70 67 72 61 64 65 3a 20 68 32 63 0d 0a Upgrade: h2c..
14:34:15.831103 == Info: Received 101
14:34:15.831118 == Info: Using HTTP2, server supports multiplexing
14:34:15.831131 == Info: Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
14:34:17.831253 == Info: Operation timed out after 2003 milliseconds with 0 bytes received
14:34:17.831512 == Info: Connection #0 to host localhost left intact
curl: (28) Operation timed out after 2003 milliseconds with 0 bytes received
prior knowledge (working):
dl@Laurents-Air fortio % /opt/homebrew/Cellar/curl/7.83.0/bin/curl --trace - --trace-time -m 2 --http2-prior-knowledge -d foo "localhost:8001/echo?delay=1s"
14:35:43.087749 == Info: Trying 127.0.0.1:8001...
14:35:43.088298 == Info: Connected to localhost (127.0.0.1) port 8001 (#0)
14:35:43.088327 == Info: Using HTTP2, server supports multiplexing
14:35:43.088338 == Info: Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
14:35:43.088396 == Info: h2h3 [:method: POST]
14:35:43.088402 == Info: h2h3 [:path: /echo?delay=1s]
14:35:43.088407 == Info: h2h3 [:scheme: http]
14:35:43.088412 == Info: h2h3 [:authority: localhost:8001]
14:35:43.088417 == Info: h2h3 [user-agent: curl/7.83.0]
14:35:43.088421 == Info: h2h3 [accept: */*]
14:35:43.088426 == Info: h2h3 [content-length: 3]
14:35:43.088430 == Info: h2h3 [content-type: application/x-www-form-urlencoded]
14:35:43.088442 == Info: Using Stream ID: 1 (easy handle 0x14c013a00)
14:35:43.088469 => Send header, 158 bytes (0x9e)
0000: 50 4f 53 54 20 2f 65 63 68 6f 3f 64 65 6c 61 79 POST /echo?delay
0010: 3d 31 73 20 48 54 54 50 2f 32 0d 0a 48 6f 73 74 =1s HTTP/2..Host
0020: 3a 20 6c 6f 63 61 6c 68 6f 73 74 3a 38 30 30 31 : localhost:8001
0030: 0d 0a 75 73 65 72 2d 61 67 65 6e 74 3a 20 63 75 ..user-agent: cu
0040: 72 6c 2f 37 2e 38 33 2e 30 0d 0a 61 63 63 65 70 rl/7.83.0..accep
0050: 74 3a 20 2a 2f 2a 0d 0a 63 6f 6e 74 65 6e 74 2d t: */*..content-
0060: 6c 65 6e 67 74 68 3a 20 33 0d 0a 63 6f 6e 74 65 length: 3..conte
0070: 6e 74 2d 74 79 70 65 3a 20 61 70 70 6c 69 63 61 nt-type: applica
0080: 74 69 6f 6e 2f 78 2d 77 77 77 2d 66 6f 72 6d 2d tion/x-www-form-
0090: 75 72 6c 65 6e 63 6f 64 65 64 0d 0a 0d 0a urlencoded....
14:35:43.088555 => Send data, 3 bytes (0x3)
0000: 66 6f 6f foo
14:35:43.088564 == Info: We are completely uploaded and fine
14:35:43.088786 == Info: Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
14:35:44.092396 <= Recv header, 13 bytes (0xd)
0000: 48 54 54 50 2f 32 20 32 30 30 20 0d 0a HTTP/2 200 ..
14:35:44.092445 <= Recv header, 49 bytes (0x31)
0000: 63 6f 6e 74 65 6e 74 2d 74 79 70 65 3a 20 61 70 content-type: ap
0010: 70 6c 69 63 61 74 69 6f 6e 2f 78 2d 77 77 77 2d plication/x-www-
0020: 66 6f 72 6d 2d 75 72 6c 65 6e 63 6f 64 65 64 0d form-urlencoded.
0030: 0a .
14:35:44.092484 <= Recv header, 37 bytes (0x25)
0000: 64 61 74 65 3a 20 53 61 74 2c 20 31 34 20 4d 61 date: Sat, 14 Ma
0010: 79 20 32 30 32 32 20 32 31 3a 33 35 3a 34 34 20 y 2022 21:35:44
0020: 47 4d 54 0d 0a GMT..
14:35:44.092515 <= Recv header, 19 bytes (0x13)
0000: 63 6f 6e 74 65 6e 74 2d 6c 65 6e 67 74 68 3a 20 content-length:
0010: 33 0d 0a 3..
14:35:44.092537 <= Recv header, 2 bytes (0x2)
0000: 0d 0a ..
14:35:44.092557 <= Recv data, 3 bytes (0x3)
0000: 66 6f 6f foo
foo14:35:44.092602 == Info: Connection #0 to host localhost left intact
working server side:
2022/05/14 14:35:43 h2c: attempting h2c with prior knowledge.
2022/05/14 14:35:43 http2: server connection from 127.0.0.1:50901 on 0x14000282460
2022/05/14 14:35:43 http2: Framer 0x14000282620: wrote SETTINGS len=24, settings: MAX_FRAME_SIZE=1048576, MAX_CONCURRENT_STREAMS=250, MAX_HEADER_LIST_SIZE=1048896, INITIAL_WINDOW_SIZE=1048576
2022/05/14 14:35:43 http2: server: client 127.0.0.1:50901 said hello
2022/05/14 14:35:43 http2: Framer 0x14000282620: wrote WINDOW_UPDATE len=4 (conn) incr=983041
2022/05/14 14:35:43 http2: Framer 0x14000282620: read SETTINGS len=18, settings: MAX_CONCURRENT_STREAMS=100, INITIAL_WINDOW_SIZE=33554432, ENABLE_PUSH=0
2022/05/14 14:35:43 http2: server read frame SETTINGS len=18, settings: MAX_CONCURRENT_STREAMS=100, INITIAL_WINDOW_SIZE=33554432, ENABLE_PUSH=0
2022/05/14 14:35:43 http2: server processing setting [MAX_CONCURRENT_STREAMS = 100]
2022/05/14 14:35:43 http2: server processing setting [INITIAL_WINDOW_SIZE = 33554432]
2022/05/14 14:35:43 http2: server processing setting [ENABLE_PUSH = 0]
2022/05/14 14:35:43 http2: Framer 0x14000282620: wrote SETTINGS flags=ACK len=0
2022/05/14 14:35:43 http2: Framer 0x14000282620: read WINDOW_UPDATE len=4 (conn) incr=33488897
2022/05/14 14:35:43 http2: server read frame WINDOW_UPDATE len=4 (conn) incr=33488897
2022/05/14 14:35:43 http2: Framer 0x14000282620: read HEADERS flags=END_HEADERS stream=1 len=72
2022/05/14 14:35:43 http2: decoded hpack field header field ":method" = "POST"
2022/05/14 14:35:43 http2: decoded hpack field header field ":path" = "/echo?delay=1s"
2022/05/14 14:35:43 http2: decoded hpack field header field ":scheme" = "http"
2022/05/14 14:35:43 http2: decoded hpack field header field ":authority" = "localhost:8001"
2022/05/14 14:35:43 http2: decoded hpack field header field "user-agent" = "curl/7.83.0"
2022/05/14 14:35:43 http2: decoded hpack field header field "accept" = "*/*"
2022/05/14 14:35:43 http2: decoded hpack field header field "content-length" = "3"
2022/05/14 14:35:43 http2: decoded hpack field header field "content-type" = "application/x-www-form-urlencoded"
2022/05/14 14:35:43 http2: server read frame HEADERS flags=END_HEADERS stream=1 len=72
2022/05/14 14:35:43 http2: Framer 0x14000282620: read DATA flags=END_STREAM stream=1 len=3 data="foo"
2022/05/14 14:35:43 http2: server read frame DATA flags=END_STREAM stream=1 len=3 data="foo"
2022/05/14 14:35:43 http2: Framer 0x14000282620: read SETTINGS flags=ACK len=0
2022/05/14 14:35:43 http2: server read frame SETTINGS flags=ACK len=0
web2 req: /echo?delay=1s
2022/05/14 14:35:43 http2: Framer 0x14000282620: wrote WINDOW_UPDATE len=4 (conn) incr=3
2022/05/14 14:35:44 http2: server encoding header ":status" = "200"
2022/05/14 14:35:44 http2: server encoding header "content-type" = "application/x-www-form-urlencoded"
2022/05/14 14:35:44 http2: server encoding header "date" = "Sat, 14 May 2022 21:35:44 GMT"
2022/05/14 14:35:44 http2: server encoding header "content-length" = "3"
2022/05/14 14:35:44 http2: Framer 0x14000282620: wrote HEADERS flags=END_HEADERS stream=1 len=54
2022/05/14 14:35:44 http2: Framer 0x14000282620: wrote DATA flags=END_STREAM stream=1 len=3 data="foo"
I think the server is not correctly configured. Here is the revised version that works: https://go.dev/play/p/hO_OnkMS-wz
I will highlight the changes at the end of this comment.
According to the doc of https://pkg.go.dev/golang.org/x/net/http2#ConfigureServer,
ConfigureServer must be called before s begins serving.
And here is the now deleted http2 server demo that could be helpful: https://github.com/golang/net/blob/69e39bad7dc2bbb411fa35755c46020969029fa7/http2/h2demo/h2demo.go
package main
import (
- "crypto/tls"
"fmt"
"io"
"log"
- "net"
"net/http"
- "net/url"
"golang.org/x/net/http2"
- "golang.org/x/net/http2/h2c"
)
func server() {
h2s := &http2.Server{}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Got %v %v request from %v\n", r.Method, r.Proto, r.RemoteAddr)
body, err := io.ReadAll(r.Body)
if err != nil {
log.Fatalf("server body read err: %v\n", err)
}
fmt.Printf("read %d from body\n", len(body))
w.WriteHeader(http.StatusAccepted)
fmt.Fprintf(w, "Hello, %v; HTTP Version: %v\n\nbody:\n%s\n", r.URL.Path, r.Proto, string(body))
})
+ http.HandleFunc("/", handler)
+
server := &http.Server{
Addr: ":8001",
- Handler: h2c.NewHandler(handler, h2s),
}
+ http2.ConfigureServer(server, h2s)
fmt.Printf("H2c Server starting on %s\n", server.Addr)
err := server.ListenAndServe()
if err != nil {
panic(err)
}
}
func main() {
server()
}
@ZekeLu this issue is about the h2c (http2 using cleartext) upgrade process. your example drops the important part out
Ah, I totally missed the h2c part. Thank you for pointing that out and sorry for making the noise.
yeah as I mentioned in the description (sorry if it's noisy/unclear) --http2-prior-knowledge
does work fine
The h2c
package handles protocol upgrades by reading the initial HTTP/1 request and rewriting it into an HTTP/2 stream of bytes:
https://go.googlesource.com/net/+/refs/heads/master/http2/h2c/h2c.go#208
It then takes those converted HTTP/2 bytes, prepends them to the subsequent HTTP/2 stream sent from the client, and presents this to the HTTP/2 server impementation.
The conversion from HTTP/1 to HTTP/2 does not appear to support request bodies at all, so any initial HTTP/1 request with a body is mishandled. Also, I think reading from the request body at all (even when it's not present) may be broken due to the synthetic HTTP/2 request not including an END_STREAM flag.
To make things even more fun, a fundamental problem with this approach is that it doesn't deal with HTTP/2 flow control at all.
I honestly have no idea how to fix this without completely rearchitecting h2c support. If we want to support h2c, we should stop rewriting HTTP/1 requests into HTTP/2 and have a real mechanism for handing off an upgraded request to the HTTP/2 server.
Change https://go.dev/cl/407454 mentions this issue: http2/h2c: connection handoff without translation
I used the your cl branch and I can confirm this fixes the issue I reported! thanks so much! do you know when it'll be available on the main branch?
while I have you I would love an h2c upgrade client and a way for the ReverseProxy to do h2c prior knowledge (h2c:// urls?) (the reason I need all this is to write a simple proxy that can do h1.1, h2c and grpc proxy https://github.com/fortio/proxy still doesn't work for grpc after this fix - possibly because I don't know how to change the ReverseProxy Director http.Request to tell it to try h2)
Answering my own question, I got it working !!
rp := httputil.ReverseProxy{
Director: Director,
Transport: &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
},
}
Still wondering how to configure as h2c upgrade though so it would work with h1.1 targets with a single transport
unfortunately because of https://github.com/golang/go/issues/44840#issuecomment-1133417501 I can't seem to use my fork to make a release (do I miss something obvious in my go.mod?)
Edit: found a workaround, change the import and go.mod in the fork...
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes
What operating system and processor architecture are you using (
go env
)?go env
OutputWhat did you do?
h2c server - with code to read request body:
https://go.dev/play/p/yaKhF5u4ut6
What did you expect to see?
to succeed - it hangs
note that --http2-prior-knowledge or --http1.1 works fine
What did you see instead?
hang on curl side, server side:
If not doing a POST (ie without a body) the ReadAll hangs (that I could workaround by checking the method, even though it probably shouldn't hang)