cloud-control-panel/main.go

396 lines
12 KiB
Go

package main
import "log"
import "fmt"
import "os"
import "time"
import "os/user"
import "runtime"
import "runtime/debug"
// this is the king of dns libraries
import "github.com/miekg/dns"
import "git.wit.com/wit/gui"
import pb "git.wit.com/wit/witProtobuf"
import "git.wit.com/jcarr/dnssecsocket"
import "github.com/gobuffalo/packr"
import "github.com/davecgh/go-spew/spew"
// will try to get this hosts FQDN
import "github.com/Showmax/go-fqdn"
var GITCOMMIT string // this is passed in as an ldflag
var GOVERSION string // this is passed in as an ldflag
var BUILDTIME string // this is passed in as an ldflag
// use mergo to merge structs
// import "github.com/imdario/mergo"
// mergo.Merge(&dest, src)
// always sorted slice (new project)
// https://github.com/yaa110/sslice
// several smart slice functions (new project. April 2019)
// https://github.com/elliotchance/pie
// look into this for dns if possible
// https://en.wikipedia.org/wiki/DNSCrypt
// https://en.wikipedia.org/wiki/DNS_over_HTTPS
// DNS over TLS plugin for coredns
// uses protobuf's and gRPC in pb/dns.proto
// https://github.com/coredns/coredns/tree/master/plugin/tls
// https://github.com/coredns/coredns/blob/master/pb/dns.proto
// cross platform openvpn that may work for IPv6
// https://github.com/mysteriumnetwork/go-openvpnwe
func onExit(err error) {
log.Println("Sleep for 1 second")
time.Sleep(1 * 1000 * 1000 * 1000)
// save the protobuf.Config as a JSON config file
saveConfig()
if (err != nil) {
panic(err)
}
os.Exit(0)
}
var packrBox packr.Box
func lookupAAAA(hostname string) string {
// lookup the IP address from DNS
dnsRR := dnssecsocket.Dnstrace(hostname, "AAAA")
spew.Dump(dnsRR)
if (dnsRR == nil) {
return "BROKEN"
}
ipaddr := dns.Field(dnsRR, 1)
log.Println("ipaddr", ipaddr)
return ipaddr
}
func main() {
// This puts all the files in that directory in the binary
// This directory includes the default config file if there is not already one
packrBox = packr.NewBox("./resources")
// This will parse the command line and config file
// This uses a protobuf definition and then Marshal's
// and unmarshal's that into a JSON config file
// that is human readable and editable. This is the easy way
// to pass around a configuration structure throughout a
// golang application IMHO
parseConfig()
for key, foo := range config.Accounts {
log.Println("FOUND ACCOUNT = ", key, foo)
}
// pass a pointer to the config so the gui can access it
gui.Data.Config = config
initChannel()
go processEvents()
go gorillaDial("v000185.testing.com.customers.wprod.wit.com:9000")
go watchGUI()
user, err := user.Current()
if err != nil {
onExit(err)
}
gui.Data.Width = int(config.Width)
gui.Data.Height = int(config.Height)
// use this to discover what the OS thinks it's hostname is
// seems to be cross platform (?)
hostname := fqdn.Get()
log.Println("fqdn.Get() = ", hostname)
gui.Data.Hostname = hostname
// this is a recursive dig for the AAAA record
// TODO: check for dns hijacking
ipAAAA := lookupAAAA(hostname)
gui.Data.IPv6 = ipAAAA
gui.Data.Version = "v0.7"
gui.Data.GitCommit = GITCOMMIT
gui.Data.GoVersion = GOVERSION
gui.Data.Buildtime = BUILDTIME
gui.Data.MouseClick = mainMouseClick
gui.Data.HomeDir = user.HomeDir
// Set output debugging level
gui.Data.Debug = config.Debugging
gui.Data.DebugTable = config.Debugtable
log.Println("gui.Data.Debug = ", gui.Data.Debug)
log.Println("gui.Data.DebugTable = ", gui.Data.DebugTable)
// Current User
log.Println("Hi " + user.Name + " (id: " + user.Uid + ")")
log.Println("Username: " + user.Username)
log.Println("Home Dir: " + user.HomeDir)
// Get "Real" User under sudo.
// More Info: https://stackoverflow.com/q/29733575/402585
log.Println("Real User: " + os.Getenv("SUDO_USER"))
// make this the main loop in an attempt to figure out the crashes
// do not change this until the GUI is stable
gui.GoMainWindow()
}
// This is the handler for all mosue clicks (buttons, areas, etc))
//
// This is massive for a reason. EVERY MOUSE CLICK COMES HERE
// the 'gui' code is kinda just a holder. It will pass everything
// here and we have to sort out what to do with the click
// at least, that is the current design because I thought it
// might be a good approach. Time will tell...
//
func mainMouseClick(b *gui.ButtonMap) {
defer r() // a golang trick to try to not crash on certain errors
if (b == nil) {
log.Println("mainMouseClick() BACK IN MAIN CONTROL PANEL CODE (button is nil) WHY DID THIS HAPPEN?")
log.Println("mainMouseClick() BACK IN MAIN CONTROL PANEL CODE (button is nil) WHY DID THIS HAPPEN?")
onExit(fmt.Errorf("mainMouseClick() got b = nil"))
}
log.Println("mainMouseClick() b.Action =", b.Action)
gui.Data.Current = b.Account
log.Println("mainMouseClick() BACK IN CONTROL PANEL CODE")
if (b.Account != nil) {
log.Println("\tmainMouseClick() setting current account = ", b.Account.Nick)
}
if (b.Action == "BACK") {
gui.Data.State = "splash"
} else if (b.Action == "QUIT") {
onExit(nil)
} else if (b.Action == "CREATE") {
log.Println("\tTRY TO ADD A NEW VIRTUAL MACHINE")
log.Println("\tTRIGGER CREATE VM")
gui.Data.State = "CREATE"
} else if (b.Action == "CONFIG") {
loadDefaultConfig()
gui.Data.State = "done"
} else if (b.Action == "DEBUG") {
log.Println("\tdebug.PrintStack() (SHOULD BE JUST THIS goroutine)")
debug.PrintStack()
} else if (b.Action == "DEBUG FULL") {
log.Println("\tATTEMPT FULL STACK DUMP")
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
log.Printf("%s", buf)
log.Println("\tFINISHED FULL STACK DUMP")
} else if (b.Action == "ADD") {
log.Println("\tSHOULD ADD ACCOUNT HERE")
if (gui.Data.CurrentTab != nil) {
if (gui.Data.CurrentTab.EntryNick != nil) {
nick := gui.Data.CurrentTab.EntryNick.Text()
username := gui.Data.CurrentTab.EntryUser.Text()
password := gui.Data.CurrentTab.EntryPass.Text()
log.Println("\tEntryNick =", nick)
log.Println("\tEntryName =", username)
log.Println("\tEntryPass =", password)
acc := new(pb.Account)
acc.Nick = nick
acc.Username = username
acc.Password = password
config.Accounts = append(config.Accounts, acc)
}
}
} else if (b.Action == "LOGIN") {
log.Println("\tTRIGGER LOGIN ACCOUNT")
gui.Data.State = "SEND LOGIN"
// TODO: move this into a seperate goroutine
event := pb.MakeLoginEvent()
event.Account = b.Account
prepareAndSend(event)
count := 0
for {
log.Println("\tSleep() in buttonClick() gui.Data.State =", gui.Data.State)
time.Sleep(200 * time.Millisecond)
if (gui.Data.State == "NEW PROTOBUF") {
if (currentMessage == nil) {
gui.SocketError()
gui.Data.State = "done"
} else {
log.Println("LOGIN currentMessage =", currentMessage)
if (currentMessage.Type == pb.Event_OK) {
msg := "On account " + b.Account.Nick + "\n"
log.Println("\tLOGIN WAS OK!", msg)
log.Println("\tLOGIN WAS OK! old button.Account was =", b.Account)
log.Println("\tLOGIN WAS OK! currentMessage.Account =", currentMessage.Account)
if (b.Account.Id == currentMessage.Account.Id) {
if (b.Account.Token != currentMessage.Account.Token) {
log.Println("\tLOGIN SENT NEW TOKEN")
b.Account.Token = currentMessage.Account.Token
log.Println("\tLOGIN WAS OK! old button.Account is now =", b.Account)
}
}
log.Println("\tLOGIN WAS OK!")
gui.MessageWindow("Login OK", msg)
} else if (currentMessage.Type == pb.Event_FAIL) {
log.Println("\tLOGIN FAILED")
log.Println("\tLOGIN FAILED")
log.Println("\tLOGIN FAILED")
msg := "On account " + b.Account.Nick + "\n"
msg += "pb.Comment = " + currentMessage.Comment + "\n"
msg += "pb.Id = " + fmt.Sprintf("%d", currentMessage.Id) + "\n"
msg += "pb.Email = " + b.Account.Email + "\n"
msg += "pb.Username = " + b.Account.Username + "\n"
gui.ErrorWindow("Login Failed", msg)
}
currentMessage = nil
gui.Data.State = "done"
}
return
}
// TODO: fix this with an actual timeout
count += 1
if (count > 10) {
log.Println("\tERROR: waited too long for a resposne")
currentMessage = nil
gui.Data.State = "done"
return
}
}
} else if (b.Action == "SHOW") {
log.Println("\tTRIGGER DISPLAY ACCOUNT")
gui.Data.State = "SEND WEBSOCKET"
event := pb.MakeGetEvent()
event.Account = b.Account
prepareAndSend(event)
count := 0
for {
log.Println("\tSleep() in buttonClick() gui.Data.State =", gui.Data.State)
time.Sleep(200 * time.Millisecond)
if (gui.Data.State == "NEW PROTOBUF") {
if (currentMessage == nil) {
gui.SocketError()
gui.Data.State = "done"
} else {
count := countVMS(currentMessage)
log.Println("\tSHOW VMS currentMessage =", currentMessage)
log.Println("\tSHOW VMS count =", count)
if (count != 0) {
name := "Virtual Machines (" + b.Account.Nick + ")"
mh := gui.AddVmsTab(name, count, b.Account)
ReadReceivedData(currentMessage, mh)
}
currentMessage = nil
gui.Data.State = "done"
}
return
}
count += 1
if (count > 10) {
log.Println("\tERROR: waited too long for a resposne")
currentMessage = nil
gui.Data.State = "done"
return
}
}
}
if (gui.Data.State == "splash") {
gui.ShowAccountQuestionTab()
gui.Data.State = "account1"
return
} else if (gui.Data.State == "account1") {
gui.ShowAccountTab()
gui.Data.State = "main"
} else if (gui.Data.State == "main") {
gui.ShowMainTab()
gui.Data.State = "done"
} else if (gui.Data.State == "QUIT") {
onExit(nil)
}
}
//
// Doing this can sometimes avoid a panic() on things like:
// panic: runtime error: slice bounds out of range
//
// In debugging mode, always panic() and never try to recover()
//
func r() {
if (gui.Data.Debug == false) {
if r := recover(); r != nil {
log.Println("recover() SOMETHING IS REALLY BROKEN r =", r)
log.Println("recover() SOMETHING IS REALLY BROKEN r =", r)
log.Println("recover() SOMETHING IS REALLY BROKEN r =", r)
}
}
}
func prepareAndSend(event *pb.Event) {
if (event.Account == nil) {
log.Println("\tmain.prepareAndSend() ERROR event.Token = nil")
log.Println("\tmain.prepareAndSend() ERROR event.Token = nil")
log.Println("\tmain.prepareAndSend() ERROR event.Token = nil")
} else {
s := event.Account.Token
log.Println("\tmain.prepareAndSend() event.Token =", s) // s[len(s)-24:]) // this can panic because strings.() is stupid
}
gorillaSendProtobuf(event)
gui.Data.State = "READ PROTOBUF"
}
// this watches the GUI primarily to process protobuf's
func watchGUI() {
count := 0
for {
if (count > 10) {
log.Println("Sleep() in watchGUI() gui.Data.State =", gui.Data.State)
count = 0
}
count += 1
time.Sleep(200 * time.Millisecond)
if (gui.Data.State == "CREATE") {
log.Println("\tTRIGGERING CREATE")
event := pb.MakeAddVmEvent()
prepareAndSend(event)
}
if (gui.Data.State == "kill") {
log.Println("gui.State = kill")
log.Println("gui.State = kill")
log.Println("gui.State = kill")
onExit(nil)
}
if (gui.Data.State == "splash") {
if (config == nil) {
log.Println("gui.State = splash BUT SOMETHING HAS GONE VERY WRONG")
log.Println("gui.State = splash BUT SOMETHING HAS GONE VERY WRONG")
log.Println("gui.State = splash config = nil")
os.Exit(-1)
}
for key, _ := range config.Accounts {
log.Println("gui.State = splash BUT THERE IS AN ACCOUNT Account = ", config.Accounts[key])
// log.Println("gui.State = splash BUT THERE IS AN ACCOUNT Username = ", config.Accounts[key])
log.Println("SETTING gui.State = main")
gui.Data.State = "main";
if (config.Accounts[key] == nil) {
log.Println("THIS IS LAME. CONFIG FILE MADE NULL ACCOUNTS key =", key)
}
}
}
}
}