mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Remove managed add maxmind
This commit is contained in:
@@ -4,12 +4,7 @@
|
|||||||
gerbil:
|
gerbil:
|
||||||
start_port: 51820
|
start_port: 51820
|
||||||
base_endpoint: "{{.DashboardDomain}}"
|
base_endpoint: "{{.DashboardDomain}}"
|
||||||
{{if .HybridMode}}
|
|
||||||
managed:
|
|
||||||
id: "{{.HybridId}}"
|
|
||||||
secret: "{{.HybridSecret}}"
|
|
||||||
|
|
||||||
{{else}}
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: "https://{{.DashboardDomain}}"
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
log_level: "info"
|
log_level: "info"
|
||||||
@@ -28,6 +23,7 @@ server:
|
|||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
credentials: false
|
credentials: false
|
||||||
|
{{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
|
||||||
{{if .EnableEmail}}
|
{{if .EnableEmail}}
|
||||||
email:
|
email:
|
||||||
smtp_host: "{{.EmailSMTPHost}}"
|
smtp_host: "{{.EmailSMTPHost}}"
|
||||||
@@ -41,4 +37,3 @@ flags:
|
|||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: false
|
disable_user_create_org: false
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
{{end}}
|
|
||||||
@@ -33,7 +33,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
- 21820:21820/udp
|
- 21820:21820/udp
|
||||||
- 443:{{if .HybridMode}}8443{{else}}443{{end}}
|
- 443:443
|
||||||
- 80:80
|
- 80:80
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
|
|||||||
@@ -3,17 +3,12 @@ api:
|
|||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
{{if not .HybridMode}}
|
|
||||||
http:
|
http:
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||||
pollInterval: "5s"
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
filename: "/etc/traefik/dynamic_config.yml"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
{{else}}
|
|
||||||
file:
|
|
||||||
directory: "/var/dynamic"
|
|
||||||
watch: true
|
|
||||||
{{end}}
|
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
@@ -27,7 +22,7 @@ log:
|
|||||||
maxBackups: 3
|
maxBackups: 3
|
||||||
maxAge: 3
|
maxAge: 3
|
||||||
compress: true
|
compress: true
|
||||||
{{if not .HybridMode}}
|
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
letsencrypt:
|
letsencrypt:
|
||||||
acme:
|
acme:
|
||||||
@@ -36,22 +31,18 @@ certificatesResolvers:
|
|||||||
email: "{{.LetsEncryptEmail}}"
|
email: "{{.LetsEncryptEmail}}"
|
||||||
storage: "/letsencrypt/acme.json"
|
storage: "/letsencrypt/acme.json"
|
||||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
{{end}}
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
websecure:
|
websecure:
|
||||||
address: ":443"
|
address: ":443"
|
||||||
{{if .HybridMode}} proxyProtocol:
|
|
||||||
trustedIPs:
|
|
||||||
- 0.0.0.0/0
|
|
||||||
- ::1/128{{end}}
|
|
||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
{{if not .HybridMode}} http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"{{end}}
|
certResolver: "letsencrypt"
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|||||||
166
install/main.go
166
install/main.go
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -48,10 +47,8 @@ type Config struct {
|
|||||||
InstallGerbil bool
|
InstallGerbil bool
|
||||||
TraefikBouncerKey string
|
TraefikBouncerKey string
|
||||||
DoCrowdsecInstall bool
|
DoCrowdsecInstall bool
|
||||||
|
EnableGeoblocking bool
|
||||||
Secret string
|
Secret string
|
||||||
HybridMode bool
|
|
||||||
HybridId string
|
|
||||||
HybridSecret string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedContainer string
|
type SupportedContainer string
|
||||||
@@ -98,24 +95,6 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\n=== Generating Configuration Files ===")
|
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)
|
||||||
@@ -125,6 +104,15 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\nConfiguration files created successfully!")
|
fmt.Println("\nConfiguration files created successfully!")
|
||||||
|
|
||||||
|
// Download MaxMind database if requested
|
||||||
|
if config.EnableGeoblocking {
|
||||||
|
fmt.Println("\n=== Downloading MaxMind Database ===")
|
||||||
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
|
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||||
|
fmt.Println("You can download it manually later if needed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\n=== Starting installation ===")
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
@@ -174,7 +162,7 @@ func main() {
|
|||||||
fmt.Println("Looks like you already installed Pangolin!")
|
fmt.Println("Looks like you already installed Pangolin!")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
|
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) {
|
||||||
@@ -230,7 +218,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.HybridMode && !alreadyInstalled {
|
if !alreadyInstalled {
|
||||||
// Setup Token Section
|
// Setup Token Section
|
||||||
fmt.Println("\n=== Setup Token ===")
|
fmt.Println("\n=== Setup Token ===")
|
||||||
|
|
||||||
@@ -251,9 +239,7 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\nInstallation complete!")
|
fmt.Println("\nInstallation complete!")
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
||||||
@@ -328,66 +314,38 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
|
|
||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
for {
|
|
||||||
response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "")
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
|
|
||||||
config.HybridMode = true
|
// Set default dashboard domain after base domain is collected
|
||||||
break
|
defaultDashboardDomain := ""
|
||||||
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
|
if config.BaseDomain != "" {
|
||||||
config.HybridMode = false
|
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||||
break
|
}
|
||||||
}
|
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||||
fmt.Println("Please answer 'yes' or 'no'")
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||||
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
|
||||||
|
// Email configuration
|
||||||
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
|
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
||||||
|
|
||||||
|
if config.EnableEmail {
|
||||||
|
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||||
|
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||||
|
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||||
|
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||||
|
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.HybridMode {
|
// Validate required fields
|
||||||
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
|
if config.BaseDomain == "" {
|
||||||
|
fmt.Println("Error: Domain name is required")
|
||||||
if alreadyHaveCreds {
|
os.Exit(1)
|
||||||
config.HybridId = readString(reader, "Enter your ID", "")
|
}
|
||||||
config.HybridSecret = readString(reader, "Enter your secret", "")
|
if config.LetsEncryptEmail == "" {
|
||||||
}
|
fmt.Println("Error: Let's Encrypt email is required")
|
||||||
|
os.Exit(1)
|
||||||
// Try to get public IP as default
|
|
||||||
publicIP := getPublicIP()
|
|
||||||
if publicIP != "" {
|
|
||||||
fmt.Printf("Detected public IP: %s\n", publicIP)
|
|
||||||
}
|
|
||||||
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP)
|
|
||||||
config.InstallGerbil = true
|
|
||||||
} else {
|
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
|
||||||
|
|
||||||
// Set default dashboard domain after base domain is collected
|
|
||||||
defaultDashboardDomain := ""
|
|
||||||
if config.BaseDomain != "" {
|
|
||||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
|
||||||
}
|
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
|
||||||
|
|
||||||
// Email configuration
|
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
|
||||||
|
|
||||||
if config.EnableEmail {
|
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if config.BaseDomain == "" {
|
|
||||||
fmt.Println("Error: Domain name is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if config.LetsEncryptEmail == "" {
|
|
||||||
fmt.Println("Error: Let's Encrypt email is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advanced configuration
|
// Advanced configuration
|
||||||
@@ -395,6 +353,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
fmt.Println("\n=== Advanced Configuration ===")
|
fmt.Println("\n=== Advanced Configuration ===")
|
||||||
|
|
||||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
||||||
|
config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", false)
|
||||||
|
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
fmt.Println("Error: Dashboard Domain name is required")
|
fmt.Println("Error: Dashboard Domain name is required")
|
||||||
@@ -429,11 +388,6 @@ func createConfigFiles(config Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// the hybrid does not need the dynamic config
|
|
||||||
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip .DS_Store
|
// skip .DS_Store
|
||||||
if strings.Contains(path, ".DS_Store") {
|
if strings.Contains(path, ".DS_Store") {
|
||||||
return nil
|
return nil
|
||||||
@@ -663,18 +617,30 @@ func checkPortsAvailable(port int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkIsPangolinInstalledWithHybrid() bool {
|
func downloadMaxMindDatabase() error {
|
||||||
// Check if config/config.yml exists and contains hybrid section
|
fmt.Println("Downloading MaxMind GeoLite2 Country database...")
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
|
||||||
return false
|
// Download the GeoLite2 Country database
|
||||||
|
if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz",
|
||||||
|
"https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil {
|
||||||
|
return fmt.Errorf("failed to download GeoLite2 database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read config file to check for hybrid section
|
// Extract the database
|
||||||
content, err := os.ReadFile("config/config.yml")
|
if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for hybrid section
|
// Find the .mmdb file and move it to the config directory
|
||||||
return bytes.Contains(content, []byte("managed:"))
|
if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil {
|
||||||
|
return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the downloaded files
|
||||||
|
if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to clean up temporary files: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
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 = "https://pangolin.fossorial.io/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")
|
|
||||||
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user