Files
2023-08-01 20:16:22 +02:00

301 lines
9.0 KiB
Go

package main
import (
"embed"
"encoding/json"
"fmt"
"net/http"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/cockroachdb/pebble"
"github.com/fiatjaf/makeinvoice"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
_ "github.com/lib/pq"
"github.com/rs/cors"
"github.com/rs/zerolog"
)
type Settings struct {
Host string `envconfig:"HOST" default:"0.0.0.0"`
Port string `envconfig:"PORT" required:"true"`
Domain string `envconfig:"DOMAIN" required:"true"`
DBDirectory string `envconfig:"DB_DIR" required:"false" default:""`
Relays string `envconfig:"RELAYS" required:"false" default:""`
// 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" 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"`
NostrPrivateKey string `envconfig:"NOSTR_PRIVATE_KEY" required:"false" default:""`
ForwardMainPageUrl string `envconfig:"FORWARD_URL" required:"false"`
Nip05 bool `envconfig:"NIP05" default:"false" required:"false"`
GetNostrProfile bool `envconfig:"GET_NOSTR_PROFILE" required:"false" default:"false"`
ForceMigrate bool `envconfig:"FORCE_MIGRATE" default:"false"`
TorProxyURL string `envconfig:"TOR_PROXY_URL" default:""`
NotifyNostrUsers bool `envconfig:"NOTIFY_NOSTR_USERS" required:"false" default:"true"`
AllowRegistration bool `envconfig:"ALLOW_REGISTRATION" required:"false" default:"true"`
AllowAPI bool `envconfig:"ALLOW_API" required:"false" default:"true"`
LNDprivateOnly bool `envconfig:"LND_PRIVATE_ONLY" required:"false" default:"false"`
}
var (
s Settings
db *pebble.DB
router = mux.NewRouter()
log = zerolog.New(os.Stderr).Output(zerolog.ConsoleWriter{Out: os.Stderr})
)
// array of additional relays
var Relays []string
//go:embed index.html
var indexHTML string
//go:embed grab.html
var grabHTML string
//go:embed static
var static embed.FS
func main() {
godotenv.Load(".env")
err := envconfig.Process("", &s)
if err != nil {
log.Fatal().Err(err).Msg("couldn't process envconfig.")
}
// parse our relays
Relays = strings.Split(s.Relays, ",")
// Check if relays are not specified and add our bootstrap relays
if len(Relays) == 1 && Relays[0] == "" {
Relays = []string{
"wss://relay.damus.io", // Main Damus Relay
"wss://nostr.mutinywallet.com", // Special broadcast relay
"wss://relay.nostrgraph.net", // Special broadcast relay
"wss://nos.lol", // Large relay
"wss://relay.snort.social", // Large relay for snort Users
}
}
// increase default makeinvoice client timeout because people are using tor
makeinvoice.Client = &http.Client{Timeout: 25 * time.Second}
s.Domain = strings.ToLower(s.Domain)
if s.TorProxyURL != "" {
TorProxyURL = s.TorProxyURL
makeinvoice.TorProxyURL = s.TorProxyURL
}
dbName := path.Join(s.DBDirectory, 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)
}
}
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("/.well-known/nostr.json").Methods("GET").
HandlerFunc(handleNip05)
router.Path("/lnaddress").HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
//renderHTML(w, indexHTML, map[string]interface{}{})
renderHTML(w, indexHTML, struct {
AllowRegistration string `json:"allowregistration"`
NotifyNostrUsers string `json:"notifynostr"`
}{strconv.FormatBool(s.AllowRegistration), strconv.FormatBool(s.NotifyNostrUsers)})
},
)
router.Path("/").HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if s.ForwardMainPageUrl != "" {
http.Redirect(w, r, s.ForwardMainPageUrl, http.StatusSeeOther)
} else {
http.Redirect(w, r, "/lnaddress", http.StatusSeeOther)
}
},
)
router.PathPrefix("/static/").Handler(http.FileServer(http.FS(static)))
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
}
// 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
}
}
r.ParseForm()
v1 := r.FormValue("notifyzaps")
var notifyZaps = false
if v1 == "on" {
notifyZaps = true
}
v2 := r.FormValue("notifycomments")
var notifyComments = false
if v2 == "on" {
notifyComments = true
}
v3 := r.FormValue("notifynonzaps")
var notifyNonZaps = false
if v3 == "on" {
notifyNonZaps = true
}
pin, inv, err := SaveName(name, domain, &Params{
Kind: r.FormValue("kind"),
Host: r.FormValue("host"),
Key: r.FormValue("key"),
Pak: r.FormValue("pak"),
Waki: r.FormValue("waki"),
NodeId: r.FormValue("nodeid"),
Rune: r.FormValue("rune"),
Npub: r.FormValue("npub"),
NotifyZaps: notifyZaps,
NotifyZapComment: notifyComments,
NotifyNonZap: notifyNonZaps,
}, r.FormValue("pin"), false, "")
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err.Error())
return
}
renderHTML(w, grabHTML, struct {
PIN string `json:"pin"`
Invoice string `json:"invoice"`
Name string `json:"name"`
ActualDomain string `json:"actual_domain"`
}{pin, inv, name, domain})
},
)
//Alternative API function that brutally deletes previous user and gives them a new name and pin.
//Also works when user didn't exist before.
//Returns Status ok (bool) and pin needed to authorize next call. Save this and the name e.g. in a DB for the user.
//http Post the following content to yourdomain/api/easy
//Expected input with lnbits example:
// { new StringContent(thecurrentnameyouwanttochange), "currentname" },
// { new StringContent(thenewname), "name" },
// { new StringContent(https://yoursatdressdomain.com), "domain" },
// { new StringContent("lnbits"), "kind" },
// { new StringContent("https://lnbits.yourdomain.com"), "host" },
// { new StringContent(lnbitsapikey), "key" },
// { new StringContent(pin), "pin" },
if s.AllowAPI {
router.Path("/api/easy/").HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
newname := r.FormValue("name")
currentName := r.FormValue("currentname")
domain := r.FormValue("domain")
currentPin := r.FormValue("pin")
params := Params{
Kind: r.FormValue("kind"),
Host: r.FormValue("host"),
Key: r.FormValue("key"),
Pak: r.FormValue("pak"),
Waki: r.FormValue("waki"),
NodeId: r.FormValue("nodeid"),
Rune: r.FormValue("rune"),
Npub: r.FormValue("npub"),
}
pin, _, err := SaveName(newname, domain, &params, currentPin, true, currentName)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err.Error())
return
}
params.Pin = pin
response := ResponseEasy{
Ok: true,
Pin: pin,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(response)
},
)
router.Path("/api/listUsers/").HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
var allusers []Params
allusers, err = GetAllUsers(s.Domain)
var result = ""
for _, user := range allusers {
result += user.Name + "@" + s.Domain + "\n"
}
fmt.Fprint(w, result)
},
)
api := router.PathPrefix("/api/v1").Subrouter()
api.Use(authenticate)
// unauthenticated
if s.AllowRegistration {
api.HandleFunc("/claim", ClaimAddress).Methods("POST")
}
// authenticated routes; X-Pin in header or in json request body
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: cors.Default().Handler(router),
Addr: s.Host + ":" + s.Port,
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
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)
}