Files
phishingclub/backend/install/installer.go
Ronni Skansing 61b1019ba3 import example templates on install
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
2025-10-11 10:15:33 +02:00

510 lines
14 KiB
Go

package install
import (
"bufio"
"bytes"
"embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
"time"
)
//go:embed systemd.service
var installFiles embed.FS
const (
// Installation paths
installDir = "/opt/phishingclub"
binaryName = "phishingclub"
dataDir = "data"
// User and group
serviceUser = "phishingclub"
serviceGroup = "phishingclub"
// Service
serviceName = "phishingclub"
)
// Install handles the complete installation process interactively
func Install() error {
return RunInteractiveInstall()
}
// InstallNonInteractive handles the non-interactive installation process
func InstallNonInteractive() error {
if os.Geteuid() != 0 {
return fmt.Errorf("installation must be run as root")
}
steps := []struct {
name string
fn func() error
}{
{"check sqlite dependency", checkSQLiteDependency},
{"create user and group", createUserAndGroup},
{"create directories", createDirectories},
{"install binary", installBinary},
{"install systemd service", installSystemdService},
{"set permissions", setPermissions},
{"enable service", enableService},
{"start service", startService},
{"print info", outputCredentialsAndInfo},
}
for _, step := range steps {
fmt.Printf("Step: %s\n", step.name)
if err := step.fn(); err != nil {
return fmt.Errorf("%s: %w", step.name, err)
}
}
fmt.Println()
fmt.Println("Installer completed successfully! 🐟")
fmt.Println()
fmt.Println("If the service is restarted before the first used is setup, the password will change! - Check the logs")
fmt.Println()
fmt.Println("# Tips")
fmt.Println("'journalctl -u phishingclub.service -f' to see logs")
fmt.Println("'systemctl status phishingclub' to check status of the service")
fmt.Println("")
fmt.Println()
return nil
}
// Update handles the update process of the application
func Update() error {
if os.Geteuid() != 0 {
return fmt.Errorf("update must be run as root")
}
// Check if service exists
if err := checkServiceExists(); err != nil {
return fmt.Errorf("service check failed: %w", err)
}
steps := []struct {
name string
fn func() error
}{
{"stop service", stopService},
{"backup current binary", backupCurrentBinary},
{"update binary", updateBinary},
{"start service", startService},
}
for _, step := range steps {
fmt.Printf("Step: %s\n", step.name)
if err := step.fn(); err != nil {
return fmt.Errorf("%s: %w", step.name, err)
}
}
fmt.Println()
fmt.Println("# Post-update Status Check")
// Give the service a moment to stabilize
time.Sleep(2 * time.Second)
cmd := exec.Command("systemctl", "status", serviceName)
output, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("⚠️ Warning: Service may not be running properly after update")
fmt.Printf("Service status output:\n%s\n", string(output))
fmt.Printf("Check status with: systemctl status %s\n", serviceName)
return nil
}
if !strings.Contains(string(output), "active (running)") {
fmt.Println("⚠️ Warning: Service is not in 'active' state after update")
fmt.Printf("Service status output:\n%s\n", string(output))
return nil
}
fmt.Println("✅ Service is running")
fmt.Println()
fmt.Println("Update completed! 🐟")
fmt.Println()
return nil
}
// checkServiceExists verifies that the service is installed
func checkServiceExists() error {
servicePath := filepath.Join("/etc/systemd/system", serviceName+".service")
if _, err := os.Stat(servicePath); os.IsNotExist(err) {
return fmt.Errorf("service is not installed. Please run --install first")
}
return nil
}
// stopService stops the running service
func stopService() error {
cmd := exec.Command("systemctl", "stop", serviceName)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to stop service: %s, error: %w", string(output), err)
}
// Wait a moment to ensure the service is fully stopped
time.Sleep(2 * time.Second)
return nil
}
// backupCurrentBinary creates a backup of the current binary
func backupCurrentBinary() error {
currentBinary := filepath.Join(installDir, binaryName)
backupBinary := filepath.Join(installDir, binaryName+".backup")
// #nosec
input, err := os.ReadFile(currentBinary)
if err != nil {
return fmt.Errorf("failed to read current binary: %w", err)
}
if err := os.WriteFile(backupBinary, input, 0600); err != nil {
return fmt.Errorf("failed to write backup binary: %w", err)
}
return nil
}
// updateBinary updates the binary with the new version
func updateBinary() error {
executable, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
// #nosec
input, err := os.ReadFile(executable)
if err != nil {
return fmt.Errorf("failed to read new binary: %w", err)
}
binaryPath := filepath.Join(installDir, binaryName)
if err := os.WriteFile(binaryPath, input, 0600); err != nil {
return fmt.Errorf("failed to write new binary: %w", err)
}
// Set proper ownership
if err := setPermissions(); err != nil {
return fmt.Errorf("failed to set permissions: %w", err)
}
return nil
}
func checkSQLiteDependency() error {
// Check if sqlite3 is installed
if _, err := exec.LookPath("sqlite3"); err != nil {
fmt.Println("SQLite3 is not installed. Attempting to install...")
// Detect package manager and install sqlite
if err := installSQLite(); err != nil {
return fmt.Errorf("failed to install sqlite: %w", err)
}
}
// Verify sqlite installation
cmd := exec.Command("sqlite3", "--version")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("sqlite verification failed: %w", err)
}
fmt.Printf("SQLite version: %s", output)
return nil
}
func installSQLite() error {
// Detect the package manager and install sqlite
var cmd *exec.Cmd
// Check for apt (Debian/Ubuntu)
if _, err := exec.LookPath("apt"); err == nil {
cmd = exec.Command("apt", "update")
err := cmd.Run() // Update package list
if err != nil {
fmt.Printf("ERR: %s\n", err)
}
cmd = exec.Command("apt", "install", "-y", "sqlite3")
} else if _, err := exec.LookPath("yum"); err == nil {
// Check for yum (RHEL/CentOS)
cmd = exec.Command("yum", "install", "-y", "sqlite")
} else if _, err := exec.LookPath("dnf"); err == nil {
// Check for dnf (Fedora)
cmd = exec.Command("dnf", "install", "-y", "sqlite")
} else if _, err := exec.LookPath("pacman"); err == nil {
// Check for pacman (Arch)
cmd = exec.Command("pacman", "-S", "--noconfirm", "sqlite")
} else {
return fmt.Errorf("no supported package manager found (apt, yum, dnf, or pacman)")
}
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to install sqlite: %s, error: %w", string(output), err)
}
return nil
}
// enableService enables the systemd service so it gets started on boot
func enableService() error {
if err := exec.Command("systemctl", "enable", serviceName).Run(); err != nil {
return fmt.Errorf("failed to enable service: %w", err)
}
return nil
}
// startService starts the systemd service
func startService() error {
if err := exec.Command("systemctl", "start", serviceName).Run(); err != nil {
return fmt.Errorf("failed to start service: %w", err)
}
return nil
}
func Uninstall() error {
if os.Geteuid() != 0 {
return fmt.Errorf("uninstallation must be run as root")
}
// Display warning and confirmation prompt
fmt.Println("⚠️ WARNING: Uninstallation will remove ALL components of Phishing Club, including:")
fmt.Println(" • The application binary and its service")
fmt.Println(" • ALL configuration files")
fmt.Println(" • ALL data, including the database")
fmt.Println(" • The phishingclub user and group")
fmt.Println("\nThis operation CANNOT be undone!")
reader := bufio.NewReader(os.Stdin)
fmt.Print("\nType 'YES' (all caps) to confirm uninstallation: ")
confirmation, _ := reader.ReadString('\n')
confirmation = strings.TrimSpace(confirmation)
if confirmation != "YES" {
fmt.Println("Uninstallation cancelled.")
return fmt.Errorf("uninstallation cancelled by user")
}
return performUninstall()
}
// UninstallNonInteractive performs uninstallation without confirmation prompts
func UninstallNonInteractive() error {
if os.Geteuid() != 0 {
return fmt.Errorf("uninstallation must be run as root")
}
return performUninstall()
}
// performUninstall handles the actual uninstallation process
func performUninstall() error {
fmt.Println("Uninstalling Phishing Club...")
// Stop and disable service
err := exec.Command("systemctl", "stop", serviceName).Run()
if err != nil {
fmt.Printf("Warning: Failed to stop service: %v\n", err)
// Continue with uninstallation
}
err = exec.Command("systemctl", "disable", serviceName).Run()
if err != nil {
fmt.Printf("Warning: Failed to disable service: %v\n", err)
// Continue with uninstallation
}
// Remove service file
servicePath := filepath.Join("/etc/systemd/system", serviceName+".service")
err = os.Remove(servicePath)
if err != nil && !os.IsNotExist(err) {
fmt.Printf("Warning: Failed to remove service unit file: %v\n", err)
// Continue with uninstallation
}
// Reload systemd
err = exec.Command("systemctl", "daemon-reload").Run()
if err != nil {
fmt.Printf("Warning: Failed to reload systemctl daemon: %v\n", err)
// Continue with uninstallation
}
// Remove installation directory
err = os.RemoveAll(installDir)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove install directory: %w", err)
}
// Remove user and group
fmt.Printf("Removing user and group: %s\n", serviceUser)
err = exec.Command("userdel", serviceUser).Run()
if err != nil {
fmt.Printf("Warning: Failed to delete user %s: %v\n", serviceUser, err)
// Continue with uninstallation
}
_ = exec.Command("groupdel", serviceGroup).Run()
// Group deletion errors are not critical
fmt.Println("\n✅ Uninstallation completed successfully!")
fmt.Println("All Phishing Club components have been removed from your system.")
return nil
}
func createUserAndGroup() error {
// Check if group exists
if err := exec.Command("getent", "group", serviceGroup).Run(); err != nil {
cmd := exec.Command("groupadd", serviceGroup)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to create group: %w", err)
}
}
// Check if user exists
if err := exec.Command("getent", "passwd", serviceUser).Run(); err != nil {
cmd := exec.Command("useradd",
"-r",
"-g", serviceGroup,
"-s", "/bin/false",
serviceUser,
)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
}
return nil
}
func createDirectories() error {
dirs := []string{
installDir,
filepath.Join(installDir, dataDir),
filepath.Join(installDir, dataDir, "assets"),
filepath.Join(installDir, dataDir, "assets", "shared"),
filepath.Join(installDir, dataDir, "attachments"),
filepath.Join(installDir, dataDir, "attachments", "shared"),
filepath.Join(installDir, dataDir, "certs"),
filepath.Join(installDir, dataDir, "certs", "own-managed"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0750); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}
return nil
}
func installBinary() error {
executable, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
// #nosec
input, err := os.ReadFile(executable)
if err != nil {
return fmt.Errorf("failed to read executable: %w", err)
}
binaryPath := filepath.Join(installDir, binaryName)
// #nosec
if err := os.WriteFile(binaryPath, input, 0750); err != nil {
return fmt.Errorf("failed to write binary: %w", err)
}
return nil
}
func installSystemdService() error {
serviceTemplate, err := installFiles.ReadFile("systemd.service")
if err != nil {
return fmt.Errorf("failed to read service template: %w", err)
}
// Create template data with all required fields
data := struct {
User string
Group string
InstallDir string
BinaryPath string
ConfigPath string
DataDir string
}{
User: serviceUser,
Group: serviceGroup,
InstallDir: installDir,
BinaryPath: filepath.Join(installDir, binaryName),
ConfigPath: filepath.Join(installDir, "config.json"),
DataDir: filepath.Join(installDir, dataDir),
}
// Parse and execute the template
tmpl, err := template.New("service").Parse(string(serviceTemplate))
if err != nil {
return fmt.Errorf("failed to parse service template: %w", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return fmt.Errorf("failed to execute service template: %w", err)
}
servicePath := filepath.Join("/etc/systemd/system", serviceName+".service")
// #nosec
if err := os.WriteFile(servicePath, buf.Bytes(), 0644); err != nil {
return fmt.Errorf("failed to write service file: %w", err)
}
if err := exec.Command("systemctl", "daemon-reload").Run(); err != nil {
return fmt.Errorf("failed to reload systemd: %w", err)
}
return nil
}
func setPermissions() error {
// #nosec
cmd := exec.Command("chown", "-R",
fmt.Sprintf("%s:%s", serviceUser, serviceGroup),
installDir,
)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to set ownership: %w", err)
}
return nil
}
func outputCredentialsAndInfo() error {
time.Sleep(3 * time.Second)
fmt.Println()
fmt.Println("<<< IMPORTANT >>>")
steps := []string{
"journalctl -u phishingclub.service -r -n 5000 --no-pager --output=cat | grep 'Username:' -m1",
"journalctl -u phishingclub.service -r -n 5000 --no-pager --output=cat | grep 'Password:' -m1",
"journalctl -u phishingclub.service -r -n 5000 --no-pager --output=cat | grep 'Phishing HTTPS' -m1 -B1 | tac",
"journalctl -u phishingclub.service -r -n 5000 --no-pager --output=cat | grep 'Phishing HTTP server' -m1 -B1 | tac",
"journalctl -u phishingclub.service -r -n 5000 --no-pager --output=cat | grep 'Admin server' -m1 -B1 | tac",
}
for _, t := range steps {
cmd := exec.Command("sh", "-c", t)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to get all install information: %w", err)
}
}
return nil
}