martian icon indicating copy to clipboard operation
martian copied to clipboard

HTTP/2 issues with curl - peer does not support HTTP/2 properly

Open denandz opened this issue 2 years ago • 0 comments

Hello, I'm currently attempting to build an HTTP2 logging proxy using Martian. The test snippet below works fine with Firefox; however, when posting data with curl via the proxy, I encounter errors. The following figures shows the results when going via the HTTP2 proxy (code provided).

GET works fine:

$ curl -v -x 127.0.0.1:8080 -k https://example.com
* Expire in 0 ms for 6 (transfer 0x55f31d912fb0)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55f31d912fb0)
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
* allocate connect buffer!
* Establish HTTP proxy tunnel to example.com:443
> CONNECT example.com:443 HTTP/1.1
...snip...
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x55f31d912fb0)
> GET / HTTP/2
> Host: example.com
> User-Agent: curl/7.64.0
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
< HTTP/2 200 
< age: 303681
< cache-control: max-age=604800
< content-type: text/html; charset=UTF-8
< date: Tue, 01 Mar 2022 05:56:07 GMT
< etag: "3147526947+ident"
< expires: Tue, 08 Mar 2022 05:56:07 GMT
< last-modified: Thu, 17 Oct 2019 07:18:26 GMT
< server: ECS (sab/572B)
< vary: Accept-Encoding
< x-cache: HIT
< content-length: 1256
< 
<!doctype html>
<html>
<head>
    <title>Example Domain</title>
...snip...

With the following log:

##Headers##
:method: GET
:path: /
:scheme: https
:authority: example.com
user-agent: curl/7.64.0
accept: */*
##Headers##
:status: 200
age: 303681
cache-control: max-age=604800
content-type: text/html; charset=UTF-8
date: Tue, 01 Mar 2022 05:56:07 GMT
etag: "3147526947+ident"
expires: Tue, 08 Mar 2022 05:56:07 GMT
last-modified: Thu, 17 Oct 2019 07:18:26 GMT
server: ECS (sab/572B)
vary: Accept-Encoding
x-cache: HIT
content-length: 1256
##Message##
<!doctype html>
<html>
<head>
...snip...

However, POST requests fail:

$ curl -v -x 127.0.0.1:8080 -k https://example.com -d 'foo=bar'
* Expire in 0 ms for 6 (transfer 0x55995f82efb0)
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55995f82efb0)
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
* allocate connect buffer!
* Establish HTTP proxy tunnel to example.com:443
> CONNECT example.com:443 HTTP/1.1
...snip...
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x55995f82efb0)
> POST / HTTP/2
> Host: example.com
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Length: 7
> Content-Type: application/x-www-form-urlencoded
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* We are completely uploaded and fine
* http2 error: Remote peer returned unexpected data while we expected SETTINGS frame.  Perhaps, peer does not support HTTP/2 properly.
* Connection #0 to host 127.0.0.1 left intact
curl: (16) Error in the HTTP2 framing layer

Can you please advise? Is this an issue with my implementation or an underlying problem with Martian? The same results occur regardless of whether i use my logging processor or set the processors to nil.

Proxy code to replicate the issue below:

package main

import (
	"crypto/tls"
	"fmt"
	"log"
	"net"
	"net/http"
	"net/url"
	"time"

	"github.com/google/martian/v3"
	"github.com/google/martian/v3/h2"
	"github.com/google/martian/v3/mitm"

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

type loggingProcessor struct {
	sink h2.Processor
}

func (p *loggingProcessor) Header(
	headers []hpack.HeaderField,
	streamEnded bool,
	priority http2.PriorityParam,
) error {
	println("##Headers##")
	for _, header := range headers {
		println(header.Name + ": " + header.Value)
	}

	return p.sink.Header(headers, streamEnded, priority)
}

func (p *loggingProcessor) Data(data []byte, streamEnded bool) error {
	println("##Message##")
	println(string(data))
	return p.sink.Data(data, streamEnded)
}

func (p *loggingProcessor) Priority(priority http2.PriorityParam) error {
	return p.sink.Priority(priority)
}

func (p *loggingProcessor) RSTStream(errCode http2.ErrCode) error {
	return p.sink.RSTStream(errCode)
}

func (p *loggingProcessor) PushPromise(promiseID uint32, headers []hpack.HeaderField) error {
	return p.sink.PushPromise(promiseID, headers)
}

func main() {
	fmt.Println("Http2 Proxy Test")

	p := martian.NewProxy()
	x509c, priv, _ := mitm.NewAuthority("martian.proxy", "Martian Authority", 30*24*time.Hour)
	mc, err := mitm.NewConfig(x509c, priv)
	if err != nil {
		fmt.Printf("creating mitm config: %v", err)
		return
	}
	mc.SetValidity(time.Hour)
	mc.SetOrganization("Martian Proxy")
	mc.SetH2Config(&h2.Config{
		AllowedHostsFilter: func(_ string) bool { return true },
		StreamProcessorFactories: []h2.StreamProcessorFactory{
			func(url *url.URL, sinks *h2.Processors) (h2.Processor, h2.Processor) {
				procCtoS := &loggingProcessor{
					sink: sinks.ForDirection(h2.ClientToServer),
				}
				procStoC := &loggingProcessor{
					sink: sinks.ForDirection(h2.ServerToClient),
				}
				return procCtoS, procStoC
				//return nil, nil
			},
		},
		EnableDebugLogs: true,
	})

	p.SetMITM(mc)

	tr := &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	}
	p.SetRoundTripper(tr)

	l, err := net.Listen("tcp", "0.0.0.0:8080")
	if err != nil {
		log.Fatal(err)
	}

	if err != nil {
		fmt.Printf("creating proxy: %v", err)
		return
	}

	done := make(chan bool)
	go func(done chan bool) {
		p.Serve(l)
		<-done
	}(done)

	fmt.Printf("serving on 0.0.0.0:8080\n")
	<-done
}

denandz avatar Mar 01 '22 06:03 denandz