Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic passthrough HTTP/2 support. #539

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
module github.com/elazarl/goproxy

require github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
go 1.23

require (
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
golang.org/x/net v0.26.0
)

require golang.org/x/text v0.16.0 // indirect
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
171 changes: 171 additions & 0 deletions h2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package goproxy

import (
"bufio"
"crypto/tls"
"errors"
"io"
"net"
"net/http"
"strings"

"golang.org/x/net/http2"
)

// H2Transport is an implementation of RoundTripper that abstracts an entire
// HTTP/2 session, sending all client frames to the server and responses back
// to the client.
type H2Transport struct {
ClientReader io.Reader
ClientWriter io.Writer
TLSConfig *tls.Config
Host string
}

// RoundTrip executes an HTTP/2 session (including all contained streams).
// The request and response are ignored but any error encountered during the
// proxying from the session is returned as a result of the invocation.
func (r *H2Transport) RoundTrip(prefaceReq *http.Request) (*http.Response, error) {
raddr := r.Host
if !strings.Contains(raddr, ":") {
raddr = raddr + ":443"
}
rawServerTLS, err := dial("tcp", raddr)
if err != nil {
return nil, err
}
defer rawServerTLS.Close()
// Ensure that we only advertise HTTP/2 as the accepted protocol.
r.TLSConfig.NextProtos = []string{http2.NextProtoTLS}
// Initiate TLS and check remote host name against certificate.
rawServerTLS = tls.Client(rawServerTLS, r.TLSConfig)
if err = rawServerTLS.(*tls.Conn).Handshake(); err != nil {
return nil, err
}
if r.TLSConfig == nil || !r.TLSConfig.InsecureSkipVerify {
if err = rawServerTLS.(*tls.Conn).VerifyHostname(raddr[:strings.LastIndex(raddr, ":")]); err != nil {
return nil, err
}
}
// Send new client preface to match the one parsed in req.
if _, err := io.WriteString(rawServerTLS, http2.ClientPreface); err != nil {
return nil, err
}
serverTLSReader := bufio.NewReader(rawServerTLS)
cToS := http2.NewFramer(rawServerTLS, r.ClientReader)
sToC := http2.NewFramer(r.ClientWriter, serverTLSReader)
errSToC := make(chan error)
errCToS := make(chan error)
go func() {
for {
if err := proxyFrame(sToC); err != nil {
errSToC <- err
break
}
}
}()
go func() {
for {
if err := proxyFrame(cToS); err != nil {
errCToS <- err
break
}
}
}()
for i := 0; i < 2; i++ {
select {
case err := <-errSToC:
if err != io.EOF {
return nil, err
}
case err := <-errCToS:
if err != io.EOF {
return nil, err
}
}
}
return nil, nil
}

func dial(network, addr string) (c net.Conn, err error) {
addri, err := net.ResolveTCPAddr(network, addr)
if err != nil {
return
}
c, err = net.DialTCP(network, nil, addri)
return
}

// proxyFrame reads a single frame from the Framer and, when successful, writes
// a ~identical one back to the Framer.
func proxyFrame(fr *http2.Framer) error {
f, err := fr.ReadFrame()
if err != nil {
return err
}
switch f.Header().Type {
case http2.FrameData:
tf := f.(*http2.DataFrame)
terr := fr.WriteData(tf.StreamID, tf.StreamEnded(), tf.Data())
if terr == nil && tf.StreamEnded() {
terr = io.EOF
}
return terr
case http2.FrameHeaders:
tf := f.(*http2.HeadersFrame)
terr := fr.WriteHeaders(http2.HeadersFrameParam{
StreamID: tf.StreamID,
BlockFragment: tf.HeaderBlockFragment(),
EndStream: tf.StreamEnded(),
EndHeaders: tf.HeadersEnded(),
PadLength: 0,
Priority: tf.Priority,
})
if terr == nil && tf.StreamEnded() {
terr = io.EOF
}
return terr
case http2.FrameContinuation:
tf := f.(*http2.ContinuationFrame)
return fr.WriteContinuation(tf.StreamID, tf.HeadersEnded(), tf.HeaderBlockFragment())
case http2.FrameGoAway:
tf := f.(*http2.GoAwayFrame)
return fr.WriteGoAway(tf.StreamID, tf.ErrCode, tf.DebugData())
case http2.FramePing:
tf := f.(*http2.PingFrame)
return fr.WritePing(tf.IsAck(), tf.Data)
case http2.FrameRSTStream:
tf := f.(*http2.RSTStreamFrame)
return fr.WriteRSTStream(tf.StreamID, tf.ErrCode)
case http2.FrameSettings:
tf := f.(*http2.SettingsFrame)
if tf.IsAck() {
return fr.WriteSettingsAck()
}
var settings []http2.Setting
// NOTE: If we want to parse headers, need to handle
// settings where s.ID == http2.SettingHeaderTableSize and
// accordingly update the Framer options.
for i := 0; i < tf.NumSettings(); i++ {
settings = append(settings, tf.Setting(i))
}
return fr.WriteSettings(settings...)
case http2.FrameWindowUpdate:
tf := f.(*http2.WindowUpdateFrame)
return fr.WriteWindowUpdate(tf.StreamID, tf.Increment)
case http2.FramePriority:
tf := f.(*http2.PriorityFrame)
return fr.WritePriority(tf.StreamID, tf.PriorityParam)
case http2.FramePushPromise:
tf := f.(*http2.PushPromiseFrame)
return fr.WritePushPromise(http2.PushPromiseParam{
StreamID: tf.StreamID,
PromiseID: tf.PromiseID,
BlockFragment: tf.HeaderBlockFragment(),
EndHeaders: tf.HeadersEnded(),
PadLength: 0,
})
default:
return errors.New("Unsupported frame: " + string(f.Header().Type))
}
}
24 changes: 24 additions & 0 deletions https.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,30 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request

req, resp := proxy.filterRequest(req, ctx)
if resp == nil {
if req.Method == "PRI" {
// Handle HTTP/2 connections.

// NOTE: As of 1.22, golang's http module will not recognize or
// parse the HTTP Body for PRI requests. This leaves the body of
// the http2.ClientPreface ("SM\r\n\r\n") on the wire which we need
// to clear before setting up the connection.
_, err := clientTlsReader.Discard(6)
if err != nil {
ctx.Warnf("Failed to process HTTP2 client preface: %v", err)
return
}
if !proxy.AllowHTTP2 {
ctx.Warnf("HTTP2 connection failed: disallowed")
return
}
tr := H2Transport{clientTlsReader, rawClientTls, tlsConfig.Clone(), host}
if _, err := tr.RoundTrip(req); err != nil {
ctx.Warnf("HTTP2 connection failed: %v", err)
} else {
ctx.Logf("Exiting on EOF")
}
return
}
if isWebSocketRequest(req) {
ctx.Logf("Request looks like websocket upgrade.")
proxy.serveWebsocketTLS(ctx, w, req, tlsConfig, rawClientTls)
Expand Down
1 change: 1 addition & 0 deletions proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type ProxyHttpServer struct {
ConnectDialWithReq func(req *http.Request, network string, addr string) (net.Conn, error)
CertStore CertStorage
KeepHeader bool
AllowHTTP2 bool
}

var hasPort = regexp.MustCompile(`:\d+$`)
Expand Down