diff --git a/aperture.go b/aperture.go index 9e99747..5b29973 100644 --- a/aperture.go +++ b/aperture.go @@ -96,10 +96,10 @@ func start() error { } // Create the proxy and connect it to lnd. - genInvoiceReq := func() (*lnrpc.Invoice, error) { + genInvoiceReq := func(price int64) (*lnrpc.Invoice, error) { return &lnrpc.Invoice{ Memo: "LSAT", - Value: 1, + Value: price, }, nil } servicesProxy, err := createProxy(cfg, genInvoiceReq, etcdClient) diff --git a/auth/authenticator.go b/auth/authenticator.go index 9ce5db8..ce568ea 100644 --- a/auth/authenticator.go +++ b/auth/authenticator.go @@ -59,9 +59,13 @@ func (l *LsatAuthenticator) Accept(header *http.Header, serviceName string) bool // // NOTE: This is part of the Authenticator interface. func (l *LsatAuthenticator) FreshChallengeHeader(r *http.Request, - serviceName string) (http.Header, error) { + serviceName string, servicePrice int64) (http.Header, error) { - service := lsat.Service{Name: serviceName, Tier: lsat.BaseTier} + service := lsat.Service{ + Name: serviceName, + Tier: lsat.BaseTier, + Price: servicePrice, + } mac, paymentRequest, err := l.minter.MintLSAT( context.Background(), service, ) diff --git a/auth/interface.go b/auth/interface.go index 6a44e62..70129a5 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -18,7 +18,7 @@ type Authenticator interface { // FreshChallengeHeader returns a header containing a challenge for the // user to complete. - FreshChallengeHeader(*http.Request, string) (http.Header, error) + FreshChallengeHeader(*http.Request, string, int64) (http.Header, error) } // Minter is an entity that is able to mint and verify LSATs for a set of diff --git a/auth/mock_authenticator.go b/auth/mock_authenticator.go index 8078f3f..e52ff17 100644 --- a/auth/mock_authenticator.go +++ b/auth/mock_authenticator.go @@ -32,7 +32,7 @@ func (a MockAuthenticator) Accept(header *http.Header, _ string) bool { // FreshChallengeHeader returns a header containing a challenge for the user to // complete. func (a MockAuthenticator) FreshChallengeHeader(r *http.Request, - _ string) (http.Header, error) { + _ string, _ int64) (http.Header, error) { header := r.Header header.Set( diff --git a/challenger.go b/challenger.go index 0ea3925..9d8b4bd 100644 --- a/challenger.go +++ b/challenger.go @@ -12,7 +12,7 @@ import ( // InvoiceRequestGenerator is a function type that returns a new request for the // lnrpc.AddInvoice call. -type InvoiceRequestGenerator func() (*lnrpc.Invoice, error) +type InvoiceRequestGenerator func(price int64) (*lnrpc.Invoice, error) // LndChallenger is a challenger that uses an lnd backend to create new LSAT // payment challenges. @@ -57,10 +57,10 @@ func NewLndChallenger(cfg *authConfig, genInvoiceReq InvoiceRequestGenerator) ( // request (invoice) and the corresponding payment hash. // // NOTE: This is part of the Challenger interface. -func (l *LndChallenger) NewChallenge() (string, lntypes.Hash, error) { +func (l *LndChallenger) NewChallenge(price int64) (string, lntypes.Hash, error) { // Obtain a new invoice from lnd first. We need to know the payment hash // so we can add it as a caveat to the macaroon. - invoice, err := l.genInvoiceReq() + invoice, err := l.genInvoiceReq(price) if err != nil { log.Errorf("Error generating invoice request: %v", err) return "", lntypes.ZeroHash, err diff --git a/lsat/service.go b/lsat/service.go index 5f10b5e..ff5e8d1 100644 --- a/lsat/service.go +++ b/lsat/service.go @@ -46,6 +46,9 @@ type Service struct { // Tier is the tier of the LSAT-enabled service. Tier ServiceTier + + // Price of service LSAT in satoshis. + Price int64 } // NewServicesCaveat creates a new services caveat with the provided caveats. diff --git a/mint/mint.go b/mint/mint.go index 17c5fcc..b2c905d 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -27,7 +27,7 @@ type Challenger interface { // payment request. The payment hash is also returned as a convenience // to avoid having to decode the payment request in order to retrieve // its payment hash. - NewChallenge() (string, lntypes.Hash, error) + NewChallenge(price int64) (string, lntypes.Hash, error) } // SecretStore is the store responsible for storing LSAT secrets. These secrets @@ -93,9 +93,13 @@ func New(cfg *Config) *Mint { func (m *Mint) MintLSAT(ctx context.Context, services ...lsat.Service) (*macaroon.Macaroon, string, error) { + // Let the LSAT value as the price of the most expensive of the + // services. + price := maximumPrice(services) + // We'll start by retrieving a new challenge in the form of a Lightning // payment request to present the requester of the LSAT with. - paymentRequest, paymentHash, err := m.cfg.Challenger.NewChallenge() + paymentRequest, paymentHash, err := m.cfg.Challenger.NewChallenge(price) if err != nil { return nil, "", err } @@ -143,6 +147,20 @@ func (m *Mint) MintLSAT(ctx context.Context, return mac, paymentRequest, nil } +// maximumPrice determines the necessary price to use for a collection +// of services. +func maximumPrice(services []lsat.Service) int64 { + var max int64 + + for _, service := range services { + if service.Price > max { + max = service.Price + } + } + + return max +} + // createUniqueIdentifier creates a new LSAT identifier bound to a payment hash // and a randomly generated ID. func createUniqueIdentifier(paymentHash lntypes.Hash) ([]byte, error) { diff --git a/mint/mock_test.go b/mint/mock_test.go index 46f5542..e14a700 100644 --- a/mint/mock_test.go +++ b/mint/mock_test.go @@ -26,7 +26,7 @@ func newMockChallenger() *mockChallenger { return &mockChallenger{} } -func (d *mockChallenger) NewChallenge() (string, lntypes.Hash, error) { +func (d *mockChallenger) NewChallenge(price int64) (string, lntypes.Hash, error) { return testPayReq, testHash, nil } diff --git a/proxy/proxy.go b/proxy/proxy.go index 22ed0bf..0af70d5 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -97,7 +97,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { case authLevel.IsOn(): if !p.authenticator.Accept(&r.Header, target.Name) { prefixLog.Infof("Authentication failed. Sending 402.") - p.handlePaymentRequired(w, r, target.Name) + p.handlePaymentRequired(w, r, target.Name, target.Price) return } @@ -116,7 +116,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } if !ok { - p.handlePaymentRequired(w, r, target.Name) + p.handlePaymentRequired(w, r, target.Name, target.Price) return } _, err = target.freebieDb.TallyFreebie(r, remoteIP) @@ -281,11 +281,11 @@ func addCorsHeaders(header http.Header) { // handlePaymentRequired returns fresh challenge header fields and status code // to the client signaling that a payment is required to fulfil the request. func (p *Proxy) handlePaymentRequired(w http.ResponseWriter, r *http.Request, - serviceName string) { + serviceName string, servicePrice int64) { addCorsHeaders(r.Header) - header, err := p.authenticator.FreshChallengeHeader(r, serviceName) + header, err := p.authenticator.FreshChallengeHeader(r, serviceName, servicePrice) if err != nil { log.Errorf("Error creating new challenge header: %v", err) sendDirectResponse( diff --git a/proxy/service.go b/proxy/service.go index 48b4cfa..6d084cf 100644 --- a/proxy/service.go +++ b/proxy/service.go @@ -9,6 +9,7 @@ import ( "regexp" "strings" + "github.com/btcsuite/btcutil" "github.com/lightninglabs/aperture/auth" "github.com/lightninglabs/aperture/freebie" ) @@ -19,6 +20,16 @@ var ( filePrefixBase64 = filePrefix + "+base64" ) +const ( + // defaultServicePrice is price in satoshis to be used as the default + // service price. + defaultServicePrice = 1 + + // maxServicePrice is the maximum price in satoshis that can be used + // to create an invoice through lnd. + maxServicePrice = btcutil.SatoshiPerBitcoin * 100000 +) + // Service generically specifies configuration data for backend services to the // Kirin proxy. type Service struct { @@ -69,6 +80,10 @@ type Service struct { // correspond to the caveat's condition. Constraints map[string]string `long:"constraints" description:"The service constraints to enforce at the base tier"` + // Price is the custom LSAT value in satoshis to be used for the + // service's endpoint. + Price int64 `long:"price" description:"Static LSAT value in satoshis to be used for this service"` + // AuthWhitelistPaths is an optional list of regular expressions that // are matched against the path of the URL of a request. If the request // URL matches any of those regular expressions, the call is treated as @@ -154,6 +169,23 @@ func prepareServices(services []*Service) error { "whitelist: %v", err) } } + + // Check that the price for the service is not negative and not + // more than the maximum amount allowed by lnd. If no price, or + // a price of zero satoshis, is set the then default price of 1 + // satoshi is to be used. + switch { + case service.Price == 0: + log.Debugf("Using default LSAT price of %v satoshis for "+ + "service %s.", defaultServicePrice, service.Name) + service.Price = defaultServicePrice + case service.Price < 0: + return fmt.Errorf("negative price set for "+ + "service %s", service.Name) + case service.Price > maxServicePrice: + return fmt.Errorf("maximum price exceeded for "+ + "service %s", service.Name) + } } return nil } diff --git a/sample-conf.yaml b/sample-conf.yaml index 163f468..cc2f2f5 100644 --- a/sample-conf.yaml +++ b/sample-conf.yaml @@ -78,6 +78,9 @@ services: constraints: "valid_until": "2020-01-01" + # The LSAT value in satoshis for the service. + price: 1 + - name: "service2" hostregexp: "service2.com:8083" pathregexp: '^/.*$' @@ -85,6 +88,7 @@ services: protocol: https constraints: "valid_until": "2020-01-01" + price: 1 # Settings for a Tor instance to allow requests over Tor as onion services. # Configuring Tor is optional. diff --git a/services.go b/services.go index d1d3d96..0f5a51d 100644 --- a/services.go +++ b/services.go @@ -27,7 +27,11 @@ func newStaticServiceLimiter(proxyServices []*proxy.Service) *staticServiceLimit constraints := make(map[lsat.Service][]lsat.Caveat) for _, proxyService := range proxyServices { - s := lsat.Service{Name: proxyService.Name, Tier: lsat.BaseTier} + s := lsat.Service{ + Name: proxyService.Name, + Tier: lsat.BaseTier, + Price: proxyService.Price, + } capabilities[s] = lsat.NewCapabilitiesCaveat( proxyService.Name, proxyService.Capabilities, )