From 7cc9267cc5cb89586763d68dd811fb681b7f44ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=B5=B7=E6=B6=9B?= Date: Wed, 20 Jun 2018 08:20:27 +0800 Subject: [PATCH] support h2c with prior knowledge --- gin.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++ gin_test.go | 25 +++++++++++++++++++++++ h2c.go | 37 ++++++++++++++++++++++++++++++++++ vendor/vendor.json | 24 ++++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 h2c.go diff --git a/gin.go b/gin.go index b91fc2fa57..29b3e051f3 100644 --- a/gin.go +++ b/gin.go @@ -5,13 +5,17 @@ package gin import ( + "fmt" "html/template" + "io" "net" "net/http" "os" + "strings" "sync" "github.com/gin-gonic/gin/render" + "golang.org/x/net/http2" ) const ( @@ -103,6 +107,7 @@ type Engine struct { noMethod HandlersChain pool sync.Pool trees methodTrees + h2Server *http2.Server } var _ IRouter = &Engine{} @@ -282,6 +287,9 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo { func (engine *Engine) Run(addr ...string) (err error) { defer func() { debugPrintError(err) }() + // TODO support h2Server configuration + engine.h2Server = &http2.Server{} + address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine) @@ -318,6 +326,10 @@ func (engine *Engine) RunUnix(file string) (err error) { // ServeHTTP conforms to the http.Handler interface. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if engine.tryHandleH2C(w, req) { + return + } + c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req @@ -328,6 +340,44 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { engine.pool.Put(c) } +func (engine *Engine) tryHandleH2C(w http.ResponseWriter, req *http.Request) bool { + if req.Method != "PRI" || req.URL.Path != "*" || req.ProtoMajor < 2 { + return false + } + + hijacker, ok := w.(http.Hijacker) + if !ok { + return false + } + + conn, rw, err := hijacker.Hijack() + if err != nil { + return false + } + + const expectedBody = "SM\r\n\r\n" + buf := make([]byte, len(expectedBody)) + n, err := io.ReadFull(rw, buf) + + if err != nil { + panic(fmt.Sprintf("Error h2c with prior knowledge: %v", err)) + } + + if string(buf[:n]) != expectedBody { + return false + } + + conn2 := &rwConn{ + Conn: conn, + Reader: io.MultiReader(strings.NewReader(http2.ClientPreface), rw), + BufWriter: rw.Writer, + } + + engine.h2Server.ServeConn(conn2, &http2.ServeConnOpts{Handler: engine}) + + return true +} + // HandleContext re-enter a context that has been rewritten. // This can be done by setting c.Request.URL.Path to your new target. // Disclaimer: You can loop yourself to death with this, use wisely. diff --git a/gin_test.go b/gin_test.go index 3ac60577ea..9bb6be2e34 100644 --- a/gin_test.go +++ b/gin_test.go @@ -9,12 +9,14 @@ import ( "fmt" "html/template" "io/ioutil" + "net" "net/http" "reflect" "testing" "time" "github.com/stretchr/testify/assert" + "golang.org/x/net/http2" ) func formatAsDate(t time.Time) string { @@ -93,6 +95,29 @@ func TestLoadHTMLGlob(t *testing.T) { td() } +func TestLoadHTMLGlobOverH2c(t *testing.T) { + td := setupHTMLGlob(t, DebugMode, false) + + http := http.Client{ + Transport: &http2.Transport{ + AllowHTTP: true, + DialTLS: func(netw, addr string, cfg *tls.Config) (net.Conn, error) { + return net.Dial(netw, addr) + }, + }, + } + + res, err := http.Get("http://127.0.0.1:8888/test") + if err != nil { + fmt.Println(err) + } + + resp, _ := ioutil.ReadAll(res.Body) + assert.Equal(t, "

Hello world

", string(resp[:])) + + td() +} + func TestLoadHTMLGlob2(t *testing.T) { td := setupHTMLGlob(t, TestMode, false) res, err := http.Get("http://127.0.0.1:8888/test") diff --git a/h2c.go b/h2c.go new file mode 100644 index 0000000000..f9fe1dd8df --- /dev/null +++ b/h2c.go @@ -0,0 +1,37 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "io" + "net" +) + +type bufWriter interface { + io.Writer + Flush() error +} + +// rwConn implements net.Conn but overrides Read and Write so that reads and +// writes are forwarded to the provided io.Reader and bufWriter. +type rwConn struct { + net.Conn + io.Reader + BufWriter bufWriter +} + +// Read forwards reads to the underlying Reader. +func (c *rwConn) Read(p []byte) (int, error) { + return c.Reader.Read(p) +} + +// Write forwards writes to the underlying bufWriter and immediately flushes. +func (c *rwConn) Write(p []byte) (int, error) { + n, err := c.BufWriter.Write(p) + if err := c.BufWriter.Flush(); err != nil { + return 0, err + } + return n, err +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 5ace49df56..be01c4cc71 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -102,6 +102,30 @@ "revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a", "revisionTime": "2016-10-18T08:54:36Z" }, + { + "checksumSHA1": "pCY4YtdNKVBYRbNvODjx8hj0hIs=", + "path": "golang.org/x/net/http/httpguts", + "revision": "db08ff08e8622530d9ed3a0e8ac279f6d4c02196", + "revisionTime": "2018-06-11T16:35:41Z" + }, + { + "checksumSHA1": "NNo0AcF8P5+b/140t0Wox/HFkkw=", + "path": "golang.org/x/net/http2", + "revision": "db08ff08e8622530d9ed3a0e8ac279f6d4c02196", + "revisionTime": "2018-06-11T16:35:41Z" + }, + { + "checksumSHA1": "leSW9aM30mATlWs/eeqhQQh/3eo=", + "path": "golang.org/x/net/http2/hpack", + "revision": "db08ff08e8622530d9ed3a0e8ac279f6d4c02196", + "revisionTime": "2018-06-11T16:35:41Z" + }, + { + "checksumSHA1": "RcrB7tgYS/GMW4QrwVdMOTNqIU8=", + "path": "golang.org/x/net/idna", + "revision": "db08ff08e8622530d9ed3a0e8ac279f6d4c02196", + "revisionTime": "2018-06-11T16:35:41Z" + }, { "checksumSHA1": "S0DP7Pn7sZUmXc55IzZnNvERu6s=", "path": "golang.org/x/sync/errgroup",