Skip to content

Commit

Permalink
implement parsing of the CONNECT-IP request
Browse files Browse the repository at this point in the history
This intentionally omits requests that define URI template variables,
since we don't support IP flow forwarding yet.
  • Loading branch information
marten-seemann committed Oct 6, 2024
1 parent abf1511 commit 0e78f1d
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 2 deletions.
2 changes: 1 addition & 1 deletion capsule.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package masque
package connectip

import (
"encoding/binary"
Expand Down
2 changes: 1 addition & 1 deletion capsule_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package masque
package connectip

import (
"bytes"
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ module github.com/quic-go/connect-ip-go
go 1.22

require (
github.com/dunglas/httpsfv v1.0.2
github.com/quic-go/quic-go v0.47.1-0.20241002141227-b2233591adc7
github.com/stretchr/testify v1.9.0
github.com/yosida95/uritemplate/v3 v3.0.2
)

require (
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
Expand Down Expand Up @@ -37,6 +39,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
Expand Down
100 changes: 100 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package connectip

import (
"errors"
"fmt"
"net/http"
"net/url"
"reflect"

"github.com/dunglas/httpsfv"
"github.com/yosida95/uritemplate/v3"
)

const (
requestProtocol = "connect-ip"
capsuleHeader = "Capsule-Protocol"
)

var capsuleProtocolHeaderValue string

func init() {
v, err := httpsfv.Marshal(httpsfv.NewItem(true))
if err != nil {
panic(fmt.Sprintf("failed to marshal capsule protocol header value: %v", err))

Check warning on line 24 in request.go

View check run for this annotation

Codecov / codecov/patch

request.go#L24

Added line #L24 was not covered by tests
}
capsuleProtocolHeaderValue = v
}

// Request is the parsed CONNECT-IP request returned from ParseRequest.
// It currently doesn't have any fields, since masque-go doesn't support IP flow forwarding.
type Request struct{}

// RequestParseError is returned from ParseRequest if parsing the CONNECT-UDP request fails.
// It is recommended that the request is rejected with the corresponding HTTP status code.
type RequestParseError struct {
HTTPStatus int
Err error
}

func (e *RequestParseError) Error() string { return e.Err.Error() }
func (e *RequestParseError) Unwrap() error { return e.Err }

Check warning on line 41 in request.go

View check run for this annotation

Codecov / codecov/patch

request.go#L41

Added line #L41 was not covered by tests

func ParseRequest(r *http.Request, template *uritemplate.Template) (*Request, error) {
if len(template.Varnames()) > 0 {
return nil, errors.New("connect-ip-go currently does not support IP flow forwarding")
}

u, err := url.Parse(template.Raw())
if err != nil {
return nil, &RequestParseError{
HTTPStatus: http.StatusInternalServerError,
Err: fmt.Errorf("failed to parse template: %w", err),

Check warning on line 52 in request.go

View check run for this annotation

Codecov / codecov/patch

request.go#L50-L52

Added lines #L50 - L52 were not covered by tests
}
}
if r.Method != http.MethodConnect {
return nil, &RequestParseError{
HTTPStatus: http.StatusMethodNotAllowed,
Err: fmt.Errorf("expected CONNECT request, got %s", r.Method),
}
}
if r.Proto != requestProtocol {
return nil, &RequestParseError{
HTTPStatus: http.StatusNotImplemented,
Err: fmt.Errorf("unexpected protocol: %s", r.Proto),
}
}
if r.Host != u.Host {
return nil, &RequestParseError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("host in :authority (%s) does not match template host (%s)", r.Host, u.Host),
}
}
capsuleHeaderValues, ok := r.Header[capsuleHeader]
if !ok {
return nil, &RequestParseError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("missing Capsule-Protocol header"),
}
}
item, err := httpsfv.UnmarshalItem(capsuleHeaderValues)
if err != nil {
return nil, &RequestParseError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("invalid capsule header value: %s", capsuleHeaderValues),
}
}
if v, ok := item.Value.(bool); !ok {
return nil, &RequestParseError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("incorrect capsule header value type: %s", reflect.TypeOf(item.Value)),
}
} else if !v {
return nil, &RequestParseError{
HTTPStatus: http.StatusBadRequest,
Err: fmt.Errorf("incorrect capsule header value: %t", item.Value),
}
}

