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, "