From 1258be9beff2e45c94ba5f7c29db65be02a1e2d4 Mon Sep 17 00:00:00 2001 From: Jeff Carr Date: Fri, 29 Dec 2023 01:36:10 -0600 Subject: [PATCH] add digital ocean & DNS state windows lists digital ocean droplets create a new digital ocean droplet knows what needs to be done to get IPv4 and IPv6 to work update windows on Show() make a window for the state of DNS specific to the hostname Signed-off-by: Jeff Carr --- .gitignore | 1 + digitalocean/listDroplets.go | 49 ++++ digitalocean/listKeys.go | 31 +++ digitalocean/mainWindow.go | 18 ++ dns-https.go | 43 ++++ dnsLookupStatus.go | 2 +- examples/control-panel-digitalocean/Makefile | 25 ++ examples/control-panel-digitalocean/main.go | 124 ++++++++++ gui.go | 39 ++- hostname.go | 29 +-- hostnameStatus.go | 240 +++++++++++++++++++ structs.go | 5 +- 12 files changed, 574 insertions(+), 32 deletions(-) create mode 100644 digitalocean/listDroplets.go create mode 100644 digitalocean/listKeys.go create mode 100644 digitalocean/mainWindow.go create mode 100644 examples/control-panel-digitalocean/Makefile create mode 100644 examples/control-panel-digitalocean/main.go create mode 100644 hostnameStatus.go diff --git a/.gitignore b/.gitignore index cd8a456..8f6539e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ control-panel-dns *.swp /plugins/* +examples/control-panel-digitalocean/control-panel-digitalocean examples/control-panel-cloudflare/control-panel-cloudflare diff --git a/digitalocean/listDroplets.go b/digitalocean/listDroplets.go new file mode 100644 index 0000000..a46be29 --- /dev/null +++ b/digitalocean/listDroplets.go @@ -0,0 +1,49 @@ +package digitalocean + +import ( + "context" + "fmt" + + "golang.org/x/oauth2" + + "github.com/digitalocean/godo" +) + +// ListDroplets fetches and prints out the droplets along with their IPv4 and IPv6 addresses. +func ListDroplets(token string) error { + // OAuth token for authentication. + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + + // OAuth2 client. + oauthClient := oauth2.NewClient(context.Background(), tokenSource) + + // DigitalOcean client. + client := godo.NewClient(oauthClient) + + // Context. + ctx := context.TODO() + + // List all droplets. + droplets, _, err := client.Droplets.List(ctx, &godo.ListOptions{}) + if err != nil { + return err + } + + // Iterate over droplets and print their details. + for _, droplet := range droplets { + fmt.Printf("Droplet: %s\n", droplet.Name) + for _, network := range droplet.Networks.V4 { + if network.Type == "public" { + fmt.Printf("IPv4: %s\n", network.IPAddress) + } + } + for _, network := range droplet.Networks.V6 { + if network.Type == "public" { + fmt.Printf("IPv6: %s\n", network.IPAddress) + } + } + fmt.Println("-------------------------") + } + + return nil +} diff --git a/digitalocean/listKeys.go b/digitalocean/listKeys.go new file mode 100644 index 0000000..000a66d --- /dev/null +++ b/digitalocean/listKeys.go @@ -0,0 +1,31 @@ +package digitalocean + +import ( + "context" + "fmt" + + "golang.org/x/oauth2" + + "github.com/digitalocean/godo" +) + +func GetSSHKeyID(token, name string) (string, error) { + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + oauthClient := oauth2.NewClient(context.Background(), tokenSource) + client := godo.NewClient(oauthClient) + + // List all keys. + keys, _, err := client.Keys.List(context.Background(), &godo.ListOptions{}) + if err != nil { + return "", err + } + + // Find the key by name. + for _, key := range keys { + if key.Name == name { + return key.Fingerprint, nil + } + } + + return "", fmt.Errorf("SSH Key not found") +} diff --git a/digitalocean/mainWindow.go b/digitalocean/mainWindow.go new file mode 100644 index 0000000..78f2d03 --- /dev/null +++ b/digitalocean/mainWindow.go @@ -0,0 +1,18 @@ +package digitalocean + +import ( + "log" + + "go.wit.com/gui" +) + +func MakeWindow(n *gui.Node) *gui.Node { + log.Println("digitalocean MakeWindow() START") + + win := n.NewWindow("DigitalOcean Control Panel") + + // box := g1.NewGroup("data") + group := win.NewGroup("data") + log.Println("digitalocean MakeWindow() END", group) + return win +} diff --git a/dns-https.go b/dns-https.go index c61aa2f..6579903 100644 --- a/dns-https.go +++ b/dns-https.go @@ -63,3 +63,46 @@ func dnsAAAAlookupDoH(domain string) ([]string, error) { return ipv6Addresses, nil } + +// dnsLookupDoH performs a DNS lookup for AAAA records over HTTPS. +func lookupDoH(hostname string, rrType string) []string { + var values []string + + // Construct the URL for a DNS query with Google's DNS-over-HTTPS API + url := fmt.Sprintf("https://dns.google/resolve?name=%s&type=%s", hostname, rrType) + + log.Println("curl", url) + + // Perform the HTTP GET request + resp, err := http.Get(url) + if err != nil { + log.Error(err, "error performing DNS-over-HTTPS request") + return nil + } + defer resp.Body.Close() + + // Read and unmarshal the response body + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Error(fmt.Errorf("error reading response: %w", err)) + return nil + } + + var data struct { + Answer []struct { + Data string `json:"data"` + } `json:"Answer"` + } + + if err := json.Unmarshal(body, &data); err != nil { + log.Error(fmt.Errorf("error unmarshaling response: %w", err)) + return nil + } + + // Extract the IPv6 addresses + for _, answer := range data.Answer { + values = append(values, answer.Data) + } + + return values +} diff --git a/dnsLookupStatus.go b/dnsLookupStatus.go index 280ac92..9166f72 100644 --- a/dnsLookupStatus.go +++ b/dnsLookupStatus.go @@ -92,7 +92,7 @@ func NewDigStatusWindow(p *gui.Node) *digStatus { ds.ready = false ds.hidden = true - ds.window = p.NewWindow("DNS Lookup Status") + ds.window = p.NewWindow("DNS Resolver Status") ds.window.Custom = func () { ds.hidden = true ds.window.Hide() diff --git a/examples/control-panel-digitalocean/Makefile b/examples/control-panel-digitalocean/Makefile new file mode 100644 index 0000000..dc158f8 --- /dev/null +++ b/examples/control-panel-digitalocean/Makefile @@ -0,0 +1,25 @@ +# export GO111MODULE="off" +run: build + ./control-panel-digitalocean + +build-release: + go get -v -u -x . + go build + ./control-panel-digitalocean + +build: + go get -v -x . + go build + +update: + go get -v -u -x . + +log: + reset + tail -f /tmp/witgui.* /tmp/guilogfile + +gocui: build + ./control-panel-digitalocean -gui gocui + +quiet: + ./control-panel-digitalocean >/tmp/witgui.log.stderr 2>&1 diff --git a/examples/control-panel-digitalocean/main.go b/examples/control-panel-digitalocean/main.go new file mode 100644 index 0000000..b1228f7 --- /dev/null +++ b/examples/control-panel-digitalocean/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "fmt" + "os" + + "golang.org/x/oauth2" + + "go.wit.com/log" + "go.wit.com/gui" + "github.com/digitalocean/godo" + "go.wit.com/control-panel-dns/digitalocean" +) + +var title string = "Digital Ocean Control Panel" + +/* +// createDroplet creates a new droplet in the specified region with the given name. +func createDroplet(token, name, region, size, image string) (*godo.Droplet, error) { + // Create an OAuth2 token. + tokenSource := &oauth2.Token{ + AccessToken: token, + } + + // Create an OAuth2 client. + oauthClient := oauth2.NewClient(context.Background(), tokenSource) + + // Create a DigitalOcean client with the OAuth2 client. + client := godo.NewClient(oauthClient) + + // Define the create request. + createRequest := &godo.DropletCreateRequest{ + Name: name, + Region: region, + Size: size, + Image: godo.DropletCreateImage{ + Slug: image, + }, + } + + // Create the droplet. + ctx := context.TODO() + newDroplet, _, err := client.Droplets.Create(ctx, createRequest) + if err != nil { + return nil, err + } + + return newDroplet, nil +} +*/ + +func main() { + // Your personal API token from DigitalOcean. + token := os.Getenv("DIGITALOCEAN_TOKEN") + if token == "" { + log.Fatal("Please set your DigitalOcean API token in the DIGITALOCEAN_TOKEN environment variable") + } + + // List droplets and their IP addresses. + err := digitalocean.ListDroplets(token) + if err != nil { + log.Fatalf("Error listing droplets: %s\n", err) + } + + // initialize a new GO GUI instance + myGui := gui.New().Default() + + // draw the cloudflare control panel window + win := digitalocean.MakeWindow(myGui) + win.SetText(title) + + // This is just a optional goroutine to watch that things are alive + gui.Watchdog() + gui.StandardExit() + + os.Exit(0) + + // Parameters for the droplet you wish to create. + name := "ipv6.wit.com" + region := "nyc1" // New York City region. + size := "s-1vcpu-1gb" // Size of the droplet. + image := "ubuntu-20-04-x64" // Image slug for Ubuntu 20.04 (LTS) x64. + + // Create a new droplet. + droplet, err := createDropletNew(token, name, region, size, image) + if err != nil { + log.Fatalf("Something went wrong: %s\n", err) + } + + fmt.Printf("Created droplet ID %d with name %s\n", droplet.ID, droplet.Name) +} + +// createDroplet creates a new droplet in the specified region with the given name. +func createDropletNew(token, name, region, size, image string) (*godo.Droplet, error) { + // Create an OAuth2 token. + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) + + // Create an OAuth2 client. + oauthClient := oauth2.NewClient(context.Background(), tokenSource) + + // Create a DigitalOcean client with the OAuth2 client. + client := godo.NewClient(oauthClient) + + // Define the create request. + createRequest := &godo.DropletCreateRequest{ + Name: name, + Region: region, + Size: size, + Image: godo.DropletCreateImage{ + Slug: image, + }, + IPv6: true, // Enable IPv6 + } + + // Create the droplet. + ctx := context.TODO() + newDroplet, _, err := client.Droplets.Create(ctx, createRequest) + if err != nil { + return nil, err + } + + return newDroplet, nil +} diff --git a/gui.go b/gui.go index 54f2093..3301712 100644 --- a/gui.go +++ b/gui.go @@ -308,20 +308,38 @@ func dnsTab(title string) { }) me.fix.Disable() - me.digStatusButton = me.mainStatus.NewButton("Show DNS Lookup Status", func () { + me.digStatusButton = me.mainStatus.NewButton("Resolver Status", func () { if (me.digStatus == nil) { log.Info("drawing the digStatus window START") - me.digStatus = NewDigStatusWindow(me.window) + me.digStatus = NewDigStatusWindow(myGui) log.Info("drawing the digStatus window END") me.digStatusButton.SetText("Hide DNS Lookup Status") + me.digStatus.Update() + return + } + if me.digStatus.hidden { + me.digStatusButton.SetText("Hide Resolver Status") + me.digStatus.Show() + me.digStatus.Update() } else { - if me.digStatus.hidden { - me.digStatusButton.SetText("Hide DNS Lookup Status") - me.digStatus.Show() - } else { - me.digStatusButton.SetText("Show DNS Lookup Status") - me.digStatus.Hide() - } + me.digStatusButton.SetText("Resolver Status") + me.digStatus.Hide() + } + }) + me.hostnameStatusButton = me.mainStatus.NewButton("Show hostname DNS Status", func () { + if (me.hostnameStatus == nil) { + me.hostnameStatus = NewHostnameStatusWindow(myGui) + me.hostnameStatusButton.SetText("Hide " + me.hostname + " DNS Status") + me.hostnameStatus.Update() + return + } + if me.hostnameStatus.hidden { + me.hostnameStatusButton.SetText("Hide " + me.hostname + " DNS Status") + me.hostnameStatus.Show() + me.hostnameStatus.Update() + } else { + me.hostnameStatusButton.SetText("Show " + me.hostname + " DNS Status") + me.hostnameStatus.Hide() } }) @@ -344,7 +362,7 @@ func statusGrid(n *gui.Node) { me.statusIPv6.Set("known") gridP.NewLabel("hostname =") - me.hostnameStatus = gridP.NewLabel("invalid") + me.hostnameStatusOLD = gridP.NewLabel("invalid") gridP.NewLabel("dns resolution") me.DnsSpeed = gridP.NewLabel("unknown") @@ -383,6 +401,7 @@ func updateDNS() { } me.digStatus.Update() + me.hostnameStatus.Update() // log.Println("digAAAA()") aaaa = digAAAA(h) diff --git a/hostname.go b/hostname.go index 32897d5..81aa136 100644 --- a/hostname.go +++ b/hostname.go @@ -1,29 +1,19 @@ -// inspired from: -// https://github.com/mactsouk/opensource.com.git -// and -// https://coderwall.com/p/wohavg/creating-a-simple-tcp-server-in-go - +// figures out if your hostname is valid +// then checks if your DNS is setup correctly package main import ( - "log" -// "net" "strings" + "go.wit.com/log" "go.wit.com/shell" - "go.wit.com/control-panel-dns/cloudflare" "github.com/miekg/dns" + // will try to get this hosts FQDN + "github.com/Showmax/go-fqdn" ) -// will try to get this hosts FQDN -import "github.com/Showmax/go-fqdn" - -// this is the king of dns libraries -// import "github.com/miekg/dns" - - func getHostname() { var err error var s string = "gui.Label == nil" @@ -59,15 +49,15 @@ func getHostname() { test = hshort + "." + dn if (me.hostname != test) { debug(LogInfo, "me.hostname", me.hostname, "does not equal", test) - if (me.hostnameStatus.S != "BROKEN") { + if (me.hostnameStatusOLD.S != "BROKEN") { debug(LogChange, "me.hostname", me.hostname, "does not equal", test) me.changed = true - me.hostnameStatus.SetText("BROKEN") + me.hostnameStatusOLD.SetText("BROKEN") } } else { - if (me.hostnameStatus.S != "VALID") { + if (me.hostnameStatusOLD.S != "VALID") { debug(LogChange, "me.hostname", me.hostname, "is valid") - me.hostnameStatus.SetText("VALID") + me.hostnameStatusOLD.SetText("VALID") me.changed = true } // enable the cloudflare button if the provider is cloudflare @@ -140,7 +130,6 @@ func digAAAA(hostname string) []string { log.Println("digAAAA() RUNNING dnsAAAAlookupDoH(domain)") ipv6Addresses, _ = dnsAAAAlookupDoH(hostname) log.Println("digAAAA() has ipv6Addresses =", strings.Join(ipv6Addresses, " ")) - log.Printf("digAAAA() IPv6 Addresses for %s:\n", hostname) for _, addr := range ipv6Addresses { log.Println(addr) } diff --git a/hostnameStatus.go b/hostnameStatus.go new file mode 100644 index 0000000..0c00ef2 --- /dev/null +++ b/hostnameStatus.go @@ -0,0 +1,240 @@ +/* + figures out if your hostname is valid + then checks if your DNS is setup correctly +*/ + +package main + +import ( + "os" + "fmt" + "time" + "reflect" + "strings" + + "go.wit.com/log" + "go.wit.com/gui" + "go.wit.com/control-panel-dns/cloudflare" +) + +type hostnameStatus struct { + ready bool + hidden bool + + hostname string // my hostname. Example: "test.wit.com" + + window *gui.Node + + status *cloudflare.OneLiner + summary *cloudflare.OneLiner + speed *cloudflare.OneLiner + speedActual *cloudflare.OneLiner + + dnsA *cloudflare.OneLiner + dnsAAAA *cloudflare.OneLiner + dnsAPI *cloudflare.OneLiner + + statusIPv4 *cloudflare.OneLiner + statusIPv6 *cloudflare.OneLiner + + // Details Group + currentIPv4 *cloudflare.OneLiner + currentIPv6 *cloudflare.OneLiner +} + +func NewHostnameStatusWindow(p *gui.Node) *hostnameStatus { + var hs *hostnameStatus + hs = new(hostnameStatus) + + hs.ready = false + hs.hidden = true + hs.hostname = me.hostname + + hs.window = p.NewWindow( hs.hostname + " Status") + hs.window.Custom = func () { + hs.hidden = true + hs.window.Hide() + } + box := hs.window.NewBox("hBox", true) + group := box.NewGroup("Summary") + grid := group.NewGrid("LookupStatus", 2, 2) + + hs.status = cloudflare.NewOneLiner(grid, "status").Set("unknown") + hs.statusIPv4 = cloudflare.NewOneLiner(grid, "IPv4").Set("unknown") + hs.statusIPv6 = cloudflare.NewOneLiner(grid, "IPv6").Set("unknown") + + group.Pad() + grid.Pad() + + group = box.NewGroup("Details") + grid = group.NewGrid("LookupDetails", 2, 2) + + hs.currentIPv4 = cloudflare.NewOneLiner(grid, "Current IPv4") + hs.currentIPv6 = cloudflare.NewOneLiner(grid, "Current IPv6") + + hs.dnsAPI = cloudflare.NewOneLiner(grid, "dns API provider").Set("unknown") + hs.dnsA = cloudflare.NewOneLiner(grid, "dns IPv4 resource records").Set("unknown") + hs.dnsAAAA = cloudflare.NewOneLiner(grid, "dns IPv6 resource records").Set("unknown") + hs.speed = cloudflare.NewOneLiner(grid, "speed").Set("unknown") + hs.speedActual = cloudflare.NewOneLiner(grid, "actual").Set("unknown") + + group.Pad() + grid.Pad() + + hs.hidden = false + hs.ready = true + return hs +} + +func (hs *hostnameStatus) Update() { + log.Info("hostnameStatus() Update() START") + if hs == nil { + log.Error("hostnameStatus() Update() hs == nil") + return + } + duration := timeFunction(func () { + hs.updateStatus() + }) + s := fmt.Sprint(duration) + hs.set(hs.speedActual, s) + + if (duration > 500 * time.Millisecond ) { + hs.set(hs.speed, "SLOW") + } else if (duration > 100 * time.Millisecond ) { + hs.set(hs.speed, "OK") + } else { + hs.set(hs.speed, "FAST") + } + log.Info("hostnameStatus() Update() END") +} + +// Returns true if the status is valid +func (hs *hostnameStatus) Ready() bool { + if hs == nil {return false} + return hs.ready +} + +// Returns true if IPv4 is working +func (hs *hostnameStatus) IPv4() bool { + if ! hs.Ready() {return false} + if (hs.statusIPv4.Get() == "OK") { + return true + } + if (hs.statusIPv4.Get() == "GOOD") { + return true + } + return false +} + +// Returns true if IPv6 is working +func (hs *hostnameStatus) IPv6() bool { + if ! hs.Ready() {return false} + if (hs.statusIPv6.Get() == "GOOD") { + return true + } + return false +} + +func (hs *hostnameStatus) setIPv4(s string) { + hs.statusIPv4.Set(s) + if ! hs.Ready() {return} +} + +func (hs *hostnameStatus) setIPv6(s string) { + hs.statusIPv6.Set(s) + if ! hs.Ready() {return} +} + +func (hs *hostnameStatus) set(a any, s string) { + if ! hs.Ready() {return} + if hs.hidden { + return + } + if a == nil { + return + } + var n *gui.Node + if reflect.TypeOf(a) == reflect.TypeOf(n) { + n = a.(*gui.Node) + n.SetText(s) + return + } + var ol *cloudflare.OneLiner + if reflect.TypeOf(a) == reflect.TypeOf(ol) { + ol = a.(*cloudflare.OneLiner) + if ol == nil { + log.Println("ol = nil", reflect.TypeOf(a), "a =", a) + return + } + log.Println("SETTING ol:", ol) + ol.Set(s) + return + } + log.Error("unknown type TypeOf(a) =", reflect.TypeOf(a), "a =", a) + os.Exit(0) +} + +func (hs *hostnameStatus) updateStatus() { + var s string + var vals []string + log.Info("updateStatus() START") + if ! hs.Ready() { return } + + vals = lookupDoH(hs.hostname, "AAAA") + + log.Println("IPv6 Addresses for ", hs.hostname, "=", vals) + if len(vals) == 0 { + s = "(none)" + hs.setIPv6("NEED VPN") + } else { + for _, addr := range vals { + log.Println(addr) + s += addr + " (DELETE)" + hs.setIPv6("NEEDS DELETE") + } + } + hs.set(hs.dnsAAAA, s) + + vals = lookupDoH(hs.hostname, "A") + log.Println("IPv4 Addresses for ", hs.hostname, "=", vals) + s = strings.Join(vals, "\n") + if (s == "") { + s = "(none)" + hs.setIPv4("NEEDS CNAME") + } + hs.set(hs.dnsA, s) + + vals = lookupDoH(hs.hostname, "CNAME") + s = strings.Join(vals, "\n") + if (s != "") { + hs.set(hs.dnsA, "CNAME " + s) + hs.setIPv4("GOOD") + } + + hs.currentIPv4.Set(me.IPv4.S) + hs.currentIPv6.Set(me.IPv6.S) + + if hs.IPv4() && hs.IPv4() { + hs.status.Set("GOOD") + } else { + hs.status.Set("BROKEN") + } + + hs.dnsAPI.Set(me.DnsAPI.S) +} + +func (hs *hostnameStatus) Show() { + log.Info("hostnameStatus.Show() window") + if hs.hidden { + hs.window.Show() + } + hs.hidden = false +} + +func (hs *hostnameStatus) Hide() { + log.Info("hostnameStatus.Hide() window") + if ! hs.hidden { + hs.window.Hide() + } + hs.hidden = true +} diff --git a/structs.go b/structs.go index 3cace0b..a75c518 100644 --- a/structs.go +++ b/structs.go @@ -16,7 +16,7 @@ type Host struct { hostname string // mirrors domainname *gui.Node // kernel.org hostshort *gui.Node // hostname -s - hostnameStatus *gui.Node // is the hostname configured correctly in the OS? + hostnameStatusOLD *gui.Node // is the hostname configured correctly in the OS? // fqdn string // mirrors.kernel.org // dnsTTL int `default:"3"` // Recheck DNS is working every TTL (in seconds) @@ -77,6 +77,9 @@ type Host struct { digStatus *digStatus statusIPv6 *cloudflare.OneLiner digStatusButton *gui.Node + + hostnameStatus *hostnameStatus + hostnameStatusButton *gui.Node } type IPtype struct {