Remove cloud server configuration files and references and add dynmamic
dns Shit
This commit is contained in:
903
config/ansible/tasks/global/utils/dynamic-dns-cf.go
Normal file
903
config/ansible/tasks/global/utils/dynamic-dns-cf.go
Normal file
@@ -0,0 +1,903 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CloudFlare API structures
|
||||
type CloudFlareResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []CloudFlareError `json:"errors"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Messages []CloudFlareMessage `json:"messages"`
|
||||
}
|
||||
|
||||
type CloudFlareError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type CloudFlareMessage struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type DNSRecord struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
TTL int `json:"ttl"`
|
||||
ZoneID string `json:"zone_id"`
|
||||
}
|
||||
|
||||
type Zone struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type TokenVerification struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type NotificationInfo struct {
|
||||
RecordName string
|
||||
OldIP string
|
||||
NewIP string
|
||||
IsNew bool
|
||||
}
|
||||
|
||||
// Configuration
|
||||
type Config struct {
|
||||
APIToken string
|
||||
RecordNames []string
|
||||
IPSources []string
|
||||
DryRun bool
|
||||
Verbose bool
|
||||
Force bool
|
||||
TTL int
|
||||
TelegramBotToken string
|
||||
TelegramChatID string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// Default IP sources
|
||||
var defaultIPSources = []string{
|
||||
"https://ifconfig.co/ip",
|
||||
"https://ip.seeip.org",
|
||||
"https://ipv4.icanhazip.com",
|
||||
"https://api.ipify.org",
|
||||
}
|
||||
|
||||
func main() {
|
||||
config := &Config{
|
||||
Client: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
// Command line flags
|
||||
var ipSourcesFlag string
|
||||
var recordsFlag string
|
||||
var listZones bool
|
||||
var testTelegram bool
|
||||
flag.StringVar(&recordsFlag, "record", "", "DNS A record name(s) to update - comma-separated for multiple (required)")
|
||||
flag.StringVar(&ipSourcesFlag, "ip-sources", "", "Comma-separated list of IP detection services (optional)")
|
||||
flag.BoolVar(&config.DryRun, "dry-run", false, "Show what would be done without making changes")
|
||||
flag.BoolVar(&config.Verbose, "verbose", false, "Enable verbose logging")
|
||||
flag.BoolVar(&listZones, "list-zones", false, "List all accessible zones and exit")
|
||||
flag.BoolVar(&config.Force, "force", false, "Force update even if IP hasn't changed")
|
||||
flag.BoolVar(&testTelegram, "test-telegram", false, "Send a test Telegram notification and exit")
|
||||
flag.IntVar(&config.TTL, "ttl", 300, "TTL for DNS record in seconds")
|
||||
|
||||
// Custom usage function
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "CloudFlare Dynamic DNS Tool\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Updates CloudFlare DNS A records with your current public IP address.\n")
|
||||
fmt.Fprintf(os.Stderr, "Supports multiple records, dry-run mode, and Telegram notifications.\n\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "USAGE:\n")
|
||||
fmt.Fprintf(os.Stderr, " %s [OPTIONS]\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, "REQUIRED ENVIRONMENT VARIABLES:\n")
|
||||
fmt.Fprintf(os.Stderr, " CLOUDFLARE_API_TOKEN CloudFlare API token with Zone:DNS:Edit permissions\n")
|
||||
fmt.Fprintf(os.Stderr, " Get from: https://dash.cloudflare.com/profile/api-tokens\n\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "OPTIONAL ENVIRONMENT VARIABLES:\n")
|
||||
fmt.Fprintf(os.Stderr, " TELEGRAM_BOT_TOKEN Telegram bot token for notifications\n")
|
||||
fmt.Fprintf(os.Stderr, " TELEGRAM_CHAT_ID Telegram chat ID to send notifications to\n\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "OPTIONS:\n")
|
||||
flag.PrintDefaults()
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nEXAMPLES:\n")
|
||||
fmt.Fprintf(os.Stderr, " # Update single record\n")
|
||||
fmt.Fprintf(os.Stderr, " %s -record home.example.com\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # Update multiple records\n")
|
||||
fmt.Fprintf(os.Stderr, " %s -record \"home.example.com,api.example.com,vpn.mydomain.net\"\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # Dry run with verbose output\n")
|
||||
fmt.Fprintf(os.Stderr, " %s -dry-run -verbose -record home.example.com\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # Force update even if IP hasn't changed\n")
|
||||
fmt.Fprintf(os.Stderr, " %s -force -record home.example.com\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # Custom TTL and IP sources\n")
|
||||
fmt.Fprintf(os.Stderr, " %s -record home.example.com -ttl 600 -ip-sources \"https://ifconfig.co/ip,https://api.ipify.org\"\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # List accessible CloudFlare zones\n")
|
||||
fmt.Fprintf(os.Stderr, " %s -list-zones\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, " # Test Telegram notifications\n")
|
||||
fmt.Fprintf(os.Stderr, " %s -test-telegram\n\n", os.Args[0])
|
||||
|
||||
fmt.Fprintf(os.Stderr, "SETUP:\n")
|
||||
fmt.Fprintf(os.Stderr, " 1. Create CloudFlare API token:\n")
|
||||
fmt.Fprintf(os.Stderr, " - Go to https://dash.cloudflare.com/profile/api-tokens\n")
|
||||
fmt.Fprintf(os.Stderr, " - Use 'Edit zone DNS' template\n")
|
||||
fmt.Fprintf(os.Stderr, " - Select your zones\n")
|
||||
fmt.Fprintf(os.Stderr, " - Copy token and set CLOUDFLARE_API_TOKEN environment variable\n\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, " 2. Optional: Setup Telegram notifications:\n")
|
||||
fmt.Fprintf(os.Stderr, " - Message @BotFather on Telegram to create a bot\n")
|
||||
fmt.Fprintf(os.Stderr, " - Get your chat ID by messaging your bot, then visit:\n")
|
||||
fmt.Fprintf(os.Stderr, " https://api.telegram.org/bot<BOT_TOKEN>/getUpdates\n")
|
||||
fmt.Fprintf(os.Stderr, " - Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID environment variables\n\n")
|
||||
|
||||
fmt.Fprintf(os.Stderr, "NOTES:\n")
|
||||
fmt.Fprintf(os.Stderr, " - Records can be in different CloudFlare zones\n")
|
||||
fmt.Fprintf(os.Stderr, " - Only updates when IP actually changes (unless -force is used)\n")
|
||||
fmt.Fprintf(os.Stderr, " - Supports both root domains and subdomains\n")
|
||||
fmt.Fprintf(os.Stderr, " - Telegram notifications sent only when IP changes\n")
|
||||
fmt.Fprintf(os.Stderr, " - Use -dry-run to test without making changes\n\n")
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Validate required arguments (unless listing zones or testing telegram)
|
||||
if recordsFlag == "" && !listZones && !testTelegram {
|
||||
fmt.Fprintf(os.Stderr, "Error: -record flag is required\n")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Parse record names
|
||||
if recordsFlag != "" {
|
||||
config.RecordNames = strings.Split(recordsFlag, ",")
|
||||
// Trim whitespace from each record name
|
||||
for i, record := range config.RecordNames {
|
||||
config.RecordNames[i] = strings.TrimSpace(record)
|
||||
}
|
||||
}
|
||||
|
||||
// Get API token from environment
|
||||
config.APIToken = os.Getenv("CLOUDFLARE_API_TOKEN")
|
||||
if config.APIToken == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: CLOUDFLARE_API_TOKEN environment variable is required\n")
|
||||
fmt.Fprintf(os.Stderr, "Get your API token from: https://dash.cloudflare.com/profile/api-tokens\n")
|
||||
fmt.Fprintf(os.Stderr, "Create a token with 'Zone:DNS:Edit' permissions for your zone\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get optional Telegram credentials
|
||||
config.TelegramBotToken = os.Getenv("TELEGRAM_BOT_TOKEN")
|
||||
config.TelegramChatID = os.Getenv("TELEGRAM_CHAT_ID")
|
||||
|
||||
if config.Verbose && config.TelegramBotToken != "" && config.TelegramChatID != "" {
|
||||
fmt.Println("Telegram notifications enabled")
|
||||
}
|
||||
|
||||
// Parse IP sources
|
||||
if ipSourcesFlag != "" {
|
||||
config.IPSources = strings.Split(ipSourcesFlag, ",")
|
||||
} else {
|
||||
config.IPSources = defaultIPSources
|
||||
}
|
||||
|
||||
if config.Verbose {
|
||||
fmt.Printf("Config: Records=%v, TTL=%d, DryRun=%v, Force=%v, IPSources=%v\n",
|
||||
config.RecordNames, config.TTL, config.DryRun, config.Force, config.IPSources)
|
||||
}
|
||||
|
||||
// If testing telegram, do that and exit (skip API token validation)
|
||||
if testTelegram {
|
||||
if err := testTelegramNotification(config); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error testing Telegram: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Validate API token
|
||||
if err := validateToken(config); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error validating API token: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if config.Verbose {
|
||||
fmt.Println("API token validated successfully")
|
||||
}
|
||||
|
||||
// If listing zones, do that and exit
|
||||
if listZones {
|
||||
if err := listAllZones(config); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error listing zones: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Get current public IP
|
||||
currentIP, err := getCurrentIP(config)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting current IP: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if config.Verbose {
|
||||
fmt.Printf("Current public IP: %s\n", currentIP)
|
||||
fmt.Printf("Processing %d record(s)\n", len(config.RecordNames))
|
||||
}
|
||||
|
||||
// Process each record
|
||||
var totalUpdates int
|
||||
var allNotifications []NotificationInfo
|
||||
|
||||
for _, recordName := range config.RecordNames {
|
||||
if config.Verbose {
|
||||
fmt.Printf("\n--- Processing record: %s ---\n", recordName)
|
||||
}
|
||||
|
||||
// Find the zone for the record
|
||||
zoneName, zoneID, err := findZoneForRecord(config, recordName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error finding zone for %s: %v\n", recordName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if config.Verbose {
|
||||
fmt.Printf("Found zone: %s (ID: %s)\n", zoneName, zoneID)
|
||||
}
|
||||
|
||||
// Find existing DNS record
|
||||
record, err := findDNSRecordByName(config, zoneID, recordName)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error finding DNS record %s: %v\n", recordName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Compare IPs
|
||||
if record != nil {
|
||||
if record.Content == currentIP && !config.Force {
|
||||
fmt.Printf("DNS record %s already points to %s - no update needed\n", recordName, currentIP)
|
||||
continue
|
||||
}
|
||||
|
||||
if config.Verbose {
|
||||
if record.Content == currentIP {
|
||||
fmt.Printf("DNS record %s already points to %s, but forcing update\n",
|
||||
recordName, currentIP)
|
||||
} else {
|
||||
fmt.Printf("DNS record %s currently points to %s, needs update to %s\n",
|
||||
recordName, record.Content, currentIP)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if config.Verbose {
|
||||
fmt.Printf("DNS record %s does not exist, will create it\n", recordName)
|
||||
}
|
||||
}
|
||||
|
||||
// Update or create record
|
||||
if config.DryRun {
|
||||
if record != nil {
|
||||
if record.Content == currentIP && config.Force {
|
||||
fmt.Printf("DRY RUN: Would force update DNS record %s (already %s)\n",
|
||||
recordName, currentIP)
|
||||
} else {
|
||||
fmt.Printf("DRY RUN: Would update DNS record %s from %s to %s\n",
|
||||
recordName, record.Content, currentIP)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("DRY RUN: Would create DNS record %s with IP %s\n",
|
||||
recordName, currentIP)
|
||||
}
|
||||
|
||||
// Collect notification info for dry-run
|
||||
if record == nil || record.Content != currentIP || config.Force {
|
||||
var oldIPForNotification string
|
||||
if record != nil {
|
||||
oldIPForNotification = record.Content
|
||||
}
|
||||
allNotifications = append(allNotifications, NotificationInfo{
|
||||
RecordName: recordName,
|
||||
OldIP: oldIPForNotification,
|
||||
NewIP: currentIP,
|
||||
IsNew: record == nil,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
var wasUpdated bool
|
||||
var oldIP string
|
||||
|
||||
if record != nil {
|
||||
oldIP = record.Content
|
||||
err = updateDNSRecordByName(config, zoneID, record.ID, recordName, currentIP)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error updating DNS record %s: %v\n", recordName, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Successfully updated DNS record %s to %s\n", recordName, currentIP)
|
||||
wasUpdated = true
|
||||
} else {
|
||||
err = createDNSRecordByName(config, zoneID, recordName, currentIP)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating DNS record %s: %v\n", recordName, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Successfully created DNS record %s with IP %s\n", recordName, currentIP)
|
||||
wasUpdated = true
|
||||
}
|
||||
|
||||
// Collect notification info for actual updates
|
||||
if wasUpdated && (record == nil || oldIP != currentIP || config.Force) {
|
||||
allNotifications = append(allNotifications, NotificationInfo{
|
||||
RecordName: recordName,
|
||||
OldIP: oldIP,
|
||||
NewIP: currentIP,
|
||||
IsNew: record == nil,
|
||||
})
|
||||
totalUpdates++
|
||||
}
|
||||
}
|
||||
|
||||
// Send batch notification if there were any changes
|
||||
if len(allNotifications) > 0 {
|
||||
sendBatchTelegramNotification(config, allNotifications, config.DryRun)
|
||||
}
|
||||
|
||||
if !config.DryRun && config.Verbose {
|
||||
fmt.Printf("\nProcessed %d record(s), %d update(s) made\n", len(config.RecordNames), totalUpdates)
|
||||
}
|
||||
}
|
||||
|
||||
func validateToken(config *Config) error {
|
||||
req, err := http.NewRequest("GET", "https://api.cloudflare.com/client/v4/user/tokens/verify", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var cfResp CloudFlareResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !cfResp.Success {
|
||||
return fmt.Errorf("token validation failed: %v", cfResp.Errors)
|
||||
}
|
||||
|
||||
var tokenInfo TokenVerification
|
||||
if err := json.Unmarshal(cfResp.Result, &tokenInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tokenInfo.Status != "active" {
|
||||
return fmt.Errorf("token is not active, status: %s", tokenInfo.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCurrentIP(config *Config) (string, error) {
|
||||
var lastError error
|
||||
|
||||
for _, source := range config.IPSources {
|
||||
if config.Verbose {
|
||||
fmt.Printf("Trying IP source: %s\n", source)
|
||||
}
|
||||
|
||||
resp, err := config.Client.Get(source)
|
||||
if err != nil {
|
||||
lastError = err
|
||||
if config.Verbose {
|
||||
fmt.Printf("Failed to get IP from %s: %v\n", source, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
lastError = err
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
lastError = fmt.Errorf("HTTP %d from %s", resp.StatusCode, source)
|
||||
continue
|
||||
}
|
||||
|
||||
ip := strings.TrimSpace(string(body))
|
||||
if ip != "" {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
lastError = fmt.Errorf("empty response from %s", source)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("failed to get IP from any source, last error: %v", lastError)
|
||||
}
|
||||
|
||||
func findZoneForRecord(config *Config, recordName string) (string, string, error) {
|
||||
// Extract domain from record name (e.g., "sub.example.com" -> try "example.com", "com")
|
||||
parts := strings.Split(recordName, ".")
|
||||
|
||||
if config.Verbose {
|
||||
fmt.Printf("Finding zone for record: %s\n", recordName)
|
||||
}
|
||||
|
||||
for i := 0; i < len(parts); i++ {
|
||||
zoneName := strings.Join(parts[i:], ".")
|
||||
|
||||
|
||||
|
||||
req, err := http.NewRequest("GET",
|
||||
fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", zoneName), nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var cfResp CloudFlareResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&cfResp)
|
||||
resp.Body.Close()
|
||||
|
||||
if err != nil || !cfResp.Success {
|
||||
continue
|
||||
}
|
||||
|
||||
var zones []Zone
|
||||
if err := json.Unmarshal(cfResp.Result, &zones); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(zones) > 0 {
|
||||
return zones[0].Name, zones[0].ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("no zone found for record %s", recordName)
|
||||
}
|
||||
|
||||
func findDNSRecordByName(config *Config, zoneID string, recordName string) (*DNSRecord, error) {
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?type=A&name=%s",
|
||||
zoneID, recordName)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var cfResp CloudFlareResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !cfResp.Success {
|
||||
return nil, fmt.Errorf("API error: %v", cfResp.Errors)
|
||||
}
|
||||
|
||||
var records []DNSRecord
|
||||
if err := json.Unmarshal(cfResp.Result, &records); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, nil // Record doesn't exist
|
||||
}
|
||||
|
||||
return &records[0], nil
|
||||
}
|
||||
|
||||
func updateDNSRecordByName(config *Config, zoneID, recordID, recordName, ip string) error {
|
||||
data := map[string]interface{}{
|
||||
"type": "A",
|
||||
"name": recordName,
|
||||
"content": ip,
|
||||
"ttl": config.TTL,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneID, recordID)
|
||||
req, err := http.NewRequest("PUT", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var cfResp CloudFlareResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !cfResp.Success {
|
||||
return fmt.Errorf("API error: %v", cfResp.Errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDNSRecordByName(config *Config, zoneID, recordName, ip string) error {
|
||||
data := map[string]interface{}{
|
||||
"type": "A",
|
||||
"name": recordName,
|
||||
"content": ip,
|
||||
"ttl": config.TTL,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records", zoneID)
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var cfResp CloudFlareResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !cfResp.Success {
|
||||
return fmt.Errorf("API error: %v", cfResp.Errors)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listAllZones(config *Config) error {
|
||||
req, err := http.NewRequest("GET", "https://api.cloudflare.com/client/v4/zones", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+config.APIToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var cfResp CloudFlareResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&cfResp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !cfResp.Success {
|
||||
return fmt.Errorf("API error: %v", cfResp.Errors)
|
||||
}
|
||||
|
||||
var zones []Zone
|
||||
if err := json.Unmarshal(cfResp.Result, &zones); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Found %d accessible zones:\n", len(zones))
|
||||
for _, zone := range zones {
|
||||
fmt.Printf(" - %s (ID: %s)\n", zone.Name, zone.ID)
|
||||
}
|
||||
|
||||
if len(zones) == 0 {
|
||||
fmt.Println("No zones found. Make sure your API token has Zone:Read permissions.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendTelegramNotification(config *Config, record *DNSRecord, oldIP, newIP string, isDryRun bool) {
|
||||
// Skip if Telegram is not configured
|
||||
if config.TelegramBotToken == "" || config.TelegramChatID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var message string
|
||||
dryRunPrefix := ""
|
||||
if isDryRun {
|
||||
dryRunPrefix = "🧪 DRY RUN - "
|
||||
}
|
||||
|
||||
if record == nil {
|
||||
message = fmt.Sprintf("%s🆕 DNS Record Created\n\n"+
|
||||
"Record: %s\n"+
|
||||
"New IP: %s\n"+
|
||||
"TTL: %d seconds",
|
||||
dryRunPrefix, "test-record", newIP, config.TTL)
|
||||
} else {
|
||||
message = fmt.Sprintf("%s🔄 IP Address Changed\n\n"+
|
||||
"Record: %s\n"+
|
||||
"Old IP: %s\n"+
|
||||
"New IP: %s\n"+
|
||||
"TTL: %d seconds",
|
||||
dryRunPrefix, "test-record", oldIP, newIP, config.TTL)
|
||||
}
|
||||
|
||||
// Prepare Telegram API request
|
||||
telegramURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", config.TelegramBotToken)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"chat_id": config.TelegramChatID,
|
||||
"text": message,
|
||||
"parse_mode": "HTML",
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
if config.Verbose {
|
||||
fmt.Printf("Failed to marshal Telegram payload: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send notification
|
||||
req, err := http.NewRequest("POST", telegramURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
if config.Verbose {
|
||||
fmt.Printf("Failed to create Telegram request: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
if config.Verbose {
|
||||
fmt.Printf("Failed to send Telegram notification: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
if config.Verbose {
|
||||
fmt.Println("Telegram notification sent successfully")
|
||||
}
|
||||
} else {
|
||||
if config.Verbose {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Printf("Telegram notification failed (HTTP %d): %s\n", resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testTelegramNotification(config *Config) error {
|
||||
if config.TelegramBotToken == "" || config.TelegramChatID == "" {
|
||||
return fmt.Errorf("Telegram not configured. Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID environment variables")
|
||||
}
|
||||
|
||||
fmt.Println("Testing Telegram notification...")
|
||||
|
||||
// Send a test message
|
||||
message := "🧪 Dynamic DNS Test\n\n" +
|
||||
"This is a test notification from your CloudFlare Dynamic DNS tool.\n\n" +
|
||||
"✅ Telegram integration is working correctly!"
|
||||
|
||||
telegramURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", config.TelegramBotToken)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"chat_id": config.TelegramChatID,
|
||||
"text": message,
|
||||
"parse_mode": "HTML",
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal payload: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", telegramURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
fmt.Println("✅ Test notification sent successfully!")
|
||||
if config.Verbose {
|
||||
fmt.Printf("Response: %s\n", string(body))
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("failed to send notification (HTTP %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
func sendBatchTelegramNotification(config *Config, notifications []NotificationInfo, isDryRun bool) {
|
||||
// Skip if Telegram is not configured
|
||||
if config.TelegramBotToken == "" || config.TelegramChatID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if len(notifications) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var message string
|
||||
dryRunPrefix := ""
|
||||
if isDryRun {
|
||||
dryRunPrefix = "🧪 DRY RUN - "
|
||||
}
|
||||
|
||||
if len(notifications) == 1 {
|
||||
// Single record notification
|
||||
notif := notifications[0]
|
||||
if notif.IsNew {
|
||||
message = fmt.Sprintf("%s🆕 DNS Record Created\n\n"+
|
||||
"Record: %s\n"+
|
||||
"New IP: %s\n"+
|
||||
"TTL: %d seconds",
|
||||
dryRunPrefix, notif.RecordName, notif.NewIP, config.TTL)
|
||||
} else if notif.OldIP == notif.NewIP {
|
||||
message = fmt.Sprintf("%s🔄 DNS Record Force Updated\n\n"+
|
||||
"Record: %s\n"+
|
||||
"IP: %s (unchanged)\n"+
|
||||
"TTL: %d seconds\n"+
|
||||
"Note: Forced update requested",
|
||||
dryRunPrefix, notif.RecordName, notif.NewIP, config.TTL)
|
||||
} else {
|
||||
message = fmt.Sprintf("%s🔄 IP Address Changed\n\n"+
|
||||
"Record: %s\n"+
|
||||
"Old IP: %s\n"+
|
||||
"New IP: %s\n"+
|
||||
"TTL: %d seconds",
|
||||
dryRunPrefix, notif.RecordName, notif.OldIP, notif.NewIP, config.TTL)
|
||||
}
|
||||
} else {
|
||||
// Multiple records notification
|
||||
var newCount, updatedCount int
|
||||
for _, notif := range notifications {
|
||||
if notif.IsNew {
|
||||
newCount++
|
||||
} else {
|
||||
updatedCount++
|
||||
}
|
||||
}
|
||||
|
||||
message = fmt.Sprintf("%s📋 Multiple DNS Records Updated\n\n", dryRunPrefix)
|
||||
if newCount > 0 {
|
||||
message += fmt.Sprintf("🆕 Created: %d record(s)\n", newCount)
|
||||
}
|
||||
if updatedCount > 0 {
|
||||
message += fmt.Sprintf("🔄 Updated: %d record(s)\n", updatedCount)
|
||||
}
|
||||
message += fmt.Sprintf("\nNew IP: %s\nTTL: %d seconds\n\nRecords:", notifications[0].NewIP, config.TTL)
|
||||
|
||||
for _, notif := range notifications {
|
||||
if notif.IsNew {
|
||||
message += fmt.Sprintf("\n• %s (new)", notif.RecordName)
|
||||
} else if notif.OldIP == notif.NewIP {
|
||||
message += fmt.Sprintf("\n• %s (forced)", notif.RecordName)
|
||||
} else {
|
||||
message += fmt.Sprintf("\n• %s (%s → %s)", notif.RecordName, notif.OldIP, notif.NewIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send the notification using the same logic as single notifications
|
||||
telegramURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", config.TelegramBotToken)
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"chat_id": config.TelegramChatID,
|
||||
"text": message,
|
||||
"parse_mode": "HTML",
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
if config.Verbose {
|
||||
fmt.Printf("Failed to marshal Telegram payload: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", telegramURL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
if config.Verbose {
|
||||
fmt.Printf("Failed to create Telegram request: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := config.Client.Do(req)
|
||||
if err != nil {
|
||||
if config.Verbose {
|
||||
fmt.Printf("Failed to send Telegram notification: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 200 {
|
||||
if config.Verbose {
|
||||
fmt.Println("Telegram notification sent successfully")
|
||||
}
|
||||
} else {
|
||||
if config.Verbose {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Printf("Telegram notification failed (HTTP %d): %s\n", resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user