diff --git a/CHANGELOG.md b/CHANGELOG.md index 80aa2be..b54ffe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Versions +## 1.4.8 + +- Add HTTPS connection with mandatory TLS certificates +- Add swap statistics (Total amount, cost, PPM) + ## 1.4.7 - Remove resthost from peerswap.conf diff --git a/README.md b/README.md index a434b18..5097c3b 100644 --- a/README.md +++ b/README.md @@ -202,10 +202,10 @@ To convert some BTC on your node into L-BTC you don't need any third party (but Taken from [here](https://help.blockstream.com/hc/en-us/articles/900000632703-How-do-I-peg-in-BTC-to-the-Liquid-Network-). -*Hint for Umbrel guys:* To save keystrokes, add these aliases to ~/.profile, then ```source .profile``` +*Hint for Umbrel:* To save keystrokes, add these aliases to ~/.profile, then ```source .profile``` ``` -alias lncli="/home/umbrel/umbrel/scripts/app compose lightning exec -T lnd lncli" -alias bcli="/home/umbrel/umbrel/scripts/app compose bitcoin exec bitcoind bitcoin-cli" +alias lncli="docker exec -it lightning_lnd_1 lncli" `(Umbrel 0.5 only)` +alias bcli="docker exec -it bitcoin_bitcoind_1 bitcoin-cli" `(Umbrel 0.5 only)` alias ecli="docker exec -it elements_node_1 elements-cli -rpcuser=elements -rpcpassword=" ``` diff --git a/SECURITY.md b/SECURITY.md index 4c90dec..c350bc9 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,10 +1,6 @@ -# Security Disclosure +# Security Protocol -**Assuming the local network is secure** - -PeerSwap Web UI is currently a beta-grade software that makes the assumption that the local network is secure. This means local network communication is unencrypted using plain text HTTP. - -Bootstrapping a secure connection over an insecure network and avoiding MITM attacks without being able to rely on certificate authorities is not an easy problem to solve. +PeerSwap Web UI HTTP server offers secure communication with the clients via TLS. When HTTPS option is enabled, a self-signed root Certificate Authority certificate CA.crt is created first. It is then used to sign two certificates: server.crt and client.crt. Both CA.crt and client.crt need to be installed on the client's devices, to bootstrap a secure connection with the server. The server.crt certificate is used during the TLS handshake to authenticate the server to the client. Our communication channel is now encrypted and no third party can eavesdrop or connect to the server. ## Privacy Disclosure diff --git a/cmd/psweb/config/cln.go b/cmd/psweb/config/cln.go index 3092f3c..394adf9 100644 --- a/cmd/psweb/config/cln.go +++ b/cmd/psweb/config/cln.go @@ -165,6 +165,10 @@ func setPeerswapVariable(section, variableName, defaultValue, newValue, envKey s v = s } + if v == "" { + return "" // no value was set in peerswap.conf + } + if isString { v = "\"" + v + "\"" } diff --git a/cmd/psweb/config/common.go b/cmd/psweb/config/common.go index 3a9ee37..87d1850 100644 --- a/cmd/psweb/config/common.go +++ b/cmd/psweb/config/common.go @@ -46,6 +46,10 @@ type Configuration struct { AutoSwapThresholdAmount uint64 AutoSwapThresholdPPM uint64 AutoSwapTargetPct uint64 + SecureConnection bool + ServerIPs string + SerialNumber int64 // for CA-signed server certificates + SecurePort string } var Config Configuration @@ -79,6 +83,8 @@ func Load(dataDir string) { Config.AutoSwapThresholdAmount = 2000000 Config.AutoSwapThresholdPPM = 300 Config.AutoSwapTargetPct = 50 + Config.SecureConnection = false + Config.SecurePort = "1985" if os.Getenv("NETWORK") == "testnet" { Config.Chain = "testnet" diff --git a/cmd/psweb/config/lnd.go b/cmd/psweb/config/lnd.go index ae5d5b8..a5aad40 100644 --- a/cmd/psweb/config/lnd.go +++ b/cmd/psweb/config/lnd.go @@ -76,19 +76,40 @@ func LoadPS() { // get bitcoin RPC from LND config host = getLndConfSetting("bitcoind.rpchost") + user := getLndConfSetting("bitcoind.rpcuser") + pass := getLndConfSetting("bitcoind.rpcpass") - if host == "" { + port = "8332" + if Config.Chain == "testnet" { + port = "18332" + } + + // env variables take priority + if os.Getenv("BITCOIN_HOST") != "" { + host = os.Getenv("BITCOIN_HOST") + } + + if os.Getenv("BITCOIN_PORT") != "" { + port = os.Getenv("BITCOIN_PORT") + } + + if os.Getenv("BITCOIN_USER") != "" { + user = os.Getenv("BITCOIN_USER") + } + + if os.Getenv("BITCOIN_PASS") != "" { + pass = os.Getenv("BITCOIN_PASS") + } + + if host == "" || user == "" || pass == "" { + // fallback Config.BitcoinHost = GetBlockIoHost() Config.BitcoinUser = "" Config.BitcoinPass = "" } else { - port := "8332" - if Config.Chain == "testnet" { - port = "18332" - } Config.BitcoinHost = "http://" + host + ":" + port - Config.BitcoinUser = getLndConfSetting("bitcoind.rpcuser") - Config.BitcoinPass = getLndConfSetting("bitcoind.rpcpass") + Config.BitcoinUser = user + Config.BitcoinPass = pass } } @@ -101,6 +122,7 @@ func SavePS() { //key, default, new value, env key t += setPeerswapdVariable("host", "localhost:42069", Config.RpcHost, "") + t += setPeerswapdVariable("rpchost", "", "", "") // will keep the same if set // remove resthost // t += setPeerswapdVariable("resthost", "localhost:42070", "", "") t += setPeerswapdVariable("lnd.host", "localhost:10009", "", "LND_HOST") @@ -163,7 +185,11 @@ func setPeerswapdVariable(variableName, defaultValue, newValue, envKey string) s } else if s := GetPeerswapLNDSetting(variableName); s != "" { v = s } - return variableName + "=" + v + "\n" + if v == "" { + return "" // no value was set in peerswap.conf + } else { + return variableName + "=" + v + "\n" + } } func GetPeerswapLNDSetting(searchVariable string) string { diff --git a/cmd/psweb/config/tls.go b/cmd/psweb/config/tls.go new file mode 100644 index 0000000..7b45af1 --- /dev/null +++ b/cmd/psweb/config/tls.go @@ -0,0 +1,275 @@ +package config + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "fmt" + "math/big" + "net" + "os" + "path/filepath" + "strings" + "time" + + "software.sslmate.com/src/go-pkcs12" +) + +// generates Certificate Autonrity CA.crt +func GenerateCA() error { + crtPath := filepath.Join(Config.DataDir, "CA.crt") + keyPath := filepath.Join(Config.DataDir, "CA.key") + + // Generate RSA private key + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + + // Save private key to file + privKeyFile, err := os.Create(keyPath) + if err != nil { + return err + } + defer privKeyFile.Close() + pem.Encode(privKeyFile, &pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privKey)}) + + // Create a certificate signing request (CSR) + csrTemplate := x509.CertificateRequest{ + Subject: pkix.Name{ + Organization: []string{"PeerSwap Web UI"}, + }, + SignatureAlgorithm: x509.SHA256WithRSA, + } + csrDER, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privKey) + if err != nil { + return err + } + + // Parse the CSR + certFromCSR, err := x509.ParseCertificateRequest(csrDER) + if err != nil { + return err + } + if err := certFromCSR.CheckSignature(); err != nil { + return err + } + + // Create a certificate based on the CSR + certTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: certFromCSR.Subject, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), // Valid for 10 years + BasicConstraintsValid: true, + IsCA: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, certFromCSR.PublicKey.(crypto.PublicKey), privKey) + if err != nil { + return err + } + + // Save the signed certificate to file + signedCertFile, err := os.Create(crtPath) + if err != nil { + return err + } + defer signedCertFile.Close() + pem.Encode(signedCertFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + return nil +} + +func GenereateServerCertificate() error { + crtPath := filepath.Join(Config.DataDir, "server.crt") + keyPath := filepath.Join(Config.DataDir, "server.key") + crtPathCA := filepath.Join(Config.DataDir, "CA.crt") + keyPathCA := filepath.Join(Config.DataDir, "CA.key") + + IPs := strings.Split(Config.ServerIPs, " ") + + // Generate RSA private key for the server + serverPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + + // Save server private key to file + serverPrivKeyFile, err := os.Create(keyPath) + if err != nil { + return err + } + defer serverPrivKeyFile.Close() + pem.Encode(serverPrivKeyFile, &pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(serverPrivKey)}) + + // Get the hostname of the machine + hostname, err := os.Hostname() + if err != nil { + return err + } + + // Set certificate name + subject := pkix.Name{ + Organization: []string{"PeerSwap Web UI"}, + } + + // Add alternative IP addresses + var ipAdresses []net.IP + for _, ip := range IPs { + ipAdresses = append(ipAdresses, net.ParseIP(ip)) + } + + // Load the CA private key + caPrivKeyPEM, err := os.ReadFile(keyPathCA) + if err != nil { + return err + } + caPrivKeyBlock, _ := pem.Decode(caPrivKeyPEM) + caPrivKey, err := x509.ParsePKCS1PrivateKey(caPrivKeyBlock.Bytes) + if err != nil { + return err + } + + // Load the CA certificate + caCertPEM, err := os.ReadFile(crtPathCA) + if err != nil { + return err + } + caCertBlock, _ := pem.Decode(caCertPEM) + caCert, err := x509.ParseCertificate(caCertBlock.Bytes) + if err != nil { + return err + } + + // Create the server certificate template + serverCertTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: subject, + SignatureAlgorithm: x509.SHA256WithRSA, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), // Valid for 10 years + DNSNames: []string{ + "localhost", + hostname + ".local"}, + IPAddresses: ipAdresses, + } + + // Sign the server certificate with the CA's private key + serverCertDER, err := x509.CreateCertificate(rand.Reader, &serverCertTemplate, caCert, &serverPrivKey.PublicKey, caPrivKey) + if err != nil { + return err + } + + // Save the signed server certificate to file + serverCertFile, err := os.Create(crtPath) + if err != nil { + return err + } + defer serverCertFile.Close() + pem.Encode(serverCertFile, &pem.Block{Type: "CERTIFICATE", Bytes: serverCertDER}) + + Save() + + return nil +} + +func GenerateClientCertificate(password string) error { + crtPathCA := filepath.Join(Config.DataDir, "CA.crt") + keyPathCA := filepath.Join(Config.DataDir, "CA.key") + + // Generate RSA private key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + + // Load CA certificate + caCertPEM, err := os.ReadFile(crtPathCA) + if err != nil { + return err + } + + // Load CA key + caKeyPEM, err := os.ReadFile(keyPathCA) + if err != nil { + return err + } + + caCertBlock, _ := pem.Decode(caCertPEM) + if caCertBlock == nil { + return errors.New("pem.Decode(caCertPEM)") + } + + caCert, err := x509.ParseCertificate(caCertBlock.Bytes) + if err != nil { + return err + } + + caKeyBlock, _ := pem.Decode(caKeyPEM) + if caKeyBlock == nil { + fmt.Printf("Failed to parse CA private key PEM\n") + return err + } + + caKey, err := x509.ParsePKCS1PrivateKey(caKeyBlock.Bytes) + if err != nil { + return err + } + + certTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1234567890), + Subject: pkix.Name{ + Organization: []string{"PSWeb Client"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), // Valid for 10 years + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + // Create client certificate + clientCertBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, caCert, &privateKey.PublicKey, caKey) + if err != nil { + return err + } + + // Export the certificate and key to PKCS#12 + p12Data, err := pkcs12.Modern.Encode(privateKey, &x509.Certificate{ + Raw: clientCertBytes, + }, []*x509.Certificate{caCert}, password) + if err != nil { + return err + } + + err = os.WriteFile(filepath.Join(Config.DataDir, "client.p12"), p12Data, 0644) + if err != nil { + return err + } + + return nil +} + +const charset = "abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ23456789" + +// GeneratePassword generates a random password of a given length +func GeneratePassword(length int) (string, error) { + password := make([]byte, length) + charsetLength := big.NewInt(int64(len(charset))) + + for i := range password { + randomIndex, err := rand.Int(rand.Reader, charsetLength) + if err != nil { + return "", err + } + password[i] = charset[randomIndex.Int64()] + } + + return string(password), nil +} diff --git a/cmd/psweb/main.go b/cmd/psweb/main.go index 6aaa20d..40484ce 100644 --- a/cmd/psweb/main.go +++ b/cmd/psweb/main.go @@ -1,6 +1,8 @@ package main import ( + "crypto/tls" + "crypto/x509" "embed" "encoding/json" "errors" @@ -33,7 +35,7 @@ import ( const ( // App version tag - version = "v1.4.7" + version = "v1.4.8" ) type SwapParams struct { @@ -46,7 +48,7 @@ type SwapParams struct { var ( aliasCache = make(map[string]string) templates = template.New("") - //go:embed static + //go:embed static/* staticFiles embed.FS //go:embed templates/*.gohtml tplFolder embed.FS @@ -119,11 +121,11 @@ func main() { var staticFS = http.FS(staticFiles) fs := http.FileServer(staticFS) - // Serve static files - http.Handle("/static/", fs) - r := mux.NewRouter() + // Serve static files + r.PathPrefix("/static/").Handler(fs) + // Serve templates r.HandleFunc("/", indexHandler) r.HandleFunc("/swap", swapHandler) @@ -141,16 +143,31 @@ func main() { r.HandleFunc("/bitcoin", bitcoinHandler) r.HandleFunc("/pegin", peginHandler) r.HandleFunc("/bumpfee", bumpfeeHandler) + r.HandleFunc("/ca", caHandler) + + if config.Config.SecureConnection { + // HTTP redirection + go func() { + httpMux := http.NewServeMux() + httpMux.HandleFunc("/", redirectToHTTPS) + log.Println("Starting HTTP server on port " + config.Config.ListenPort + " for redirection...") + if err := http.ListenAndServe(":"+config.Config.ListenPort, httpMux); err != nil { + log.Fatalf("Failed to start HTTP server: %v\n", err) + } + }() - // Start the server - http.Handle("/", retryMiddleware(r)) - go func() { - if err := http.ListenAndServe(":"+config.Config.ListenPort, nil); err != nil { - log.Fatal(err) - } - }() - - log.Println("Listening on http://localhost:" + config.Config.ListenPort) + go serveHTTPS(retryMiddleware(r)) + log.Println("Listening on https://localhost:" + config.Config.SecurePort) + } else { + // Start HTTP server + http.Handle("/", retryMiddleware(r)) + go func() { + if err := http.ListenAndServe(":"+config.Config.ListenPort, nil); err != nil { + log.Fatal(err) + } + }() + log.Println("Listening on http://localhost:" + config.Config.ListenPort) + } // Start timer to run every minute go startTimer() @@ -172,13 +189,63 @@ func main() { signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGPIPE) // Wait for termination signal - <-signalChan - log.Println("Received termination signal") + sig := <-signalChan + log.Printf("Received termination signal: %s\n", sig) // Exit the program gracefully os.Exit(0) } +func redirectToHTTPS(w http.ResponseWriter, r *http.Request) { + url := "https://" + strings.Split(r.Host, ":")[0] + ":" + config.Config.SecurePort + r.URL.String() + http.Redirect(w, r, url, http.StatusTemporaryRedirect) +} + +func serveHTTPS(handler http.Handler) { + // Load your certificate and private key + certFile := filepath.Join(config.Config.DataDir, "server.crt") + keyFile := filepath.Join(config.Config.DataDir, "server.key") + + //regenerate from CA if deleted + if !fileExists(certFile) { + config.GenereateServerCertificate() + } + + // Load your server certificate and private key + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + log.Fatalf("Failed to load server certificate: %v", err) + } + + // Load CA certificate + caCert, err := os.ReadFile(filepath.Join(config.Config.DataDir, "CA.crt")) + if err != nil { + log.Fatalf("Failed to load CA certificate: %v", err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + // Configure TLS settings + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientCAs: caCertPool, + ClientAuth: tls.RequireAndVerifyClientCert, // Require and verify client certificate + MinVersion: tls.VersionTLS12, // Force TLS 1.2 or higher + } + + server := &http.Server{ + Addr: ":" + config.Config.SecurePort, + Handler: handler, + TLSConfig: tlsConfig, + } + + // Start the HTTPS server + err = server.ListenAndServeTLS(certFile, keyFile) + if err != nil { + log.Fatalf("Failed to start HTTPS server: %v\n", err) + } +} + func indexHandler(w http.ResponseWriter, r *http.Request) { if config.Config.ElementsPass == "" || config.Config.ElementsUser == "" { @@ -346,14 +413,12 @@ func indexHandler(w http.ResponseWriter, r *http.Request) { err = templates.ExecuteTemplate(w, "homepage", data) if err != nil { log.Fatalln(err) - http.Error(w, http.StatusText(500), 500) } } func peerHandler(w http.ResponseWriter, r *http.Request) { keys, ok := r.URL.Query()["id"] if !ok || len(keys[0]) < 1 { - fmt.Println("URL parameter 'id' is missing") http.Error(w, http.StatusText(500), 500) return } @@ -590,14 +655,12 @@ func peerHandler(w http.ResponseWriter, r *http.Request) { err = templates.ExecuteTemplate(w, "peer", data) if err != nil { log.Fatalln(err) - http.Error(w, http.StatusText(500), 500) } } func swapHandler(w http.ResponseWriter, r *http.Request) { keys, ok := r.URL.Query()["id"] if !ok || len(keys[0]) < 1 { - fmt.Println("URL parameter 'id' is missing") http.Error(w, http.StatusText(500), 500) return } @@ -651,7 +714,6 @@ func swapHandler(w http.ResponseWriter, r *http.Request) { err = templates.ExecuteTemplate(w, "swap", data) if err != nil { log.Fatalln(err) - http.Error(w, http.StatusText(500), 500) } } @@ -659,7 +721,6 @@ func swapHandler(w http.ResponseWriter, r *http.Request) { func updateHandler(w http.ResponseWriter, r *http.Request) { keys, ok := r.URL.Query()["id"] if !ok || len(keys[0]) < 1 { - fmt.Println("URL parameter 'id' is missing") http.Error(w, http.StatusText(500), 500) return } @@ -794,6 +855,17 @@ func configHandler(w http.ResponseWriter, r *http.Request) { errorMessage = keys[0] } + // Get the hostname of the machine + hostname, _ := os.Hostname() + + // populate server IP if empty + if config.Config.ServerIPs == "" { + ip := strings.Split(r.Host, ":")[0] + if ip != "localhost" && ip != "127.0.0.1" { + config.Config.ServerIPs = ip + } + } + type Page struct { ErrorMessage string PopUpMessage string @@ -803,6 +875,7 @@ func configHandler(w http.ResponseWriter, r *http.Request) { Version string Latest string Implementation string + HTTPS string } data := Page{ @@ -814,13 +887,85 @@ func configHandler(w http.ResponseWriter, r *http.Request) { Version: version, Latest: latestVersion, Implementation: ln.Implementation, + HTTPS: "https://" + hostname + ".local:" + config.Config.ListenPort, } - // executing template named "error" + // executing template named "config" err := templates.ExecuteTemplate(w, "config", data) if err != nil { log.Fatalln(err) - http.Error(w, http.StatusText(500), 500) + } +} + +func caHandler(w http.ResponseWriter, r *http.Request) { + //check for error message to display + errorMessage := "" + keys, ok := r.URL.Query()["err"] + if ok && len(keys[0]) > 0 { + errorMessage = keys[0] + } + + // Get the hostname of the machine + hostname, _ := os.Hostname() + + urls := []string{ + "https://localhost:" + config.Config.SecurePort, + "https://" + hostname + ".local:" + config.Config.SecurePort, + } + + if config.Config.ServerIPs != "" { + for _, ip := range strings.Split(config.Config.ServerIPs, " ") { + urls = append(urls, "https://"+ip+":"+config.Config.SecurePort) + } + } + + password, err := config.GeneratePassword(10) + if err != nil { + log.Println("GeneratePassword:", err) + redirectWithError(w, r, "/config?", err) + return + } + + type Page struct { + ErrorMessage string + PopUpMessage string + MempoolFeeRate float64 + ColorScheme string + Config config.Configuration + URLs []string + Password string + } + + data := Page{ + ErrorMessage: errorMessage, + PopUpMessage: "", + MempoolFeeRate: mempoolFeeRate, + ColorScheme: config.Config.ColorScheme, + Config: config.Config, + URLs: urls, + Password: password, + } + + if !fileExists(filepath.Join(config.Config.DataDir, "CA.crt")) { + err := config.GenerateCA() + if err != nil { + log.Println("Error generating CA.crt:", err) + redirectWithError(w, r, "/config?", err) + return + } + } + + err = config.GenerateClientCertificate(password) + if err != nil { + log.Println("Error generating client.p12:", err) + redirectWithError(w, r, "/config?", err) + return + } + + // executing template named "ca" + err = templates.ExecuteTemplate(w, "ca", data) + if err != nil { + log.Fatalln(err) } } @@ -913,7 +1058,6 @@ func liquidHandler(w http.ResponseWriter, r *http.Request) { err = templates.ExecuteTemplate(w, "liquid", data) if err != nil { log.Fatalln(err) - http.Error(w, http.StatusText(500), 500) } } @@ -935,6 +1079,17 @@ func submitHandler(w http.ResponseWriter, r *http.Request) { defer cleanup() switch action { + case "enableHTTPS": + // restart with HTTPS listener + if err := config.GenereateServerCertificate(); err == nil { + config.Config.SecureConnection = true + config.Save() + url := "https://" + strings.Split(r.Host, ":")[0] + ":" + config.Config.SecurePort + restart(w, r, url) + } else { + redirectWithError(w, r, "/ca?", err) + return + } case "keySend": dest := r.FormValue("nodeId") message := r.FormValue("keysendMessage") @@ -1166,6 +1321,43 @@ func saveConfigHandler(w http.ResponseWriter, r *http.Request) { return } + secureConnection, err := strconv.ParseBool(r.FormValue("secureConnection")) + if err != nil { + redirectWithError(w, r, "/config?", err) + return + } + + // display CA certificate installation instructions + if secureConnection && !config.Config.SecureConnection { + config.Config.ServerIPs = r.FormValue("serverIPs") + config.Save() + http.Redirect(w, r, "/ca", http.StatusSeeOther) + return + } + + if r.FormValue("serverIPs") != config.Config.ServerIPs { + config.Config.ServerIPs = r.FormValue("serverIPs") + if secureConnection { + if err := config.GenereateServerCertificate(); err == nil { + config.Save() + url := "https://" + strings.Split(r.Host, ":")[0] + ":" + config.Config.SecurePort + restart(w, r, url) + } else { + log.Println("GenereateServerCertificate:", err) + redirectWithError(w, r, "/config?", err) + return + } + } + } + + // restart to listen on HTTP + if !secureConnection && config.Config.SecureConnection { + config.Config.SecureConnection = false + config.Save() + url := "http://" + strings.Split(r.Host, ":")[0] + ":" + config.Config.ListenPort + restart(w, r, url) + } + allowSwapRequests, err := strconv.ParseBool(r.FormValue("allowSwapRequests")) if err != nil { redirectWithError(w, r, "/config?", err) @@ -2317,7 +2509,11 @@ func convertSwapsToHTMLTable(swaps []*peerswaprpc.PrettyPrintSwap, nodeId string TimeStamp int64 HtmlBlob string } - var unsortedTable []Table + var ( + unsortedTable []Table + totalAmount uint64 + totalCost int64 + ) for _, swap := range swaps { // filter by node Id @@ -2336,7 +2532,7 @@ func convertSwapsToHTMLTable(swaps []*peerswaprpc.PrettyPrintSwap, nodeId string } table := "" - table += "" + table += "" tm := timePassedAgo(time.Unix(swap.CreatedAt, 0).UTC()) @@ -2345,10 +2541,15 @@ func convertSwapsToHTMLTable(swaps []*peerswaprpc.PrettyPrintSwap, nodeId string table += "" // clicking on swap status will filter swaps with equal status - table += "" + state := simplifySwapState(swap.State) + table += "" table += visualiseSwapState(swap.State, false) + " " table += " " + formatWithThousandSeparators(swap.Amount) + "" + if state == "success" { + totalAmount += swap.Amount + } + asset := "🌊" if swap.Asset == "btc" { asset = "â‚¿" @@ -2367,6 +2568,7 @@ func convertSwapsToHTMLTable(swaps []*peerswaprpc.PrettyPrintSwap, nodeId string cost := swapCost(swap) if cost != 0 { + totalCost += cost ppm := cost * 1_000_000 / int64(swap.Amount) table += " " + formatSigned(cost) + "" } @@ -2411,7 +2613,17 @@ func convertSwapsToHTMLTable(swaps []*peerswaprpc.PrettyPrintSwap, nodeId string } table += t.HtmlBlob } + table += "" + + // show total amount and cost + ppm := int64(0) + if totalAmount > 0 { + ppm = totalCost * 1_000_000 / int64(totalAmount) + } + + table += "

