diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..198f9db --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +keyserver.log diff --git a/README.md b/README.md index eb1036a..4698c23 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ # keyserver -Easily server HTTP and DNS keys for proper payload protection + +### Compiled Binaries +You can retrieve the latest release of keyserver binaries in the Releases page. + +### Build +If you would prefer to build the source yourself, make sure Go is +installed and execute the following: + +``` +go get -u github.com/leoloobeek/keyserver +``` + +This project does use the following dependency: +github.com/op/go-logging + diff --git a/alerts.config b/alerts.config new file mode 100644 index 0000000..1825ce9 --- /dev/null +++ b/alerts.config @@ -0,0 +1,10 @@ +{ + "SlackWebhookURL": "", + + "SMTPServer": "smtp.gmail.com", + "SMTPPort": 465, + "MailFrom": "", + "Password": "", + "MailTo": "" +} + diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..5ba5e91 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,952 @@ +package cmd + +// Thanks to @evilsocket's Bettercap2 (https://github.com/bettercap/bettercap) +// for pointing me to github.com/chzyer/readline and having a good example to work off of + +import ( + "bufio" + "fmt" + "io" + "os" + "strconv" + "strings" + "time" + + "github.com/chzyer/readline" + "github.com/leoloobeek/keyserver/logger" + "github.com/leoloobeek/keyserver/servers" +) + +// CmdInfo holds all commands for a menu +type CmdInfo struct { + MenuType string + Items map[string]*MenuItem + TabCompleters map[string]*readline.Instance + HttpServer *servers.HttpServer + DnsServer *servers.DnsServer +} + +func (c *CmdInfo) MainMenu() { + menuItems := c.getMainMenuItems() + c.TabCompleters[c.MenuType].Config.AutoComplete = menuItems.Completer + + for { + line, err := c.TabCompleters[c.MenuType].Readline() + if err == readline.ErrInterrupt { + if len(line) == 0 { + break + } else { + continue + } + } else if err == io.EOF { + break + } + + words := strings.Split(strings.TrimSpace(line), " ") + + switch words[0] { + case "config": + if len(words) != 2 { + + } else { + if strings.ToLower(words[1]) == "dns" { + c.MenuType = "Dns" + } else if strings.ToLower(words[1]) == "http" { + c.MenuType = "Http" + } else { + fmt.Printf("[!] Unknown menu type: %s\n", words[1]) + } + return + } + case "start": + errorMsg := "[!] Either start 'http' or 'dns'" + if len(words) == 2 { + if strings.ToLower(words[1]) == "http" { + startHttpServer(c.HttpServer) + } else if strings.ToLower(words[1]) == "dns" { + startDnsServer(c.DnsServer) + } else { + fmt.Println(errorMsg) + } + } else { + fmt.Println(errorMsg) + } + case "stop": + errorMsg := "[!] Either stop 'http' or 'dns'" + if len(words) == 2 { + if strings.ToLower(words[1]) == "http" { + stopHttpServer(c.HttpServer) + } else if strings.ToLower(words[1]) == "dns" { + stopDnsServer(c.DnsServer) + } else { + fmt.Println(errorMsg) + } + } else { + fmt.Println(errorMsg) + } + case "restart": + errorMsg := "[!] Either restart 'http' or 'dns'" + if len(words) == 2 { + if strings.ToLower(words[1]) == "http" { + stopHttpServer(c.HttpServer) + if !c.HttpServer.Running { + startHttpServer(c.HttpServer) + } + } else if strings.ToLower(words[1]) == "dns" { + stopDnsServer(c.DnsServer) + if !c.DnsServer.Running { + startDnsServer(c.DnsServer) + } + } else { + fmt.Println(errorMsg) + } + } else { + fmt.Println(errorMsg) + } + case "new": + if len(words) != 2 { + fmt.Println("[!] Either create an 'httpkey' or 'dnskey'") + } else { + if words[1] == "httpkey" { + c.MenuType = "HttpKey" + return + } + if words[1] == "dnskey" { + c.MenuType = "DnsKey" + return + } + } + case "status": + fmt.Println() + running := "not running" + if c.HttpServer.Running { + running = "running" + } + fmt.Printf("HTTP: (%d keys, %s)\n", len(c.HttpServer.Keys), running) + printKeys(c.HttpServer.Keys) + + running = "not running" + if c.DnsServer.Running { + running = "running" + } + fmt.Printf("DNS: (%d keys, %s)\n", len(c.DnsServer.Keys), running) + printKeys(c.DnsServer.Keys) + fmt.Println() + case "info": + if len(words) != 2 { + fmt.Println("[!] Use `info ` to view details about a specific key") + } else { + httpKeyFound, dnsKeyFound := findKey(words[1], c.HttpServer.Keys, c.DnsServer.Keys) + if httpKeyFound != "" { + printKey(c.HttpServer.Keys[httpKeyFound], httpKeyFound) + } + if dnsKeyFound != "" { + printKey(c.DnsServer.Keys[dnsKeyFound], dnsKeyFound) + } + } + case "on": + if len(words) != 2 { + fmt.Println("[!] Use `on ` to manually turn on a key") + } else { + httpKeyFound, dnsKeyFound := findKey(words[1], c.HttpServer.Keys, c.DnsServer.Keys) + if httpKeyFound != "" { + c.HttpServer.Keys[httpKeyFound].On = true + logger.Log.Noticef("[KEYCHANGE] - HTTP Key '%s' has been turned on!", httpKeyFound) + if !c.HttpServer.Running { + fmt.Println("[-] HTTP Server isn't running...") + } + } + if dnsKeyFound != "" { + c.DnsServer.Keys[dnsKeyFound].On = true + logger.Log.Noticef("[KEYCHANGE] - DNS Key '%s' has been turned on!", dnsKeyFound) + if !c.DnsServer.Running { + fmt.Println("[-] DNS Server isn't running...") + } + } + } + case "off": + if len(words) != 2 { + fmt.Println("[!] Use `off ` to manually turn off a key, constraints will still turn it on") + } else { + httpKeyFound, dnsKeyFound := findKey(words[1], c.HttpServer.Keys, c.DnsServer.Keys) + if httpKeyFound != "" { + c.HttpServer.Keys[httpKeyFound].On = false + logger.Log.Noticef("[KEYCHANGE] - HTTP Key '%s' has been turned off!", httpKeyFound) + if !c.HttpServer.Running { + fmt.Println("[-] HTTP Server isn't running...") + } + } + if dnsKeyFound != "" { + c.DnsServer.Keys[dnsKeyFound].On = false + logger.Log.Noticef("[KEYCHANGE] - DNS Key '%s' has been turned off!", dnsKeyFound) + if !c.DnsServer.Running { + fmt.Println("[-] DNS Server isn't running...") + } + } + } + case "disable": + if len(words) != 2 { + fmt.Println("[!] Use `disable ` to disable a key indefinitely") + } else { + httpKeyFound, dnsKeyFound := findKey(words[1], c.HttpServer.Keys, c.DnsServer.Keys) + if httpKeyFound != "" { + c.HttpServer.Keys[httpKeyFound].On = false + c.HttpServer.Keys[httpKeyFound].Disabled = true + logger.Log.Noticef("[KEYCHANGE] - HTTP Key %s has been turned diabled! Constraints will have no effect.", httpKeyFound) + if !c.HttpServer.Running { + fmt.Println("[-] HTTP Server isn't running...") + } + } + if dnsKeyFound != "" { + c.DnsServer.Keys[dnsKeyFound].On = false + c.DnsServer.Keys[dnsKeyFound].Disabled = true + logger.Log.Noticef("[KEYCHANGE] - DNS Key %s has been turned disabled! Constraints will have no effect.", dnsKeyFound) + if !c.DnsServer.Running { + fmt.Println("[-] DNS Server isn't running...") + } + } + } + case "alert": + if len(words) != 2 { + fmt.Println("[!] Use `alert ` to turn on alerting for a key") + } else { + httpKeyFound, dnsKeyFound := findKey(words[1], c.HttpServer.Keys, c.DnsServer.Keys) + if httpKeyFound != "" { + c.HttpServer.Keys[httpKeyFound].SendAlerts = true + fmt.Printf("[*] Alerting for %s enabled\n", httpKeyFound) + } + if dnsKeyFound != "" { + c.DnsServer.Keys[dnsKeyFound].SendAlerts = true + fmt.Printf("[*] Alerting for %s enabled\n", dnsKeyFound) + } + } + case "noalert": + if len(words) != 2 { + fmt.Println("[!] Use `noalert ` to turn off alerting for a key") + } else { + httpKeyFound, dnsKeyFound := findKey(words[1], c.HttpServer.Keys, c.DnsServer.Keys) + if httpKeyFound != "" { + c.HttpServer.Keys[httpKeyFound].SendAlerts = false + fmt.Printf("[*] Alerting for %s disabled\n", httpKeyFound) + } + if dnsKeyFound != "" { + c.DnsServer.Keys[dnsKeyFound].SendAlerts = false + fmt.Printf("[*] Alerting for %s disabled\n", dnsKeyFound) + } + } + case "remove": + if len(words) != 2 { + fmt.Println("[!] Use `remove ` to remove a key") + } else { + httpKeyFound, dnsKeyFound := findKey(words[1], c.HttpServer.Keys, c.DnsServer.Keys) + if httpKeyFound != "" { + if response := askForPermission("[>] Remove this key? [y/N] "); response { + delete(c.HttpServer.Keys, httpKeyFound) + } + } + if dnsKeyFound != "" { + if response := askForPermission("[>] Remove this key? [y/N] "); response { + delete(c.DnsServer.Keys, dnsKeyFound) + } + } + } + case "clearhits": + if len(words) != 2 { + fmt.Println("[!] Use `clearhits ` to remove a key") + } else { + httpKeyFound, dnsKeyFound := findKey(words[1], c.HttpServer.Keys, c.DnsServer.Keys) + if httpKeyFound != "" { + c.HttpServer.Keys[httpKeyFound].ClearHits() + } + if dnsKeyFound != "" { + c.DnsServer.Keys[dnsKeyFound].ClearHits() + } + } + case "time": + printCurrentTime() + case "help": + menuItems.printHelp() + case "exit": + c.MenuType = "Quit" + return + case "": + continue + default: + fmt.Println("[!] Invalid command!") + } + } +} + +func (c *CmdInfo) HttpMenu() { + menuItems := getHttpMenuItems(c.HttpServer) + c.TabCompleters[c.MenuType].Config.AutoComplete = menuItems.Completer + + for { + line, err := c.TabCompleters[c.MenuType].Readline() + if err == readline.ErrInterrupt { + if len(line) == 0 { + break + } else { + continue + } + } else if err == io.EOF { + break + } + + words := strings.Split(strings.TrimSpace(line), " ") + + switch words[0] { + case "start": + startHttpServer(c.HttpServer) + case "stop": + stopHttpServer(c.HttpServer) + case "restart": + stopHttpServer(c.HttpServer) + if !c.HttpServer.Running { + startHttpServer(c.HttpServer) + } + case "info": + printHttpStatus(c.HttpServer) + case "unset": + if len(words) == 2 { + setting := strings.ToLower(words[1]) + + found := false + for k, v := range c.HttpServer.State { + if strings.ToLower(k) == setting { + v.Value = v.Default + found = true + } + } + if !found { + fmt.Printf("[!] Server setting does not exist: " + words[1]) + } + } + case "set": + if len(words) > 1 { + setting := strings.ToLower(words[1]) + found := "" + for k := range c.HttpServer.State { + if strings.ToLower(k) == setting { + found = k + break + } + } + if found != "" { + errorMsg := fmt.Sprintf("[!] Invalid command, use 'help %s' for more info", found) + switch found { + case "CertPath", "KeyPath": + if len(words) > 2 { + filePath := strings.Join(words[2:], " ") + // Check if file actually exists + _, err := os.Stat(filePath) + if err == nil { + c.HttpServer.State[found].Value = filePath + } else { + fmt.Printf("[!] Error reading file: %s\n", err) + } + } else { + fmt.Println(errorMsg) + } + default: + // by default we will blindly set the value to word[2:] (everything after the second word on the line) + if len(words) > 2 { + c.HttpServer.State[found].Value = strings.Join(words[2:], " ") + } else { + fmt.Println(errorMsg) + } + } + } + } + case "help": + if len(words) != 2 { + fmt.Println() + menuItems.printHelp() + fmt.Println("Use help to learn more about each setting") + fmt.Println() + } else { + if _, ok := c.HttpServer.State[words[1]]; ok { + fmt.Println() + fmt.Println(c.HttpServer.State[words[1]].Help) + fmt.Println() + } else { + fmt.Printf("[!] The DNS server setting %s does not exist!", words[1]) + } + } + case "exit", "back": + c.MenuType = "Main" + return + case "": + continue + default: + fmt.Println("[!] Invalid command!") + } + } +} + +func (c *CmdInfo) DnsMenu() { + menuItems := getDnsMenuItems(c.DnsServer) + c.TabCompleters[c.MenuType].Config.AutoComplete = menuItems.Completer + + for { + line, err := c.TabCompleters[c.MenuType].Readline() + if err == readline.ErrInterrupt { + if len(line) == 0 { + break + } else { + continue + } + } else if err == io.EOF { + break + } + + words := strings.Split(strings.TrimSpace(line), " ") + + switch words[0] { + case "start": + startDnsServer(c.DnsServer) + case "stop": + stopDnsServer(c.DnsServer) + case "restart": + stopDnsServer(c.DnsServer) + if !c.DnsServer.Running { + startDnsServer(c.DnsServer) + } + case "info": + printDnsStatus(c.DnsServer) + case "unset": + if len(words) == 2 { + setting := strings.ToLower(words[1]) + + found := false + for k, v := range c.DnsServer.State { + if strings.ToLower(k) == setting { + v.Value = v.Default + found = true + } + } + if !found { + fmt.Printf("[!] Server setting does not exist: " + words[1]) + } + } + case "set": + if len(words) > 1 { + setting := strings.ToLower(words[1]) + found := "" + for k := range c.DnsServer.State { + if strings.ToLower(k) == setting { + found = k + break + } + } + if found != "" { + errorMsg := fmt.Sprintf("[!] Invalid command, use 'help %s' for more info", found) + switch found { + case "DefaultTTL": + if len(words) == 3 { + // Update the DefaultTTL uint attribute + val, err := strconv.ParseUint(words[2], 10, 32) + if err != nil { + fmt.Printf("[!] %s is not a valid number\n", words[2]) + } else { + c.DnsServer.DefaultTTL = uint(val) + c.DnsServer.State[found].Value = words[2] + } + } else { + fmt.Println(errorMsg) + } + default: + // by default we will blindly set the value to word[2:] (everything after the second word on the line) + if len(words) > 2 { + c.DnsServer.State[found].Value = strings.Join(words[2:], " ") + } else { + fmt.Println(errorMsg) + } + } + } + } + case "help": + if len(words) != 2 { + fmt.Println() + menuItems.printHelp() + fmt.Println("Use help to learn more about each setting") + fmt.Println() + } else { + if _, ok := c.DnsServer.State[words[1]]; ok { + fmt.Println() + fmt.Println(c.DnsServer.State[words[1]].Help) + fmt.Println() + } else { + fmt.Printf("[!] The DNS server setting %s does not exist!", words[1]) + } + } + case "exit", "back": + c.MenuType = "Main" + return + case "": + continue + default: + fmt.Println("[!] Invalid command!") + } + } +} + +func (c *CmdInfo) HttpKeyMenu() { + keyName := "NewHttpKey" + key := &servers.Key{ + Type: "http", + Disabled: false, + SendAlerts: false, + Data: servers.HttpKeyData(), + Hashes: make(map[string]string), + HitCounter: make(map[string]int), + } + key.HitCounter[servers.GetToday()] = 0 + key.Constraints = key.GetHttpKeyConstraints() + + menuItems := getHttpKeyMenuItems(key) + c.TabCompleters[c.MenuType].Config.AutoComplete = menuItems.Completer + + for { + line, err := c.TabCompleters[c.MenuType].Readline() + if err == readline.ErrInterrupt { + if len(line) == 0 { + break + } else { + continue + } + } else if err == io.EOF { + break + } + + words := strings.Split(strings.TrimSpace(line), " ") + + switch words[0] { + case "info": + printKeyMenuStatus(key, keyName) + case "help": + menuItems.printHelp() + case "unset": + if len(words) == 2 { + setting := strings.ToLower(words[1]) + + found := false + for k, v := range key.Data { + if strings.ToLower(k) == setting { + v.Value = "" + found = true + } + } + if !found { + for k, v := range key.Constraints { + if strings.ToLower(k) == setting { + v.Constraint = "" + found = true + } + } + if !found { + fmt.Printf("[!] Setting does not exist: " + words[1]) + } + } + } + case "set": + if len(words) > 2 { + setting := strings.ToLower(words[1]) + found := "" + isConstraint := false + for k := range key.Data { + if strings.ToLower(k) == setting { + found = k + break + } + } + for k := range key.Constraints { + if strings.ToLower(k) == setting { + found = k + isConstraint = true + break + } + } + if setting == "name" { + found = "name" + } + if found != "" { + switch found { + case "name": + keyName = words[2] + default: + // by default we will blindly set the value to word[2:] (everything after the second word on the line) + if isConstraint { + key.Constraints[found].Constraint = strings.Join(words[2:], " ") + } else { + key.Data[found].Value = strings.Join(words[2:], " ") + } + } + } + } else { + fmt.Println("[!] Invalid command, use `set ] Add this key? [y/N] ") + if response { + err := c.HttpServer.AddKey(key, keyName) + if err == nil { + c.MenuType = "Main" + return + } + fmt.Printf("[!] Error adding key: %s\n", err) + } + case "exit", "back": + c.MenuType = "Main" + return + case "": + continue + default: + fmt.Println("[!] Invalid command!") + } + } +} + +func (c *CmdInfo) DnsKeyMenu() { + keyName := "NewDnsKey" + key := &servers.Key{ + Type: "dns", + Disabled: false, + SendAlerts: false, + Data: servers.DnsKeyData(), + Hashes: make(map[string]string), + HitCounter: make(map[string]int), + } + key.HitCounter[servers.GetToday()] = 0 + key.Constraints = key.GetDnsKeyConstraints() + + menuItems := getDnsKeyMenuItems(key) + c.TabCompleters[c.MenuType].Config.AutoComplete = menuItems.Completer + + for { + line, err := c.TabCompleters[c.MenuType].Readline() + if err == readline.ErrInterrupt { + if len(line) == 0 { + break + } else { + continue + } + } else if err == io.EOF { + break + } + + words := strings.Split(strings.TrimSpace(line), " ") + + switch words[0] { + case "info": + printKeyMenuStatus(key, keyName) + case "help": + menuItems.printHelp() + case "unset": + if len(words) == 2 { + setting := strings.ToLower(words[1]) + + found := false + for k, v := range key.Data { + if strings.ToLower(k) == setting { + v.Value = "" + found = true + } + } + if !found { + for k, v := range key.Constraints { + if strings.ToLower(k) == setting { + v.Constraint = "" + found = true + } + } + if !found { + fmt.Printf("[!] Setting does not exist: " + words[1]) + } + } + } + case "set": + if len(words) > 2 { + setting := strings.ToLower(words[1]) + found := "" + isConstraint := false + for k := range key.Data { + if strings.ToLower(k) == setting { + found = k + break + } + } + for k := range key.Constraints { + if strings.ToLower(k) == setting { + found = k + isConstraint = true + break + } + } + if setting == "name" { + found = "name" + } + if found != "" { + switch found { + case "name": + keyName = words[2] + default: + // by default we will blindly set the value to word[2:] (everything after the second word on the line) + if isConstraint { + key.Constraints[found].Constraint = strings.Join(words[2:], " ") + } else { + key.Data[found].Value = strings.Join(words[2:], " ") + } + } + } + } else { + fmt.Println("[!] Invalid command, use `set ] Add this key? [y/N] ") + if response { + err := c.DnsServer.AddKey(key, keyName) + if err == nil { + c.MenuType = "Main" + return + } + fmt.Printf("[!] Error adding key: %s\n", err) + } + case "exit", "back": + c.MenuType = "Main" + return + case "": + continue + default: + fmt.Println("[!] Invalid command!") + } + } +} + +func startHttpServer(h *servers.HttpServer) { + if h.Running { + fmt.Println("[!] HTTP server already running, use 'restart'") + return + } + if h.State["CertPath"].Value != "" && h.State["KeyPath"].Value != "" { + h.StartHTTPS() + } else { + h.StartHTTP() + } + time.Sleep(1 * time.Second) + if !h.Running { + fmt.Println("[!] Error occurred starting the HTTP server, port already in use?") + } else { + fmt.Println("[+] HTTP server successfully started!") + } +} + +func stopHttpServer(h *servers.HttpServer) { + if !h.Running { + fmt.Printf("[!] HTTP server isn't running\n") + return + } + err := h.Server.Shutdown(nil) + if err != nil { + fmt.Printf("[!] Error shutting down HTTP gracefully: %s\n", err) + return + } + time.Sleep(1 * time.Second) + fmt.Println("[*] HTTP server stopped") + h.Running = false +} + +func printHttpStatus(h *servers.HttpServer) { + fmt.Println() + fmt.Println("HTTP Key Server") + fmt.Printf("Running: %s\n", isRunning(h.Running)) + + // Print modifiable settings + settings := servers.AlphabetizeSettings(h.State) + for _, name := range settings { + fmt.Printf(" %s %s\n", columnString(name+returnAsterisk(h.State[name].Required)), h.State[name].Value) + } + fmt.Println() +} + +func startDnsServer(d *servers.DnsServer) { + if d.Running { + fmt.Println("[!] DNS server already running, use 'restart'") + return + } + d.StartDNS() + time.Sleep(1 * time.Second) + if !d.Running { + fmt.Println("[!] Error occurred starting the DNS server, port already in use?") + } else { + fmt.Println("[+] DNS server successfully started!") + } +} + +func stopDnsServer(d *servers.DnsServer) { + if !d.Running { + fmt.Println("[!] DNS server isn't running") + return + } + err := d.Server.Shutdown() + if err != nil { + fmt.Printf("[!] Error shutting down DNS gracefully: %s\n", err) + return + } + time.Sleep(1 * time.Second) + fmt.Println("[*] DNS server stopped") + d.Running = false +} + +func printDnsStatus(d *servers.DnsServer) { + fmt.Println() + fmt.Println("DNS Key Server") + fmt.Printf("Running: %s\n", isRunning(d.Running)) + + // Print modifiable settings + settings := servers.AlphabetizeSettings(d.State) + for _, name := range settings { + fmt.Printf(" %s %s\n", columnString(name+returnAsterisk(d.State[name].Required)), columnString(d.State[name].Value)) + } + fmt.Println() +} + +// Prints status of menu items when selecting Key attributes +func printKeyMenuStatus(k *servers.Key, name string) { + fmt.Println() + fmt.Println("Key: ") + fmt.Printf(" %s '%s'\n", columnString("Name:"), name) + + keyData := servers.AlphabetizeKeyData(k.Data) + for _, name := range keyData { + fmt.Printf(" %s '%s'\n", columnString(name+":"), k.Data[name].Value) + fmt.Printf(" %s\n", k.Data[name].Description) + } + fmt.Println("\nConstraints:") + constraints := servers.AlphabetizeConstraints(k.Constraints) + for _, name := range constraints { + fmt.Printf(" %s '%s'\n", columnString(name+":"), k.Constraints[name].Constraint) + fmt.Printf(" %s\n", k.Constraints[name].Description) + } + fmt.Println() +} + +// printKey shows more detail for one specific key by name +func printKey(key *servers.Key, name string) { + fmt.Println() + fmt.Printf("Name: %s (%s)\n", name, key.Type) + + if key.Type == "dns" { + fmt.Printf("Hostname: %s\n", key.Data["Hostname"].Value) + fmt.Printf("Record Type: %s\n", key.Data["RecordType"].Value) + fmt.Printf("Response: %s\n", key.Data["Response"].Value) + fmt.Printf("TTL: %s\n", key.Data["TTL"].Value) + } else if key.Type == "http" { + fmt.Printf("URL: %s\n", key.Data["URL"].Value) + fmt.Printf("FilePath: %s\n", key.Data["FilePath"].Value) + } else { + fmt.Printf("[!] Unknown key type: %s\n", key.Type) + return + } + + fmt.Println() + fmt.Println("Hashes of response:") + for k, v := range key.Hashes { + fmt.Printf("%s: '%s'\n", k, v) + } + + fmt.Println() + constraints := servers.AlphabetizeConstraints(key.Constraints) + fmt.Println("Constraints:") + for _, name := range constraints { + fmt.Printf(" %s '%s'\n", columnString(name), key.Constraints[name].Constraint) + } + fmt.Println() +} + +func printKeys(keys map[string]*servers.Key) { + if len(keys) == 0 { + return + } + fmt.Println() + fmt.Println("Keys ---") + for name, key := range keys { + fmt.Printf(" Name: %s\n", name) + + if key.Type == "dns" { + fmt.Printf(" Hostname: %s\n", key.Data["Hostname"].Value) + } else if key.Type == "http" { + fmt.Printf(" URL: %s\n", key.Data["URL"].Value) + } else { + fmt.Printf("[!] Unknown key type: %s\n", key.Type) + continue + } + fmt.Printf(" Hits Today: %d\n", key.GetHits()) + fmt.Printf(" Last Hit: %s\n", key.LastHit) + if key.SendAlerts { + fmt.Println(" Alerts: Enabled") + } else { + fmt.Println(" Alerts: Disabled") + } + + active, reason := key.IsActive(nil, nil) + if active { + fmt.Printf(" Active: YES (%s)\n", reason) + } else { + if reason != "" { + reason = "(" + reason + ")" + } + fmt.Printf(" Active: NO %s\n", reason) + } + fmt.Println() + } +} + +func printCurrentTime() { + fmt.Println(time.Now().Format("Jan 2 15:04")) +} + +func isRunning(result bool) string { + if result { + return "Yes" + } + return "No" +} + +func returnAsterisk(result bool) string { + if result { + return "*:" + } + return ": " +} + +func askForPermission(q string) bool { + reader := bufio.NewReader(os.Stdin) + fmt.Print(q) + confirm, err := reader.ReadString('\n') + confirm = strings.TrimSpace(confirm) + if confirm == "n" || confirm == "N" || err != nil { + return false + } + return true +} + +// searches by name for a http or dns key, returns (httpKeyName, dnsKeyName), each string is empty if not found +func findKey(input string, httpKeys map[string]*servers.Key, dnsKeys map[string]*servers.Key) (string, string) { + var dnsKeyName string + var httpKeyName string + setting := strings.ToLower(input) + + for k := range httpKeys { + if strings.ToLower(k) == setting { + httpKeyName = k + } + } + for k := range dnsKeys { + if strings.ToLower(k) == setting { + dnsKeyName = k + } + } + + return httpKeyName, dnsKeyName +} diff --git a/cmd/menu_items.go b/cmd/menu_items.go new file mode 100644 index 0000000..bd60f19 --- /dev/null +++ b/cmd/menu_items.go @@ -0,0 +1,563 @@ +package cmd + +// Thanks to @evilsocket's Bettercap2 (https://github.com/bettercap/bettercap) +// for pointing me to github.com/chzyer/readline and having a good example to work off of + +import ( + "fmt" + "io/ioutil" + "sort" + "strings" + + "github.com/chzyer/readline" + "github.com/leoloobeek/keyserver/servers" +) + +// MenuItems holds all commands for a menu +type MenuItems struct { + MenuType string + Items map[string]*MenuItem + Completer *readline.PrefixCompleter +} + +// MenuItem describes each command, help and usage, and completion +type MenuItem struct { + Help string + Example string + Error string + Completer *readline.PrefixCompleter +} + +func filterInput(r rune) (rune, bool) { + switch r { + // block CtrlZ feature + case readline.CharCtrlZ: + return r, false + } + return r, true +} + +func InitializeCompleters() map[string]*readline.Instance { + // initialize tab completers + + // MainMenu + mmInst, err := readline.NewEx(&readline.Config{ + Prompt: "keyserver > ", + AutoComplete: nil, + InterruptPrompt: "^C", + EOFPrompt: "exit", + + HistorySearchFold: true, + FuncFilterInputRune: filterInput, + }) + + // HttpMenu + hmInst, err := readline.NewEx(&readline.Config{ + Prompt: "keyserver (http) > ", + AutoComplete: nil, + InterruptPrompt: "^C", + EOFPrompt: "exit", + + HistorySearchFold: true, + FuncFilterInputRune: filterInput, + }) + // DnsMenu + dmInst, err := readline.NewEx(&readline.Config{ + Prompt: "keyserver (dns) > ", + AutoComplete: nil, + InterruptPrompt: "^C", + EOFPrompt: "exit", + + HistorySearchFold: true, + FuncFilterInputRune: filterInput, + }) + + // HttpKeyMenu + hkmInst, err := readline.NewEx(&readline.Config{ + Prompt: "keyserver (httpkey) > ", + AutoComplete: nil, + InterruptPrompt: "^C", + EOFPrompt: "exit", + + HistorySearchFold: true, + FuncFilterInputRune: filterInput, + }) + + // DnsKeyMenu + dkmInst, err := readline.NewEx(&readline.Config{ + Prompt: "keyserver (dnskey) > ", + AutoComplete: nil, + InterruptPrompt: "^C", + EOFPrompt: "exit", + + HistorySearchFold: true, + FuncFilterInputRune: filterInput, + }) + + if err != nil { + panic(err) + } + + return map[string]*readline.Instance{ + "Main": mmInst, + "Http": hmInst, + "Dns": dmInst, + "HttpKey": hkmInst, + "DnsKey": dkmInst, + } +} + +func (c *CmdInfo) getMainMenuItems() *MenuItems { + + items := defaultItems() + + items["config"] = &MenuItem{ + Help: "Configure a server (http or dns)", + Example: "config http", + Completer: readline.NewPrefixCompleter( + readline.PcItem("http"), + readline.PcItem("dns"), + ), + } + + items["start"] = &MenuItem{ + Help: "Start a server (http or dns)", + Example: "start http", + Completer: readline.NewPrefixCompleter( + readline.PcItem("http"), + readline.PcItem("dns"), + ), + } + + items["stop"] = &MenuItem{ + Help: "Stop a server (http or dns)", + Example: "stop http", + Completer: readline.NewPrefixCompleter( + readline.PcItem("http"), + readline.PcItem("dns"), + ), + } + + items["restart"] = &MenuItem{ + Help: "Restart a server (http or dns)", + Example: "restart http", + Completer: readline.NewPrefixCompleter( + readline.PcItem("http"), + readline.PcItem("dns"), + ), + } + + items["new"] = &MenuItem{ + Help: "Create a new dnskey or httpkey", + Example: "new httpkey", + Completer: readline.NewPrefixCompleter( + readline.PcItem("httpkey"), + readline.PcItem("dnskey"), + ), + } + + items["status"] = &MenuItem{ + Help: "Show status of servers and keys", + Example: "status", + Completer: readline.NewPrefixCompleter(), + } + + items["info"] = &MenuItem{ + Help: "Show detailed information for a specific key", + Example: "info ", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(c.getAllKeys())), + } + + items["on"] = &MenuItem{ + Help: "Manually turn on a specific key", + Example: "on ", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(c.getAllKeys())), + } + + items["off"] = &MenuItem{ + Help: "Manually turn off a specific key", + Example: "off ", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(c.getAllKeys())), + } + + items["disable"] = &MenuItem{ + Help: "Disable a key and never respond with active key, even if a constraint matches", + Example: "off ", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(c.getAllKeys())), + } + + items["alert"] = &MenuItem{ + Help: "Enable alerting for a key", + Example: "alert ", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(c.getAllKeys())), + } + + items["noalert"] = &MenuItem{ + Help: "Disable alerting for a key", + Example: "noalert ", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(c.getAllKeys())), + } + + items["remove"] = &MenuItem{ + Help: "Remove a key", + Example: "remove ", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(c.getAllKeys())), + } + + items["clearhits"] = &MenuItem{ + Help: "Disable alerting for a key", + Example: "noalert ", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(c.getAllKeys())), + } + + items["time"] = &MenuItem{ + Help: "Display current time on keyserver (useful when setting time constraints)", + Example: "time", + Completer: readline.NewPrefixCompleter(), + } + + completer := []readline.PrefixCompleterInterface{} + for name, mi := range items { + item := readline.PcItem(name) + item.Children = mi.Completer.Children + completer = append(completer, item) + } + + return &MenuItems{ + MenuType: "Main", + Items: items, + Completer: readline.NewPrefixCompleter(completer...), + } +} + +func getHttpMenuItems(h *servers.HttpServer) *MenuItems { + + items := getConfigMenuItems(h.State) + + // Update help's completer with DNS settings + items["help"].Completer = readline.NewPrefixCompleter(readline.PcItemDynamic(getSettings(h.State))) + + completer := []readline.PrefixCompleterInterface{} + for name, mi := range items { + item := readline.PcItem(name) + item.Children = mi.Completer.Children + completer = append(completer, item) + } + + return &MenuItems{ + MenuType: "Http", + Items: items, + Completer: readline.NewPrefixCompleter(completer...), + } +} + +func getDnsMenuItems(d *servers.DnsServer) *MenuItems { + + items := getConfigMenuItems(d.State) + + // Update help's completer with DNS settings + items["help"].Completer = readline.NewPrefixCompleter(readline.PcItemDynamic(getSettings(d.State))) + + completer := []readline.PrefixCompleterInterface{} + for name, mi := range items { + item := readline.PcItem(name) + item.Children = mi.Completer.Children + completer = append(completer, item) + } + + return &MenuItems{ + MenuType: "Dns", + Items: items, + Completer: readline.NewPrefixCompleter(completer...), + } +} + +// These menu items will consist between both http and dns config menus +func getConfigMenuItems(ss map[string]*servers.ServerSetting) map[string]*MenuItem { + + items := defaultItems() + + items["info"] = &MenuItem{ + Help: "Show all settings", + Example: "info", + Completer: readline.NewPrefixCompleter(), + } + + items["set"] = &MenuItem{ + Help: "Choose settings", + Example: "set Listen 0.0.0.0", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(getSettings(ss))), + } + + items["unset"] = &MenuItem{ + Help: "Remote settings", + Example: "unset Listen", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(getSettings(ss))), + } + + items["start"] = &MenuItem{ + Help: "Start the server", + Example: "start", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(getSettings(ss))), + } + + items["restart"] = &MenuItem{ + Help: "Restart the server", + Example: "restart", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(getSettings(ss))), + } + + items["stop"] = &MenuItem{ + Help: "Stop the server", + Example: "stop", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(getSettings(ss))), + } + + return items +} + +func getHttpKeyMenuItems(k *servers.Key) *MenuItems { + + items := getKeyMenuItems(k) + + completer := []readline.PrefixCompleterInterface{} + for name, mi := range items { + item := readline.PcItem(name) + item.Children = mi.Completer.Children + completer = append(completer, item) + } + + return &MenuItems{ + MenuType: "HttpKey", + Items: items, + Completer: readline.NewPrefixCompleter(completer...), + } +} + +func getDnsKeyMenuItems(k *servers.Key) *MenuItems { + + items := getKeyMenuItems(k) + + completer := []readline.PrefixCompleterInterface{} + for name, mi := range items { + item := readline.PcItem(name) + item.Children = mi.Completer.Children + completer = append(completer, item) + } + + return &MenuItems{ + MenuType: "DnsKey", + Items: items, + Completer: readline.NewPrefixCompleter(completer...), + } +} + +// These menu items will consist between both http and dns key config menus +func getKeyMenuItems(k *servers.Key) map[string]*MenuItem { + + items := defaultItems() + + items["done"] = &MenuItem{ + Help: "Once key is set, finish and add it to the server's keys", + Example: "done", + Error: "", + Completer: readline.NewPrefixCompleter(), + } + + items["info"] = &MenuItem{ + Help: "Show all settings", + Example: "info", + Error: "", + Completer: readline.NewPrefixCompleter(), + } + + items["set"] = &MenuItem{ + Help: "Choose settings or constraints", + Example: "set Name MyKey", + Error: "Did not choose a valid setting", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(getSettingsAndConstraints(k))), + } + + items["unset"] = &MenuItem{ + Help: "Remove settings or constraints", + Example: "unset Time", + Error: "Did not choose a valid setting", + Completer: readline.NewPrefixCompleter(readline.PcItemDynamic(getSettingsAndConstraints(k))), + } + + return items +} + +/* +func (m *MenuInformation) getDropperMenuItems() *MenuItems { + items := defaultItems() + + dropperSettings := readline.NewPrefixCompleter( + readline.PcItem("SonarName", readline.PcItemDynamic(m.getRunningSonarNames())), + readline.PcItem("Filename"), + readline.PcItem("Lang"), + readline.PcItem("Format"), + readline.PcItem("StagingURL"), + readline.PcItem("UserAgent"), + readline.PcItem("CustomHeaders"), + ) + + items["help"].Completer = dropperSettings + + items["info"] = &MenuItem{ + Help: "Show sonar info and settings (alias for 'show sonarinfo'", + Example: "info", + Completer: readline.NewPrefixCompleter(), + } + + items["show"] = &MenuItem{ + Help: "Show langs or formats that can be set", + Example: "show langs", + Error: "Specify an option to show (langs,formats)", + Completer: readline.NewPrefixCompleter( + readline.PcItem("langs"), + readline.PcItem("formats"), + ), + } + + items["set"] = &MenuItem{ + Help: "Set a language or dropper", + Example: "set Format mshta", + Error: "Options to set: Filename, Lang, Format, StagingURL, UserAgent, CustomHeaders", + Completer: dropperSettings, + } + + items["generate"] = &MenuItem{ + Help: "Generate a dropper based on the set options", + Example: "generate", + Error: "Error generating dropper file, are you missing required options?", + Completer: readline.NewPrefixCompleter(), + } + + completer := []readline.PrefixCompleterInterface{} + for name, mi := range items { + item := readline.PcItem(name) + item.Children = mi.Completer.Children + completer = append(completer, item) + } + + return &MenuItems{ + MenuType: "MainMenu", + Items: items, + Completer: readline.NewPrefixCompleter(completer...), + } +} +*/ +// +// Helpers +// + +// TODO: Make this better, tab complete directories/etc. +func listFiles() func(string) []string { + return func(line string) []string { + path := "./" + names := make([]string, 0) + files, _ := ioutil.ReadDir(path) + for _, f := range files { + names = append(names, f.Name()) + } + return names + } +} + +/* +func getModulePaths() func(string) []string { + return func(line string) []string { + var result []string + for _, modulePath := range m.SubInfo.ModulePaths { + result = append(result, strings.TrimSuffix(strings.TrimPrefix(modulePath, "modules/"), ".json")) + } + return result + } +} +*/ + +func getSettingsAndConstraints(k *servers.Key) func(string) []string { + return func(line string) []string { + result := []string{"Name"} + for name, _ := range k.Data { + result = append(result, name) + } + for name, _ := range k.Constraints { + result = append(result, name) + } + return result + } +} + +func (c *CmdInfo) getAllKeys() func(string) []string { + return func(line string) []string { + var result []string + for name, _ := range c.HttpServer.Keys { + result = append(result, name) + } + for name, _ := range c.DnsServer.Keys { + result = append(result, name) + } + return result + } +} + +func getSettings(settings map[string]*servers.ServerSetting) func(string) []string { + return func(line string) []string { + var result []string + for name, _ := range settings { + result = append(result, name) + } + return result + } +} + +func defaultItems() map[string]*MenuItem { + items := make(map[string]*MenuItem) + + items["help"] = &MenuItem{ + Help: "Displays all commands with help description", + Example: "help", + Completer: readline.NewPrefixCompleter(), + } + + items["back"] = &MenuItem{ + Help: "Go back a menu", + Example: "back", + Completer: readline.NewPrefixCompleter(), + } + + items["exit"] = &MenuItem{ + Help: "Exits keyserver. The alias 'quit' also works", + Example: "exit", + Completer: readline.NewPrefixCompleter(), + } + + return items +} + +func (mis *MenuItems) printHelp() { + // Get map into alphabetical order + keys := make([]string, len(mis.Items)) + i := 0 + // fill temp array with keys of mis.Items + for k := range mis.Items { + keys[i] = k + i++ + } + sort.Strings(keys) + + // Print out + fmt.Println("Commands:") + for _, key := range keys { + fmt.Printf("\t%s%s\n", columnString(key), mis.Items[key].Help) + } +} + +func columnString(str string) string { + if len(str) > 18 { + return str[:18] + " " + } + return str + (strings.Repeat(" ", (19 - len(str)))) +} diff --git a/keyserver.go b/keyserver.go new file mode 100644 index 0000000..d0492f8 --- /dev/null +++ b/keyserver.go @@ -0,0 +1,205 @@ +package main + +// +// keyserver - Leo Loobeek 2018 +// + +import ( + "fmt" + + "github.com/leoloobeek/keyserver/cmd" + "github.com/leoloobeek/keyserver/logger" + "github.com/leoloobeek/keyserver/servers" +) + +func main() { + fmt.Println() + + logger.Init() + logger.Log.Info("Keyserver starting up...") + + c := cmd.CmdInfo{ + MenuType: "Main", + TabCompleters: cmd.InitializeCompleters(), + HttpServer: servers.GetHttpServer(), + DnsServer: servers.GetDnsServer(), + } + +Endless: + for { + switch c.MenuType { + case "Main": + c.MainMenu() + case "Http": + c.HttpMenu() + case "Dns": + c.DnsMenu() + case "HttpKey": + c.HttpKeyMenu() + case "DnsKey": + c.DnsKeyMenu() + case "Quit": + break Endless + default: + c.MenuType = "Main" + } + } +} + +/* + + // Setup DNS server settings + is := &lib.DNSServer{ + State: &lib.ServerState{ + Listen: "0.0.0.0:53", + Domain: "domain.com", + DefaultTTL: 10800, + KeyTTL: 180, + }, + SendingKey: false, + } + // Setup HTTP server settings + h := &lib.HTTPServer{ + Listen: ":80", + } + + status(is) + + var completer = readline.NewPrefixCompleter( + readline.PcItem("start", + readline.PcItem("http"), + readline.PcItem("dns"), + ), + readline.PcItem("dnskey", + readline.PcItem("on"), + readline.PcItem("off"), + ), + readline.PcItem("show"), + readline.PcItem("exit"), + readline.PcItem("add", + readline.PcItem("dnskey"), + ), + readline.PcItem("set", + readline.PcItem("dnsdomain"), + ), + ) + + // setup readline + l, err := readline.NewEx(&readline.Config{ + Prompt: "\033[31mkeyserver >\033[0m ", + AutoComplete: completer, + InterruptPrompt: "^C", + EOFPrompt: "exit", + + HistorySearchFold: true, + FuncFilterInputRune: filterInput, + }) + + if err != nil { + panic(err) + } + defer l.Close() + +Endless: + for { + line, err := l.Readline() + if err == readline.ErrInterrupt { + if len(line) == 0 { + break + } else { + continue + } + } else if err == io.EOF { + break + } + words := strings.Split(strings.TrimSpace(line), " ") + + switch words[0] { + case "start": + if len(words) == 2 { + if words[1] == "http" { + h.StartHTTP() + log.Println("[+] HTTP server started!") + } else if words[1] == "dns" { + is.StartDNS() + log.Println("[+] DNS server started!") + } + } + case "stop": + if len(words) == 2 { + if words[1] == "http" { + if err := h.Server.Shutdown(nil); err != nil { + log.Printf("Error shutting down HTTP server: %s", err) + } else { + log.Println("HTTP server shutdown successfully") + } + } else if words[1] == "dns" { + if err := is.Server.Shutdown(); err != nil { + log.Printf("Error shutting down DNS server: %s", err) + } else { + log.Println("DNS server shutdown successfully") + } + } + } + case "add": + if len(words) > 3 { + if words[1] == "dnskey" { + hostname := words[2] + value := words[3] + if len(words) > 4 { + value = strings.Join(words[3:], " ") + } + sha256 := lib.GenerateSHA256(value) + sha512 := lib.GenerateSHA512(value) + is.Key = &lib.DnsKey{ + Hostname: hostname, + Value: value, + Sha256: sha256, + Sha512: sha512, + } + fmt.Println("[+] DnsKey added!") + } + } else { + fmt.Println("[!] Syntax: add dnskey ") + } + case "dnskey": + if len(words) == 2 { + if words[1] == "on" { + log.Println("[*] Responding with DnsKey to valid queries") + is.SendingKey = true + } else if words[1] == "off" { + log.Println("[*] No longer responding with DnsKey") + is.SendingKey = false + } + } + case "show": + status(is) + case "set": + if len(words) == 3 { + if words[1] == "dnsdomain" { + is.State.Domain = words[2] + } + } + case "quit", "exit": + break Endless + case "": + continue + default: + fmt.Printf("[!] Unknown menu option: %s. Try 'help'\n", words[0]) + } + } +} + +func status(is *lib.DNSServer) { + fmt.Println() + fmt.Println("DNS Domain: " + is.State.Domain) + if is.Key != nil { + fmt.Println("DnsKey Hostname: " + is.Key.Hostname) + fmt.Println("DnsKey Value: " + is.Key.Value) + fmt.Println("DnsKey Sha256: " + is.Key.Sha256) + fmt.Println("DnsKey Sha512: " + is.Key.Sha512) + fmt.Println("Active: " + strconv.FormatBool(is.SendingKey)) + } + fmt.Println() +} +*/ diff --git a/logger/alerts.go b/logger/alerts.go new file mode 100644 index 0000000..c5a24b9 --- /dev/null +++ b/logger/alerts.go @@ -0,0 +1,90 @@ +package logger + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/smtp" + "os" +) + +var Alerts = parseConfig() + +type AlertConfig struct { + SlackWebhookURL string + SMTPServer string + SMTPPort int + MailFrom string + Password string + MailTo string +} + +func (ac *AlertConfig) SendAlerts(message string) { + if ac.SlackWebhookURL != "" { + ac.slackAlert(message) + } + if ac.SMTPServer != "" && ac.MailFrom != "" && ac.MailTo != "" && ac.Password != "" { + ac.emailAlert(message) + } +} + +// slackAlert sends an alert to Slack via web hook +func (ac *AlertConfig) slackAlert(message string) { + text := map[string]string{ + "text": message, + } + postData, err := json.Marshal(text) + if err != nil { + return + } + + _, err = http.Post(ac.SlackWebhookURL, "application/json", bytes.NewBuffer(postData)) + if err != nil { + fmt.Println("[!] Error sending Slack hook") + } +} + +// emailAlert sends an alert to an email address +func (ac *AlertConfig) emailAlert(message string) { + subject := "KeyServer Alert" + email := fmt.Sprintf("From: %s\nTo: %s\nSubject: %s\n\n%s", + ac.MailFrom, ac.MailTo, subject, message) + + server := fmt.Sprintf("%s:%s", ac.SMTPServer, ac.SMTPPort) + err := smtp.SendMail(server, + smtp.PlainAuth("", ac.MailFrom, ac.Password, ac.SMTPServer), + ac.MailFrom, []string{ac.MailTo}, []byte(email)) + + if err != nil { + return + } +} + +func parseConfig() *AlertConfig { + configPath := "alerts.config" + + file, err := os.Open(configPath) + if err != nil { + fmt.Println("[!] Error reading alerts.config") + return &AlertConfig{} + } + + decoder := json.NewDecoder(file) + config := AlertConfig{} + err = decoder.Decode(&config) + if err != nil { + fmt.Println("[!] Error parsing config.json:", err) + return &AlertConfig{} + } + return &config +} + +func readFile(path string) ([]byte, error) { + fileBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + return fileBytes, nil +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..3eb71c0 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,43 @@ +package logger + +import ( + "os" + + "github.com/op/go-logging" +) + +// Logging for keyserver: +// - Generic requests = Info +// - Active key response = Notice +// - Key active/inactive change = Notice +// - Inactive key response = Warn + +var Log = logging.MustGetLogger("keyserver") + +// Init sets up the logger +func Init() { + // color for stdout + colorFormat := logging.MustStringFormatter( + `%{color}%{time:02/Jan/2006 15:04:05} - %{message}%{color:reset}`, + ) + // no color for file output + plainFormat := logging.MustStringFormatter( + `%{time:02/Jan/2006 15:04:05} - %{message}`, + ) + + logFile, err := os.OpenFile("./keyserver.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) + if err != nil { + panic(err) + } + fileBackendRaw := logging.NewLogBackend(logFile, "", 0) + fileBackend := logging.NewBackendFormatter(fileBackendRaw, plainFormat) + + stdoutBackendRaw := logging.NewLogBackend(os.Stdout, "", 0) + stdoutBackend := logging.NewBackendFormatter(stdoutBackendRaw, colorFormat) + + // Start by only showing notice and higher with stdout + stdoutLeveled := logging.AddModuleLevel(stdoutBackend) + stdoutLeveled.SetLevel(logging.NOTICE, "") + + logging.SetBackend(stdoutLeveled, fileBackend) +} diff --git a/servers/handlers.go b/servers/handlers.go new file mode 100644 index 0000000..fb2547d --- /dev/null +++ b/servers/handlers.go @@ -0,0 +1,201 @@ +package servers + +import ( + "fmt" + "net" + "net/http" + "strconv" + "strings" + + "github.com/leoloobeek/keyserver/logger" + "github.com/miekg/dns" +) + +// +// HTTP Handling +// + +// ServeHTTP allows SubHTTPServer to handle http requests +// The requested URL path needs to match a key's Data["URL"].Value to +// evaluate the Key +func (h *HttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // add cache control headers regardless of the response + h.cacheHTTPHeaders(w) + + remoteAddr := parseProxyHeaders(r) + + // Log all requests + logger.Log.Infof("[HTTP] - %s \"%s %s\" \"%s\"", remoteAddr, r.Method, r.URL.Path, r.Header.Get("User-Agent")) + // loop through all keys and see if any URL matches + for name, key := range h.Keys { + if r.URL.Path == key.Data["URL"].Value { + // IsActive() will consider both manually setting the key and constraints + if active, _ := key.IsActive(r, nil); active { + fileBytes, err := ReadFile(key.Data["FilePath"].Value) + if err != nil { + logger.Log.Warningf("[ERROR] - Error reading HTML file: %s", err) + } else { + key.UpdateHits() + msg := fmt.Sprintf("[HTTPKEY:ON] - Responding with active HTTP Key '%s'", name) + logger.Log.Noticef(msg) + if key.SendAlerts { + logger.Alerts.SendAlerts(msg) + } + w.Write(fileBytes) + return + } + } else { + key.UpdateHits() + msg := fmt.Sprintf("[HTTPKEY:OFF] - Access attempt for inactive HTTP Key '%s'", name) + logger.Log.Warningf(msg) + if key.SendAlerts { + logger.Alerts.SendAlerts(msg) + } + } + } + } + w.Write(h.getDefaultPage()) +} + +// getDefaultPage returns the default page bytes or '404 Not Found' +func (h *HttpServer) getDefaultPage() []byte { + if h.State["DefaultPage"].Value != "" { + fileBytes, err := ReadFile(h.State["DefaultPage"].Value) + if err == nil { + return fileBytes + } + } + return []byte("404 Not Found") +} + +// cacheHTTPHeaders adds some headers to prevent caching on the user side +func (h *HttpServer) cacheHTTPHeaders(w http.ResponseWriter) { + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") +} + +// parseProxyHeaders helps keyserver work with redirectors +func parseProxyHeaders(r *http.Request) string { + hdr := r.Header.Get("X-Forwarded-For") + if hdr == "" { + return strings.Split(r.RemoteAddr, ":")[0] + } + return hdr + " (P)" +} + +// +// DNS Handling +// + +// ServeDNS handles the DNS queries +func (d *DnsServer) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { + m := new(dns.Msg) + m.SetReply(r) + + for _, q := range r.Question { + switch q.Qtype { + case dns.TypeA: + logger.Log.Infof("[DNS] - Received A query for %s", q.Name) + resp, ttl, keyName := d.getActiveDNSKeys(&q) + if resp != "" { + ipResp := net.ParseIP(resp) + if ipResp != nil { + d.AppendResult(q, m, &dns.A{A: ipResp}, d.getTTL(ttl)) + w.WriteMsg(m) + return + } + logger.Log.Warningf("[ERROR] - DNS Key '%s' response is not a valid IP, no response returned", keyName) + } + m.SetRcode(r, 3) // 3 - NXDomain - Non-Existent Domain + case dns.TypeTXT: + logger.Log.Infof("[DNS] - Received TXT query for %s", q.Name) + resp, ttl, _ := d.getActiveDNSKeys(&q) + if resp != "" { + d.AppendResult(q, m, &dns.TXT{Txt: []string{resp}}, d.getTTL(ttl)) + w.WriteMsg(m) + return + } + m.SetRcode(r, 3) // 3 - NXDomain - Non-Existent Domain + } + } + w.WriteMsg(m) +} + +// getActiveDNSKeys is leveraged by ServeDNS to get any active key responses back +// returns: DNS response, TTL, and key name +func (d *DnsServer) getActiveDNSKeys(q *dns.Question) (string, string, string) { + hostname := strings.Split(q.Name, ".")[0] + // loop through all keys and see if any record and hostname matches + for name, key := range d.Keys { + if hostname == key.Data["Hostname"].Value && q.Qtype == recordStringToUint(key.Data["RecordType"].Value) { + // IsActive() will consider both manually setting the key and constraints + if active, _ := key.IsActive(nil, q); active { + key.UpdateHits() + msg := fmt.Sprintf("[DNSKEY:ON] - Responding with active DNS Key '%s'", name) + logger.Log.Noticef(msg) + if key.SendAlerts { + logger.Alerts.SendAlerts(msg) + } + return key.Data["Response"].Value, key.Data["TTL"].Value, name + } else { + key.UpdateHits() + msg := fmt.Sprintf("[DNSKEY:OFF] - Access attempt for inactive DNS Key '%s'", name) + logger.Log.Warningf(msg) + if key.SendAlerts { + logger.Alerts.SendAlerts(msg) + } + } + } + } + return "", "", "" +} + +// AppendResult prepares response for ServeDNS +// Taken directly from OJ's code +func (is *DnsServer) AppendResult(q dns.Question, m *dns.Msg, rr dns.RR, ttl uint) { + hdr := dns.RR_Header{Name: q.Name, Class: q.Qclass, Ttl: uint32(ttl)} + + if rrS, ok := rr.(*dns.A); ok { + hdr.Rrtype = dns.TypeA + rrS.Hdr = hdr + } else if rrS, ok := rr.(*dns.CNAME); ok { + hdr.Rrtype = dns.TypeCNAME + rrS.Hdr = hdr + } else if rrS, ok := rr.(*dns.NS); ok { + hdr.Rrtype = dns.TypeNS + rrS.Hdr = hdr + } else if rrS, ok := rr.(*dns.TXT); ok { + hdr.Rrtype = dns.TypeTXT + rrS.Hdr = hdr + } + + if q.Qtype == dns.TypeANY || q.Qtype == rr.Header().Rrtype { + m.Answer = append(m.Answer, rr) + } else { + m.Extra = append(m.Extra, rr) + } + +} + +func recordStringToUint(record string) uint16 { + switch record { + case "TXT": + return dns.TypeTXT + case "A": + return dns.TypeA + default: + return dns.TypeNone + } +} + +// If theres a TTL for the key, return that +func (d *DnsServer) getTTL(value string) uint { + if value != "" { + ttl, err := strconv.ParseUint(value, 10, 32) + if err == nil { + return uint(ttl) + } + } + return d.DefaultTTL +} diff --git a/servers/keys.go b/servers/keys.go new file mode 100644 index 0000000..68b2e64 --- /dev/null +++ b/servers/keys.go @@ -0,0 +1,457 @@ +package servers + +import ( + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "errors" + "io/ioutil" + "net/http" + "regexp" + "sort" + "strconv" + "strings" + "time" + + "github.com/miekg/dns" +) + +// Key contains attributes that fit both Http and Dns keys +type Key struct { + Type string + On bool + Disabled bool + SendAlerts bool + HitCounter map[string]int + LastHit string + Data map[string]*KeyData + Constraints map[string]*KeyConstraint + Hashes map[string]string +} + +type KeyData struct { + Description string + Value string +} + +type KeyConstraint struct { + Description string + Constraint string + ConstraintRegex *regexp.Regexp + HttpValidator func(constraint string, r *http.Request) bool + DnsValidator func(constraint string, q *dns.Question) bool +} + +// +// HTTP Key +// + +func HttpKeyData() map[string]*KeyData { + data := make(map[string]*KeyData) + + data["FilePath"] = &KeyData{ + Description: "The path to the file to serve (html will be used as key)", + Value: "wwwroot/file.html", + } + + data["URL"] = &KeyData{ + Description: "The URL of the HTTP request", + Value: "/content/file.html", + } + + return data +} + +// GetHttpKeyConstraints returns all possible key constraints for an HttpKey +func (k *Key) GetHttpKeyConstraints() map[string]*KeyConstraint { + constraints := make(map[string]*KeyConstraint) + + constraints["Time"] = &KeyConstraint{ + Description: "Turn on key within timeframe by minutes: (00:00-23:59)", + Constraint: "", + ConstraintRegex: regexp.MustCompile("^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]-([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$"), + HttpValidator: k.TimeHttpConstraint, + } + + constraints["HitLimit"] = &KeyConstraint{ + Description: "Turn off the key after a certain number of hits are received", + Constraint: "", + ConstraintRegex: regexp.MustCompile("^[0-9]+$"), + HttpValidator: k.HitLimitHttpConstraint, + } + + constraints["HitMax"] = &KeyConstraint{ + Description: "Turn on the key after a certain number of hits are received", + Constraint: "", + ConstraintRegex: regexp.MustCompile("^[0-9]+$"), + HttpValidator: k.HitMaxHttpConstraint, + } + + return constraints +} + +// TimeHttpConstraint is a key constraint that returns true if the current time falls +// within a specified timeframe. http.Request data not needed as we just want the current time. +func (k *Key) TimeHttpConstraint(constraint string, r *http.Request) bool { + return timeConstraint(constraint) +} + +// HitLimitHttpConstraint is a key constraint that returns true if the number of hits +// is below the supplied limit +func (k *Key) HitLimitHttpConstraint(constraint string, r *http.Request) bool { + limit, err := strconv.Atoi(constraint) + if err == nil { + if k.GetHits() < limit { + return true + } + } + return false +} + +// HitMaxHttpConstraint is a key constraint that returns true if the number of hits +// is above the supplied value +func (k *Key) HitMaxHttpConstraint(constraint string, r *http.Request) bool { + value, err := strconv.Atoi(constraint) + if err == nil { + if k.GetHits() > value { + return true + } + } + return false +} + +// UserAgentConstraint is a key constraint that returns true if the current time falls +// within a specified timeframe. requestData is null as the data we want is the current time. +func (k *Key) UserAgentHttpConstraint(constraint string, r *http.Request) bool { + if r == nil { + return false + } + if constraint == r.Header.Get("User-Agent") { + return true + } + return false +} + +// AddKey does the fun stuff, takes in the data generates the hasehs and adds +// it to the end of the Keys slice within the HttpServer +func (h *HttpServer) AddKey(k *Key, name string) error { + if strings.Contains(name, " ") { + return errors.New("Key name contains spaces") + } + if _, exists := h.Keys[name]; exists { + return errors.New("Key name already exists!") + } + + if err := validateKeyConstraints(k.Constraints); err != nil { + return err + } + + fileContents, err := ReadFile(k.Data["FilePath"].Value) + if err != nil { + return err + } + k.Hashes = BuildKey(string(fileContents)) + + h.Keys[name] = k + return nil +} + +// +// DNS Key +// + +func DnsKeyData() map[string]*KeyData { + data := make(map[string]*KeyData) + + data["Hostname"] = &KeyData{ + Description: "The hostname for the DNS request, do not include FQDN", + Value: "mail", + } + + data["RecordType"] = &KeyData{ + Description: "The record type: A or TXT", + Value: "TXT", + } + + data["Response"] = &KeyData{ + Description: "The response to send back for the request.", + Value: "", + } + + data["TTL"] = &KeyData{ + Description: "The TTL for the DNS response (in seconds). Important to keep smaller than the delay if using retries.", + Value: "180", + } + + return data +} + +func (k *Key) GetDnsKeyConstraints() map[string]*KeyConstraint { + constraints := make(map[string]*KeyConstraint) + + constraints["Time"] = &KeyConstraint{ + Description: "Turn on key within timeframe by minutes: (00:00-23:59)", + Constraint: "", + ConstraintRegex: regexp.MustCompile("^([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]-([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$"), + DnsValidator: k.TimeDnsConstraint, + } + + constraints["HitLimit"] = &KeyConstraint{ + Description: "Turn off the key after a certain number of hits are received", + Constraint: "", + ConstraintRegex: regexp.MustCompile("^[0-9]+$"), + DnsValidator: k.HitLimitDnsConstraint, + } + + constraints["HitMax"] = &KeyConstraint{ + Description: "Turn on the key after a certain number of hits are received", + Constraint: "", + ConstraintRegex: regexp.MustCompile("^[0-9]+$"), + DnsValidator: k.HitMaxDnsConstraint, + } + + return constraints + +} + +// TimeConstraint is a key constraint that returns true if the current time falls +// within a specified timeframe. DNS request data not needed as we just want the current time. +func (k *Key) TimeDnsConstraint(constraint string, q *dns.Question) bool { + return timeConstraint(constraint) +} + +// HitLimitDnsConstraint is a key constraint that returns true if the number of hits +// is below the supplied limit +func (k *Key) HitLimitDnsConstraint(constraint string, q *dns.Question) bool { + limit, err := strconv.Atoi(constraint) + if err == nil { + if k.GetHits() < limit { + return true + } + } + return false +} + +// HitMaxDnsConstraint is a key constraint that returns true if the number of hits +// is above the supplied value +func (k *Key) HitMaxDnsConstraint(constraint string, q *dns.Question) bool { + value, err := strconv.Atoi(constraint) + if err == nil { + if k.GetHits() > value { + return true + } + } + return false +} + +// AddKey does the fun stuff, takes in the data generates the hasehs and adds +// it to the end of the Keys slice within the HttpServer +func (d *DnsServer) AddKey(k *Key, name string) error { + if strings.Contains(name, " ") { + return errors.New("Key name contains spaces") + } + if _, exists := d.Keys[name]; exists { + return errors.New("Key name already exists!") + } + + if err := validateKeyConstraints(k.Constraints); err != nil { + return err + } + + k.Hashes = BuildKey(k.Data["Response"].Value) + + // remove unused constraints + for name, _ := range k.Constraints { + if k.Constraints[name].Constraint == "" { + delete(k.Constraints, name) + } + } + d.Keys[name] = k + return nil +} + +// +// Key functions for HTTP and DNS +// + +// IsActive determines whether a key is active for the HttpServer +// The string returned is the "reason" the key is active or inactive, manually turned +// on or due to a constraint +func (k *Key) IsActive(r *http.Request, q *dns.Question) (bool, string) { + if k.Disabled { + return false, "disabled" + } + + var reasons []string + var active bool + if k.On { + active = true + reasons = append(reasons, "Manual") + } + + for name, _ := range k.Constraints { + if k.Type == "http" { + if k.Constraints[name].HttpValidator(k.Constraints[name].Constraint, r) { + active = true + reasons = append(reasons, name) + } + } else if k.Type == "dns" { + if k.Constraints[name].DnsValidator(k.Constraints[name].Constraint, q) { + active = true + reasons = append(reasons, name) + } + + } + } + + return active, strings.Join(reasons, ", ") +} + +// timeConstraint is handled by both DNS and HTTP TimeConstraint methods +func timeConstraint(constraint string) bool { + layout := "15:04" + startTime, err := time.Parse(layout, strings.Split(constraint, "-")[0]) + if err != nil { + return false + } + endTime, err := time.Parse(layout, strings.Split(constraint, "-")[1]) + if err != nil { + return false + } + nowStr := time.Now().Format("15:04") + nowTime, err := time.Parse(layout, nowStr) + if err != nil { + return false + } + // This allows us to be inclusive of the times + startTime = startTime.Add(-(time.Millisecond * 1)) + endTime = endTime.Add(time.Millisecond * 1) + + if nowTime.After(startTime) && nowTime.Before(endTime) { + return true + } + return false +} + +// +// Hashing stuff +// + +// BuildKey takes the string data and generates all supported hashes +// A map with the hash type as key and hash as value is returned +func BuildKey(s string) map[string]string { + return map[string]string{ + "sha512": GenerateSHA512(s), + //"sha256": GenerateSHA256(s), - holding off on supporting multiple hashing types for now + } +} + +// GenerateSHA512 takes a string, generates a SHA512 hash +// and sends back as hex string +func GenerateSHA512(s string) string { + sha := sha512.New() + sha.Write([]byte(s)) + + return hex.EncodeToString(sha.Sum(nil)) +} + +// GenerateSHA256 takes a string, generates a SHA256 hash +// and sends back as hex string +func GenerateSHA256(s string) string { + sha := sha256.New() + sha.Write([]byte(s)) + + return hex.EncodeToString(sha.Sum(nil)) +} + +// +// Helpers +// + +// GetHits returns the hit counter but ensures theres an entry for +// the current day. +func (k *Key) GetHits() int { + today := GetToday() + if _, exists := k.HitCounter[today]; !exists { + k.HitCounter[today] = 0 + } + return k.HitCounter[today] + +} + +// UpdateHits updates the HitCounter for the current day +func (k *Key) UpdateHits() { + today := GetToday() + if _, exists := k.HitCounter[today]; !exists { + k.HitCounter[today] = 0 + } + k.HitCounter[today]++ + k.LastHit = time.Now().Format("01/02/2006 15:04:05") +} + +// ClearHits sets the current day to 0 hits +func (k *Key) ClearHits() { + today := GetToday() + if _, exists := k.HitCounter[today]; !exists { + k.HitCounter[today] = 0 + } + k.HitCounter[today] = 0 +} + +func GetToday() string { + return time.Now().Format("01/02/2006") +} + +// ReadFile returns the contents as a []byte +func ReadFile(path string) ([]byte, error) { + fileBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + return fileBytes, nil +} + +// AlphabetizeKeyData takes in a map of KeyData and returns +// the keys/names in alphabetical order. +func AlphabetizeKeyData(keyData map[string]*KeyData) []string { + // Get map into alphabetical order + keys := make([]string, len(keyData)) + i := 0 + // fill temp array with keys of mis.Items + for k := range keyData { + keys[i] = k + i++ + } + sort.Strings(keys) + + return keys +} + +// AlphabetizeConstraints takes in a map of KeyData and returns +// the keys/names in alphabetical order. +func AlphabetizeConstraints(constraints map[string]*KeyConstraint) []string { + // Get map into alphabetical order + keys := make([]string, len(constraints)) + i := 0 + // fill temp array with keys of mis.Items + for k := range constraints { + keys[i] = k + i++ + } + sort.Strings(keys) + + return keys +} + +// validateKeyConstraints loops through all key constraints, and if value is not empty, +// ensures the value matches the constraint's regex. This is used when attempting to add +// a key. If one fails, we just return that error, for now. +func validateKeyConstraints(constraints map[string]*KeyConstraint) error { + for name, kc := range constraints { + if kc.Constraint != "" && !kc.ConstraintRegex.MatchString(kc.Constraint) { + e := name + " value is not valid" + return errors.New("Key constraint error, " + e) + } + } + return nil +} diff --git a/servers/servers.go b/servers/servers.go new file mode 100644 index 0000000..bfae4cd --- /dev/null +++ b/servers/servers.go @@ -0,0 +1,204 @@ +package servers + +// All the DNS server code came from OJ Reeves (@TheColonial) +// See https://www.youtube.com/watch?v=FeH2Yrw68f8 + +import ( + "net/http" + "sort" + + "github.com/miekg/dns" +) + +// +// Structs +// + +// ServerSetting for various settings for DNS and HTTP servers +type ServerSetting struct { + Value string + Default string + Required bool + Help string +} + +// HttpServer struct, uses following map keys for modifiable settings +// "Listen": listening IP address +// "Port": listening port +// "DefaultPage": default page returned with no key matchings +// "ServerHeader": option HTTP response 'Server' header +type HttpServer struct { + Server *http.Server + State map[string]*ServerSetting + Keys map[string]*Key + Running bool +} + +// DnsServer struct, uses following map keys for modifiable settings +// "Listen": listening IP address +// "Domain": the root level domain name we're authoratative over +// "DefaultTTL": default Time To Live (TTL) for DNS responses +type DnsServer struct { + State map[string]*ServerSetting + Server *dns.Server + Keys map[string]*Key + DefaultTTL uint + Running bool + SendingKey bool +} + +// +// HTTP functions +// + +// GetHttpServer returns a starting point for the HttpServer and +// HttpState structs for use throughout keyserver +func GetHttpServer() *HttpServer { + + state := make(map[string]*ServerSetting) + + state["Listen"] = &ServerSetting{ + Value: "127.0.0.1", + Default: "127.0.0.1", + Required: true, + Help: "The listening IP address. Requires root/admin for 0.0.0.0.", + } + + state["Port"] = &ServerSetting{ + Value: "8080", + Default: "8080", + Required: true, + Help: "The port to listen on. Requires root/admin for ports < 1024.", + } + + state["CertPath"] = &ServerSetting{ + Value: "", + Default: "", + Required: false, + Help: "Certificate to run an HTTPS server.", + } + + state["KeyPath"] = &ServerSetting{ + Value: "", + Default: "", + Required: false, + Help: "Private key to run an HTTPS server.", + } + + state["DefaultPage"] = &ServerSetting{ + Value: "wwwroot/error.html", + Default: "", + Required: false, + Help: "The default page to send for non-key requests. If empty, '404 Not Found' will be returned.", + } + + return &HttpServer{ + State: state, + Running: false, + Keys: make(map[string]*Key), + } +} + +// StartHTTP is the exported function to call and get +// the HTTP server running. +func (h *HttpServer) StartHTTP() { + mux := http.NewServeMux() + mux.Handle("/", h) + + addr := h.State["Listen"].Value + ":" + h.State["Port"].Value + // see net/http docs, this is where to set TLS up as well + h.Server = &http.Server{Addr: addr, Handler: mux} + h.Running = true + + go func() { + if err := h.Server.ListenAndServe(); err != nil { + h.Running = false + } + }() +} + +// StartHTTPS is the exported function to call and get +// the HTTPS/TLS server running. +func (h *HttpServer) StartHTTPS() { + mux := http.NewServeMux() + mux.Handle("/", h) + + addr := h.State["Listen"].Value + ":" + h.State["Port"].Value + h.Server = &http.Server{Addr: addr, Handler: mux} + h.Running = true + go func() { + if err := h.Server.ListenAndServeTLS(h.State["CertPath"].Value, h.State["CertPath"].Value); err != nil { + h.Running = false + } + }() +} + +// GetDnsServer returns a starting point for the DnsServer and +// DnsState structs for use throughout keyserver +func GetDnsServer() *DnsServer { + + state := make(map[string]*ServerSetting) + + state["Listen"] = &ServerSetting{ + Value: "127.0.0.1", + Default: "127.0.0.1", + Required: true, + Help: "The listening IP address. Requires root/admin for 0.0.0.0.", + } + + state["Port"] = &ServerSetting{ + Value: "5333", + Default: "5333", + Required: true, + Help: "The port to listen on. Requires root/admin for ports < 1024.", + } + + state["Domain"] = &ServerSetting{ + Value: "", + Default: "", + Required: true, + Help: "The root domain name for the nameserver. Example: domain.com", + } + + state["DefaultTTL"] = &ServerSetting{ + Value: "10800", + Default: "10800", + Required: true, + Help: "The default TTL response for DNS queries", + } + + return &DnsServer{ + DefaultTTL: 10800, + State: state, + Keys: make(map[string]*Key), + Running: false, + } +} + +// StartDNS starts the DNS server in the background +func (d *DnsServer) StartDNS() { + addr := d.State["Listen"].Value + ":" + d.State["Port"].Value + d.Server = &dns.Server{Addr: addr, Net: "udp", Handler: d} + d.Running = true + go func() { + if err := d.Server.ListenAndServe(); err != nil { + d.Running = false + } + }() +} + +// AlphabetizeSettings takes in a map of ServerSettings and returns +// the keys/names in alphabetical order. +func AlphabetizeSettings(settings map[string]*ServerSetting) []string { + // Get map into alphabetical order + keys := make([]string, len(settings)) + i := 0 + // fill temp array with keys of mis.Items + for k := range settings { + keys[i] = k + i++ + } + sort.Strings(keys) + + return keys +} diff --git a/wwwroot/file.html b/wwwroot/file.html new file mode 100644 index 0000000..5037247 --- /dev/null +++ b/wwwroot/file.html @@ -0,0 +1,6 @@ + + + +

It Works!

+ +