diff --git a/.gitignore b/.gitignore index f1c181e..527e4ea 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,9 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out + +## Own + +# lnd connection files +invoice.macaroon +tls.cert diff --git a/README.md b/README.md index c7851d9..83931d3 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Middlewares for: - [X] [net/http](https://golang.org/pkg/net/http/) `Handler` - Compatible with routers like [gorilla/mux](https://github.com/gorilla/mux) and [chi](https://github.com/go-chi/chi) - [X] [Gin](https://github.com/gin-gonic/gin) -- [ ] [Echo](https://github.com/labstack/echo) +- [X] [Echo](https://github.com/labstack/echo) An API gateway is on the roadmap as well, which you can use to monetize your API that's written in *any* language, not just in Go. @@ -157,10 +157,15 @@ func main() { r.GET("/ping", func(c *gin.Context) { c.String(200, "pong") }) + r.Run() // listen and serve on 0.0.0.0:8080 } ``` +### Echo + +See [example](examples/echo/main.go) + Related Projects ---------------- diff --git a/RELEASES.md b/RELEASES.md index 931d5ed..12a092b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,24 +8,28 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a vNext ----- +Enhancements: + +- Added: `pay.NewEchoMiddleware(...)` - A middleware factory function for [Echo](https://github.com/labstack/echo) (issue [#2](https://github.com/philippgille/ln-paywall/issues/2)) + v0.2.0 (2018-07-29) ------------------- Enhancements: -- Added: Interface `pay.StorageClient` - an abstraction for multiple storage clients, which allows you to write your own storage client for storing the preimages that have already been used as payment proof in a request +- Added: Interface `pay.StorageClient` - an abstraction for multiple storage clients, which allows you to write your own storage client for storing the preimages that have already been used as payment proof in a request (issue [#1](https://github.com/philippgille/ln-paywall/issues/1)) - Methods `WasUsed(string) (bool, error)` and `SetUsed(string) error` -- Added: Struct `pay.RedisClient` - implements the `StorageClient` interface +- Added: Struct `pay.RedisClient` - implements the `StorageClient` interface (issue [#1](https://github.com/philippgille/ln-paywall/issues/1)) - Factory function `NewRedisClient(...)` - Added: Var `pay.DefaultRedisOptions` - a `Redisoptions` object with default values -- Added: Struct `pay.GoMap` - implements the `StorageClient` interface +- Added: Struct `pay.GoMap` - implements the `StorageClient` interface (issue [#1](https://github.com/philippgille/ln-paywall/issues/1)) - Factory function `NewGoMap()` - Improved: Increased middleware performance and decreased load on the connected lnd when invalid requests with the `x-preimage` header are received (invalid because the preimage was already used) - Instead of first getting a corresponding invoice for a preimage from the lnd and *then* checking if the preimage was used already, the order of these operations was switched, because then, if the preimage was already used, no request to lnd needs to be made anymore. - Improved: All fields of the struct `pay.RedisOptions` are now optional Breaking changes: -- Changed: `pay.NewHandlerFuncMiddleware(...)`, `pay.NewHandlerMiddleware(...)` and `pay.NewGinMiddleware(...)` now take a `ln.StorageClient` instead of a `*redis.Client` as parameter +- Changed: `pay.NewHandlerFuncMiddleware(...)`, `pay.NewHandlerMiddleware(...)` and `pay.NewGinMiddleware(...)` now take a `ln.StorageClient` instead of a `*redis.Client` as parameter (issue [#1](https://github.com/philippgille/ln-paywall/issues/1)) - Changed: `ln.CheckPreimage(...)` was renamed to `ln.CheckInvoice(...)` and doesn't check the storage anymore. The `ln` methods are supposed to just handle lightning related things and nothing else. This shouldn't affect anyone, because `ln` is meant to be used only internally. - Removed: Package `lnrpc` - Instead of using our own generated lnd gRPC Go file, import the one from Lightning Labs. This shouldn't affect anyone, because it was meant to be used only internally. diff --git a/examples/echo/main.go b/examples/echo/main.go new file mode 100644 index 0000000..b2bb200 --- /dev/null +++ b/examples/echo/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo" + + "github.com/philippgille/ln-paywall/pay" +) + +func main() { + e := echo.New() + + // Configure and use middleware + invoiceOptions := pay.InvoiceOptions{ + Amount: 1, + Memo: "Ping API call", + } + lndOptions := pay.LNDoptions{ + Address: "123.123.123.123:10009", + CertFile: "tls.cert", + MacaroonFile: "invoice.macaroon", + } + redisClient := pay.NewRedisClient(pay.DefaultRedisOptions) // Connects to localhost:6379 + e.Use(pay.NewEchoMiddleware(invoiceOptions, lndOptions, redisClient, nil)) + + e.GET("/ping", func(c echo.Context) error { + return c.String(http.StatusOK, "pong") + }) + + e.Logger.Fatal(e.Start(":8080")) // Start server +} diff --git a/pay/middleware.go b/pay/middleware.go index f2f1f6b..ad661d8 100644 --- a/pay/middleware.go +++ b/pay/middleware.go @@ -7,6 +7,8 @@ import ( "os" "github.com/gin-gonic/gin" + "github.com/labstack/echo" + "github.com/labstack/echo/middleware" "github.com/philippgille/ln-paywall/ln" ) @@ -107,7 +109,6 @@ func NewGinMiddleware(invoiceOptions InvoiceOptions, lndOptions LNDoptions, stor ctx.Abort() } else { stdOutLogger.Printf("Sending invoice in response: %v", invoice) - // Note: w.Header().Set(...) must be called before w.WriteHeader(...)! ctx.Header("Content-Type", "application/vnd.lightning.bolt11") ctx.Status(http.StatusPaymentRequired) // The actual invoice goes into the body @@ -139,6 +140,74 @@ func NewGinMiddleware(invoiceOptions InvoiceOptions, lndOptions LNDoptions, stor } } +// NewEchoMiddleware returns an Echo middleware in the form of an echo.MiddlewareFunc. +func NewEchoMiddleware(invoiceOptions InvoiceOptions, lndOptions LNDoptions, storageClient StorageClient, skipper middleware.Skipper) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + if skipper == nil { + skipper = middleware.DefaultSkipper + } + return func(ctx echo.Context) error { + if skipper(ctx) { + return next(ctx) + } + // Check if the request contains a header with the preimage that we need to check if the requester paid + preimage := ctx.Request().Header.Get("x-preimage") + if preimage == "" { + // Generate the invoice + invoice, err := ln.GenerateInvoice(invoiceOptions.Amount, invoiceOptions.Memo, + lndOptions.Address, lndOptions.CertFile, lndOptions.MacaroonFile) + if err != nil { + errorMsg := fmt.Sprintf("Couldn't generate invoice: %+v", err) + log.Println(errorMsg) + return &echo.HTTPError{ + Code: http.StatusBadRequest, + Message: errorMsg, + Internal: err, + } + } else { + stdOutLogger.Printf("Sending invoice in response: %v", invoice) + ctx.Response().Header().Set("Content-Type", "application/vnd.lightning.bolt11") + ctx.Response().Status = http.StatusPaymentRequired + // The actual invoice goes into the body + ctx.Response().Write([]byte(invoice)) + return &echo.HTTPError{ + Code: http.StatusPaymentRequired, + Message: invoice, + } + } + } else { + // Check if the provided preimage belongs to a settled API payment invoice and that it wasn't already used and store used preimages + ok, err := handlePreimage(preimage, lndOptions.Address, lndOptions.CertFile, lndOptions.MacaroonFile, storageClient) + if err != nil { + errorMsg := fmt.Sprintf("An error occured during checking the preimage: %+v", err) + log.Printf("%v\n", errorMsg) + return &echo.HTTPError{ + Code: http.StatusBadRequest, + Message: errorMsg, + Internal: err, + } + } else { + if !ok { + errorMsg := "The provided preimage is invalid" + log.Printf("%v: %v\n", errorMsg, preimage) + return &echo.HTTPError{ + Code: http.StatusBadRequest, + Message: errorMsg, + Internal: err, + } + } else { + preimageHash, err := ln.HashPreimage(preimage) + if err == nil { + stdOutLogger.Printf("The provided preimage is valid. Continuing to the next HandlerFunc. Preimage hash: %v\n", preimageHash) + } + } + } + } + return next(ctx) + } + } +} + // StorageClient is an abstraction for different storage client implementations. // A storage client must only be able to check if a preimage was already used for a payment bofore // and to store a preimage that was used before.