-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement parsing of the CONNECT-IP request
This intentionally omits requests that define URI template variables, since we don't support IP flow forwarding yet.
- Loading branch information
1 parent
abf1511
commit 0e78f1d
Showing
6 changed files
with
203 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package masque | ||
package connectip | ||
|
||
import ( | ||
"encoding/binary" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
package masque | ||
package connectip | ||
|
||
import ( | ||
"bytes" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
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 } | ||
|
||
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), | ||
} | ||
} | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |