lntip/lnme.go

282 lines
9.3 KiB
Go
Raw Permalink Normal View History

2019-01-07 00:35:26 +00:00
package main
import (
"crypto/sha256"
2022-01-30 12:36:56 +00:00
"embed"
2020-10-25 21:15:39 +00:00
"flag"
"fmt"
2022-02-01 19:57:28 +00:00
"io/fs"
2021-08-27 09:18:49 +00:00
"log"
"net/http"
"os"
"strconv"
2021-08-27 09:18:49 +00:00
"strings"
2020-10-25 21:15:39 +00:00
"github.com/bumi/lnme/ln"
"github.com/bumi/lnme/lnurl"
2020-10-18 20:29:18 +00:00
"github.com/didip/tollbooth/v6"
"github.com/didip/tollbooth/v6/limiter"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/toml"
2020-10-25 21:15:39 +00:00
"github.com/knadh/koanf/providers/basicflag"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
2019-01-07 00:35:26 +00:00
)
const DEFAULT_LISTEN = ":1323"
2020-10-23 18:38:18 +00:00
// Middleware for request limited to prevent too many requests
// TODO: move to file
func LimitMiddleware(lmt *limiter.Limiter) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return echo.HandlerFunc(func(c echo.Context) error {
httpError := tollbooth.LimitByRequest(lmt, c.Response(), c.Request())
if httpError != nil {
return c.String(httpError.StatusCode, httpError.Message)
}
return next(c)
})
}
}
2019-01-07 00:35:26 +00:00
var stdOutLogger = log.New(os.Stdout, "", log.LstdFlags)
type Invoice struct {
2019-02-20 00:31:03 +00:00
Value int64 `json:"value"`
Memo string `json:"memo"`
}
2022-02-01 19:23:42 +00:00
//go:embed files/assets/*
2022-01-30 12:36:56 +00:00
var embeddedAssets embed.FS
//go:embed files/root/index.html
var indexPage string
2019-01-07 00:35:26 +00:00
func main() {
2020-10-25 21:15:39 +00:00
cfg := LoadConfig()
2019-01-07 00:35:26 +00:00
2020-10-25 21:15:39 +00:00
e := echo.New()
2020-10-23 18:38:18 +00:00
2020-10-24 09:38:44 +00:00
// Serve static files if configured
if cfg.String("static-path") != "" {
e.Static("/", cfg.String("static-path"))
2020-10-24 09:38:44 +00:00
// Serve default page
} else if !cfg.Bool("disable-website") {
2022-01-30 12:36:56 +00:00
stdOutLogger.Print("Running embedded page")
e.GET("/", func(c echo.Context) error {
2022-01-31 07:35:08 +00:00
return c.HTML(200, indexPage)
2022-01-30 12:36:56 +00:00
})
2019-02-20 00:31:03 +00:00
}
2022-02-01 19:57:28 +00:00
assetSubdir, err := fs.Sub(embeddedAssets, "files/assets")
if err != nil {
log.Fatal(err)
}
2020-10-23 18:38:18 +00:00
// Embed static files and serve those on /lnme (e.g. /lnme/lnme.js)
2022-02-01 19:57:28 +00:00
assetHandler := http.FileServer(http.FS(assetSubdir))
2020-10-23 18:38:18 +00:00
e.GET("/lnme/*", echo.WrapHandler(http.StripPrefix("/lnme/", assetHandler)))
2020-10-24 09:38:44 +00:00
// CORS settings
if !cfg.Bool("disable-cors") {
2019-02-20 00:31:03 +00:00
e.Use(middleware.CORS())
}
2020-10-23 18:38:18 +00:00
2020-10-24 09:38:44 +00:00
// Recover middleware recovers from panics anywhere in the request chain
2019-01-07 00:35:26 +00:00
e.Use(middleware.Recover())
2020-10-24 09:38:44 +00:00
// Request limit per second. DoS protection
if cfg.Int("request-limit") > 0 {
limiter := tollbooth.NewLimiter(cfg.Float64("request-limit"), nil)
e.Use(LimitMiddleware(limiter))
}
2020-10-24 09:38:44 +00:00
// Setup lightning client
stdOutLogger.Printf("Connecting to %s", cfg.String("lnd-address"))
lndOptions := ln.LNDoptions{
Address: cfg.String("lnd-address"),
CertFile: cfg.String("lnd-cert-path"),
CertHex: cfg.String("lnd-cert"),
MacaroonFile: cfg.String("lnd-macaroon-path"),
MacaroonHex: cfg.String("lnd-macaroon"),
TorExePath: cfg.String("tor-exe-path"),
2019-01-07 00:35:26 +00:00
}
lnClient, err := ln.NewLNDclient(lndOptions)
if err != nil {
2019-02-20 00:31:03 +00:00
stdOutLogger.Print("Error initializing LND client:")
2019-01-07 00:35:26 +00:00
panic(err)
}
2020-10-23 18:38:18 +00:00
// Endpoint URLs compatible to the LND REST API v1
2020-10-24 09:38:44 +00:00
//
// Create new invoice
e.POST("/v1/invoices", func(c echo.Context) error {
i := new(Invoice)
if err := c.Bind(i); err != nil {
2019-02-21 01:13:31 +00:00
stdOutLogger.Printf("Bad request: %s", err)
return c.JSON(http.StatusBadRequest, "Bad request")
}
2023-01-23 09:08:26 +00:00
invoice, err := lnClient.AddInvoice(i.Value, i.Memo, nil, cfg.Bool("enable-private-channels"))
2019-01-07 00:35:26 +00:00
if err != nil {
2019-02-21 01:13:31 +00:00
stdOutLogger.Printf("Error creating invoice: %s", err)
return c.JSON(http.StatusInternalServerError, "Error adding invoice")
2019-01-07 00:35:26 +00:00
}
return c.JSON(http.StatusOK, invoice)
})
2020-10-24 09:38:44 +00:00
// Get next BTC onchain address
e.POST("/v1/newaddress", func(c echo.Context) error {
address, err := lnClient.NewAddress()
if err != nil {
stdOutLogger.Printf("Error getting a new BTC address: %s", err)
return c.JSON(http.StatusInternalServerError, "Error getting address")
2020-10-24 09:38:44 +00:00
}
return c.JSON(http.StatusOK, address)
})
2020-10-24 09:38:44 +00:00
// Check invoice status
2021-08-27 09:18:49 +00:00
e.GET("/v1/invoice/:paymentHash", func(c echo.Context) error {
paymentHash := c.Param("paymentHash")
invoice, err := lnClient.GetInvoice(paymentHash)
2019-02-21 01:13:31 +00:00
if err != nil {
stdOutLogger.Printf("Error looking up invoice: %s", err)
return c.JSON(http.StatusInternalServerError, "Error fetching invoice")
}
2019-01-07 00:35:26 +00:00
return c.JSON(http.StatusOK, invoice)
})
if !cfg.Bool("disable-ln-address") {
lnurlHandler := func(c echo.Context) error {
host := c.Request().Host
2022-05-07 10:50:15 +00:00
proto := c.Scheme()
// TODO: support RFC7239 Forwarded header
if c.Request().Header.Get("X-Forwarded-Host") != "" {
host = c.Request().Header.Get("X-Forwarded-Host")
}
2022-05-07 10:50:15 +00:00
if c.Request().Header.Get("X-Forwarded-Proto") != "" {
proto = c.Request().Header.Get("X-Forwarded-Proto")
}
name := c.Param("name")
lightningAddress := name + "@" + host
2021-08-28 20:28:03 +00:00
lnurlMetadata := "[[\"text/identifier\", \"" + lightningAddress + "\"], [\"text/plain\", \"Sats for " + lightningAddress + "\"]]"
lnurlpCommentAllowed := cfg.Int64("lnurlp-comment-allowed")
if amount := c.QueryParam("amount"); amount == "" {
lnurlPayResponse1 := lnurl.LNURLPayResponse1{
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
Callback: fmt.Sprintf("%s://%s%s", proto, host, c.Request().URL.Path),
MinSendable: 1000,
MaxSendable: 100000000,
EncodedMetadata: lnurlMetadata,
CommentAllowed: lnurlpCommentAllowed,
Tag: "payRequest",
}
return c.JSON(http.StatusOK, lnurlPayResponse1)
} else {
stdOutLogger.Printf("New LightningAddress request amount: %s", amount)
msats, err := strconv.ParseInt(amount, 10, 64)
if err != nil || msats < 1000 {
stdOutLogger.Printf("Invalid amount: %s", amount)
return c.JSON(http.StatusOK, lnurl.LNURLErrorResponse{Status: "ERROR", Reason: "Invalid Amount"})
}
sats := msats / 1000 // we need sats
comment := c.QueryParam("comment")
if commentLength := int64(len(comment)); commentLength > lnurlpCommentAllowed {
stdOutLogger.Printf("Invalid comment length: %d", commentLength)
return c.JSON(http.StatusOK, lnurl.LNURLErrorResponse{Status: "ERROR", Reason: "Invalid comment length"})
}
metadataHash := sha256.Sum256([]byte(lnurlMetadata))
2023-01-23 09:08:26 +00:00
invoice, err := lnClient.AddInvoice(sats, comment, metadataHash[:], cfg.Bool("enable-private-channels"))
if err != nil {
stdOutLogger.Printf("Error creating invoice: %s", err)
return c.JSON(http.StatusOK, lnurl.LNURLErrorResponse{Status: "ERROR", Reason: "Server Error"})
}
lnurlPayResponse2 := lnurl.LNURLPayResponse2{
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
PR: invoice.PaymentRequest,
Routes: make([][]lnurl.RouteInfo, 0),
Disposable: false,
SuccessAction: &lnurl.SuccessAction{Tag: "message", Message: "Thanks, payment received!"},
}
return c.JSON(http.StatusOK, lnurlPayResponse2)
}
}
e.GET("/.well-known/lnurlp/:name", lnurlHandler)
e.GET("/lnurlp/:name", lnurlHandler)
}
2020-10-23 18:38:18 +00:00
// Debug test endpoint
e.GET("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, "pong")
})
2021-08-27 09:18:49 +00:00
port := cfg.String("port")
// Special case for PORT instead of LNME_PORT due to cloud-providers
2021-08-27 09:18:49 +00:00
if os.Getenv("PORT") != "" {
port = os.Getenv("PORT")
}
listen := cfg.String("listen")
if listen != "" && port != "" {
log.Fatalf("Port and listen options are mutually exclusive, please just use listen.")
}
if listen == "" {
if port != "" {
stdOutLogger.Printf("Please use listen instead of deprecated port setting.")
listen = fmt.Sprintf(":%s", port)
} else {
listen = DEFAULT_LISTEN
}
}
e.Logger.Fatal(e.Start(listen))
}
func LoadConfig() *koanf.Koanf {
2020-10-25 21:15:39 +00:00
k := koanf.New(".")
2020-10-25 21:15:39 +00:00
f := flag.NewFlagSet("LnMe", flag.ExitOnError)
f.String("lnd-address", "localhost:10009", "The host and port of the LND gRPC server.")
f.String("lnd-macaroon-path", "~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon", "Path to the LND macaroon file.")
2022-04-15 00:57:51 +00:00
f.String("lnd-macaroon", "", "HEX string of LND macaroon file.")
f.String("lnd-cert-path", "~/.lnd/tls.cert", "Path to the LND tls.cert file.")
2022-04-15 00:57:51 +00:00
f.String("lnd-cert", "", "HEX string of LND tls cert file.")
f.Int64("lnurlp-comment-allowed", 210, "Allowed length of LNURL-pay comments.")
f.Bool("disable-website", false, "Disable default embedded website.")
f.Bool("disable-ln-address", false, "Disable Lightning Address handling")
f.Bool("disable-cors", false, "Disable CORS headers.")
2023-01-23 09:08:26 +00:00
f.Bool("enable-private-channels", false, "Adds private routing hints to invoices")
f.Float64("request-limit", 5, "Request limit per second.")
f.String("static-path", "", "Path to a static assets directory.")
f.String("port", "", "Port to bind on (deprecated - use listen).")
f.String("listen", "", fmt.Sprintf("Address to bind on. (default \"%s\")", DEFAULT_LISTEN))
f.String("tor-exe-path", "tor", "Path to the Tor executable. Used when connecting through Tor. (default: tor)")
2020-10-25 21:15:39 +00:00
var configPath string
f.StringVar(&configPath, "config", "config.toml", "Path to a .toml config file.")
2020-10-25 21:15:39 +00:00
f.Parse(os.Args[1:])
2020-10-25 21:15:39 +00:00
// Load config from flags, including defaults
if err := k.Load(basicflag.Provider(f, "."), nil); err != nil {
log.Fatalf("Error loading config: %v", err)
}
2020-10-25 21:15:39 +00:00
// Load config from environment variables
k.Load(env.Provider("LNME_", ".", func(s string) string {
return strings.Replace(strings.ToLower(strings.TrimPrefix(s, "LNME_")), "_", "-", -1)
}), nil)
2020-10-25 21:15:39 +00:00
// Load config from file if available
if configPath != "" {
if _, err := os.Stat(configPath); err == nil {
if err := k.Load(file.Provider(configPath), toml.Parser()); err != nil {
log.Fatalf("Error loading config file: %v", err)
}
}
}
2020-10-25 21:15:39 +00:00
return k
2019-01-07 00:35:26 +00:00
}