Total: " + toMil(totalAmount) + ", Cost: " + formatSigned(totalCost) + " sats, PPM: " + formatSigned(ppm) + " +

+
+
+

Enable HTTPS

+
+ +

To enable secure TLS connection it is necessary to install two certificates on all devices that will be permitted to access PeerSwap Web UI:

+
+

1. "Trusted Root Certification Authority" certificate CA.crt will inform the browser that the server is genuine.

+

2. "Personal" certificate client.p12 will authenticate the client to the server.

+
+

Use secure methods to copy these files from peerswap data folder onto your devices. The password for client.p12 is "{{.Password}}". Important: write down this password before proceeding as it will not be saved nor displayed again.

+
+

After restart, PeerSwap Web UI will be listening on:

+

    + {{range .URLs}} +
  1. {{.}}
  2. + {{end}} +
+
+

HTTP endpoint at port {{.Config.ListenPort}} will redirect all traffic to HTTPS port {{.Config.SecurePort}}. Clients without the above certificates will be denied connection.

+
+
+ + +
+
+
+
+
+
+

Certificates Installation

+

Windows 10

+

1. Double click on client.p12, enter the password, allow Automatically select store.

+

2. Double click on CA.crt, select "Trusted Root Certification Authorities" store.

+
+

Android

+

1. Tap on client.p12, enter the password, the certificate will be installed.

+

2. For CA.crt, open Settings, search for "CA Certificate", "Install anyway", select the file.

+
+

iOS

+

1. E-mail the certificates to yourself as an attachment.

+

2. One certificate at a time, tap on the attachment twice to download and launch.

+

3. In Settings app, Profile Downloaded, Install.

+
+

Ubuntu

+

1. Install client.p12 in Firefox Settings - Privacy&Security - Certificates - Your Certificates - Import - Enter password.

+

2. Install CA.crt via command line: +

sudo apt-get install -y ca-certificates

+

sudo cp ~/.peerswap/CA.crt /usr/local/share/ca-certificates

+

sudo update-ca-certificates

+
+
+
+ + {{template "footer" .}} +{{end}} \ No newline at end of file diff --git a/cmd/psweb/templates/config.gohtml b/cmd/psweb/templates/config.gohtml index 93591bf..ecfe9a3 100644 --- a/cmd/psweb/templates/config.gohtml +++ b/cmd/psweb/templates/config.gohtml @@ -68,7 +68,9 @@ {{end}} - + + + @@ -95,7 +97,7 @@ - + @@ -136,8 +138,6 @@ - -
@@ -155,6 +155,8 @@
+ +
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+

