diff --git a/README.md b/README.md index fa97ed6..554faf2 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ Federated Lightning Address Server 1. Download the binary from the releases page (or compile with `go build` or `go get`) 2. Set the following environment variables somehow (using example values from bitmia.com): +(note that DOMAIN can be a comma-seperated list or a single domain, when using multiple domains +you need to make sure "Host" HTTP header is forwarded to satdress process if you have some reverse-proxy) ``` PORT=17422 diff --git a/api.go b/api.go index d6a9ade..4ca9cc5 100644 --- a/api.go +++ b/api.go @@ -18,6 +18,7 @@ type Response struct { type SuccessClaim struct { Name string `json:"name"` + Domain string `json:"domain"` PIN string `json:"pin"` Invoice string `json:"invoice"` } @@ -25,7 +26,7 @@ type SuccessClaim struct { // not authenticated, if correct pin is provided call returns the SuccessClaim func ClaimAddress(w http.ResponseWriter, r *http.Request) { params := parseParams(r) - pin, inv, err := SaveName(params.Name, params, params.Pin) + pin, inv, err := SaveName(params.Name, params.Domain, params, params.Pin) if err != nil { sendError(w, 400, "could not register name: %s", err.Error()) return @@ -33,8 +34,8 @@ func ClaimAddress(w http.ResponseWriter, r *http.Request) { response := Response{ Ok: true, - Message: fmt.Sprintf("claimed %v@%v", params.Name, s.Domain), - Data: SuccessClaim{params.Name, pin, inv}, + Message: fmt.Sprintf("claimed %v@%v", params.Name, params.Domain), + Data: SuccessClaim{params.Name, params.Domain, pin, inv}, } // TODO: middleware for responses that adds this header @@ -45,18 +46,19 @@ func ClaimAddress(w http.ResponseWriter, r *http.Request) { func GetUser(w http.ResponseWriter, r *http.Request) { name := mux.Vars(r)["name"] - params, err := GetName(name) + domain := mux.Vars(r)["domain"] + params, err := GetName(name, domain) if err != nil { sendError(w, 400, err.Error()) return } // add pin to response because sometimes not saved in database; after first call to /api/v1/claim - params.Pin = ComputePIN(name) + params.Pin = ComputePIN(name, domain) response := Response{ Ok: true, - Message: fmt.Sprintf("%v@%v found", params.Name, s.Domain), + Message: fmt.Sprintf("%v@%v found", params.Name, domain), Data: params, } @@ -68,6 +70,7 @@ func GetUser(w http.ResponseWriter, r *http.Request) { func UpdateUser(w http.ResponseWriter, r *http.Request) { params := parseParams(r) name := mux.Vars(r)["name"] + domain := mux.Vars(r)["domain"] // if pin not in json request body get it from header if params.Pin == "" { @@ -75,12 +78,12 @@ func UpdateUser(w http.ResponseWriter, r *http.Request) { params.Pin = r.Header.Get("X-Pin") } - if _, _, err := SaveName(name, params, params.Pin); err != nil { + if _, _, err := SaveName(name, domain, params, params.Pin); err != nil { sendError(w, 500, err.Error()) return } - updatedParams, err := GetName(name) + updatedParams, err := GetName(name, domain) if err != nil { sendError(w, 500, err.Error()) return @@ -89,7 +92,7 @@ func UpdateUser(w http.ResponseWriter, r *http.Request) { // return the updated values or just http.StatusCreated? response := Response{ Ok: true, - Message: fmt.Sprintf("updated %v@%v parameters", params.Name, s.Domain), + Message: fmt.Sprintf("updated %v@%v parameters", params.Name, domain), Data: updatedParams, } @@ -100,14 +103,15 @@ func UpdateUser(w http.ResponseWriter, r *http.Request) { func DeleteUser(w http.ResponseWriter, r *http.Request) { name := mux.Vars(r)["name"] - if err := DeleteName(name); err != nil { + domain := mux.Vars(r)["domain"] + if err := DeleteName(name, domain); err != nil { sendError(w, 500, err.Error()) return } response := Response{ Ok: true, - Message: fmt.Sprintf("deleted %v@%v", name, s.Domain), + Message: fmt.Sprintf("deleted %v@%v", name, domain), Data: nil, } @@ -119,6 +123,20 @@ func DeleteUser(w http.ResponseWriter, r *http.Request) { // authentication middleware func authenticate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // check domain + domain := mux.Vars(r)["domain"] + available := getDomains(s.Domain) + found := false + for _, one := range available { + if one == domain { + found = true + } + } + if !found { + sendError(w, 400, "could not use domain: %s", domain) + return + } + // exempt /claim from authentication check; if strings.HasPrefix(r.URL.Path, "/api/v1/claim") { next.ServeHTTP(w, r) @@ -136,7 +154,7 @@ func authenticate(next http.Handler) http.Handler { providedPin = parseParams(r).Pin } - if providedPin != ComputePIN(name) { + if providedPin != ComputePIN(name, domain) { err = fmt.Errorf("wrong pin") } diff --git a/db.go b/db.go index 15b8fa4..411dca0 100644 --- a/db.go +++ b/db.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "os" "strings" "github.com/cockroachdb/pebble" @@ -14,6 +15,7 @@ import ( type Params struct { Name string `json:"name"` + Domain string `json:"domain,omitempty"` Kind string `json:"kind"` Host string `json:"host"` Key string `json:"key"` @@ -26,13 +28,16 @@ type Params struct { func SaveName( name string, + domain string, params *Params, providedPin string, ) (pin string, inv string, err error) { name = strings.ToLower(name) - key := []byte(name) + domain = strings.ToLower(domain) - pin = ComputePIN(name) + key := []byte(getID(name, domain)) + + pin = ComputePIN(name, domain) if _, closer, err := db.Get(key); err == nil { defer closer.Close() @@ -45,6 +50,7 @@ func SaveName( } params.Name = name + params.Domain = domain // check if the given data works if inv, err = makeInvoice(params, 1000, &pin); err != nil { @@ -60,10 +66,8 @@ func SaveName( return pin, inv, nil } -func GetName(name string) (*Params, error) { - name = strings.ToLower(name) - - val, closer, err := db.Get([]byte(name)) +func GetName(name, domain string) (*Params, error) { + val, closer, err := db.Get([]byte(getID(name, domain))) if err != nil { return nil, err } @@ -75,12 +79,12 @@ func GetName(name string) (*Params, error) { } params.Name = name + params.Domain = domain return ¶ms, nil } -func DeleteName(name string) error { - name = strings.ToLower(name) - key := []byte(name) +func DeleteName(name, domain string) error { + key := []byte(getID(name, domain)) if err := db.Delete(key, pebble.Sync); err != nil { return err @@ -89,9 +93,62 @@ func DeleteName(name string) error { return nil } -func ComputePIN(name string) string { - name = strings.ToLower(name) +func ComputePIN(name, domain string) string { mac := hmac.New(sha256.New, []byte(s.Secret)) - mac.Write([]byte(name + "@" + s.Domain)) + mac.Write([]byte(getID(name, domain))) return hex.EncodeToString(mac.Sum(nil)) } + +func getID(name, domain string) string { + if s.GlobalUsers { + return strings.ToLower(name) + } else { + return strings.ToLower(fmt.Sprintf("%s@%s", name, domain)) + } +} + +func tryMigrate(old, new string) { + if _, err := os.Stat(old); os.IsNotExist(err) { + return + } + + log.Info().Str("db", old).Msg("Migrating db") + + newDb, err := pebble.Open(new, nil) + if err != nil { + log.Fatal().Err(err).Str("path", new).Msg("failed to open db.") + } + defer newDb.Close() + + oldDb, err := pebble.Open(old, nil) + if err != nil { + log.Fatal().Err(err).Str("path", old).Msg("failed to open db.") + } + defer oldDb.Close() + + iter := oldDb.NewIter(nil) + defer iter.Close() + + for iter.First(); iter.Valid(); iter.Next() { + log.Debug().Str("db", string(iter.Key())).Msg("Migrating key") + var params Params + if err := json.Unmarshal(iter.Value(), ¶ms); err != nil { + log.Debug().Err(err).Msg("Unmarshal error") + continue + } + + params.Domain = old // old database name was domain + + // save it + data, err := json.Marshal(params) + if err != nil { + log.Debug().Err(err).Msg("Marshal error") + continue + } + + if err := newDb.Set([]byte(getID(params.Name, params.Domain)), data, pebble.Sync); err != nil { + log.Debug().Err(err).Msg("Set error") + continue + } + } +} diff --git a/grab.html b/grab.html index 2a3b5d5..6adef28 100644 --- a/grab.html +++ b/grab.html @@ -13,7 +13,7 @@
Success!
- {{ name }}@{{ domain }} is your new Lightning Address! + {{ name }}@{{ actual_domain }} is your new Lightning Address!
In order to edit the configuration of this address in the future you diff --git a/html.go b/html.go index ca92443..89186e9 100644 --- a/html.go +++ b/html.go @@ -8,18 +8,24 @@ import ( ) type BaseData struct { - Domain string `json:"domain"` - SiteOwnerName string `json:"siteOwnerName"` - SiteOwnerURL string `json:"siteOwnerURL"` - SiteName string `json:"siteName"` + Domains []string `json:"domains"` + SiteOwnerName string `json:"siteOwnerName"` + SiteOwnerURL string `json:"siteOwnerURL"` + SiteName string `json:"siteName"` + UsernameInfo string `json:"usernameInfo"` } func renderHTML(w http.ResponseWriter, html string, extraData interface{}) { + info := "Desired Username" + if s.GlobalUsers { + info = "Desired Username (unique across all domains)" + } base, _ := json.Marshal(BaseData{ - Domain: s.Domain, + Domains: getDomains(s.Domain), SiteOwnerName: s.SiteOwnerName, SiteOwnerURL: s.SiteOwnerURL, SiteName: s.SiteName, + UsernameInfo: info, }) extra, _ := json.Marshal(extraData) diff --git a/index.html b/index.html index cd14616..bfc1287 100644 --- a/index.html +++ b/index.html @@ -24,11 +24,15 @@
- +
- @{{ domain }} + @{{ domains[0] }} + @ +
diff --git a/lnurl.go b/lnurl.go index 2630eff..82e4f8d 100644 --- a/lnurl.go +++ b/lnurl.go @@ -5,23 +5,47 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/fiatjaf/go-lnurl" "github.com/gorilla/mux" ) func handleLNURL(w http.ResponseWriter, r *http.Request) { - username := mux.Vars(r)["username"] + username := mux.Vars(r)["user"] - params, err := GetName(username) + domains := getDomains(s.Domain) + domain := "" + + if len(domains) == 1 { + domain = domains[0] + } else { + hostname := r.URL.Host + if hostname == "" { + hostname = r.Host + } + + for _, one := range getDomains(s.Domain) { + if strings.Contains(hostname, one) { + domain = one + break + } + } + if domain == "" { + json.NewEncoder(w).Encode(lnurl.ErrorResponse("incorrect domain")) + return + } + } + + params, err := GetName(username, domain) if err != nil { - log.Error().Err(err).Str("name", username).Msg("failed to get name") + log.Error().Err(err).Str("name", username).Str("domain", domain).Msg("failed to get name") json.NewEncoder(w).Encode(lnurl.ErrorResponse(fmt.Sprintf( - "failed to get name %s", username))) + "failed to get name %s@%s", username, domain))) return } - log.Info().Str("username", username).Msg("got lnurl request") + log.Info().Str("username", username).Str("domain", domain).Msg("got lnurl request") if amount := r.URL.Query().Get("amount"); amount == "" { // check if the receiver accepts comments @@ -41,7 +65,7 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(lnurl.LNURLPayResponse1{ LNURLResponse: lnurl.LNURLResponse{Status: "OK"}, - Callback: fmt.Sprintf("https://%s/.well-known/lnurlp/%s", s.Domain, username), + Callback: fmt.Sprintf("https://%s/.well-known/lnurlp/%s", domain, username), MinSendable: minSendable, MaxSendable: maxSendable, EncodedMetadata: makeMetadata(params), diff --git a/main.go b/main.go index 859617f..0e6461a 100644 --- a/main.go +++ b/main.go @@ -17,15 +17,19 @@ import ( ) type Settings struct { - Host string `envconfig:"HOST" default:"0.0.0.0"` - Port string `envconfig:"PORT" required:"true"` - Domain string `envconfig:"DOMAIN" required:"true"` + Host string `envconfig:"HOST" default:"0.0.0.0"` + Port string `envconfig:"PORT" required:"true"` + Domain string `envconfig:"DOMAIN" required:"true"` + // GlobalUsers means that user@ part is globally unique across all domains + // WARNING: if you toggle this existing users won't work anymore for safety reasons! + GlobalUsers bool `envconfig:"GLOBAL_USERS" required:"false" default:false` Secret string `envconfig:"SECRET" required:"true"` SiteOwnerName string `envconfig:"SITE_OWNER_NAME" required:"true"` SiteOwnerURL string `envconfig:"SITE_OWNER_URL" required:"true"` SiteName string `envconfig:"SITE_NAME" required:"true"` - TorProxyURL string `envconfig:"TOR_PROXY_URL"` + ForceMigrate bool `envconfig:"FORCE_MIGRATE" required:"false" default:false` + TorProxyURL string `envconfig:"TOR_PROXY_URL"` } var s Settings @@ -57,12 +61,19 @@ func main() { makeinvoice.TorProxyURL = s.TorProxyURL } - db, err = pebble.Open(s.Domain, nil) - if err != nil { - log.Fatal().Err(err).Str("path", s.Domain).Msg("failed to open db.") + dbName := fmt.Sprintf("%v-multiple.db", s.SiteName) + if _, err := os.Stat(dbName); os.IsNotExist(err) || s.ForceMigrate { + for _, one := range getDomains(s.Domain) { + tryMigrate(one, dbName) + } } - router.Path("/.well-known/lnurlp/{username}").Methods("GET"). + db, err = pebble.Open(dbName, nil) + if err != nil { + log.Fatal().Err(err).Str("path", dbName).Msg("failed to open db.") + } + + router.Path("/.well-known/lnurlp/{user}").Methods("GET"). HandlerFunc(handleLNURL) router.Path("/").HandlerFunc( @@ -76,8 +87,23 @@ func main() { router.Path("/grab").HandlerFunc( func(w http.ResponseWriter, r *http.Request) { name := r.FormValue("name") + if name == "" || r.FormValue("kind") == "" { + sendError(w, 500, "internal error") + return + } - pin, inv, err := SaveName(name, &Params{ + // might not get domain back + domain := r.FormValue("domain") + if domain == "" { + if !strings.Contains(s.Domain, ",") { + domain = s.Domain + } else { + sendError(w, 500, "internal error") + return + } + } + + pin, inv, err := SaveName(name, domain, &Params{ Kind: r.FormValue("kind"), Host: r.FormValue("host"), Key: r.FormValue("key"), @@ -91,10 +117,11 @@ func main() { } renderHTML(w, grabHTML, struct { - PIN string `json:"pin"` - Invoice string `json:"invoice"` - Name string `json:"name"` - }{pin, inv, name}) + PIN string `json:"pin"` + Invoice string `json:"invoice"` + Name string `json:"name"` + ActualDomain string `json:"actual_domain"` + }{pin, inv, name, domain}) }, ) @@ -105,9 +132,9 @@ func main() { api.HandleFunc("/claim", ClaimAddress).Methods("POST") // authenticated routes; X-Pin in header or in json request body - api.HandleFunc("/users/{name}", GetUser).Methods("GET") - api.HandleFunc("/users/{name}", UpdateUser).Methods("PUT") - api.HandleFunc("/users/{name}", DeleteUser).Methods("DELETE") + api.HandleFunc("/users/{name}@{domain}", GetUser).Methods("GET") + api.HandleFunc("/users/{name}@{domain}", UpdateUser).Methods("PUT") + api.HandleFunc("/users/{name}@{domain}", DeleteUser).Methods("DELETE") srv := &http.Server{ Handler: router, @@ -118,3 +145,10 @@ func main() { log.Debug().Str("addr", srv.Addr).Msg("listening") srv.ListenAndServe() } + +func getDomains(s string) []string { + splitFn := func(c rune) bool { + return c == ',' + } + return strings.FieldsFunc(s, splitFn) +} diff --git a/makeinvoice.go b/makeinvoice.go index b362063..81ee391 100644 --- a/makeinvoice.go +++ b/makeinvoice.go @@ -12,10 +12,10 @@ import ( func makeMetadata(params *Params) string { metadata, _ := sjson.Set("[]", "0.0", "text/identifier") - metadata, _ = sjson.Set(metadata, "0.1", params.Name+"@"+s.Domain) + metadata, _ = sjson.Set(metadata, "0.1", params.Name+"@"+params.Domain) metadata, _ = sjson.Set(metadata, "1.0", "text/plain") - metadata, _ = sjson.Set(metadata, "1.1", "Satoshis to "+params.Name+"@"+s.Domain+".") + metadata, _ = sjson.Set(metadata, "1.1", "Satoshis to "+params.Name+"@"+params.Domain+".") // TODO support image, custom description diff --git a/static/style.css b/static/style.css index 7695e4a..f2d0eef 100644 --- a/static/style.css +++ b/static/style.css @@ -128,6 +128,19 @@ label { background-color: #f3f3f3; } +select#domain { + width: 50%; + height: 35px; + outline: none; + padding: 0 10px; + font-size: 14px; + border-radius: 5px; + margin-bottom: 25px; + letter-spacing: 0.5px; + border: 1px solid #999; + background-color: #f3f3f3; +} + .input.full-width { width: calc(100% - 20px); } @@ -145,6 +158,18 @@ label { letter-spacing: 0.5px; } +.suffix-hack { + height: 35px; + padding: 0 10px; + margin-bottom: 25px; + display: inline-flex; + font-size: 16px; + font-weight: 600; + align-items: center; + word-break: keep-all; + letter-spacing: 0.5px; +} + select { height: 35px; outline: none; @@ -260,4 +285,9 @@ select { padding: 0; margin-bottom: 25px; } + + .suffix-hack { + padding: 0; + margin-bottom: 0px; + } }