From ac3201b8446727afae6de927fc427c4caa2ab3e6 Mon Sep 17 00:00:00 2001 From: J the Code Monkey Date: Thu, 12 Sep 2024 19:51:40 -0400 Subject: [PATCH] feat: implement strfry relay --- cmd/install.go | 52 +++++- cmd/root.go | 3 +- pkg/manager/apt.go | 72 ++++---- pkg/network/certbot.go | 11 +- pkg/network/firewall.go | 3 +- .../khatru_pyramid}/install.go | 31 ++-- .../khatru_pyramid}/nginx_http.go | 24 +-- .../khatru_pyramid}/nginx_https.go | 22 +-- .../khatru_pyramid}/service.go | 61 +++---- pkg/relays/strfry/apt.go | 27 +++ pkg/relays/strfry/install.go | 93 ++++++++++ pkg/relays/strfry/nginx_http.go | 62 +++++++ pkg/relays/strfry/nginx_https.go | 133 ++++++++++++++ pkg/relays/strfry/service.go | 162 ++++++++++++++++++ pkg/utils/utils.go | 43 +++++ 15 files changed, 677 insertions(+), 122 deletions(-) rename pkg/{relay => relays/khatru_pyramid}/install.go (70%) rename pkg/{network => relays/khatru_pyramid}/nginx_http.go (74%) rename pkg/{network => relays/khatru_pyramid}/nginx_https.go (89%) rename pkg/{relay => relays/khatru_pyramid}/service.go (77%) create mode 100644 pkg/relays/strfry/apt.go create mode 100644 pkg/relays/strfry/install.go create mode 100644 pkg/relays/strfry/nginx_http.go create mode 100644 pkg/relays/strfry/nginx_https.go create mode 100644 pkg/relays/strfry/service.go diff --git a/cmd/install.go b/cmd/install.go index 773a31d..107af41 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -1,12 +1,12 @@ package cmd import ( - "github.com/pterm/pterm" - "github.com/nodetec/relaywiz/pkg/manager" "github.com/nodetec/relaywiz/pkg/network" - "github.com/nodetec/relaywiz/pkg/relay" + "github.com/nodetec/relaywiz/pkg/relays/khatru_pyramid" + "github.com/nodetec/relaywiz/pkg/relays/strfry" "github.com/nodetec/relaywiz/pkg/ui" + "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -23,7 +23,23 @@ var installCmd = &cobra.Command{ pterm.Println(pterm.Yellow("Leave email empty if you don't want to receive notifications from Let's Encrypt about your SSL cert.")) pterm.Println() ssl_email, _ := pterm.DefaultInteractiveTextInput.Show("Email address") - pubkey, _ := pterm.DefaultInteractiveTextInput.Show("Public key (hex not npub)") + + pterm.Println() + // Supported relay options + options := []string{"Khatru Pyramid", "strfry"} + + // Use PTerm's interactive select feature to present the options to the user and capture their selection + // The Show() method displays the options and waits for the user's input + selectedRelayOption, _ := pterm.DefaultInteractiveSelect.WithOptions(options).Show() + + // Display the selected option to the user with a green color for emphasis + pterm.Info.Printfln("Selected option: %s", pterm.Green(selectedRelayOption)) + + var pubkey string + if selectedRelayOption == "Khatru Pyramid" { + pterm.Println() + pubkey, _ = pterm.DefaultInteractiveTextInput.Show("Public key (hex not npub)") + } pterm.Println() pterm.Println(pterm.Yellow("If you make a mistake, you can always re-run this installer.")) @@ -32,11 +48,19 @@ var installCmd = &cobra.Command{ // Step 1: Install necessary packages using APT manager.AptInstallPackages() + if selectedRelayOption == "strfry" { + strfry.AptInstallDependencies() + } + // Step 2: Configure the firewall network.ConfigureFirewall() // Step 3: Configure Nginx for HTTP - network.ConfigureNginxHttp(relayDomain) + if selectedRelayOption == "Khatru Pyramid" { + khatru_pyramid.ConfigureNginxHttp(relayDomain) + } else if selectedRelayOption == "strfry" { + strfry.ConfigureNginxHttp(relayDomain) + } // Step 4: Get SSL certificates var shouldContinue = network.GetCertificates(relayDomain, ssl_email) @@ -46,13 +70,25 @@ var installCmd = &cobra.Command{ } // Step 5: Configure Nginx for HTTPS - network.ConfigureNginxHttps(relayDomain) + if selectedRelayOption == "Khatru Pyramid" { + khatru_pyramid.ConfigureNginxHttps(relayDomain) + } else if selectedRelayOption == "strfry" { + strfry.ConfigureNginxHttps(relayDomain) + } // Step 6: Download and install the relay binary - relay.InstallRelayBinary() + if selectedRelayOption == "Khatru Pyramid" { + khatru_pyramid.InstallRelayBinary() + } else if selectedRelayOption == "strfry" { + strfry.InstallRelayBinary() + } // Step 7: Set up the relay service - relay.SetupRelayService(relayDomain, pubkey) + if selectedRelayOption == "Khatru Pyramid" { + khatru_pyramid.SetupRelayService(relayDomain, pubkey) + } else if selectedRelayOption == "strfry" { + strfry.SetupRelayService(relayDomain) + } pterm.Println() pterm.Println(pterm.Magenta("The installation is complete.")) diff --git a/cmd/root.go b/cmd/root.go index 8ede668..e4cfdc4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,9 +1,8 @@ package cmd import ( - "os" - "github.com/spf13/cobra" + "os" ) var rootCmd = &cobra.Command{ diff --git a/pkg/manager/apt.go b/pkg/manager/apt.go index 92c5d9e..8d38da4 100644 --- a/pkg/manager/apt.go +++ b/pkg/manager/apt.go @@ -1,52 +1,56 @@ package manager import ( - "os/exec" - + "fmt" "github.com/pterm/pterm" + "log" + "os/exec" ) -// Function to check if a command exists -func commandExists(command string) bool { - _, err := exec.LookPath(command) - return err == nil +// Function to check if a package is installed +func IsPackageInstalled(packageName string) bool { + out, err := exec.Command("dpkg-query", "-W", "-f='${Status}'", packageName).Output() + + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + errorCode := exitError.ExitCode() + // Package not found + if errorCode == 1 { + return false + } else { + log.Fatalf("Error checking if package is installed: %v", err) + } + } + } + + status := string(out) + + if status == "'unknown ok not-installed'" { + return false + } else if status == "'install ok installed'" { + return true + } + + return false } // Function to install necessary packages func AptInstallPackages() { spinner, _ := pterm.DefaultSpinner.Start("Updating and installing packages...") - exec.Command("apt", "update", "-qq").Run() - // Check if nginx is installed, install if not - if commandExists("nginx") { - spinner.UpdateText("nginx is already installed.") - } else { - spinner.UpdateText("Installing nginx...") - exec.Command("apt", "install", "-y", "-qq", "nginx").Run() - } + exec.Command("apt", "update", "-qq").Run() - // Check if Certbot is installed, install if not - if commandExists("certbot") { - spinner.UpdateText("Certbot is already installed.") - } else { - spinner.UpdateText("Installing Certbot...") - exec.Command("apt", "install", "-y", "-qq", "certbot", "python3-certbot-nginx").Run() - } + packages := []string{"nginx", "certbot", "python3-certbot-nginx", "ufw", "fail2ban"} - // Check if ufw is installed, install if not - if commandExists("ufw") { - spinner.UpdateText("ufw is already installed.") - } else { - spinner.UpdateText("Installing ufw...") - exec.Command("apt", "install", "-y", "-qq", "ufw").Run() + // Check if package is installed, install if not + for _, p := range packages { + if IsPackageInstalled(p) { + spinner.UpdateText(fmt.Sprintf("%s is already installed.", p)) + } else { + spinner.UpdateText(fmt.Sprintf("Installing %s...", p)) + exec.Command("apt", "install", "-y", "-qq", p).Run() + } } spinner.Success("Packages updated and installed successfully.") } - -// Function to check if a package is installed -func isPackageInstalled(packageName string) bool { - cmd := exec.Command("dpkg", "-l", packageName) - err := cmd.Run() - return err == nil -} diff --git a/pkg/network/certbot.go b/pkg/network/certbot.go index e405ac9..c3cfd8e 100644 --- a/pkg/network/certbot.go +++ b/pkg/network/certbot.go @@ -3,11 +3,10 @@ package network import ( "fmt" "github.com/nodetec/relaywiz/pkg/utils" + "github.com/pterm/pterm" "log" "os" "os/exec" - - "github.com/pterm/pterm" ) // Function to get SSL certificates using Certbot @@ -33,8 +32,6 @@ func GetCertificates(domainName, email string) bool { spinner, _ := pterm.DefaultSpinner.Start("Checking SSL certificates...") - dirName := utils.GetDirectoryName(domainName) - // Check if certificates already exist if utils.FileExists(fmt.Sprintf("/etc/letsencrypt/live/%s/fullchain.pem", domainName)) && utils.FileExists(fmt.Sprintf("/etc/letsencrypt/live/%s/privkey.pem", domainName)) { @@ -42,20 +39,20 @@ func GetCertificates(domainName, email string) bool { return true } - err := os.MkdirAll(fmt.Sprintf("/var/www/%s/.well-known/acme-challenge/", dirName), 0755) + err := os.MkdirAll(fmt.Sprintf("/var/www/%s/.well-known/acme-challenge/", domainName), 0755) if err != nil { log.Fatalf("Error creating directories for Certbot: %v", err) } spinner.UpdateText("Obtaining SSL certificates...") if email == "" { - cmd := exec.Command("certbot", "certonly", "--webroot", "-w", fmt.Sprintf("/var/www/%s", dirName), "-d", domainName, "--agree-tos", "--no-eff-email", "-q", "--register-unsafely-without-email") + cmd := exec.Command("certbot", "certonly", "--webroot", "-w", fmt.Sprintf("/var/www/%s", domainName), "-d", domainName, "--agree-tos", "--no-eff-email", "-q", "--register-unsafely-without-email") err = cmd.Run() if err != nil { log.Fatalf("Certbot failed to obtain the certificate for %s: %v", domainName, err) } } else { - cmd := exec.Command("certbot", "certonly", "--webroot", "-w", fmt.Sprintf("/var/www/%s", dirName), "-d", domainName, "--email", email, "--agree-tos", "--no-eff-email", "-q") + cmd := exec.Command("certbot", "certonly", "--webroot", "-w", fmt.Sprintf("/var/www/%s", domainName), "-d", domainName, "--email", email, "--agree-tos", "--no-eff-email", "-q") err = cmd.Run() if err != nil { log.Fatalf("Certbot failed to obtain the certificate for %s: %v", domainName, err) diff --git a/pkg/network/firewall.go b/pkg/network/firewall.go index 124cf3e..015b20e 100644 --- a/pkg/network/firewall.go +++ b/pkg/network/firewall.go @@ -1,10 +1,9 @@ package network import ( + "github.com/pterm/pterm" "log" "os/exec" - - "github.com/pterm/pterm" ) // Function to configure the firewall diff --git a/pkg/relay/install.go b/pkg/relays/khatru_pyramid/install.go similarity index 70% rename from pkg/relay/install.go rename to pkg/relays/khatru_pyramid/install.go index 40433e2..9c96ff6 100644 --- a/pkg/relay/install.go +++ b/pkg/relays/khatru_pyramid/install.go @@ -1,31 +1,29 @@ -package relay +package khatru_pyramid import ( + "github.com/pterm/pterm" "io" "log" "net/http" "os" "path/filepath" - - "github.com/pterm/pterm" ) -// URL of the binary to download -const downloadURL = "https://github.com/github-tijlxyz/khatru-pyramid/releases/download/v0.0.5/khatru-pyramid-v0.0.5-linux-amd64" - -// Name of the binary after downloading -const binaryName = "nostr-relay-pyramid" +// Function to download and make the binary executable +func InstallRelayBinary() { + // URL of the binary to download + const downloadURL = "https://github.com/github-tijlxyz/khatru-pyramid/releases/download/v0.0.5/khatru-pyramid-v0.0.5-linux-amd64" -// Destination directory for the binary -const destDir = "/usr/local/bin" + // Name of the binary after downloading + const binaryName = "nostr-relay-khatru-pyramid" -// Data directory for the relay -const dataDir = "/var/lib/nostr-relay-pyramid" + // Destination directory for the binary + const destDir = "/usr/local/bin" -// Function to download and make the binary executable -func InstallRelayBinary() { + // Data directory for the relay + const dataDir = "/var/lib/nostr-relay-khatru-pyramid" - spinner, _ := pterm.DefaultSpinner.Start("Installing relay...") + spinner, _ := pterm.DefaultSpinner.Start("Installing Khatru Pyramid relay...") // Ensure the data directory exists err := os.MkdirAll(dataDir, 0755) if err != nil { @@ -75,6 +73,5 @@ func InstallRelayBinary() { log.Fatalf("Error making file executable: %v", err) } - spinner.Success("Relay installed successfully.") + spinner.Success("Khatru Pyramid relay installed successfully.") } - diff --git a/pkg/network/nginx_http.go b/pkg/relays/khatru_pyramid/nginx_http.go similarity index 74% rename from pkg/network/nginx_http.go rename to pkg/relays/khatru_pyramid/nginx_http.go index 8d66215..ae4191c 100644 --- a/pkg/network/nginx_http.go +++ b/pkg/relays/khatru_pyramid/nginx_http.go @@ -1,32 +1,33 @@ -package network +package khatru_pyramid import ( "fmt" - "github.com/nodetec/relaywiz/pkg/utils" + // "github.com/nodetec/relaywiz/pkg/utils" + "github.com/pterm/pterm" "log" "os" "os/exec" - - "github.com/pterm/pterm" ) // Function to configure nginx for HTTP func ConfigureNginxHttp(domainName string) { - spinner, _ := pterm.DefaultSpinner.Start("Configuring nginx for HTTP...") - dirName := utils.GetDirectoryName(domainName) - err := os.MkdirAll(fmt.Sprintf("/var/www/%s/.well-known/acme-challenge/", dirName), 0755) + err := os.MkdirAll(fmt.Sprintf("/var/www/%s/.well-known/acme-challenge/", domainName), 0755) if err != nil { log.Fatalf("Error creating directories: %v", err) } - err = os.Remove("/etc/nginx/conf.d/nostr_relay.conf") + const configFile = "nostr_relay_khatru_pyramid.conf" + + err = os.Remove(fmt.Sprintf("/etc/nginx/conf.d/%s", configFile)) if err != nil && !os.IsNotExist(err) { log.Fatalf("Error removing existing nginx configuration: %v", err) } - configContent := fmt.Sprintf(`map $http_upgrade $connection_upgrade { + var configContent string + + configContent = fmt.Sprintf(`map $http_upgrade $connection_upgrade { default upgrade; '' close; } @@ -55,9 +56,9 @@ server { proxy_set_header X-Forwarded-For $remote_addr; } } -`, domainName, domainName, dirName) +`, domainName, domainName, domainName) - err = os.WriteFile("/etc/nginx/conf.d/nostr_relay.conf", []byte(configContent), 0644) + err = os.WriteFile(fmt.Sprintf("/etc/nginx/conf.d/%s", configFile), []byte(configContent), 0644) if err != nil { log.Fatalf("Error writing nginx configuration: %v", err) } @@ -68,5 +69,4 @@ server { } spinner.Success("Nginx configured for HTTP") - } diff --git a/pkg/network/nginx_https.go b/pkg/relays/khatru_pyramid/nginx_https.go similarity index 89% rename from pkg/network/nginx_https.go rename to pkg/relays/khatru_pyramid/nginx_https.go index 8f0a39c..cd85f2a 100644 --- a/pkg/network/nginx_https.go +++ b/pkg/relays/khatru_pyramid/nginx_https.go @@ -1,26 +1,28 @@ -package network +package khatru_pyramid import ( "fmt" - "github.com/nodetec/relaywiz/pkg/utils" + // "github.com/nodetec/relaywiz/pkg/utils" + "github.com/pterm/pterm" "log" "os" "os/exec" - - "github.com/pterm/pterm" ) // Function to configure nginx for HTTPS func ConfigureNginxHttps(domainName string) { - dirName := utils.GetDirectoryName(domainName) - spinner, _ := pterm.DefaultSpinner.Start("Configuring nginx for HTTPS...") - err := os.Remove("/etc/nginx/conf.d/nostr_relay.conf") + + const configFile = "nostr_relay_khatru_pyramid.conf" + + err := os.Remove(fmt.Sprintf("/etc/nginx/conf.d/%s", configFile)) if err != nil && !os.IsNotExist(err) { log.Fatalf("Error removing existing nginx configuration: %v", err) } - configContent := fmt.Sprintf(`map $http_upgrade $connection_upgrade { + var configContent string + + configContent = fmt.Sprintf(`map $http_upgrade $connection_upgrade { default upgrade; '' close; } @@ -125,9 +127,9 @@ server { return 301 https://%s$request_uri; } } -`, domainName, dirName, domainName, domainName, domainName, domainName, dirName, domainName) +`, domainName, domainName, domainName, domainName, domainName, domainName, domainName, domainName) - err = os.WriteFile("/etc/nginx/conf.d/nostr_relay.conf", []byte(configContent), 0644) + err = os.WriteFile(fmt.Sprintf("/etc/nginx/conf.d/%s", configFile), []byte(configContent), 0644) if err != nil { log.Fatalf("Error writing nginx configuration: %v", err) } diff --git a/pkg/relay/service.go b/pkg/relays/khatru_pyramid/service.go similarity index 77% rename from pkg/relay/service.go rename to pkg/relays/khatru_pyramid/service.go index ced74b0..32c5081 100644 --- a/pkg/relay/service.go +++ b/pkg/relays/khatru_pyramid/service.go @@ -1,55 +1,58 @@ -package relay +package khatru_pyramid import ( + "github.com/pterm/pterm" "log" "os" "os/exec" "text/template" - "github.com/pterm/pterm" ) -// Template for the systemd service file -const serviceTemplate = `[Unit] -Description=Nostr Relay Pyramid +// Function to check if a user exists +func userExists(username string) bool { + cmd := exec.Command("id", "-u", username) + err := cmd.Run() + return err == nil +} + +// Function to set up the relay service +func SetupRelayService(domain, pubKey string) { + // Template for the systemd service file + const serviceTemplate = `[Unit] +Description=Nostr Relay Khatru Pyramid After=network.target [Service] Type=simple User=nostr +Group=nostr WorkingDirectory=/home/nostr -EnvironmentFile=/etc/systemd/system/nostr-relay-pyramid.env -ExecStart=/usr/local/bin/nostr-relay-pyramid +EnvironmentFile=/etc/systemd/system/nostr-relay-khatru-pyramid.env +ExecStart=/usr/local/bin/nostr-relay-khatru-pyramid Restart=on-failure [Install] WantedBy=multi-user.target ` -// Template for the environment file -const envTemplate = ` + // Template for the environment file + const envTemplate = ` DOMAIN={{.Domain}} -RELAY_NAME=nostr-relay-pyramid +RELAY_NAME=nostr-relay-khatru-pyramid RELAY_PUBKEY={{.PubKey}} -DATABASE_PATH=/var/lib/nostr-relay-pyramid/db -USERDATA_PATH=/var/lib/nostr-relay-pyramid/users.json +DATABASE_PATH=/var/lib/nostr-relay-khatru-pyramid/db +USERDATA_PATH=/var/lib/nostr-relay-khatru-pyramid/users.json ` + // Path for the systemd service file + const serviceFilePath = "/etc/systemd/system/nostr-relay-khatru-pyramid.service" -// Path for the systemd service file -const serviceFilePath = "/etc/systemd/system/nostr-relay-pyramid.service" + // Path for the environment file + const envFilePath = "/etc/systemd/system/nostr-relay-khatru-pyramid.env" -// Path for the environment file -const envFilePath = "/etc/systemd/system/nostr-relay-pyramid.env" + // Data directory + const dataDir = "/var/lib/nostr-relay-khatru-pyramid" -// Function to check if a user exists -func userExists(username string) bool { - cmd := exec.Command("id", "-u", username) - err := cmd.Run() - return err == nil -} - -// Function to set up the relay service -func SetupRelayService(domain, pubKey string) { - spinner, _ := pterm.DefaultSpinner.Start("Configuring nginx for HTTP...") + spinner, _ := pterm.DefaultSpinner.Start("Configuring relay service...") // Check if the service file exists and remove it if it does if _, err := os.Stat(serviceFilePath); err == nil { @@ -79,7 +82,6 @@ func SetupRelayService(domain, pubKey string) { } // Ensure the data directory exists and set ownership - const dataDir = "/var/lib/nostr-relay-pyramid" spinner.UpdateText("Creating data directory...") err := os.MkdirAll(dataDir, 0755) if err != nil { @@ -137,16 +139,15 @@ func SetupRelayService(domain, pubKey string) { // Enable and start the nostr relay service spinner.UpdateText("Enabling and starting service...") - err = exec.Command("systemctl", "enable", "nostr-relay-pyramid").Run() + err = exec.Command("systemctl", "enable", "nostr-relay-khatru-pyramid").Run() if err != nil { log.Fatalf("Error enabling nostr relay service: %v", err) } - err = exec.Command("systemctl", "start", "nostr-relay-pyramid").Run() + err = exec.Command("systemctl", "start", "nostr-relay-khatru-pyramid").Run() if err != nil { log.Fatalf("Error starting nostr relay service: %v", err) } spinner.Success("Nostr relay service configured") } - diff --git a/pkg/relays/strfry/apt.go b/pkg/relays/strfry/apt.go new file mode 100644 index 0000000..9985051 --- /dev/null +++ b/pkg/relays/strfry/apt.go @@ -0,0 +1,27 @@ +package strfry + +import ( + "fmt" + "github.com/nodetec/relaywiz/pkg/manager" + "github.com/pterm/pterm" + "os/exec" +) + +// Function to install necessary strfry package dependencies +func AptInstallDependencies() { + spinner, _ := pterm.DefaultSpinner.Start("Installing strfry dependencies...") + + packages := []string{"git", "build-essential", "libyaml-perl", "libtemplate-perl", "libregexp-grammars-perl", "libssl-dev", "zlib1g-dev", "liblmdb-dev", "libflatbuffers-dev", "libsecp256k1-dev", "libzstd-dev"} + + // Check if package is installed, install if not + for _, p := range packages { + if manager.IsPackageInstalled(p) { + spinner.UpdateText(fmt.Sprintf("%s is already installed.", p)) + } else { + spinner.UpdateText(fmt.Sprintf("Installing %s...", p)) + exec.Command("apt", "install", "-y", "-qq", p).Run() + } + } + + spinner.Success("strfry dependencies installed successfully.") +} diff --git a/pkg/relays/strfry/install.go b/pkg/relays/strfry/install.go new file mode 100644 index 0000000..19e7bbb --- /dev/null +++ b/pkg/relays/strfry/install.go @@ -0,0 +1,93 @@ +package strfry + +import ( + "fmt" + "log" + "os" + "os/exec" + + "github.com/pterm/pterm" +) + +// Function to download, build, and install the binary +func InstallRelayBinary() { + // TODO + // Create the binary on a different machine then download it instead of building it here + // Use these variables and model it after Khatru Pyramid installation + + // Temporary directory for git repository + const tempDir = "/tmp/strfry" + + // URL of the binary to download + // const downloadURL = "https://..." + + // Name of the binary after downloading + const binaryName = "nostr-relay-strfry" + + // Destination directory for the binary + const destDir = "/usr/local/bin" + + // Data directory for the relay + // const dataDir = "/var/lib/nostr-relay-strfry" + + spinner, _ := pterm.DefaultSpinner.Start("Installing strfry relay...") + + pterm.Println() + pterm.Println(pterm.Magenta("Go get coffee, this may take a few minutes...")) + pterm.Println() + + // Download + // Check for and remove existing repository + err := os.RemoveAll(fmt.Sprintf("%s", tempDir)) + if err != nil && !os.IsNotExist(err) { + log.Fatalf("Error removing existing repository: %v", err) + } + + // Download git repository + err = exec.Command("git", "clone", "https://github.com/hoytech/strfry.git", fmt.Sprintf("%s", tempDir)).Run() + if err != nil { + log.Fatalf("Error downloading repository: %v", err) + } + + // Build + // TODO + // Check for development environment variable instead of commenting and uncommenting these lines + + // Check for and remove existing binary + // When developing comment to prevent unecessary builds + // err = os.Remove(fmt.Sprintf("%s/%s", destDir, binaryName)) + // if err != nil && !os.IsNotExist(err) { + // log.Fatalf("Error removing existing binary: %v", err) + // } + + // Check if binary exists + // When developing uncomment to prevent unecessary builds + _, err = os.Stat(fmt.Sprintf("%s/%s", destDir, binaryName)) + if os.IsNotExist(err) { + // Intialize and update git submodule + err = exec.Command("git", "-C", fmt.Sprintf("%s", tempDir), "submodule", "update", "--init").Run() + if err != nil { + log.Fatalf("Error initializing and updating git submodule: %v", err) + } + + // Make setup-golpe + err = exec.Command("make", "-C", fmt.Sprintf("%s", tempDir), "setup-golpe").Run() + if err != nil { + log.Fatalf("Error making setup-golpe: %v", err) + } + + // Make -j2 + err = exec.Command("make", "-C", fmt.Sprintf("%s", tempDir), "-j2").Run() + if err != nil { + log.Fatalf("Error making -j2: %v", err) + } + + // Install + err = exec.Command("mv", fmt.Sprintf("%s/strfry", tempDir), fmt.Sprintf("%s/%s", destDir, binaryName)).Run() + if err != nil { + log.Fatalf("Error installing binary: %v", err) + } + } + + spinner.Success("strfry relay installed successfully.") +} diff --git a/pkg/relays/strfry/nginx_http.go b/pkg/relays/strfry/nginx_http.go new file mode 100644 index 0000000..6d55397 --- /dev/null +++ b/pkg/relays/strfry/nginx_http.go @@ -0,0 +1,62 @@ +package strfry + +import ( + "fmt" + "github.com/pterm/pterm" + "log" + "os" + "os/exec" +) + +// Function to configure nginx for HTTP +func ConfigureNginxHttp(domainName string) { + spinner, _ := pterm.DefaultSpinner.Start("Configuring nginx for HTTP...") + + err := os.MkdirAll(fmt.Sprintf("/var/www/%s/.well-known/acme-challenge/", domainName), 0755) + if err != nil { + log.Fatalf("Error creating directories: %v", err) + } + + const configFile = "nostr_relay_strfry.conf" + + err = os.Remove(fmt.Sprintf("/etc/nginx/conf.d/%s", configFile)) + if err != nil && !os.IsNotExist(err) { + log.Fatalf("Error removing existing nginx configuration: %v", err) + } + + var configContent string + + configContent = fmt.Sprintf(`# %s +server { + listen 80; + listen [::]:80; + server_name %s; + + location /.well-known/acme-challenge/ { + root /var/www/%s; + allow all; + } + + location / { + proxy_pass http://127.0.0.1:7777; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +`, domainName, domainName, domainName) + + err = os.WriteFile(fmt.Sprintf("/etc/nginx/conf.d/%s", configFile), []byte(configContent), 0644) + if err != nil { + log.Fatalf("Error writing nginx configuration: %v", err) + } + + err = exec.Command("systemctl", "restart", "nginx").Run() + if err != nil { + log.Fatalf("Error reloading nginx: %v", err) + } + + spinner.Success("Nginx configured for HTTP") +} diff --git a/pkg/relays/strfry/nginx_https.go b/pkg/relays/strfry/nginx_https.go new file mode 100644 index 0000000..94c8397 --- /dev/null +++ b/pkg/relays/strfry/nginx_https.go @@ -0,0 +1,133 @@ +package strfry + +import ( + "fmt" + "github.com/pterm/pterm" + "log" + "os" + "os/exec" +) + +// Function to configure nginx for HTTPS +func ConfigureNginxHttps(domainName string) { + spinner, _ := pterm.DefaultSpinner.Start("Configuring nginx for HTTPS...") + + const configFile = "nostr_relay_strfry.conf" + + err := os.Remove(fmt.Sprintf("/etc/nginx/conf.d/%s", configFile)) + if err != nil && !os.IsNotExist(err) { + log.Fatalf("Error removing existing nginx configuration: %v", err) + } + + var configContent string + + configContent = fmt.Sprintf(`server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name %s; + + root /var/www/%s; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying 404. + try_files $uri $uri/ =404; + proxy_pass http://127.0.0.1:7777; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + #### SSL Configuration #### + # Test configuration: + # https://www.ssllabs.com/ssltest/analyze.html + # https://cryptcheck.fr/ + ssl_certificate /etc/letsencrypt/live/%s/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/%s/privkey.pem; + # Verify chain of trust of OCSP response using Root CA and Intermediate certs + ssl_trusted_certificate /etc/letsencrypt/live/%s/chain.pem; + + # Only return Nginx in server header + server_tokens off; + + # TODO + # Add support to generate the file in the script + #ssl_dhparam /etc/ssl/certs/dhparam.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + + # For more information on the security of different cipher suites, you can refer to the following link: + # https://ciphersuite.info/ + # Compilation of the top cipher suites 2024: + # https://ssl-config.mozilla.org/#server=nginx + ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305"; + + # Perfect Forward Secrecy (PFS) is frequently compromised without this + ssl_prefer_server_ciphers on; + + ssl_session_tickets off; + + # Enable SSL session caching for improved performance + # Try setting ssl_session_timeout to 1d if performance is bad + ssl_session_timeout 10m; + ssl_session_cache shared:SSL:10m; + + # By default, the buffer size is 16k, which corresponds to minimal overhead when sending big responses. + # To minimize Time To First Byte it may be beneficial to use smaller values + ssl_buffer_size 8k; + + # OCSP stapling + ssl_stapling on; + ssl_stapling_verify on; + + # Security headers + # Test configuration: + # https://securityheaders.com/ + # https://observatory.mozilla.org/ + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"; + + add_header X-Frame-Options DENY; + + # Avoid MIME type sniffing + add_header X-Content-Type-Options nosniff always; + + add_header Referrer-Policy "no-referrer" always; + + add_header X-XSS-Protection 0 always; + + add_header Permissions-Policy "geolocation=(), midi=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()" always; + + # Content-Security-Policy (CSP) + add_header Content-Security-Policy "base-uri 'self'; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests;" always; +} + +server { + listen 80; + listen [::]:80; + server_name %s; + + location /.well-known/acme-challenge/ { + root /var/www/%s; + allow all; + } + + location / { + return 301 https://%s$request_uri; + } +} +`, domainName, domainName, domainName, domainName, domainName, domainName, domainName, domainName) + + err = os.WriteFile(fmt.Sprintf("/etc/nginx/conf.d/%s", configFile), []byte(configContent), 0644) + if err != nil { + log.Fatalf("Error writing nginx configuration: %v", err) + } + + err = exec.Command("systemctl", "reload", "nginx").Run() + if err != nil { + log.Fatalf("Error reloading nginx: %v", err) + } + + spinner.Success("Nginx configured for HTTPS") +} diff --git a/pkg/relays/strfry/service.go b/pkg/relays/strfry/service.go new file mode 100644 index 0000000..27e050c --- /dev/null +++ b/pkg/relays/strfry/service.go @@ -0,0 +1,162 @@ +package strfry + +import ( + "fmt" + "github.com/pterm/pterm" + "log" + "os" + "os/exec" + "text/template" +) + +// Function to check if a user exists +func userExists(username string) bool { + cmd := exec.Command("id", "-u", username) + err := cmd.Run() + return err == nil +} + +// Function to set up the relay service +// WorkingDirectory=/home/nostr +func SetupRelayService(domain string) { + // Template for the systemd service file + const serviceTemplate = `[Unit] +Description=Nostr Relay strfry +After=network.target + +[Service] +Type=simple +User=nostr +Group=nostr +ExecStart=/usr/local/bin/nostr-relay-strfry relay +Restart=on-failure +RestartSec=5 +ProtectHome=yes +NoNewPrivileges=yes +ProtectSystem=full +LimitCORE=1000000000 + +[Install] +WantedBy=multi-user.target +` + + // Path for the systemd service file + const serviceFilePath = "/etc/systemd/system/nostr-relay-strfry.service" + + // Data directory + const dataDir = "/var/lib/nostr-relay-strfry" + + spinner, _ := pterm.DefaultSpinner.Start("Configuring relay service...") + + // Check if the service file exists and remove it if it does + if _, err := os.Stat(serviceFilePath); err == nil { + err = os.Remove(serviceFilePath) + if err != nil { + log.Fatalf("Error removing service file: %v", err) + } + } + + // Ensure the user for the relay service exists + if !userExists("nostr") { + spinner.UpdateText("Creating user 'nostr'...") + err := exec.Command("adduser", "--disabled-login", "--gecos", "", "nostr").Run() + if err != nil { + log.Fatalf("Error creating user: %v", err) + } + } else { + spinner.UpdateText("User 'nostr' already exists") + } + + // Ensure the data directory exists and set ownership + spinner.UpdateText("Creating data directory...") + err := os.MkdirAll(dataDir, 0755) + if err != nil { + log.Fatalf("Error creating data directory: %v", err) + } + + // Use chown command to set ownership of the data directory to the nostr user + err = exec.Command("chown", "-R", "nostr:nostr", dataDir).Run() + if err != nil { + log.Fatalf("Error setting ownership of the data directory: %v", err) + } + + filePath := "/tmp/strfry/strfry.conf" + + // Construct the sed command to change the db path + cmd := exec.Command("sed", "-i", fmt.Sprintf(`s|db = ".*"|db = "%s"|`, dataDir), filePath) + + // Execute the command + if err = cmd.Run(); err != nil { + log.Fatalf("Error changing the db path: %v", err) + } + + // TODO + // Determine system hard limit + // Set nofiles option in config file + cmd = exec.Command("sed", "-i", `s|nofiles = .*|nofiles = 0|`, filePath) + + // Execute the command + if err = cmd.Run(); err != nil { + log.Fatalf("Error changing the nofiles option: %v", err) + } + + // Check for and remove existing config file + err = os.Remove("/etc/strfry.conf") + if err != nil && !os.IsNotExist(err) { + log.Fatalf("Error removing existing config file: %v", err) + } + + // Copy config file to /etc + err = exec.Command("cp", "/tmp/strfry/strfry.conf", "/etc").Run() + if err != nil { + log.Fatalf("Error copying config file: %v", err) + } + + // Use chown command to set ownership of the config file to the nostr user + err = exec.Command("chown", "nostr:nostr", "/etc/strfry.conf").Run() + if err != nil { + log.Fatalf("Error setting ownership of the config file: %v", err) + } + + // Create the systemd service file + spinner.UpdateText("Creating service file...") + serviceFile, err := os.Create(serviceFilePath) + if err != nil { + log.Fatalf("Error creating service file: %v", err) + } + defer serviceFile.Close() + + // TODO + // Try setting the owner to be nostr + + tmpl, err := template.New("service").Parse(serviceTemplate) + if err != nil { + log.Fatalf("Error parsing service template: %v", err) + } + + err = tmpl.Execute(serviceFile, struct{}{}) + if err != nil { + log.Fatalf("Error executing service template: %v", err) + } + + // Reload systemd to apply the new service + spinner.UpdateText("Reloading systemd daemon...") + err = exec.Command("systemctl", "daemon-reload").Run() + if err != nil { + log.Fatalf("Error reloading systemd daemon: %v", err) + } + + // Enable and start the nostr relay service + spinner.UpdateText("Enabling and starting service...") + err = exec.Command("systemctl", "enable", "nostr-relay-strfry").Run() + if err != nil { + log.Fatalf("Error enabling nostr relay service: %v", err) + } + + err = exec.Command("systemctl", "start", "nostr-relay-strfry").Run() + if err != nil { + log.Fatalf("Error starting nostr relay service: %v", err) + } + + spinner.Success("Nostr relay service configured") +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 2012d94..60c9281 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -1,6 +1,8 @@ package utils import ( + "bufio" + "fmt" "os" "strings" ) @@ -20,3 +22,44 @@ func GetDirectoryName(domainName string) string { return domainParts[0] } +func UpdateDatabasePath(filePath string, newDBPath string) { + // Define the file path and new DB path + // filePath := "your_file.txt" // Replace with your actual file path + // newDBPath := "./new-db-location/" // Replace with the new location you want + + // Read the file + file, err := os.Open(filePath) + if err != nil { + fmt.Println("Error opening file:", err) + return + } + defer file.Close() + + // Create a slice to store the modified lines + var lines []string + + // Read the file line by line + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + // Replace the line if it matches the pattern + if strings.HasPrefix(line, "db = \"") { + line = fmt.Sprintf("db = \"%s\"", newDBPath) + } + lines = append(lines, line) + } + + if err := scanner.Err(); err != nil { + fmt.Println("Error reading file:", err) + return + } + + // Write the modified content back to the file + err = os.WriteFile(filePath, []byte(strings.Join(lines, "\n")), 0644) + if err != nil { + fmt.Println("Error writing to file:", err) + return + } + + fmt.Println("Database path updated successfully!") +}