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 (?) // Windows: WMIC computersystem where caption='current_pc_name' rename new_pc_name 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.Debug 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.StartNewWindow(config, false, "SPLASH") // gui.Data.Window1 = new(gui.WindowMap) // gui.Data.Window1.AreaText = getSplashText() // this crashes here. crazy! // gui.Data.Window1.AreaText = getNEWTEXT() // 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) log.Println("mainMouseClick() b.Action =", b.Account) spew.Dump(b.Account) var wm *gui.WindowMap wm = b.WM // 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 == "NEWTEXT") { wm.AH.Attrstr = getNEWTEXT() log.Println("AH.Attrstr = ", wm.AH.Attrstr) wm.AH.Area.QueueRedrawAll() } else if (b.Action == "AREA") { 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) } } if (gui.Data.State == "splash") { gui.ShowAccountQuestionTab(wm) gui.Data.State = "account1" return } if (gui.Data.State == "main") { log.Println("gui.Data.State == main b =", b) log.Println("gui.Data.State == main wm =", wm) gui.ShowMainTab(wm) gui.Data.State = "done" return } else if (gui.Data.State == "account1") { gui.ShowAccountTab(wm, 0) gui.Data.State = "main" return } } else if (b.Action == "ADD TAB") { log.Println("\tADD TAB TRY b.Action = ", b.Action) log.Println("\tADD TAB TRY gui.ShowAccountTab(-1)") gui.ShowAccountTab(wm, -1) gui.Data.State = "done" return } 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" event := pb.MakeAddVmEvent() for key, entry := range gui.Data.AllEntries { log.Println("\tdefaultEntryChange() Data.AllEntries =", key, entry) log.Println("\tdefaultEntryChange() Data.AllEntries[key].Action =", entry.Action) if (entry.Action == "Hostname") { event.Vms[0].Hostname = entry.E.Text() } if (entry.Action == "Memory") { event.Vms[0].Memory = pint64(entry.E.Text()) } log.Println("\tdefaultEntryChange() Data.AllEntries[key].E =", entry.E.Text()) log.Println("\tdefaultEntryChange() Data.AllEntries[key].B =", entry.B) if entry.B == b { log.Println("defaultEntryChange() FOUND. Entry assigned to Button", b) } } event.Account = b.Account log.Println("\tTRIGGERING CREATE event=", event) log.Println("\tTRIGGERING CREATE event.Account=", event.Account) prepareAndSend(event) } else if (b.Action == "CONFIG") { newConfig := loadDefaultConfig() config.Accounts = newConfig.Accounts 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.EntryNick != nil) { nick := gui.Data.EntryNick.Text() username := gui.Data.EntryUser.Text() password := gui.Data.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) } if (gui.Data.State == "main") { gui.ShowMainTab(wm) gui.Data.State = "done" return } } 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(wm) 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(wm, "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(wm, "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 VM") { // gui.Data.CurrentVM = b.VM if (gui.Data.Debug) { log.Println("\tATTEMPTING TO SHOW VM IN WINDOW") // go gui.GoShowVM() } else { log.Println("\tATTEMPTING TO SHOW VM TAB", b.T) gui.CreateVmBox(wm, b.T, b.VM) } } 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(wm) gui.Data.State = "done" } else { count := countVMS(currentMessage) log.Println("\tSHOW VMS currentMessage =", currentMessage) log.Println("\tSHOW VMS count =", count) // TODO: make sure login worked & the account really has zero VMs // if (count != 0) { name := "Virtual Machines (" + b.Account.Nick + ")" mh := gui.AddVmsTab(wm, name, count, b.Account) ReadReceivedData(currentMessage, mh, b.W, b.T) // } 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 } } } } // // This is GO language concept for 'recover' to keep an app from completely crashing // // 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 // maybe this is pointless or wrong 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 == "kill") { log.Println("gui.State = kill") log.Println("gui.State = kill") log.Println("gui.State = kill") onExit(nil) } else if (gui.Data.State == "HIDE") { time.Sleep(20 * time.Millisecond) // maybe required for macos & windows refresh to work? gui.Data.Windows[0].Box1.Show() gui.Data.Windows[0].Box2.Show() gui.Data.State = "done" } } }