+ +

+
+
+
@@ -265,7 +296,7 @@ {{else}} peerswapd {{end}}

-
+

** Changing these values will restart PSWeb


diff --git a/cmd/psweb/templates/homepage.gohtml b/cmd/psweb/templates/homepage.gohtml index 7556d22..cb747d3 100644 --- a/cmd/psweb/templates/homepage.gohtml +++ b/cmd/psweb/templates/homepage.gohtml @@ -38,9 +38,7 @@ {{.ListPeers}} {{if eq .OtherPeers ""}}
- - Show Non-PeerSwap Channels - + Show Non-PeerSwap Channels
{{end}} diff --git a/cmd/psweb/templates/liquid.gohtml b/cmd/psweb/templates/liquid.gohtml index e7608f2..bcfaac6 100644 --- a/cmd/psweb/templates/liquid.gohtml +++ b/cmd/psweb/templates/liquid.gohtml @@ -10,7 +10,7 @@
- +
diff --git a/cmd/psweb/templates/peer.gohtml b/cmd/psweb/templates/peer.gohtml index 2aa92a5..97ae25d 100644 --- a/cmd/psweb/templates/peer.gohtml +++ b/cmd/psweb/templates/peer.gohtml @@ -257,13 +257,16 @@ }); // Calculate tx size assuming always two outputs - vbyteSize = 2510 + (inputs-1) * 160; + vbyteSize = 2503 + (inputs-1) * 84; fee = vbyteSize * feeRate; // peerswap spends dust change as extra fee change = {{.LiquidBalance}} - swapAmount - fee; if (change < 1000) { + // one output + vbyteSize -= 1,191; + // but the fee increases fee += change; } } diff --git a/cmd/psweb/utils.go b/cmd/psweb/utils.go index abef052..62811d2 100644 --- a/cmd/psweb/utils.go +++ b/cmd/psweb/utils.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "net/http" + "os" "strconv" "time" ) @@ -157,3 +159,20 @@ func msatToSatUp(msat uint64) uint64 { } return sat } + +func fileExists(filename string) bool { + _, err := os.Stat(filename) + if err == nil { + return true + } + if os.IsNotExist(err) { + return false + } + return false +} + +func restart(w http.ResponseWriter, r *http.Request, url string) { + // assume systemd will restart it + http.Redirect(w, r, url, http.StatusTemporaryRedirect) + os.Exit(0) +} diff --git a/go.mod b/go.mod index 3d752e9..a175258 100644 --- a/go.mod +++ b/go.mod @@ -189,4 +189,5 @@ require ( modernc.org/strutil v1.2.0 // indirect modernc.org/token v1.1.0 // indirect sigs.k8s.io/yaml v1.3.0 // indirect + software.sslmate.com/src/go-pkcs12 v0.4.0 // indirect ) diff --git a/go.sum b/go.sum index a8651fb..c6dfb63 100644 --- a/go.sum +++ b/go.sum @@ -1092,3 +1092,5 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=