diff --git a/install/config.go b/install/config.go index 3be62601..e75dd50d 100644 --- a/install/config.go +++ b/install/config.go @@ -37,15 +37,28 @@ type DynamicConfig struct { } `yaml:"http"` } -// ConfigValues holds the extracted configuration values -type ConfigValues struct { +// TraefikConfigValues holds the extracted configuration values +type TraefikConfigValues struct { DashboardDomain string LetsEncryptEmail string BadgerVersion string } +// AppConfig represents the app section of the config.yml +type AppConfig struct { + App struct { + DashboardURL string `yaml:"dashboard_url"` + LogLevel string `yaml:"log_level"` + } `yaml:"app"` +} + +type AppConfigValues struct { + DashboardURL string + LogLevel string +} + // ReadTraefikConfig reads and extracts values from Traefik configuration files -func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, error) { +func ReadTraefikConfig(mainConfigPath string) (*TraefikConfigValues, error) { // Read main config file mainConfigData, err := os.ReadFile(mainConfigPath) if err != nil { @@ -57,48 +70,33 @@ func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, return nil, fmt.Errorf("error parsing main config file: %w", err) } - // Read dynamic config file - dynamicConfigData, err := os.ReadFile(dynamicConfigPath) - if err != nil { - return nil, fmt.Errorf("error reading dynamic config file: %w", err) - } - - var dynamicConfig DynamicConfig - if err := yaml.Unmarshal(dynamicConfigData, &dynamicConfig); err != nil { - return nil, fmt.Errorf("error parsing dynamic config file: %w", err) - } - // Extract values - values := &ConfigValues{ + values := &TraefikConfigValues{ BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version, LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email, } - // Extract DashboardDomain from router rules - // Look for it in the main router rules - for _, router := range dynamicConfig.HTTP.Routers { - if router.Rule != "" { - // Extract domain from Host(`mydomain.com`) - if domain := extractDomainFromRule(router.Rule); domain != "" { - values.DashboardDomain = domain - break - } - } - } - return values, nil } -// extractDomainFromRule extracts the domain from a router rule -func extractDomainFromRule(rule string) string { - // Look for the Host(`mydomain.com`) pattern - if start := findPattern(rule, "Host(`"); start != -1 { - end := findPattern(rule[start:], "`)") - if end != -1 { - return rule[start+6 : start+end] - } +func ReadAppConfig(configPath string) (*AppConfigValues, error) { + // Read config file + configData, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("error reading config file: %w", err) } - return "" + + var appConfig AppConfig + if err := yaml.Unmarshal(configData, &appConfig); err != nil { + return nil, fmt.Errorf("error parsing config file: %w", err) + } + + values := &AppConfigValues{ + DashboardURL: appConfig.App.DashboardURL, + LogLevel: appConfig.App.LogLevel, + } + + return values, nil } // findPattern finds the start of a pattern in a string diff --git a/install/main.go b/install/main.go index 1dd0b37c..3e9c093c 100644 --- a/install/main.go +++ b/install/main.go @@ -92,6 +92,26 @@ func main() { config.DoCrowdsecInstall = false config.Secret = generateRandomSecretKey() + fmt.Println("\n=== Generating Configuration Files ===") + + // If the secret and id are not generated then generate them + if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") { + // fmt.Println("Requesting hybrid credentials from cloud...") + credentials, err := requestHybridCredentials() + if err != nil { + fmt.Printf("Error requesting hybrid credentials: %v\n", err) + fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.") + os.Exit(1) + } + config.HybridId = credentials.RemoteExitNodeId + config.HybridSecret = credentials.Secret + fmt.Printf("Your managed credentials have been obtained successfully.\n") + fmt.Printf(" ID: %s\n", config.HybridId) + fmt.Printf(" Secret: %s\n", config.HybridSecret) + fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.") + readBool(reader, "Have you adopted your node?", true) + } + if err := createConfigFiles(config); err != nil { fmt.Printf("Error creating config files: %v\n", err) os.Exit(1) @@ -146,42 +166,10 @@ func main() { } } else { - fmt.Println("Looks like you already installed, so I am going to do the setup...") - - // Read existing config to get DashboardDomain - traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") - if err != nil { - fmt.Printf("Warning: Could not read existing config: %v\n", err) - fmt.Println("You may need to manually enter your domain information.") - config = collectUserInput(reader) - } else { - config.DashboardDomain = traefikConfig.DashboardDomain - config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail - config.BadgerVersion = traefikConfig.BadgerVersion - - // Show detected values and allow user to confirm or re-enter - fmt.Println("Detected existing configuration:") - fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain) - fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail) - fmt.Printf("Badger Version: %s\n", config.BadgerVersion) - - if !readBool(reader, "Are these values correct?", true) { - config = collectUserInput(reader) - } - } - - // Check if Pangolin is already installed with hybrid section - // if checkIsPangolinInstalledWithHybrid() { - // fmt.Println("\n=== Convert to Self-Host Node ===") - // if readBool(reader, "Do you want to convert this Pangolin instance into a managed self-host node?", true) { - // fmt.Println("hello world") - // return - // } - // } - + fmt.Println("Looks like you already installed Pangolin!") } - if !checkIsCrowdsecInstalledInCompose() { + if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() { fmt.Println("\n=== CrowdSec Install ===") // check if crowdsec is installed if readBool(reader, "Would you like to install CrowdSec?", false) { @@ -190,12 +178,18 @@ func main() { // BUG: crowdsec installation will be skipped if the user chooses to install on the first installation. if readBool(reader, "Are you willing to manage CrowdSec?", false) { if config.DashboardDomain == "" { - traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") + traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml") if err != nil { fmt.Printf("Error reading config: %v\n", err) return } - config.DashboardDomain = traefikConfig.DashboardDomain + appConfig, err := ReadAppConfig("config/config.yml") + if err != nil { + fmt.Printf("Error reading config: %v\n", err) + return + } + + config.DashboardDomain = appConfig.DashboardURL config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail config.BadgerVersion = traefikConfig.BadgerVersion @@ -237,7 +231,7 @@ func main() { fmt.Println("\nInstallation complete!") - if !config.HybridMode { + if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() { fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } } @@ -327,15 +321,12 @@ func collectUserInput(reader *bufio.Reader) Config { } if config.HybridMode { - alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard?", false) + alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false) if alreadyHaveCreds { - config.HybridId = readString(reader, "Enter your hybrid ID", "") - config.HybridSecret = readString(reader, "Enter your hybrid secret", "") - } else { - // Just print instructions for right now - fmt.Println("Please visit https://pangolin.fossorial.io, create a self hosted node, and return with the credentials.") - } + config.HybridId = readString(reader, "Enter your ID", "") + config.HybridSecret = readString(reader, "Enter your secret", "") + } config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "") config.InstallGerbil = true @@ -620,11 +611,6 @@ func checkPortsAvailable(port int) error { } func checkIsPangolinInstalledWithHybrid() bool { - // Check if docker-compose.yml exists (indicating Pangolin is installed) - if _, err := os.Stat("docker-compose.yml"); err != nil { - return false - } - // Check if config/config.yml exists and contains hybrid section if _, err := os.Stat("config/config.yml"); err != nil { return false diff --git a/install/quickStart.go b/install/quickStart.go new file mode 100644 index 00000000..0904ce86 --- /dev/null +++ b/install/quickStart.go @@ -0,0 +1,109 @@ +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +const ( + FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e" + // CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" + CLOUD_API_URL = "http://localhost:4000/api/v1/remote-exit-node/quick-start" +) + +// HybridCredentials represents the response from the cloud API +type HybridCredentials struct { + RemoteExitNodeId string `json:"remoteExitNodeId"` + Secret string `json:"secret"` +} + +// APIResponse represents the full response structure from the cloud API +type APIResponse struct { + Data HybridCredentials `json:"data"` +} + +// RequestPayload represents the request body structure +type RequestPayload struct { + Token string `json:"token"` +} + +func generateValidationToken() string { + timestamp := time.Now().UnixMilli() + data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp) + obfuscated := make([]byte, len(data)) + for i, char := range []byte(data) { + obfuscated[i] = char + 5 + } + return base64.StdEncoding.EncodeToString(obfuscated) +} + +// requestHybridCredentials makes an HTTP POST request to the cloud API +// to get hybrid credentials (ID and secret) +func requestHybridCredentials() (*HybridCredentials, error) { + // Generate validation token + token := generateValidationToken() + + // Create request payload + payload := RequestPayload{ + Token: token, + } + + // Marshal payload to JSON + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal request payload: %v", err) + } + + // Create HTTP request + req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request: %v", err) + } + + // Set headers + req.Header.Set("Content-Type", "application/json") + + // Create HTTP client with timeout + client := &http.Client{ + Timeout: 30 * time.Second, + } + + // Make the request + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to make HTTP request: %v", err) + } + defer resp.Body.Close() + + // Check response status + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) + } + + // Read response body for debugging + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + + // Print the raw JSON response for debugging + // fmt.Printf("Raw JSON response: %s\n", string(body)) + + // Parse response + var apiResponse APIResponse + if err := json.Unmarshal(body, &apiResponse); err != nil { + return nil, fmt.Errorf("failed to decode API response: %v", err) + } + + // Validate response data + if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" { + return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret") + } + + return &apiResponse.Data, nil +}