Skip to content

Commit da6c168

Browse files
committed
net/http: flesh out Transport's HTTP/1 CONNECT+bidi support to match HTTP/2
Fixes #17227 Change-Id: I0f8964593d69623b85d5759f6276063ee62b2915 Reviewed-on: https://go-review.googlesource.com/c/123156 Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent e19f575 commit da6c168

File tree

5 files changed

+123
-10
lines changed

5 files changed

+123
-10
lines changed

src/net/http/request.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,9 @@ func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitF
545545
} else if r.Method == "CONNECT" && r.URL.Path == "" {
546546
// CONNECT requests normally give just the host and port, not a full URL.
547547
ruri = host
548+
if r.URL.Opaque != "" {
549+
ruri = r.URL.Opaque
550+
}
548551
}
549552
// TODO(bradfitz): escape at least newlines in ruri?
550553

src/net/http/requestwrite_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,38 @@ var reqWriteTests = []reqWriteTest{
512512
"User-Agent: Go-http-client/1.1\r\n" +
513513
"\r\n",
514514
},
515+
516+
// CONNECT without Opaque
517+
21: {
518+
Req: Request{
519+
Method: "CONNECT",
520+
URL: &url.URL{
521+
Scheme: "https", // of proxy.com
522+
Host: "proxy.com",
523+
},
524+
},
525+
// What we used to do, locking that behavior in:
526+
WantWrite: "CONNECT proxy.com HTTP/1.1\r\n" +
527+
"Host: proxy.com\r\n" +
528+
"User-Agent: Go-http-client/1.1\r\n" +
529+
"\r\n",
530+
},
531+
532+
// CONNECT with Opaque
533+
22: {
534+
Req: Request{
535+
Method: "CONNECT",
536+
URL: &url.URL{
537+
Scheme: "https", // of proxy.com
538+
Host: "proxy.com",
539+
Opaque: "backend:443",
540+
},
541+
},
542+
WantWrite: "CONNECT backend:443 HTTP/1.1\r\n" +
543+
"Host: proxy.com\r\n" +
544+
"User-Agent: Go-http-client/1.1\r\n" +
545+
"\r\n",
546+
},
515547
}
516548

517549
func TestRequestWrite(t *testing.T) {

src/net/http/transfer.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ func (t *transferWriter) shouldSendChunkedRequestBody() bool {
184184
if t.ContentLength >= 0 || t.Body == nil { // redundant checks; caller did them
185185
return false
186186
}
187+
if t.Method == "CONNECT" {
188+
return false
189+
}
187190
if requestMethodUsuallyLacksBody(t.Method) {
188191
// Only probe the Request.Body for GET/HEAD/DELETE/etc
189192
// requests, because it's only those types of requests
@@ -357,7 +360,11 @@ func (t *transferWriter) writeBody(w io.Writer) error {
357360
err = cw.Close()
358361
}
359362
} else if t.ContentLength == -1 {
360-
ncopy, err = io.Copy(w, body)
363+
dst := w
364+
if t.Method == "CONNECT" {
365+
dst = bufioFlushWriter{dst}
366+
}
367+
ncopy, err = io.Copy(dst, body)
361368
} else {
362369
ncopy, err = io.Copy(w, io.LimitReader(body, t.ContentLength))
363370
if err != nil {
@@ -1050,3 +1057,18 @@ func isKnownInMemoryReader(r io.Reader) bool {
10501057
}
10511058
return false
10521059
}
1060+
1061+
// bufioFlushWriter is an io.Writer wrapper that flushes all writes
1062+
// on its wrapped writer if it's a *bufio.Writer.
1063+
type bufioFlushWriter struct{ w io.Writer }
1064+
1065+
func (fw bufioFlushWriter) Write(p []byte) (n int, err error) {
1066+
n, err = fw.w.Write(p)
1067+
if bw, ok := fw.w.(*bufio.Writer); n > 0 && ok {
1068+
ferr := bw.Flush()
1069+
if ferr != nil && err == nil {
1070+
err = ferr
1071+
}
1072+
}
1073+
return
1074+
}

src/net/http/transport.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,15 +85,6 @@ func init() {
8585
// To explicitly enable HTTP/2 on a transport, use golang.org/x/net/http2
8686
// and call ConfigureTransport. See the package docs for more about HTTP/2.
8787
//
88-
// The Transport will send CONNECT requests to a proxy for its own use
89-
// when processing HTTPS requests, but Transport should generally not
90-
// be used to send a CONNECT request. That is, the Request passed to
91-
// the RoundTrip method should not have a Method of "CONNECT", as Go's
92-
// HTTP/1.x implementation does not support full-duplex request bodies
93-
// being written while the response body is streamed. Go's HTTP/2
94-
// implementation does support full duplex, but many CONNECT proxies speak
95-
// HTTP/1.x.
96-
//
9788
// Responses with status codes in the 1xx range are either handled
9889
// automatically (100 expect-continue) or ignored. The one
9990
// exception is HTTP status code 101 (Switching Protocols), which is

src/net/http/transport_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4887,3 +4887,68 @@ func TestTransportResponseBodyWritableOnProtocolSwitch(t *testing.T) {
48874887
t.Errorf("read %q; want %q", got, want)
48884888
}
48894889
}
4890+
4891+
func TestTransportCONNECTBidi(t *testing.T) {
4892+
defer afterTest(t)
4893+
const target = "backend:443"
4894+
cst := newClientServerTest(t, h1Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
4895+
if r.Method != "CONNECT" {
4896+
t.Errorf("unexpected method %q", r.Method)
4897+
w.WriteHeader(500)
4898+
return
4899+
}
4900+
if r.RequestURI != target {
4901+
t.Errorf("unexpected CONNECT target %q", r.RequestURI)
4902+
w.WriteHeader(500)
4903+
return
4904+
}
4905+
nc, brw, err := w.(Hijacker).Hijack()
4906+
if err != nil {
4907+
t.Error(err)
4908+
return
4909+
}
4910+
defer nc.Close()
4911+
nc.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
4912+
// Switch to a little protocol that capitalize its input lines:
4913+
for {
4914+
line, err := brw.ReadString('\n')
4915+
if err != nil {
4916+
if err != io.EOF {
4917+
t.Error(err)
4918+
}
4919+
return
4920+
}
4921+
io.WriteString(brw, strings.ToUpper(line))
4922+
brw.Flush()
4923+
}
4924+
}))
4925+
defer cst.close()
4926+
pr, pw := io.Pipe()
4927+
defer pw.Close()
4928+
req, err := NewRequest("CONNECT", cst.ts.URL, pr)
4929+
if err != nil {
4930+
t.Fatal(err)
4931+
}
4932+
req.URL.Opaque = target
4933+
res, err := cst.c.Do(req)
4934+
if err != nil {
4935+
t.Fatal(err)
4936+
}
4937+
defer res.Body.Close()
4938+
if res.StatusCode != 200 {
4939+
t.Fatalf("status code = %d; want 200", res.StatusCode)
4940+
}
4941+
br := bufio.NewReader(res.Body)
4942+
for _, str := range []string{"foo", "bar", "baz"} {
4943+
fmt.Fprintf(pw, "%s\n", str)
4944+
got, err := br.ReadString('\n')
4945+
if err != nil {
4946+
t.Fatal(err)
4947+
}
4948+
got = strings.TrimSpace(got)
4949+
want := strings.ToUpper(str)
4950+
if got != want {
4951+
t.Fatalf("got %q; want %q", got, want)
4952+
}
4953+
}
4954+
}

0 commit comments

Comments
 (0)