return &Request{}, nil
}
95 changes: 95 additions & 0 deletions request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package connectip

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/dunglas/httpsfv"
"github.com/stretchr/testify/require"
"github.com/yosida95/uritemplate/v3"
)

func newRequest(target string) *http.Request {
req := httptest.NewRequest(http.MethodGet, target, nil)
req.Method = http.MethodConnect
req.Proto = requestProtocol
req.Header.Add("Capsule-Protocol", capsuleProtocolHeaderValue)
return req
}

func TestConnectIPRequestParsing(t *testing.T) {
t.Run("valid request", func(t *testing.T) {
template := uritemplate.MustNew("https://localhost:1234/masque/ip")
req := newRequest("https://localhost:1234/masque/ip")
r, err := ParseRequest(req, template)
require.NoError(t, err)
require.Equal(t, &Request{}, r)
})

t.Run("reject templates with variables", func(t *testing.T) {
template := uritemplate.MustNew("https://localhost:1234/masque/ip?t={target}&i={ipproto}")
req := newRequest("https://localhost:1234/masque/ip?t=foobar&i=42")
_, err := ParseRequest(req, template)
require.EqualError(t, err, "connect-ip-go currently does not support IP flow forwarding")
})

template := uritemplate.MustNew("https://localhost:1234/masque/")

t.Run("wrong protocol", func(t *testing.T) {
req := newRequest("https://localhost:1234/masque")
req.Proto = "not-connect-ip"
_, err := ParseRequest(req, template)
require.EqualError(t, err, "unexpected protocol: not-connect-ip")
require.Equal(t, http.StatusNotImplemented, err.(*RequestParseError).HTTPStatus)
})

t.Run("wrong request method", func(t *testing.T) {
req := newRequest("https://localhost:1234/masque")
req.Method = http.MethodHead
_, err := ParseRequest(req, template)
require.EqualError(t, err, "expected CONNECT request, got HEAD")
require.Equal(t, http.StatusMethodNotAllowed, err.(*RequestParseError).HTTPStatus)
})

t.Run("wrong :authority", func(t *testing.T) {
req := newRequest("https://quic-go.net:1234/masque")
_, err := ParseRequest(req, template)
require.EqualError(t, err, "host in :authority (quic-go.net:1234) does not match template host (localhost:1234)")
require.Equal(t, http.StatusBadRequest, err.(*RequestParseError).HTTPStatus)
})

t.Run("missing Capsule-Protocol header", func(t *testing.T) {
req := newRequest("https://localhost:1234/masque")
req.Header.Del("Capsule-Protocol")
_, err := ParseRequest(req, template)
require.EqualError(t, err, "missing Capsule-Protocol header")
require.Equal(t, http.StatusBadRequest, err.(*RequestParseError).HTTPStatus)
})

t.Run("invalid Capsule-Protocol header", func(t *testing.T) {
req := newRequest("https://localhost:1234/masque")
req.Header.Set("Capsule-Protocol", "🤡")
_, err := ParseRequest(req, template)
require.EqualError(t, err, "invalid capsule header value: [🤡]")
require.Equal(t, http.StatusBadRequest, err.(*RequestParseError).HTTPStatus)
})

t.Run("invalid Capsule-Protocol header value type", func(t *testing.T) {
req := newRequest("https://localhost:1234/masque")
req.Header.Set("Capsule-Protocol", "1")
_, err := ParseRequest(req, template)
require.EqualError(t, err, "incorrect capsule header value type: int64")
require.Equal(t, http.StatusBadRequest, err.(*RequestParseError).HTTPStatus)
})

t.Run("invalid Capsule-Protocol header value", func(t *testing.T) {
req := newRequest("https://localhost:1234/masque")
v, err := httpsfv.Marshal(httpsfv.NewItem(false))
require.NoError(t, err)
req.Header.Set("Capsule-Protocol", v)
_, err = ParseRequest(req, template)
require.EqualError(t, err, "incorrect capsule header value: false")
require.Equal(t, http.StatusBadRequest, err.(*RequestParseError).HTTPStatus)
})
}

0 comments on commit 0e78f1d

Please sign in to comment.