Skip to content

Commit

Permalink
Escaping and inlining improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
gofeuer committed Aug 8, 2024
1 parent c03ea83 commit bc30719
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 123 deletions.
2 changes: 0 additions & 2 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ var (
ErrFailedInvoiceRequest = errors.New("failed invoice request")
ErrFailedMacaroonMinting = errors.New("failed macaroon minting")
ErrPaymentRequired = errors.New("payment required")
ErrUnknownVersion = errors.New("unknown L402 version")
ErrEmptyMacaroonData = errors.New("empty macaroon data")
)

func DefaultErrorHandler(w http.ResponseWriter, r *http.Request) {
Expand Down
18 changes: 11 additions & 7 deletions macaroon.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/binary"
"fmt"
"reflect"
"strconv"
"strings"

macaroon "gopkg.in/macaroon.v2"
Expand All @@ -25,10 +26,6 @@ type Identifier struct {
}

func UnmarshalMacaroons(macaroonBase64 string) (map[Identifier]*macaroon.Macaroon, error) {
if macaroonBase64 == "" {
return nil, ErrEmptyMacaroonData
}

macaroonBytes, err := macaroon.Base64Decode([]byte(macaroonBase64))
if err != nil {
// The macaroons might be separated by commas, so we strip them and try again
Expand All @@ -52,6 +49,7 @@ func UnmarshalMacaroons(macaroonBase64 string) (map[Identifier]*macaroon.Macaroo
}
macaroonsMap[identifier] = macaroon
}

return macaroonsMap, err
}

Expand All @@ -65,7 +63,7 @@ var (

func MarchalIdentifier(identifier Identifier) ([]byte, error) {
if identifier.Version != 0 {
return nil, fmt.Errorf("%w: %d", ErrUnknownVersion, identifier.Version)
return nil, ErrUnknownVersion(identifier.Version)
}

macaroonID := make([]byte, macaroonIDSize)
Expand All @@ -81,9 +79,9 @@ func MarchalIdentifier(identifier Identifier) ([]byte, error) {

func UnmarshalIdentifier(identifierBytes []byte) (Identifier, error) {
if len(identifierBytes) != macaroonIDSize {
return Identifier{}, ErrUnknownVersion
return Identifier{}, ErrUnknownVersion(-1)
} else if version := byteOrder.Uint16(identifierBytes); version != 0 {
return Identifier{}, fmt.Errorf("%w: %d", ErrUnknownVersion, version)
return Identifier{}, ErrUnknownVersion(version)
}

var identifier Identifier
Expand All @@ -96,3 +94,9 @@ func UnmarshalIdentifier(identifierBytes []byte) (Identifier, error) {

return identifier, nil
}

type ErrUnknownVersion int64 //nolint:errname

func (e ErrUnknownVersion) Error() string {
return "unknown L402 version: " + strconv.FormatInt(int64(e), 10)
}
18 changes: 10 additions & 8 deletions macaroon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,18 @@ func TestUnmarshalMacaroons(t *testing.T) {
expectedMacaroons map[Identifier]*macaroon.Macaroon
expectedError error
}{
"no macaroons": {
expectedError: errors.New("empty macaroon data"),
"no macaroons": { // macaroonsBase64 is guaranteed by authorizationMatcher to be a non empty string
macaroonsBase64: "",
expectedMacaroons: map[Identifier]*macaroon.Macaroon{},
expectedError: nil,
},
"defective macaroon": {
macaroonsBase64: "AGIAJEemVQUTEyNCR0exk7ek90Cg==",
expectedError: base64.CorruptInputError(28),
},
"one invalid macaroon": {
macaroonsBase64: "MDAwZWxvY2F0aW9uIAowMDEwaWRlbnRpZmllciAKMDAyZnNpZ25hdHVyZSCPtT9UwdGWx8khvYJlWY9BhJu6JUG3in2Ef49M+/Oukgo=",
expectedError: ErrUnknownVersion,
expectedError: ErrUnknownVersion(-1),
},
"one macaroon": {
macaroonsBase64: "AgJCAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAGIHqWvcIDGguzG0xeNz7kxTr4IrPg64b0EjRonYD3zkVe",
Expand Down Expand Up @@ -111,7 +113,7 @@ func TestMarchalIdentifier(t *testing.T) {
}{
"invalid version": {
version: 1,
expectedErr: ErrUnknownVersion,
expectedErr: ErrUnknownVersion(1),
},
"success": {
paymentHash: [BlockSize]byte{
Expand Down Expand Up @@ -162,15 +164,15 @@ func TestUnmarshalIdentifier(t *testing.T) {
}{
"empty value": {
macaroonID: []byte{},
expectedErr: ErrUnknownVersion,
expectedErr: ErrUnknownVersion(-1),
},
"malformed truncated value": {
macaroonID: []byte{
0, 0, // Version
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Payment Hash
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2,
},
expectedErr: ErrUnknownVersion,
expectedErr: ErrUnknownVersion(-1),
},
"malformed extended value": {
macaroonID: []byte{
Expand All @@ -181,7 +183,7 @@ func TestUnmarshalIdentifier(t *testing.T) {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
0, 5,
},
expectedErr: ErrUnknownVersion,
expectedErr: ErrUnknownVersion(-1),
},
"wrong version": {
macaroonID: []byte{
Expand All @@ -191,7 +193,7 @@ func TestUnmarshalIdentifier(t *testing.T) {
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Id
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
},
expectedErr: ErrUnknownVersion,
expectedErr: ErrUnknownVersion(2),
},
"success": {
macaroonID: []byte{
Expand Down
87 changes: 0 additions & 87 deletions middleware.go

This file was deleted.

84 changes: 84 additions & 0 deletions proxy.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package l402

import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"regexp"

macaroon "gopkg.in/macaroon.v2"
)
Expand Down Expand Up @@ -43,6 +48,85 @@ func Proxy(minter MacaroonMinter, authority AccessAuthority, options ...option)
}
}

type ContextKey string

const KeyMacaroon ContextKey = "proxy_macaroon"

func (p proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
macaroonBase64, preimageHash, found := getL402AuthorizationHeader(r)
if !found {
ctx, cancelCause := context.WithCancelCause(r.Context())
cancelCause(ErrPaymentRequired)
p.authenticator.ServeHTTP(w, r.WithContext(ctx))
return
}

macaroons, err := UnmarshalMacaroons(macaroonBase64)
if err != nil {
ctx, cancelCause := context.WithCancelCause(r.Context())
cancelCause(fmt.Errorf("%w: %w", ErrInvalidMacaroon, err))
p.errorHandler.ServeHTTP(w, r.WithContext(ctx))
return
}

ctx := context.WithValue(r.Context(), KeyMacaroon, macaroons)

if valid := validatePreimage(macaroons, preimageHash); !valid {
ctx, cancelCause := context.WithCancelCause(ctx)
cancelCause(ErrInvalidPreimage)
p.errorHandler.ServeHTTP(w, r.WithContext(ctx))
return
}

// Check if macarron is singed by a valid key and that it grants access to the requested resource
if rejection := p.accessAuthority.ApproveAccess(r, macaroons); rejection != nil {
// The presented macaroon might not have been singed properlly or was revoked
// Or the presented macaroon is valid but doesn't grant access to this resource
// So we give the client the option to re-authenticate with a proper macaroon
ctx, cancelCause := context.WithCancelCause(ctx)
cancelCause(rejection)
p.authenticator.ServeHTTP(w, r.WithContext(ctx))
return
}

// At this point the request is valid, so we proxy the API call
p.apiHandler.ServeHTTP(w, r.WithContext(ctx))
}

const (
hexBlockSize = BlockSize * 2
expectedMatches = 3 // the header, macaroonBase64 and preimageHex
)

var authorizationMatcher = regexp.MustCompile(fmt.Sprintf(`L402 (\S+):([a-f0-9]{%d})`, hexBlockSize))

func getL402AuthorizationHeader(r *http.Request) (string, Hash, bool) {
var preimageHash Hash

for _, v := range r.Header.Values("Authorization") {
if matches := authorizationMatcher.FindStringSubmatch(v); len(matches) == expectedMatches {
macaroonBase64 := matches[1]
preimageHex := []byte(matches[2])

// preimageHex is guaranteed by authorizationMatcher to be 64 hexadecimal characters
hex.Decode(preimageHash[:], preimageHex) //nolint:errcheck
preimageHash = sha256.Sum256(preimageHash[:])

return macaroonBase64, preimageHash, true
}
}
return "", Hash{}, false
}

func validatePreimage(macaroons map[Identifier]*macaroon.Macaroon, preimageHash Hash) bool {
for identifier := range macaroons {
if identifier.PaymentHash != preimageHash {
return false
}
}
return true
}

type option func(*proxy)

func WithAuthenticator(authenticator http.Handler) option {
Expand Down
Loading

0 comments on commit bc30719

Please sign in to comment.