mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-29 06:08:15 +02:00
Installer working with hybrid
This commit is contained in:
parent
8273554a1c
commit
49f0f6ec7d
3 changed files with 178 additions and 85 deletions
|
@ -37,15 +37,28 @@ type DynamicConfig struct {
|
||||||
} `yaml:"http"`
|
} `yaml:"http"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigValues holds the extracted configuration values
|
// TraefikConfigValues holds the extracted configuration values
|
||||||
type ConfigValues struct {
|
type TraefikConfigValues struct {
|
||||||
DashboardDomain string
|
DashboardDomain string
|
||||||
LetsEncryptEmail string
|
LetsEncryptEmail string
|
||||||
BadgerVersion 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
|
// 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
|
// Read main config file
|
||||||
mainConfigData, err := os.ReadFile(mainConfigPath)
|
mainConfigData, err := os.ReadFile(mainConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -57,48 +70,33 @@ func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues,
|
||||||
return nil, fmt.Errorf("error parsing main config file: %w", err)
|
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
|
// Extract values
|
||||||
values := &ConfigValues{
|
values := &TraefikConfigValues{
|
||||||
BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version,
|
BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version,
|
||||||
LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email,
|
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
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractDomainFromRule extracts the domain from a router rule
|
func ReadAppConfig(configPath string) (*AppConfigValues, error) {
|
||||||
func extractDomainFromRule(rule string) string {
|
// Read config file
|
||||||
// Look for the Host(`mydomain.com`) pattern
|
configData, err := os.ReadFile(configPath)
|
||||||
if start := findPattern(rule, "Host(`"); start != -1 {
|
if err != nil {
|
||||||
end := findPattern(rule[start:], "`)")
|
return nil, fmt.Errorf("error reading config file: %w", err)
|
||||||
if end != -1 {
|
|
||||||
return rule[start+6 : start+end]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var appConfig AppConfig
|
||||||
|
if err := yaml.Unmarshal(configData, &appConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing config file: %w", err)
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
|
values := &AppConfigValues{
|
||||||
|
DashboardURL: appConfig.App.DashboardURL,
|
||||||
|
LogLevel: appConfig.App.LogLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findPattern finds the start of a pattern in a string
|
// findPattern finds the start of a pattern in a string
|
||||||
|
|
|
@ -92,6 +92,26 @@ func main() {
|
||||||
config.DoCrowdsecInstall = false
|
config.DoCrowdsecInstall = false
|
||||||
config.Secret = generateRandomSecretKey()
|
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 {
|
if err := createConfigFiles(config); err != nil {
|
||||||
fmt.Printf("Error creating config files: %v\n", err)
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -146,42 +166,10 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Looks like you already installed, so I am going to do the setup...")
|
fmt.Println("Looks like you already installed Pangolin!")
|
||||||
|
|
||||||
// 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 !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
|
||||||
// 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
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if !checkIsCrowdsecInstalledInCompose() {
|
|
||||||
fmt.Println("\n=== CrowdSec Install ===")
|
fmt.Println("\n=== CrowdSec Install ===")
|
||||||
// check if crowdsec is installed
|
// check if crowdsec is installed
|
||||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
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.
|
// 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 readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
||||||
if config.DashboardDomain == "" {
|
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 {
|
if err != nil {
|
||||||
fmt.Printf("Error reading config: %v\n", err)
|
fmt.Printf("Error reading config: %v\n", err)
|
||||||
return
|
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.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
|
||||||
config.BadgerVersion = traefikConfig.BadgerVersion
|
config.BadgerVersion = traefikConfig.BadgerVersion
|
||||||
|
|
||||||
|
@ -237,7 +231,7 @@ func main() {
|
||||||
|
|
||||||
fmt.Println("\nInstallation complete!")
|
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)
|
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,14 +321,11 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.HybridMode {
|
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 {
|
if alreadyHaveCreds {
|
||||||
config.HybridId = readString(reader, "Enter your hybrid ID", "")
|
config.HybridId = readString(reader, "Enter your ID", "")
|
||||||
config.HybridSecret = readString(reader, "Enter your hybrid secret", "")
|
config.HybridSecret = readString(reader, "Enter your 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.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "")
|
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "")
|
||||||
|
@ -620,11 +611,6 @@ func checkPortsAvailable(port int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkIsPangolinInstalledWithHybrid() bool {
|
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
|
// Check if config/config.yml exists and contains hybrid section
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||||
return false
|
return false
|
||||||
|
|
109
install/quickStart.go
Normal file
109
install/quickStart.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue