From 41ac4f60ae4975008674999744e36eed019e6105 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 16 Jun 2024 10:34:27 +0200 Subject: [PATCH 01/11] gitignore: ignore local development files --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 66fd13c..6f3290c 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +# Local development files +/.config-dev.json +/.workdir-dev From 045e987dfdc010c88d03b317cb8a80d5745376b6 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 16 Jun 2024 10:41:01 +0200 Subject: [PATCH 02/11] config: fix syntax error --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 776355b..88255c9 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "RPCHost": "localhost:10009", "InvoiceMacaroonPath": "/home/alice/.lnd/data/chain/bitcoin/mainnet/invoices.macaroon", "TLSCertPath": "/home/alice/.lnd/tls.cert", - "WorkingDir": "/home/alice/.go-host-lnaddr",, + "WorkingDir": "/home/alice/.go-host-lnaddr", "Private": true, "LightningAddresses": [ "tips@allmysats.com" From 04917d7d5b48e69a6492adf93d757e3b4755ef7b Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 16 Jun 2024 11:43:42 +0200 Subject: [PATCH 03/11] multi: add nicer logging library --- go.mod | 6 +++- go.sum | 15 ++++++++- invoice_manager.go | 1 - main.go | 78 +++++++++++++++++++++++++++++----------------- 4 files changed, 69 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index 166dada..79fde91 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/hieblmi/go-host-lnaddr go 1.19 require ( + github.com/MadAppGang/httplog v1.3.0 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/lightningnetwork/lnd v0.17.2-beta @@ -11,6 +12,7 @@ require ( ) require ( + github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/siphash v1.0.1 // indirect github.com/andybalholm/brotli v1.0.3 // indirect @@ -39,6 +41,7 @@ require ( github.com/decred/dcrd/lru v1.0.0 // indirect github.com/dsnet/compress v0.0.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect + github.com/fatih/color v1.13.0 // indirect github.com/fergusstrange/embedded-postgres v1.10.0 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/gofrs/uuid v4.2.0+incompatible // indirect @@ -89,7 +92,8 @@ require ( github.com/lightningnetwork/lnd/tlv v1.1.1 // indirect github.com/lightningnetwork/lnd/tor v1.1.2 // indirect github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mholt/archiver/v3 v3.5.0 // indirect github.com/miekg/dns v1.1.43 // indirect diff --git a/go.sum b/go.sum index 4d3e367..235506d 100644 --- a/go.sum +++ b/go.sum @@ -37,9 +37,13 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/MadAppGang/httplog v1.3.0 h1:1XU54TO8kiqTeO+7oZLKAM3RP/cJ7SadzslRcKspVHo= +github.com/MadAppGang/httplog v1.3.0/go.mod h1:gpYEdkjh/Cda6YxtDy4AB7KY+fR7mb3SqBZw74A5hJ4= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2 h1:ZBbLwSJqkHBuFDA6DUhhse0IGJ7T5bemHyNILUjvOq4= +github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw57q4QFiWDbRnjdX8Cb3Ow0SFncRw+bA/ofY6Q83w= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= @@ -163,6 +167,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fergusstrange/embedded-postgres v1.10.0 h1:YnwF6xAQYmKLAXXrrRx4rHDLih47YJwVPvg8jeKfdNg= github.com/fergusstrange/embedded-postgres v1.10.0/go.mod h1:a008U8/Rws5FtIOTGYDYa7beVWsT3qVKyqExqYYjL+c= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= @@ -271,6 +277,7 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 h1:ajue7SzQMywqRjg2fK7dcpc0QhFG github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0/go.mod h1:r1hZAcvfFXuYmcKyCJI9wlyOPIZUJl6FCB8Cpca/NLE= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= @@ -418,12 +425,17 @@ github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQN github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -767,6 +779,7 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/invoice_manager.go b/invoice_manager.go index 086e959..73bd027 100644 --- a/invoice_manager.go +++ b/invoice_manager.go @@ -34,7 +34,6 @@ func NewInvoiceManager(cfg *InvoiceManagerConfig) *InvoiceManager { func (m *InvoiceManager) handleInvoiceCreation(config ServerConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - log.Infof("Handling invoice creation: %v\n", *r) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") diff --git a/main.go b/main.go index a44a2fe..8de1897 100644 --- a/main.go +++ b/main.go @@ -5,17 +5,18 @@ import ( "encoding/json" "flag" "fmt" + baselog "log" + "net/http" + "os" + "strings" + + "github.com/MadAppGang/httplog" "github.com/btcsuite/btclog" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/macaroons" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "gopkg.in/macaroon.v2" - "io/ioutil" - default_log "log" - "net/http" - "os" - "strings" ) type ServerConfig struct { @@ -38,12 +39,12 @@ type ServerConfig struct { } type LNUrlPay struct { - MinSendable int `json:"minSendable"` - MaxSendable int `json:"maxSendable"` - CommentAllowed int `json:"commentAllowed"` - Tag string `json:"tag"` - Metadata string `json:"metadata"` - Callback string `json:"callback"` + MinSendable int `json:"minSendable"` + MaxSendable int `json:"maxSendable"` + CommentAllowed int `json:"commentAllowed"` + Tag string `json:"tag"` + Metadata string `json:"metadata"` + Callback string `json:"callback"` } type Invoice struct { @@ -78,21 +79,23 @@ func main() { flag.Parse() file, err := os.Open(*c) if err != nil { - default_log.Fatalf("cannot open config file %v", err) + baselog.Fatalf("cannot open config file %v", err) } - defer file.Close() + defer func() { + _ = file.Close() + }() config := ServerConfig{} decoder := json.NewDecoder(file) err = decoder.Decode(&config) if err != nil { - default_log.Fatalf("cannot decode config JSON %v", err) + baselog.Fatalf("cannot decode config JSON %v", err) } workingDir := config.WorkingDir log, err = GetLogger(workingDir, "LNADDR") if err != nil { - default_log.Fatalf("cannot get logger %v", err) + baselog.Fatalf("cannot get logger %v", err) } log.Infof("Starting lightning address server on port %v...", @@ -120,10 +123,29 @@ func main() { setupNostrHandlers(config.Nostr) setupNotificators(config) - http.HandleFunc("/invoice/", invoiceManager.handleInvoiceCreation( - config), + http.HandleFunc("/invoice/", useLogger( + invoiceManager.handleInvoiceCreation(config), + )) + err = http.ListenAndServe( + fmt.Sprintf(":%d", config.AddressServerPort), nil, ) - http.ListenAndServe(fmt.Sprintf(":%d", config.AddressServerPort), nil) + if err != nil { + log.Errorf("unable to start server: %v", err) + } +} + +func useLogger(h http.HandlerFunc) http.HandlerFunc { + logger := httplog.LoggerWithConfig(httplog.LoggerConfig{ + Formatter: httplog.ChainLogFormatter( + httplog.DefaultLogFormatter, + httplog.RequestHeaderLogFormatter, + httplog.RequestBodyLogFormatter, + httplog.ResponseHeaderLogFormatter, + httplog.ResponseBodyLogFormatter, + ), + CaptureBody: true, + }) + return logger(h).ServeHTTP } func setupHandlerPerAddress(config ServerConfig) { @@ -134,13 +156,14 @@ func setupHandlerPerAddress(config ServerConfig) { for _, addr := range config.LightningAddresses { addr := strings.Split(addr, "@")[0] endpoint := fmt.Sprintf("/.well-known/lnurlp/%s", addr) - http.HandleFunc(endpoint, handleLNUrlp(config, metadata)) + http.HandleFunc( + endpoint, useLogger(handleLNUrlp(config, metadata)), + ) } } func handleLNUrlp(config ServerConfig, metadata string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - log.Infof("LNUrlp request: %v\n", *r) resp := LNUrlPay{ MinSendable: config.MinSendableMsat, MaxSendable: config.MaxSendableMsat, @@ -152,7 +175,7 @@ func handleLNUrlp(config ServerConfig, metadata string) http.HandlerFunc { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(resp) + _ = json.NewEncoder(w).Encode(resp) } } @@ -163,18 +186,17 @@ func setupNostrHandlers(nostr *NostrConfig) { http.HandleFunc( "/.well-known/nostr.json", - func(w http.ResponseWriter, r *http.Request) { + useLogger(func(w http.ResponseWriter, r *http.Request) { log.Infof("Nostr request: %#v\n", *r) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(nostr) - }, + _ = json.NewEncoder(w).Encode(nostr) + }), ) } func metadataToString(config ServerConfig) (string, error) { - thumbnailMetadata, err := thumbnailToMetadata(config.Thumbnail) if thumbnailMetadata != nil { @@ -187,7 +209,7 @@ func metadataToString(config ServerConfig) (string, error) { } func thumbnailToMetadata(thumbnailPath string) ([]string, error) { - bytes, err := ioutil.ReadFile(thumbnailPath) + bytes, err := os.ReadFile(thumbnailPath) if err != nil { return nil, err } @@ -211,7 +233,7 @@ func badRequestError(w http.ResponseWriter, reason string, args ...interface{}) { w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(Error{ + _ = json.NewEncoder(w).Encode(Error{ Status: "Error", Reason: fmt.Sprintf(reason, args...), }) @@ -260,7 +282,7 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (*grpc.ClientConn, // gRPC dial options from it. func readMacaroon(macPath string) (grpc.DialOption, error) { // Load the specified macaroon file. - macBytes, err := ioutil.ReadFile(macPath) + macBytes, err := os.ReadFile(macPath) if err != nil { return nil, fmt.Errorf("unable to read macaroon path : %v", err) } From 465865edf62e54564ef43d7b592603bce99a28db Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 16 Jun 2024 15:32:09 +0200 Subject: [PATCH 04/11] config.json: fix comment length property name --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 88255c9..c93dc5a 100644 --- a/config.json +++ b/config.json @@ -9,7 +9,7 @@ ], "MinSendableMsat": 1000, "MaxSendableMsat": 100000000, - "CommentAllowed": 150, + "MaxCommentLength": 150, "Tag": "payRequest", "Metadata": [ [ From 08774605310c41e1678924fd258752710ae234c2 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 16 Jun 2024 15:32:46 +0200 Subject: [PATCH 05/11] main: simplify config parsing --- main.go | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 8de1897..2077e58 100644 --- a/main.go +++ b/main.go @@ -77,17 +77,14 @@ func main() { "config", "./config.json", "Specify the configuration file", ) flag.Parse() - file, err := os.Open(*c) + + configBytes, err := os.ReadFile(*c) if err != nil { - baselog.Fatalf("cannot open config file %v", err) + baselog.Fatalf("cannot read config file '%s': %v", *c, err) } - defer func() { - _ = file.Close() - }() config := ServerConfig{} - decoder := json.NewDecoder(file) - err = decoder.Decode(&config) + err = json.Unmarshal(configBytes, &config) if err != nil { baselog.Fatalf("cannot decode config JSON %v", err) } From 53c084aac522be420ed5b9471ae7fb239f253cf3 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 16 Jun 2024 15:33:00 +0200 Subject: [PATCH 06/11] main: send correct status code --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 2077e58..b84134d 100644 --- a/main.go +++ b/main.go @@ -171,7 +171,7 @@ func handleLNUrlp(config ServerConfig, metadata string) http.HandlerFunc { } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") - w.WriteHeader(http.StatusCreated) + w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(resp) } } @@ -187,7 +187,7 @@ func setupNostrHandlers(nostr *NostrConfig) { log.Infof("Nostr request: %#v\n", *r) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") - w.WriteHeader(http.StatusCreated) + w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(nostr) }), ) From ef92daa5644b26e76f1c1da21cb5c12b051e94ba Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Sun, 16 Jun 2024 15:33:02 +0200 Subject: [PATCH 07/11] main: avoid macaroon expiring in 60 seconds --- main.go | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/main.go b/main.go index b84134d..cc96831 100644 --- a/main.go +++ b/main.go @@ -289,28 +289,8 @@ func readMacaroon(macPath string) (grpc.DialOption, error) { return nil, fmt.Errorf("unable to decode macaroon: %v", err) } - macConstraints := []macaroons.Constraint{ - // We add a time-based constraint to prevent replay of the - // macaroon. It's good for 60 seconds by default to make up for - // any discrepancy between client and server clocks, but leaking - // the macaroon before it becomes invalid makes it possible for - // an attacker to reuse the macaroon. In addition, the validity - // time of the macaroon is extended by the time the server clock - // is behind the client clock, or shortened by the time the - // server clock is ahead of the client clock (or invalid - // altogether if, in the latter case, this time is more than 60 - // seconds). - macaroons.TimeoutConstraint(macaroonTimeout), - } - - // Apply constraints to the macaroon. - constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...) - if err != nil { - return nil, err - } - // Now we append the macaroon credentials to the dial options. - cred, err := macaroons.NewMacaroonCredential(constrainedMac) + cred, err := macaroons.NewMacaroonCredential(mac) if err != nil { return nil, fmt.Errorf("error creating macaroon credential: %v", err) From 620ee96a5fd2d2ad011ac6ec8670ff4471d9e75a Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 17 Jun 2024 14:16:18 +0200 Subject: [PATCH 08/11] main: log connection errors --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index cc96831..1ad1f12 100644 --- a/main.go +++ b/main.go @@ -102,7 +102,7 @@ func main() { config.RPCHost, config.TLSCertPath, config.InvoiceMacaroonPath, ) if err != nil { - log.Errorf("unable to get a lnd client connection") + log.Errorf("unable to get a lnd client connection: %v", err) return } From c9095928d270a7b2727bacb25d843d4e82c9aeb3 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 17 Jun 2024 14:16:33 +0200 Subject: [PATCH 09/11] http_notif: implement HTTP notifications --- http_notif.go | 108 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 3 deletions(-) diff --git a/http_notif.go b/http_notif.go index 0284883..bfb5605 100644 --- a/http_notif.go +++ b/http_notif.go @@ -1,17 +1,119 @@ package main +import ( + "bytes" + "fmt" + "io" + "net/http" + "text/template" +) + +type encoding string + +func (e encoding) AddHeader(header http.Header) { + header.Add("Content-Type", string(e)) +} + +func (e encoding) EscapeValue(value string) string { + switch e { + case EncodingForm: + return template.URLQueryEscaper(value) + + case EncodingJson: + return template.JSEscapeString(value) + + default: + return value + } +} + +const ( + EncodingForm encoding = "application/x-www-form-urlencoded" + EncodingJson encoding = "application/json" +) + type httpNotificator struct { - URL string + URL string + Method string + Encoding encoding + BodyTemplate string } var _ notificator = (*httpNotificator)(nil) func NewHttpNotificator(cfg notificatorConfig) *httpNotificator { - return &httpNotificator{URL: cfg.Params["Target"]} + return &httpNotificator{ + URL: cfg.Params["Target"], + Method: cfg.Params["Method"], + Encoding: encoding(cfg.Params["Encoding"]), + BodyTemplate: cfg.Params["BodyTemplate"], + } } func (h *httpNotificator) Notify(amount uint64, comment string) error { - return nil // currently not implemented + bodyData := &struct { + Amount uint64 + Message string + }{ + Amount: amount, + Message: h.Encoding.EscapeValue(comment), + } + + urlTemplate, err := template.New("url").Parse(h.URL) + if err != nil { + return fmt.Errorf("error building URL template: %w", err) + } + + bodyTemplate, err := template.New("body").Parse(h.BodyTemplate) + if err != nil { + return fmt.Errorf("error building body template: %w", err) + } + + var buf bytes.Buffer + err = urlTemplate.Execute(&buf, bodyData) + if err != nil { + return fmt.Errorf("error executing URL template: %w", err) + } + url := buf.String() + + buf.Reset() + err = bodyTemplate.Execute(&buf, bodyData) + if err != nil { + return fmt.Errorf("error executing body template: %w", err) + } + + var bodyReader io.Reader + if h.Method == http.MethodPost { + bodyReader = &buf + } + + req, err := http.NewRequest(h.Method, url, bodyReader) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + + h.Encoding.AddHeader(req.Header) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("error sending request: %w", err) + } + + defer func() { + _ = resp.Body.Close() + }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("unexpected status code: %d (%s)", + resp.StatusCode, body) + } + + return nil } func (h *httpNotificator) Target() string { From 3b318ad70e6e3ab51dad535cc78c8f4c1b7bd1e4 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 17 Jun 2024 14:16:44 +0200 Subject: [PATCH 10/11] config: format, add HTTP notification example --- config.json | 122 +++++++++++++++++++++++++++++----------------------- 1 file changed, 67 insertions(+), 55 deletions(-) diff --git a/config.json b/config.json index c93dc5a..4ef4879 100644 --- a/config.json +++ b/config.json @@ -1,57 +1,69 @@ { - "RPCHost": "localhost:10009", - "InvoiceMacaroonPath": "/home/alice/.lnd/data/chain/bitcoin/mainnet/invoices.macaroon", - "TLSCertPath": "/home/alice/.lnd/tls.cert", - "WorkingDir": "/home/alice/.go-host-lnaddr", - "Private": true, - "LightningAddresses": [ - "tips@allmysats.com" - ], - "MinSendableMsat": 1000, - "MaxSendableMsat": 100000000, - "MaxCommentLength": 150, - "Tag": "payRequest", - "Metadata": [ - [ - "text/plain", - "Welcome to allmysats.com" - ], - [ - "text/identifier", - "tips@allmysats.com" - ] - ], - "Thumbnail": "/path/to/thumbnail.[jpeg|png]", - "SuccessMessage": "Thank you!", - "InvoiceCallback": "https://allmysats.com/invoice/", - "AddressServerPort": 9990, - "Nostr": { - "names": { - "myNostrUsername": "npub1h....." - }, - "relays": { - "b9b.....": ["wss://my.relay.com"] - } - }, - "Notificators": [ - { - "Type": "mail", - "MinAmount": 1000, - "Params": { - "From": "tips@allmysats.com", - "Target": "username@example.com", - "SmtpServer": "smtp.allmysats.com:587", - "Login": "tips@allmysats.com", - "Password": "somerandompassword" - } - }, - { - "Type": "telegram", - "MinAmount": 1000, - "Params": { - "ChatId": "1234567890", - "Token": "TelegramToken" - } - } - ] + "RPCHost": "localhost:10009", + "InvoiceMacaroonPath": "/home/alice/.lnd/data/chain/bitcoin/mainnet/invoices.macaroon", + "TLSCertPath": "/home/alice/.lnd/tls.cert", + "WorkingDir": "/home/alice/.go-host-lnaddr", + "Private": true, + "LightningAddresses": [ + "tips@allmysats.com" + ], + "MinSendableMsat": 1000, + "MaxSendableMsat": 100000000, + "MaxCommentLength": 150, + "Tag": "payRequest", + "Metadata": [ + [ + "text/plain", + "Welcome to allmysats.com" + ], + [ + "text/identifier", + "tips@allmysats.com" + ] + ], + "Thumbnail": "/path/to/thumbnail.[jpeg|png]", + "SuccessMessage": "Thank you!", + "InvoiceCallback": "https://allmysats.com/invoice/", + "AddressServerPort": 9990, + "Nostr": { + "names": { + "myNostrUsername": "npub1h....." + }, + "relays": { + "b9b.....": [ + "wss://my.relay.com" + ] + } + }, + "Notificators": [ + { + "Type": "mail", + "MinAmount": 1000, + "Params": { + "From": "tips@allmysats.com", + "Target": "username@example.com", + "SmtpServer": "smtp.allmysats.com:587", + "Login": "tips@allmysats.com", + "Password": "somerandompassword" + } + }, + { + "Type": "telegram", + "MinAmount": 1000, + "Params": { + "ChatId": "1234567890", + "Token": "TelegramToken" + } + }, + { + "Type": "http", + "MinAmount": 1000, + "Params": { + "Target": "https://example.com/notify?amount={{.Amount}}", + "Method": "POST", + "Encoding": "application/x-www-form-urlencoded", + "BodyTemplate": "message={{.Message}}&title=New+payment+received" + } + } + ] } From 8bbcb0165ab2aadc041755ea5f3c6d6ce98033e0 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Mon, 17 Jun 2024 14:49:52 +0200 Subject: [PATCH 11/11] multi: add configurable index page that lists users --- config.json | 3 +- go.mod | 2 ++ go.sum | 3 ++ main.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index 4ef4879..8dc8659 100644 --- a/config.json +++ b/config.json @@ -3,7 +3,8 @@ "InvoiceMacaroonPath": "/home/alice/.lnd/data/chain/bitcoin/mainnet/invoices.macaroon", "TLSCertPath": "/home/alice/.lnd/tls.cert", "WorkingDir": "/home/alice/.go-host-lnaddr", - "Private": true, + "ExternalURL": "https://allmysats.com", + "ListAllURLs": true, "LightningAddresses": [ "tips@allmysats.com" ], diff --git a/go.mod b/go.mod index 79fde91..d4090e7 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.19 require ( github.com/MadAppGang/httplog v1.3.0 github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f + github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/lightningnetwork/lnd v0.17.2-beta + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e google.golang.org/grpc v1.58.3 gopkg.in/macaroon.v2 v2.1.0 ) diff --git a/go.sum b/go.sum index 235506d..78587ca 100644 --- a/go.sum +++ b/go.sum @@ -86,6 +86,7 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2 h1:KdUfX2zKommPRa+PD0sWZUyXe9 github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d h1:yJzD/yFppdVCf6ApMkVy8cUxV0XrxdP9rVf6D87/Mng= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcwallet v0.16.10-0.20231017144732-e3ff37491e9c h1:+7tbYEUj0TYYIvuvE9YP+x5dU3FT/8J6Qh8d5YvQwrE= github.com/btcsuite/btcwallet v0.16.10-0.20231017144732-e3ff37491e9c/go.mod h1:WSKhOJWUmUOHKCKEzdt+jWAHFAE/t4RqVbCwL2pEdiU= @@ -516,6 +517,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= diff --git a/main.go b/main.go index 1ad1f12..a449152 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,12 @@ package main import ( + "bytes" "encoding/base64" "encoding/json" "flag" "fmt" + "html/template" baselog "log" "net/http" "os" @@ -12,8 +14,10 @@ import ( "github.com/MadAppGang/httplog" "github.com/btcsuite/btclog" + "github.com/btcsuite/btcutil/bech32" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/macaroons" + "github.com/skip2/go-qrcode" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "gopkg.in/macaroon.v2" @@ -24,6 +28,8 @@ type ServerConfig struct { InvoiceMacaroonPath string TLSCertPath string WorkingDir string + ExternalURL string + ListAllURLs bool LightningAddresses []string MinSendableMsat int MaxSendableMsat int @@ -119,6 +125,7 @@ func main() { setupHandlerPerAddress(config) setupNostrHandlers(config.Nostr) setupNotificators(config) + setupIndexHandler(config) http.HandleFunc("/invoice/", useLogger( invoiceManager.handleInvoiceCreation(config), @@ -193,6 +200,90 @@ func setupNostrHandlers(nostr *NostrConfig) { ) } +func setupIndexHandler(config ServerConfig) { + if !config.ListAllURLs || len(config.LightningAddresses) == 0 || + config.ExternalURL == "" { + return + } + + type user struct { + User string + Encoded string + QRCode string + } + + var users []user + for _, addr := range config.LightningAddresses { + userName := strings.Split(addr, "@")[0] + url := fmt.Sprintf("%s/.well-known/lnurlp/%s", + config.ExternalURL, userName) + + converted, err := bech32.ConvertBits([]byte(url), 8, 5, true) + if err != nil { + log.Errorf("Unable to convert url: %v", err) + } + + lnurl, err := bech32.Encode("lnurl", converted) + if err != nil { + log.Errorf("Unable to encode url: %v", err) + continue + } + + png, err := qrcode.Encode(lnurl, qrcode.Highest, 256) + if err != nil { + log.Errorf("Unable to encode QR code: %v", err) + continue + } + + users = append(users, user{ + User: userName, + Encoded: lnurl, + QRCode: base64.StdEncoding.EncodeToString(png), + }) + + } + htmlTemplate := ` + + + LNURLs + + +

LNURLs

+
    + {{range .}} +
  • +

    User: {{.User}}

    +
    +
    {{.Encoded}}
    +
  • + {{end}} +
+ + +` + + bodyTemlate, err := template.New("html").Parse(htmlTemplate) + if err != nil { + log.Errorf("Error building URL template: %w", err) + return + } + + var buf bytes.Buffer + err = bodyTemlate.Execute(&buf, users) + if err != nil { + log.Errorf("Error executing URL template: %w", err) + return + } + + http.HandleFunc( + "/", useLogger(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(buf.Bytes()) + }), + ) +} + func metadataToString(config ServerConfig) (string, error) { thumbnailMetadata, err := thumbnailToMetadata(config.Thumbnail)