From 954073b8276a2742739263491c8ecca61ddc6125 Mon Sep 17 00:00:00 2001 From: Leon Fernandez Date: Mon, 28 Oct 2024 10:05:28 +0100 Subject: [PATCH 1/2] Re-usable CLI commands --- cmd/api.go | 24 ++ cmd/bump.go | 35 ++ cmd/dawg.go | 203 +++++++++ cmd/debug.go | 432 +++++++++++++++++++ cmd/keyupload.go | 154 +++++++ cmd/mqtt.go | 1052 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/pop.go | 160 +++++++ cmd/rpz.go | 138 ++++++ cmd/slogger.go | 218 ++++++++++ globals.go | 5 + 10 files changed, 2421 insertions(+) create mode 100644 cmd/api.go create mode 100644 cmd/bump.go create mode 100644 cmd/dawg.go create mode 100644 cmd/debug.go create mode 100644 cmd/keyupload.go create mode 100644 cmd/mqtt.go create mode 100644 cmd/pop.go create mode 100644 cmd/rpz.go create mode 100644 cmd/slogger.go diff --git a/cmd/api.go b/cmd/api.go new file mode 100644 index 0000000..062d105 --- /dev/null +++ b/cmd/api.go @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se + */ + +package cmd + +import ( + "log" + + "github.com/spf13/cobra" + "github.com/dnstapir/tapir" +) + +var ApiCmd = &cobra.Command{ + Use: "api", + Short: "request a TAPIR-POP api summary", + Long: `Query TAPIR-POP for the provided API endpoints and print that out in a (hopefully) comprehensible fashion.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + log.Fatal("api must have no arguments") + } + tapir.GlobalCF.Api.ShowApi() + }, +} diff --git a/cmd/bump.go b/cmd/bump.go new file mode 100644 index 0000000..100dea2 --- /dev/null +++ b/cmd/bump.go @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se + */ + +package cmd + +import ( + "fmt" + + "github.com/dnstapir/tapir" + "github.com/miekg/dns" + "github.com/spf13/cobra" +) + +var BumpCmd = &cobra.Command{ + Use: "bump", + Short: "Instruct TAPIR-POP to bump the SOA serial of the RPZ zone", + Run: func(cmd *cobra.Command, args []string) { + resp := SendCommandCmd(tapir.CommandPost{ + Command: "bump", + Zone: dns.Fqdn(tapir.GlobalCF.Zone), + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + }, +} + +func init() { + BumpCmd.Flags().StringVarP(&tapir.GlobalCF.Zone, "zone", "z", "", "Zone name") +} + + diff --git a/cmd/dawg.go b/cmd/dawg.go new file mode 100644 index 0000000..607197c --- /dev/null +++ b/cmd/dawg.go @@ -0,0 +1,203 @@ +/* + * Copyright (c) Johan Stenstam, johan.stenstam@internetstiftelsen.se + */ + +package cmd + +import ( + "errors" + "fmt" + "os" + + "github.com/dnstapir/tapir" + "github.com/miekg/dns" + "github.com/smhanov/dawg" + "github.com/spf13/cobra" +) + +var srcformat, srcfile, dawgfile, dawgname string + +var DawgCmd = &cobra.Command{ + Use: "dawg", + Short: "Generate or interact with data stored in a DAWG file; only useable via sub-commands", +} + +var dawgCompileCmd = &cobra.Command{ + Use: "compile", + Short: "Compile a new DAWG file from either a text or a CSV source file", + Run: func(cmd *cobra.Command, args []string) { + if srcfile == "" { + fmt.Printf("Error: source file not specified.\n") + os.Exit(1) + } + + if dawgfile == "" { + fmt.Printf("Error: outfile not specified.\n") + os.Exit(1) + } + + _, err := os.Stat(srcfile) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + fmt.Printf("Error: source file \"%s\" does not exist.\n", srcfile) + os.Exit(1) + } else { + fmt.Printf("Error: %v\n", err) + } + } + + switch srcformat { + case "csv": + CompileDawgFromCSV(srcfile, dawgfile) + case "text": + CompileDawgFromText(srcfile, dawgfile) + default: + fmt.Printf("Error: format \"%s\" of source file \"%s\" unknown. Must be either \"csv\" or \"text\".\n", + srcformat, srcfile) + os.Exit(1) + } + }, +} + +var dawgLookupCmd = &cobra.Command{ + Use: "lookup", + Short: "Look up a name in an existing DAWG file", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command.`, + Run: func(cmd *cobra.Command, args []string) { + if dawgfile == "" { + fmt.Printf("Error: DAWG file not specified.\n") + os.Exit(1) + } + + if dawgname == "" { + fmt.Printf("Error: Name to look up not specified.\n") + os.Exit(1) + } + + fmt.Printf("Loading DAWG: %s\n", dawgfile) + dawgf, err := dawg.Load(dawgfile) + if err != nil { + fmt.Printf("Error from dawg.Load(%s): %v", dawgfile, err) + os.Exit(1) + } + + dawgname = dns.Fqdn(dawgname) + idx := dawgf.IndexOf(dawgname) + + switch idx { + case -1: + fmt.Printf("Name not found\n") + default: + fmt.Printf("Name %s found, index: %d\n", dawgname, idx) + } + }, +} + +var dawgListCmd = &cobra.Command{ + Use: "list", + Short: "List all names in an existing DAWG file", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command.`, + Run: func(cmd *cobra.Command, args []string) { + if dawgfile == "" { + fmt.Printf("Error: DAWG file not specified.\n") + os.Exit(1) + } + + fmt.Printf("Loading DAWG: %s\n", dawgfile) + dawgf, err := dawg.Load(dawgfile) + if err != nil { + fmt.Printf("Error from dawg.Load(%s): %v", dawgfile, err) + os.Exit(1) + } + + if tapir.GlobalCF.Debug { + fmt.Printf("DAWG has %d nodes, %d added and %d edges.\n", dawgf.NumNodes(), + dawgf.NumAdded(), dawgf.NumEdges()) + } + + count, result := tapir.ListDawg(dawgf) + fmt.Printf("%v\n", result) + if tapir.GlobalCF.Verbose { + fmt.Printf("Enumeration func was called %d times\n", count) + } + }, +} + +func init() { + DawgCmd.AddCommand(dawgCompileCmd, dawgLookupCmd, dawgListCmd) + + DawgCmd.PersistentFlags().StringVarP(&dawgfile, "dawg", "", "", + "Name of DAWG file, must end in \".dawg\"") + dawgCompileCmd.Flags().StringVarP(&srcformat, "format", "", "", + "Format of text file, either csv or text") + dawgCompileCmd.Flags().StringVarP(&srcfile, "src", "", "", + "Name of source text file") + // dawgCompileCmd.Flags().StringVarP(&outfile, "outfile", "", "", + // "Name of outfile, must end in \".dawg\"") + // dawgLookupCmd.Flags().StringVarP(&dawgfile, "dawg", "", "", + // "Name of DAWG file") + dawgLookupCmd.Flags().StringVarP(&dawgname, "name", "", "", + "Name to look up") + dawgListCmd.Flags().StringVarP(&dawgname, "name", "", "", + "Name to find prefixes of") +} + +func CompileDawgFromCSV(srcfile, outfile string) { + ofd, err := os.Create(outfile) + if err != nil { + fmt.Printf("Error creating \"%s\": %v\n", outfile, err) + os.Exit(1) + } + + sortednames, err := tapir.ParseCSV(srcfile, map[string]tapir.TapirName{}, false) + if err != nil { + fmt.Printf("Error parsing CSV source \"%s\": %v\n", srcfile, err) + os.Exit(1) + } + + if tapir.GlobalCF.Debug { + fmt.Print("Sorted list of names:\n") + for _, n := range sortednames { + fmt.Printf("%s\n", n) + } + } + + err = tapir.CreateDawg(sortednames, outfile) + if err != nil { + fmt.Printf("Error creating DAWG \"%s\" from sorted list of names: %v\n", outfile, err) + os.Exit(1) + } + + ofd.Close() +} + +func CompileDawgFromText(srcfile, outfile string) { + ofd, err := os.Create(outfile) + if err != nil { + fmt.Printf("Error creating \"%s\": %v\n", outfile, err) + os.Exit(1) + } + + sortednames, err := tapir.ParseText(srcfile, map[string]tapir.TapirName{}, false) + if err != nil { + fmt.Printf("Error parsing text source \"%s\": %v\n", srcfile, err) + os.Exit(1) + } + + if tapir.GlobalCF.Debug { + fmt.Print("Sorted list of names:\n") + for _, n := range sortednames { + fmt.Printf("%s\n", n) + } + } + + err = tapir.CreateDawg(sortednames, outfile) + if err != nil { + fmt.Printf("Error creating DAWG \"%s\" from sorted list of names: %v\n", outfile, err) + os.Exit(1) + } + + ofd.Close() +} diff --git a/cmd/debug.go b/cmd/debug.go new file mode 100644 index 0000000..6b3d3ed --- /dev/null +++ b/cmd/debug.go @@ -0,0 +1,432 @@ +/* + * Copyright (c) 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se + */ +package cmd + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/dnstapir/tapir" + "github.com/ryanuber/columnize" + + // "github.com/ryanuber/columnize" + "github.com/miekg/dns" + //"github.com/santhosh-tekuri/jsonschema/v2" + "github.com/invopop/jsonschema" + "github.com/spf13/cobra" +) + +var DebugCmd = &cobra.Command{ + Use: "debug", + Short: "Prefix command to various debug tools; do not use in production", +} + +var debugZoneDataCmd = &cobra.Command{ + Use: "zonedata", + Short: "Return the ZoneData struct for the specified zone from server", + Long: `Return the ZoneData struct from server + (mostly useful with -d JSON prettyprinter).`, + Run: func(cmd *cobra.Command, args []string) { + resp := SendDebugCmd(tapir.DebugPost{ + Command: "zonedata", + Zone: dns.Fqdn(tapir.GlobalCF.Zone), + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + // zd := resp.ZoneData + + fmt.Printf("Received %d bytes of data\n", len(resp.Msg)) + // fmt.Printf("Zone %s: RRs: %d Owners: %d\n", tapir.GlobalCF.Zone, + // len(zd.RRs), len(zd.Owners)) + if resp.Msg != "" { + fmt.Printf("%s\n", resp.Msg) + } + }, +} + +var debugColourlistsCmd = &cobra.Command{ + Use: "colourlists", + Short: "Return the white/black/greylists from the current data structures", + Run: func(cmd *cobra.Command, args []string) { + resp := SendDebugCmd(tapir.DebugPost{ + Command: "colourlists", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + fmtstring := "%-35s|%-20s|%-10s|%-10s\n" + + // fmt.Printf("Received %d bytes of data\n", len(resp.Msg)) + + // print the column headings + fmt.Printf(fmtstring, "Domain", "Source", "Src Fmt", "Colour") + fmt.Println(strings.Repeat("-", 78)) // A nice ruler over the data rows + + for _, l := range resp.Lists["whitelist"] { + for _, n := range l.Names { + fmt.Printf(fmtstring, n.Name, l.Name, "-", "white") + } + } + for _, l := range resp.Lists["blacklist"] { + for _, n := range l.Names { + fmt.Printf(fmtstring, n.Name, l.Name, "-", "black") + } + } + for _, l := range resp.Lists["greylist"] { + for _, n := range l.Names { + fmt.Printf(fmtstring, n.Name, l.Name, l.SrcFormat, "grey") + } + } + }, +} + +var debugGenRpzCmd = &cobra.Command{ + Use: "genrpz", + Short: "Return the white/black/greylists from the current data structures", + Run: func(cmd *cobra.Command, args []string) { + resp := SendDebugCmd(tapir.DebugPost{ + Command: "gen-output", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("Received %d bytes of data\n", len(resp.Msg)) + + fmt.Printf("black count=%d: %v\n", resp.BlacklistedNames) + fmt.Printf("grey count=%d: %v\n", resp.GreylistedNames) + // fmt.Printf("count=%d: %v\n", res.RpzOutput) + for _, tn := range resp.RpzOutput { + fmt.Printf("%s\n", (*tn.RR).String()) + } + }, +} + +var debugMqttStatsCmd = &cobra.Command{ + Use: "mqtt-stats", + Short: "Return the MQTT stats counters from the MQTT Engine", + Run: func(cmd *cobra.Command, args []string) { + resp := SendDebugCmd(tapir.DebugPost{ + Command: "mqtt-stats", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + if resp.Msg != "" { + fmt.Printf("%s\n", resp.Msg) + } + + var out = []string{"MQTT Topic|Msgs|Last MQTT Message|Time since last msg"} + for topic, count := range resp.MqttStats.MsgCounters { + t := resp.MqttStats.MsgTimeStamps[topic] + out = append(out, fmt.Sprintf("%s|%d|%s|%v\n", topic, count, t.Format(timelayout), time.Since(t).Round(time.Second))) + } + fmt.Printf("%s\n", columnize.SimpleFormat(out)) + }, +} + +var debugReaperStatsCmd = &cobra.Command{ + Use: "reaper-stats", + Short: "Return the reaper status for all known greylists", + Run: func(cmd *cobra.Command, args []string) { + resp := SendDebugCmd(tapir.DebugPost{ + Command: "reaper-stats", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + if resp.Msg != "" { + fmt.Printf("%s\n", resp.Msg) + } + + for greylist, data := range resp.ReaperStats { + if len(data) == 0 { + fmt.Printf("No reaper data for greylist %s\n", greylist) + continue + } + fmt.Printf("From greylist %s at the following times these names will be deleted:\n", greylist) + out := []string{"Time|Count|Names"} + for t, d := range data { + out = append(out, fmt.Sprintf("%s|%d|%v", t.Format(timelayout), len(d), d)) + } + fmt.Printf("%s\n", columnize.SimpleFormat(out)) + } + }, +} + +var popcomponent, popstatus string + +var debugUpdatePopStatusCmd = &cobra.Command{ + Use: "update-pop-status", + Short: "Update the status of a TAPIR-POP component, to trigger a status update over MQTT", + Run: func(cmd *cobra.Command, args []string) { + switch popstatus { + case "ok", "warn", "fail": + default: + fmt.Printf("Invalid status: %s\n", popstatus) + os.Exit(1) + } + + resp := SendDebugCmd(tapir.DebugPost{ + Command: "send-status", + Component: popcomponent, + Status: tapir.StringToStatus[popstatus], + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + if resp.Msg != "" { + fmt.Printf("%s\n", resp.Msg) + } + }, +} + +var zonefile string + +var debugSyncZoneCmd = &cobra.Command{ + Use: "synczone", + Short: "A brief description of your command", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("synczone called") + + if tapir.GlobalCF.Zone == "" { + fmt.Printf("Zone name not specified.\n") + os.Exit(1) + } + + if zonefile == "" { + fmt.Printf("Zone file not specified.\n") + os.Exit(1) + } + + zd := tapir.ZoneData{ + ZoneType: 3, // zonetype=3 keeps RRs in a []OwnerData, with an OwnerIndex map[string]int to locate stuff + ZoneName: tapir.GlobalCF.Zone, + Logger: log.Default(), + } + + _, err := zd.ReadZoneFile(zonefile) + if err != nil { + log.Fatalf("ReloadAuthZones: Error from ReadZoneFile(%s): %v", zonefile, err) + } + + // XXX: This will be wrong for zonetype=3 (which we're using) + fmt.Printf("----- zd.BodyRRs: ----\n") + tapir.PrintRRs(zd.BodyRRs) + fmt.Printf("----- zd.RRs (pre-sync): ----\n") + tapir.PrintRRs(zd.RRs) + zd.Sync() + fmt.Printf("----- zd.RRs (post-sync): ----\n") + tapir.PrintRRs(zd.RRs) + zd.Sync() + fmt.Printf("----- zd.RRs (post-sync): ----\n") + tapir.PrintRRs(zd.RRs) + fmt.Printf("----- zd.BodyRRs: ----\n") + tapir.PrintRRs(zd.BodyRRs) + }, +} + +var Listname string + +var debugGreylistStatusCmd = &cobra.Command{ + Use: "greylist-status", + Short: "Return the greylist status for all greylists", + Run: func(cmd *cobra.Command, args []string) { + status, buf, err := tapir.GlobalCF.Api.RequestNG(http.MethodPost, "/bootstrap", tapir.BootstrapPost{ + Command: "greylist-status", + }, false) + if err != nil { + fmt.Printf("Error from RequestNG: %v\n", err) + return + } + + if status != http.StatusOK { + fmt.Printf("HTTP Error: %s\n", buf) + return + } + var br tapir.BootstrapResponse + err = json.Unmarshal(buf, &br) + if err != nil { + fmt.Printf("Error decoding bootstrap response as a tapir.BootstrapResponse: %v. Giving up.\n", err) + return + } + if br.Error { + fmt.Printf("Bootstrap Error: %s\n", br.ErrorMsg) + } + if len(br.Msg) != 0 { + fmt.Printf("Bootstrap response: %s\n", br.Msg) + } + out := []string{"Server|Uptime|Topic|Last Msg|Time since last msg"} + + // for topic, count := range br.MsgCounters { + // out = append(out, fmt.Sprintf("%s|%v|%v|%v", topic, count, br.MsgTimeStamps[topic].Format(time.RFC3339), time.Now().Sub(br.MsgTimeStamps[topic]))) + // } + + for topic, topicdata := range br.TopicData { + // out = append(out, fmt.Sprintf("%s|%v|%s|%s|%s|%d|%s|%d|%s", server, uptime, name, src.Name, topic, topicdata.PubMsgs, topicdata.LatestPub.Format(time.RFC3339), topicdata.SubMsgs, topicdata.LatestSub.Format(time.RFC3339))) + out = append(out, fmt.Sprintf("%s|%d|%s|%d|%s", topic, topicdata.PubMsgs, topicdata.LatestPub.Format(time.RFC3339), topicdata.SubMsgs, topicdata.LatestSub.Format(time.RFC3339))) + } + + fmt.Printf("%s\n", columnize.SimpleFormat(out)) + }, +} + +var debugGenerateSchemaCmd = &cobra.Command{ + Use: "generate-schema", + Short: "Experimental: Generate the JSON schema for the current data structures", + Run: func(cmd *cobra.Command, args []string) { + + reflector := &jsonschema.Reflector{ + DoNotReference: true, + } + schema := reflector.Reflect(&tapir.WBGlist{}) // WBGlist is only used as a example. + schemaJson, err := schema.MarshalJSON() + if err != nil { + fmt.Printf("Error marshalling schema: %v\n", err) + os.Exit(1) + } + var prettyJSON bytes.Buffer + + // XXX: This doesn't work. It isn't necessary that the response is JSON. + err = json.Indent(&prettyJSON, schemaJson, "", " ") + if err != nil { + fmt.Printf("Error indenting schema: %v\n", err) + os.Exit(1) + } + fmt.Printf("%v\n", string(prettyJSON.Bytes())) + }, +} + +var debugImportGreylistCmd = &cobra.Command{ + Use: "import-greylist", + Short: "Import the current data for the named greylist from the TEM bootstrap server", + Run: func(cmd *cobra.Command, args []string) { + + if Listname == "" { + fmt.Printf("No greylist name specified, using 'dns-tapir'\n") + Listname = "dns-tapir" + } + + status, buf, err := tapir.GlobalCF.Api.RequestNG(http.MethodPost, "/bootstrap", tapir.BootstrapPost{ + Command: "export-greylist", + ListName: Listname, + Encoding: "gob", + }, false) + if err != nil { + fmt.Printf("Error from RequestNG: %v\n", err) + return + } + + if status != http.StatusOK { + fmt.Printf("HTTP Error: %s\n", buf) + return + } + // if resp.Error { + // fmt.Printf("Error: %s\n", resp.ErrorMsg) + // return + // } + + var greylist tapir.WBGlist + decoder := gob.NewDecoder(bytes.NewReader(buf)) + err = decoder.Decode(&greylist) + if err != nil { + // fmt.Printf("Error decoding greylist data: %v\n", err) + // If decoding the gob failed, perhaps we received a tapir.CommandResponse instead? + var br tapir.BootstrapResponse + err = json.Unmarshal(buf, &br) + if err != nil { + fmt.Printf("Error decoding response either as a GOB blob or as a tapir.CommandResponse: %v. Giving up.\n", err) + return + } + if br.Error { + fmt.Printf("Command Error: %s\n", br.ErrorMsg) + } + if len(br.Msg) != 0 { + fmt.Printf("Command response: %s\n", br.Msg) + } + return + } + + // fmt.Printf("%v\n", greylist) + fmt.Printf("Names present in greylist %s:", Listname) + if len(greylist.Names) == 0 { + fmt.Printf(" None\n") + } else { + fmt.Printf("\n") + out := []string{"Name|Time added|TTL|Tags"} + for _, n := range greylist.Names { + ttl := n.TTL - time.Now().Sub(n.TimeAdded).Round(time.Second) + out = append(out, fmt.Sprintf("%s|%v|%v|%v", n.Name, n.TimeAdded.Format(tapir.TimeLayout), ttl, n.TagMask)) + } + fmt.Printf("%s\n", columnize.SimpleFormat(out)) + } + + fmt.Printf("ReaperData present in greylist %s:", Listname) + if len(greylist.ReaperData) == 0 { + fmt.Printf(" None\n") + } else { + fmt.Printf("\n") + out := []string{"Time|Count|Names"} + for t, d := range greylist.ReaperData { + var names []string + for n := range d { + names = append(names, n) + } + out = append(out, fmt.Sprintf("%s|%d|%v", t.Format(timelayout), len(d), names)) + } + fmt.Printf("%s\n", columnize.SimpleFormat(out)) + } + }, +} + +func init() { + DebugCmd.AddCommand(debugSyncZoneCmd, debugZoneDataCmd, debugColourlistsCmd, debugGenRpzCmd) + DebugCmd.AddCommand(debugMqttStatsCmd, debugReaperStatsCmd) + DebugCmd.AddCommand(debugImportGreylistCmd, debugGreylistStatusCmd) + DebugCmd.AddCommand(debugGenerateSchemaCmd, debugUpdatePopStatusCmd) + + debugUpdatePopStatusCmd.Flags().StringVarP(&popcomponent, "component", "c", "", "Component name") + debugUpdatePopStatusCmd.Flags().StringVarP(&popstatus, "status", "s", "", "Component status (ok, warn, fail)") + + debugImportGreylistCmd.Flags().StringVarP(&Listname, "list", "l", "", "Greylist name") + debugSyncZoneCmd.Flags().StringVarP(&tapir.GlobalCF.Zone, "zone", "z", "", "Zone name") + debugZoneDataCmd.Flags().StringVarP(&tapir.GlobalCF.Zone, "zone", "z", "", "Zone name") + debugSyncZoneCmd.Flags().StringVarP(&zonefile, "file", "f", "", "Zone file") +} + +type DebugResponse struct { + Msg string + Data interface{} + Error bool + ErrorMsg string +} + +func SendDebugCmd(data tapir.DebugPost) tapir.DebugResponse { + _, buf, _ := tapir.GlobalCF.Api.RequestNG(http.MethodPost, "/debug", data, true) + + var dr tapir.DebugResponse + + var pretty bytes.Buffer + err := json.Indent(&pretty, buf, "", " ") + if err != nil { + fmt.Printf("JSON parse error: %v", err) + } + // fmt.Printf("Received %d bytes of data: %v\n", len(buf), pretty.String()) + // os.Exit(1) + + err = json.Unmarshal(buf, &dr) + if err != nil { + log.Fatalf("Error from json.Unmarshal: %v\n", err) + } + return dr +} diff --git a/cmd/keyupload.go b/cmd/keyupload.go new file mode 100644 index 0000000..dae9a6b --- /dev/null +++ b/cmd/keyupload.go @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se + */ +package cmd + +import ( + "crypto/sha256" + "encoding/pem" + "path/filepath" + + "fmt" + "log" + "os" + "time" + + "github.com/dnstapir/tapir" + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jws" + "github.com/spf13/cobra" +) + +var pubkeyfile string + +var KeyUploadCmd = &cobra.Command{ + Use: "keyupload", + Short: "Upload a public key to a TAPIR Core", + Long: `Upload a public key to a TAPIR Core. The key must be in PEM format.`, + Run: func(cmd *cobra.Command, args []string) { + // if len(args) != 1 { + // log.Fatal("keyupload must have exactly one argument: the path to the public key file") + // } + + var statusch = make(chan tapir.ComponentStatusUpdate, 10) + + // If any status updates arrive, print them out + go func() { + for status := range statusch { + fmt.Printf("Status update: %+v\n", status) + } + }() + + certCN, _, clientCert, err := tapir.FetchTapirClientCert(log.Default(), statusch) + if err != nil { + fmt.Printf("Error from FetchTapirClientCert: %v\n", err) + os.Exit(1) + } + + meng, err := tapir.NewMqttEngine("keyupload", mqttclientid, tapir.TapirPub, statusch, log.Default()) // pub, no sub + if err != nil { + fmt.Printf("Error from NewMqttEngine: %v\n", err) + os.Exit(1) + } + + // Start of Selection + if pubkeyfile == "" { + fmt.Println("Error: Public key file not specified") + os.Exit(1) + } + + pubkeyfile = filepath.Clean(pubkeyfile) + pubkeyData, err := os.ReadFile(pubkeyfile) + if err != nil { + fmt.Printf("Error reading public key file %s: %v\n", pubkeyfile, err) + os.Exit(1) + } + + if tapir.GlobalCF.Debug { + fmt.Printf("Public key loaded from %s\n", pubkeyfile) + fmt.Printf("Public key:\n%s\n", string(pubkeyData)) + } + + data := tapir.TapirPubKey{ + Pubkey: string(pubkeyData), + } + + // Create a new struct to send off + var certChainPEM string + for _, cert := range clientCert.Certificate { + certChainPEM += string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert})) + } + + if tapir.GlobalCF.Debug { + fmt.Printf("Client certificate chain:\n%s\n", certChainPEM) + } + + // Sign the data using the client certificate's private key and include the cert chain and key ID in the JWS header + headers := jws.NewHeaders() + headers.Set(jws.X509CertChainKey, clientCert.Certificate) + + // Compute key ID as SHA-256 hash of the client certificate + certBytes := clientCert.Leaf.Raw + hash := sha256.Sum256(certBytes) + kid := fmt.Sprintf("%x", hash) + headers.Set(jws.KeyIDKey, kid) + + // Fix the arguments to jws.Sign + jwsMessage, err := jws.Sign([]byte(data.Pubkey), jws.WithKey(jwa.RS256, clientCert.PrivateKey), jws.WithHeaders(headers)) + if err != nil { + fmt.Printf("Error signing data: %v\n", err) + os.Exit(1) + } + + fmt.Printf("JWS Key ID: %s\n", kid) + fmt.Printf("JWS Message: %s\n", string(jwsMessage)) + + msg := tapir.PubKeyUpload{ + JWSMessage: string(jwsMessage), + ClientCertPEM: certChainPEM, + } + + // mqtttopic = viper.GetString("tapir.keyupload.topic") + mqtttopic, err := tapir.MqttTopic(certCN, "tapir.keyupload.topic") + if err != nil { + fmt.Println("Error: tapir.keyupload.topic not specified in config") + os.Exit(1) + } + fmt.Printf("Using DNS TAPIR keyupload MQTT topic: %s\n", mqtttopic) + // signkey, err := tapir.FetchMqttSigningKey(mqtttopic, viper.GetString("tapir.config.signingkey")) + // if err != nil { + // fmt.Printf("Error fetching MQTT signing key: %v", err) + // os.Exit(1) + // } + meng.PubToTopic(mqtttopic, nil, "struct", false) // XXX: Brr. kludge. + + cmnder, outbox, _, err := meng.StartEngine() + if err != nil { + fmt.Printf("Error from StartEngine(): %v\n", err) + os.Exit(1) + } + + SetupInterruptHandler(cmnder) + + srcname := "foobar" // XXX: Kludge. Should be the EdgeId from the client certificate. + if srcname == "" { + fmt.Println("Error: tapir.config.srcname not specified in config") + os.Exit(1) + } + + outbox <- tapir.MqttPkgOut{ + Type: "raw", + Topic: mqtttopic, + RawData: msg, + } + + fmt.Println("[Waiting 1000 ms to ensure message has been sent]") + // Here we need to hang around for a while to ensure that the message has time to be sent. + time.Sleep(1000 * time.Millisecond) + fmt.Printf("Hopefully the public key upload message has been sent.\n") + }, +} + +func init() { + KeyUploadCmd.Flags().StringVarP(&pubkeyfile, "pubkey", "P", "", "Name of file containing public key to upload") +} diff --git a/cmd/mqtt.go b/cmd/mqtt.go new file mode 100644 index 0000000..59f1ebf --- /dev/null +++ b/cmd/mqtt.go @@ -0,0 +1,1052 @@ +/* + * Copyright (c) 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se + */ + +package cmd + +import ( + "bufio" + "bytes" + "crypto/ecdsa" + "encoding/json" + "net/http" + "path/filepath" + "regexp" + + "fmt" + "io" + "log" + "os" + "os/signal" + "strings" + "sync" + "syscall" + "time" + + "github.com/google/uuid" + + "github.com/dnstapir/tapir" + "github.com/miekg/dns" + "github.com/ryanuber/columnize" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +var mqttclientid, mqtttopic, defaulttopic, mqttgreylist, gcfgfile string + +var mqttfid string +var mqttpub, mqttsub, mqttretain, mqttconfigclear bool + +var MqttCmd = &cobra.Command{ + Use: "mqtt", + Short: "Prefix command, not usable directly", + Long: `Prefix command, not usable directly.`, +} + +var mqttEngineCmd = &cobra.Command{ + Use: "engine", + Short: "Start an MQTT engine that can publish and subscribe to topics", + Long: `Start an MQTT engine that can publish and subscribe to topics. +The engine can be configured to publish to and subscribe from the tapir config, observations and status topics.`, + Run: func(cmd *cobra.Command, args []string) { + var wg sync.WaitGroup + + var statusch = make(chan tapir.ComponentStatusUpdate, 10) + + // If any status updates arrive, print them out + go func() { + for status := range statusch { + fmt.Printf("Status update: %+v\n", status) + } + }() + + certCN, _, _, err := tapir.FetchTapirClientCert(log.Default(), statusch) + if err != nil { + fmt.Printf("Error fetching client certificate: %v", err) + os.Exit(1) + } + + var pubsub uint8 + if mqttpub { + pubsub = pubsub | tapir.TapirPub + } + if mqttsub { + pubsub = pubsub | tapir.TapirSub + } + + meng, err := tapir.NewMqttEngine("engine", mqttclientid, pubsub, statusch, log.Default()) + if err != nil { + fmt.Printf("Error from NewMqttEngine: %v\n", err) + os.Exit(1) + } + + var canPub = true + var canSub = true + var signkey *ecdsa.PrivateKey + var valkey *ecdsa.PublicKey + + var sign, validate bool + + switch mqtttopic { + case "config": + mqtttopic, err = tapir.MqttTopic(certCN, "tapir.config.topic") + if err != nil { + fmt.Printf("Error getting MQTT topic: %v\n", err) + os.Exit(1) + } + signkey, err = tapir.FetchMqttSigningKey(mqtttopic, viper.GetString("tapir.config.signingkey")) + if err != nil { + fmt.Printf("Error fetching MQTT signing key: %s\n", viper.GetString("tapir.config.signingkey")) + canPub = false + } else { + sign = true + } + valkey, err = tapir.FetchMqttValidatorKey(mqtttopic, viper.GetString("tapir.config.validatorkey")) + if err != nil { + fmt.Printf("Error fetching MQTT signing key: %s\n", viper.GetString("tapir.config.validatorkey")) + canSub = false + } else { + validate = true + } + + case "observations": + mqtttopic, err = tapir.MqttTopic(certCN, "tapir.observations.topic") + if err != nil { + fmt.Printf("Error getting MQTT topic: %v\n", err) + os.Exit(1) + } + signkey, err = tapir.FetchMqttSigningKey(mqtttopic, viper.GetString("tapir.observations.signingkey")) + if err != nil { + fmt.Printf("Error fetching MQTT signing key: %s\n", viper.GetString("tapir.observations.signingkey")) + canPub = false + } else { + sign = true + } + valkey, err = tapir.FetchMqttValidatorKey(mqtttopic, viper.GetString("tapir.observations.validatorkey")) + if err != nil { + fmt.Printf("Error fetching MQTT signing key: %s\n", viper.GetString("tapir.observations.validatorkey")) + canSub = false + } else { + validate = true + } + + case "status": + mqtttopic, err = tapir.MqttTopic(certCN, "tapir.status.topic") + if err != nil { + fmt.Printf("Error getting MQTT topic: %v\n", err) + os.Exit(1) + } + signkey, err = tapir.FetchMqttSigningKey(mqtttopic, viper.GetString("tapir.status.signingkey")) + if err != nil { + fmt.Printf("Error fetching MQTT signing key: %s\n", viper.GetString("tapir.status.signingkey")) + canPub = false + } else { + sign = true + } + valkey, err = tapir.FetchMqttValidatorKey(mqtttopic, viper.GetString("tapir.status.validatorkey")) + if err != nil { + fmt.Printf("Error fetching MQTT signing key: %s\n", viper.GetString("tapir.status.validatorkey")) + canSub = false + } else { + validate = true + } + + case "keyupload": + mqtttopic, err = tapir.MqttTopic(certCN, "tapir.keyupload.topic") + if err != nil { + fmt.Printf("Error getting MQTT topic: %v\n", err) + os.Exit(1) + } + canPub = false + canSub = true + validate = false + // signkey, err = tapir.FetchMqttSigningKey(mqtttopic, viper.GetString("tapir.status.signingkey")) + // if err != nil { + // fmt.Printf("Error fetching MQTT signing key: %s\n", viper.GetString("tapir.status.signingkey")) + // canPub = false + // } + // valkey, err = tapir.FetchMqttValidatorKey(mqtttopic, viper.GetString("tapir.status.validatorkey")) + // if err != nil { + // fmt.Printf("Error fetching MQTT signing key: %s\n", viper.GetString("tapir.status.validatorkey")) + // canSub = false + // } + + default: + log.Fatalf("Invalid MQTT topic: %s (must be config or observations)", mqtttopic) + } + + if canPub { + fmt.Printf("Adding pub topic: %s\n", mqtttopic) + // meng.AddTopic(mqtttopic, signkey, valkey) + meng.PubToTopic(mqtttopic, signkey, "struct", sign) // XXX: Brr. kludge. + } + + var subch chan tapir.MqttPkgIn + + if canSub { + fmt.Printf("Adding sub topic: %s\n", mqtttopic) + subch = make(chan tapir.MqttPkgIn, 10) + _, err = meng.SubToTopic(mqtttopic, valkey, subch, "struct", validate) // XXX: Brr. kludge. + if err != nil { + fmt.Printf("Error from SubToTopic: %v\n", err) + os.Exit(1) + } + } + + // cmnder, outbox, inbox, err := meng.StartEngine() + cmnder, outbox, _, err := meng.StartEngine() + if err != nil { + log.Fatalf("Error from StartEngine(): %v", err) + } + + stdin := bufio.NewReader(os.Stdin) + count := 0 + buf := new(bytes.Buffer) + + SetupInterruptHandler(cmnder) + + fmt.Printf("subch: %+v\n", subch) + + if mqttsub { + wg.Add(1) + go SetupSubPrinter(subch) + } + + srcname := viper.GetString("tapir.observations.srcname") + if srcname == "" { + fmt.Printf("Error: tapir.observations.srcname not specified in config") + os.Exit(1) + } + + if mqttpub { + for { + count++ + msg, err := stdin.ReadString('\n') + if err == io.EOF { + os.Exit(0) + } + fmt.Printf("Read: %s", msg) + msg = tapir.Chomp(msg) + if len(msg) == 0 { + fmt.Printf("Empty message ignored.\n") + continue + } + if strings.ToUpper(msg) == "QUIT" { + wg.Done() + break + } + + buf.Reset() + outbox <- tapir.MqttPkgOut{ + Type: "data", + Data: tapir.TapirMsg{ + Msg: msg, + SrcName: srcname, + TimeStamp: time.Now(), + }, + } + } + respch := make(chan tapir.MqttEngineResponse, 2) + meng.CmdChan <- tapir.MqttEngineCmd{Cmd: "stop", Resp: respch} + // var r tapir.MqttEngineResponse + r := <-respch + fmt.Printf("Response from MQTT Engine: %v\n", r) + } + wg.Wait() + }, +} + +var mqttTapirCmd = &cobra.Command{ + Use: "tapir", + Short: "Prefix command only usable via sub-commands", +} + +type ConfigFoo struct { + GlobalConfig tapir.GlobalConfig +} + +var mqttTapirConfigCmd = &cobra.Command{ + Use: "config", + Short: "Send TAPIR-POP global config in TapirMsg form to the tapir config MQTT topic", + Long: `Send TAPIR-POP global config in TapirMsg form to the tapir config MQTT topic. + The -F option is required and specifies the file containing the global config in YAML format. + If -R is specified, will send a retained message, otherwise will send a normal message. + If -C is specified, will clear the retained config message, otherwise will send the new config.`, + Run: func(cmd *cobra.Command, args []string) { + + var statusch = make(chan tapir.ComponentStatusUpdate, 10) + + // If any status updates arrive, print them out + go func() { + for status := range statusch { + fmt.Printf("Status update: %+v\n", status) + } + }() + + certCN, _, _, err := tapir.FetchTapirClientCert(log.Default(), statusch) + if err != nil { + fmt.Printf("Error fetching client certificate: %v", err) + os.Exit(1) + } + + meng, err := tapir.NewMqttEngine("config", mqttclientid, tapir.TapirPub, statusch, log.Default()) // pub, no sub + if err != nil { + fmt.Printf("Error from NewMqttEngine: %v\n", err) + os.Exit(1) + } + + if gcfgfile == "" { + fmt.Println("Error: Global config file not specified") + os.Exit(1) + } + + gcfgfile = filepath.Clean(gcfgfile) + gcfgData, err := os.ReadFile(gcfgfile) + if err != nil { + fmt.Printf("Error reading configuration file %s: %v\n", gcfgfile, err) + os.Exit(1) + } + + var cf ConfigFoo + err = yaml.Unmarshal(gcfgData, &cf) + if err != nil { + fmt.Printf("Error unmarshalling YAML data from file %s: %v\n", gcfgfile, err) + os.Exit(1) + } + + fmt.Printf("Global configuration loaded from %s\n", gcfgfile) + pretty, err := yaml.Marshal(cf.GlobalConfig) + if err != nil { + fmt.Printf("Error marshalling YAML data: %v\n", err) + os.Exit(1) + } + fmt.Printf("Global configuration:\n%s\n", string(pretty)) + + mqtttopic, err := tapir.MqttTopic(certCN, "tapir.config.topic") + if err != nil { + fmt.Println("Error: tapir.config.topic not specified in config") + os.Exit(1) + } + fmt.Printf("Using DNS TAPIR config MQTT topic: %s\n", mqtttopic) + signkey, err := tapir.FetchMqttSigningKey(mqtttopic, viper.GetString("tapir.config.signingkey")) + if err != nil { + fmt.Printf("Error fetching MQTT signing key: %v", err) + os.Exit(1) + } + meng.PubToTopic(mqtttopic, signkey, "struct", true) // XXX: Brr. kludge. + + cmnder, outbox, _, err := meng.StartEngine() + if err != nil { + fmt.Printf("Error from StartEngine(): %v\n", err) + os.Exit(1) + } + + SetupInterruptHandler(cmnder) + + srcname := viper.GetString("tapir.config.srcname") + if srcname == "" { + fmt.Println("Error: tapir.config.srcname not specified in config") + os.Exit(1) + } + + // var tmsg = tapir.TapirMsg{ + // SrcName: srcname, + // Creator: "tapir-cli", + // MsgType: "global-config", + // GlobalConfig: cf.GlobalConfig, + // TimeStamp: time.Now(), + // } + if mqttconfigclear { + // tmsg.Msg = "" + outbox <- tapir.MqttPkgOut{ + Type: "raw", + Topic: mqtttopic, + Retain: true, + RawData: "", + } + } else { + outbox <- tapir.MqttPkgOut{ + Type: "raw", + Topic: mqtttopic, + Retain: mqttretain, + RawData: cf.GlobalConfig, + } + } + + fmt.Println("[Waiting 1000 ms to ensure message has been sent]") + // Here we need to hang around for a while to ensure that the message has time to be sent. + time.Sleep(1000 * time.Millisecond) + fmt.Printf("Hopefully the config message has been sent.\n") + }, +} + +var mqttTapirObservationsCmd = &cobra.Command{ + Use: "observations", + Short: "Interactively create and send observations to the tapir intel MQTT topic (debug tool)", + Long: `Will query for operation (add|del|show|send|set-ttl|list-tags|quit), domain name and tags. +Will end the loop on the operation (or domain name) "QUIT"`, + Run: func(cmd *cobra.Command, args []string) { + + var statusch = make(chan tapir.ComponentStatusUpdate, 10) + + // If any status updates arrive, print them out + go func() { + for status := range statusch { + fmt.Printf("Status update: %+v\n", status) + } + }() + + certCN, _, _, err := tapir.FetchTapirClientCert(log.Default(), statusch) + if err != nil { + fmt.Printf("Error fetching client certificate: %v", err) + os.Exit(1) + } + + meng, err := tapir.NewMqttEngine("observations", mqttclientid, tapir.TapirPub, statusch, log.Default()) // pub, no sub + if err != nil { + fmt.Printf("Error from NewMqttEngine: %v\n", err) + os.Exit(1) + } + + mqtttopic, err := tapir.MqttTopic(certCN, "tapir.observations.topic") + if err != nil { + fmt.Println("Error: tapir.observations.topic not specified in config") + os.Exit(1) + } + fmt.Printf("Using DNS TAPIR observation MQTT topic: %s\n", mqtttopic) + + signkey, err := tapir.FetchMqttSigningKey(mqtttopic, viper.GetString("tapir.observations.signingkey")) + if err != nil { + log.Fatalf("Error fetching MQTT signing key: %v", err) + } + // meng.AddTopic(mqtttopic, signkey, nil) + meng.PubToTopic(mqtttopic, signkey, "struct", true) // XXX: Brr. kludge. + + cmnder, outbox, _, err := meng.StartEngine() + if err != nil { + log.Fatalf("Error from StartEngine(): %v", err) + } + + count := 0 + + SetupInterruptHandler(cmnder) + + srcname := viper.GetString("tapir.observations.srcname") + if srcname == "" { + fmt.Println("Error: tapir.observations.srcname not specified in config") + os.Exit(1) + } + + var op, names, tags string + var tmsg = tapir.TapirMsg{ + SrcName: srcname, + Creator: "tapir-cli", + MsgType: "observation", + ListType: "greylist", + TimeStamp: time.Now(), + } + + var snames []string + var tagmask tapir.TagMask + + var ops = []string{"add", "del", "show", "send", "set-ttl", "list-tags", "quit"} + fmt.Printf("Defined operations are: %v\n", ops) + + var tds []tapir.Domain + // var ttl time.Duration = 60 * time.Second + var ttl int = 60 + + cmdloop: + for { + count++ + op = tapir.TtyRadioButtonQ("Operation", "add", ops) + switch op { + case "quit": + fmt.Println("QUIT cmd recieved.") + break cmdloop + + case "set-ttl": + ttl = tapir.TtyIntQuestion("TTL (in seconds)", 60, false) + // fmt.Printf("TTL: got: %d\n", tmp) + // ttl = time.Duration(tmp) * time.Second + // fmt.Printf("TTL: got: %d ttl: %v\n", tmp, ttl) + case "add", "del": + names = tapir.TtyQuestion("Domain names", names, false) + snames = strings.Fields(names) + if len(snames) > 0 && strings.ToUpper(snames[0]) == "QUIT" { + break cmdloop + } + + if op == "add" { + retry: + for { + tags = tapir.TtyQuestion("Tags", tags, false) + tagmask, err = tapir.StringsToTagMask(strings.Fields(tags)) + if err != nil { + fmt.Printf("Error from StringsToTagMask: %v\n", err) + fmt.Printf("Defined tags are: %v\n", tapir.DefinedTags) + continue retry + } + break + } + if tapir.GlobalCF.Verbose { + fmt.Printf("TagMask: %032b\n", tagmask) + } + } + for _, name := range snames { + tds = append(tds, tapir.Domain{ + Name: dns.Fqdn(name), + TimeAdded: time.Now(), + TTL: ttl, + TagMask: tagmask, + }) + } + + if op == "add" { + tmsg.Added = append(tmsg.Added, tds...) + tmsg.Msg = "it is greater to give than to take" + } else { + tmsg.Removed = append(tmsg.Removed, tds...) + tmsg.Msg = "happiness is a negative diff" + } + tds = []tapir.Domain{} + + case "show": + var out = []string{"Domain|Tags"} + for _, td := range tmsg.Added { + out = append(out, fmt.Sprintf("ADD: %s|%032b", td.Name, td.TagMask)) + } + for _, td := range tmsg.Removed { + out = append(out, fmt.Sprintf("DEL: %s", td.Name)) + } + fmt.Println(columnize.SimpleFormat(out)) + + case "list-tags": + var out = []string{"Name|Bit"} + var tagmask tapir.TagMask + for _, t := range tapir.DefinedTags { + tagmask, _ = tapir.StringsToTagMask([]string{t}) + out = append(out, fmt.Sprintf("%s|%032b", t, tagmask)) + } + fmt.Println(columnize.SimpleFormat(out)) + + case "send": + if tapir.GlobalCF.Verbose { + fmt.Printf("Sending TAPIR-POP observation message to topic %s\n", mqtttopic) + } + outbox <- tapir.MqttPkgOut{ + Type: "data", + Topic: mqtttopic, + Retain: false, + Data: tmsg, + } + + tmsg = tapir.TapirMsg{ + SrcName: srcname, + Creator: "tapir-cli", + MsgType: "observation", + ListType: "greylist", + TimeStamp: time.Now(), + } + tds = []tapir.Domain{} + } + } + respch := make(chan tapir.MqttEngineResponse, 2) + meng.CmdChan <- tapir.MqttEngineCmd{Cmd: "stop", Resp: respch} + r := <-respch + fmt.Printf("Response from MQTT Engine: %v\n", r) + }, +} + +var mqttTapirStatusCmd = &cobra.Command{ + Use: "status", + Short: "Interactively create and send status updates to the tapir intel MQTT topic (debug tool)", + Long: `Will query for operation (add|del|show|send|set-ttl|list-tags|quit), component name and status. +Will end the loop on the operation (or component name) "QUIT"`, + Run: func(cmd *cobra.Command, args []string) { + + var statusch = make(chan tapir.ComponentStatusUpdate, 10) + + // If any status updates arrive, print them out + go func() { + for status := range statusch { + fmt.Printf("Status update: %+v\n", status) + } + }() + + certCN, _, _, err := tapir.FetchTapirClientCert(log.Default(), statusch) + if err != nil { + fmt.Printf("Error fetching client certificate: %v", err) + os.Exit(1) + } + + meng, err := tapir.NewMqttEngine("status", mqttclientid, tapir.TapirPub, statusch, log.Default()) // pub, no sub + if err != nil { + fmt.Printf("Error from NewMqttEngine: %v\n", err) + os.Exit(1) + } + + mqtttopic, err := tapir.MqttTopic(certCN, "tapir.status.topic") + if err != nil { + fmt.Println("Error: tapir.status.topic not specified in config") + os.Exit(1) + } + fmt.Printf("Using DNS TAPIR status MQTT topic: %s\n", mqtttopic) + + signkey, err := tapir.FetchMqttSigningKey(mqtttopic, viper.GetString("tapir.status.signingkey")) + if err != nil { + log.Fatalf("Error fetching MQTT signing key: %v", err) + } + + meng.PubToTopic(mqtttopic, signkey, "struct", true) // XXX: Brr. kludge. + + cmnder, outbox, _, err := meng.StartEngine() + if err != nil { + log.Fatalf("Error from StartEngine(): %v", err) + } + + count := 0 + + SetupInterruptHandler(cmnder) + + var op, cname, status string + // var tmsg = tapir.TapirMsg{ + // SrcName: "status", + // Creator: "tapir-cli", + // MsgType: "status", + // TimeStamp: time.Now(), + // } + + var ops = []string{"add", "del", "show", "send", "set-ttl", "list-tags", "quit"} + fmt.Printf("Defined operations are: %v\n", ops) + + tfs := tapir.TapirFunctionStatus{ + Function: "tapir-pop", + FunctionID: mqttfid, + ComponentStatus: map[string]tapir.TapirComponentStatus{ + "downstream-notify": { + Component: "downstream-notify", + Status: tapir.StatusFail, + ErrorMsg: "Downstream notify is boiling over", + }, + }, + } + + known_components := []string{"downstream-notify", "main-boot", "rpz-update", "mqtt-msg", "config", "rpz-update"} + + cmdloop: + for { + count++ + op = tapir.TtyRadioButtonQ("Operation", "add", ops) + switch op { + case "quit": + fmt.Println("QUIT cmd recieved.") + break cmdloop + + case "add", "del": + cname = tapir.TtyQuestion("Component name", cname, false) + if len(cname) > 0 && strings.ToUpper(cname) == "QUIT" { + break cmdloop + } + if op == "del" { + delete(tfs.ComponentStatus, cname) + continue + } + + for { + status = tapir.TtyQuestion("Status", status, false) + switch status { + case "ok", "fail", "warn": + break + default: + fmt.Printf("Error: unknown status: %s\n", status) + status = "fail" + continue + } + break + } + + _, exist := tfs.ComponentStatus[cname] + if !exist { + tfs.ComponentStatus[cname] = tapir.TapirComponentStatus{ + Component: cname, + Status: tapir.StatusOK, + ErrorMsg: "", + } + } + comp := tfs.ComponentStatus[cname] + switch status { + case "ok", "fail", "warn": + default: + fmt.Printf("Invalid status: %s\n", status) + os.Exit(1) + } + comp.Status = tapir.StringToStatus[status] + switch comp.Status { + case tapir.StatusFail: + comp.LastFail = time.Now() + comp.NumFails++ + comp.ErrorMsg = tapir.TtyQuestion("Error message", "", false) + case tapir.StatusWarn: + comp.ErrorMsg = tapir.TtyQuestion("Warning message", "", false) + comp.LastWarn = time.Now() + comp.NumWarnings++ + case tapir.StatusOK: + comp.LastSuccess = time.Now() + comp.ErrorMsg = "" + comp.Msg = tapir.TtyQuestion("Message", "", false) + } + tfs.ComponentStatus[cname] = comp + + case "show": + var out = []string{"Component|Status|ErrorMsg|Msg|NumFailures|LastFailure|LastSuccess"} + for cname, comp := range tfs.ComponentStatus { + out = append(out, fmt.Sprintf("%s|%s|%s|%s|%d|%s|%s", cname, tapir.StatusToString[comp.Status], comp.ErrorMsg, comp.Msg, comp.NumFails, + comp.LastFail.Format(tapir.TimeLayout), comp.LastSuccess.Format(tapir.TimeLayout))) + } + fmt.Println(columnize.SimpleFormat(out)) + + case "list-comp": + fmt.Printf("%v\n", known_components) + + case "send": + if tapir.GlobalCF.Verbose { + fmt.Printf("Sending TAPIR-POP status message to topic %s\n", mqtttopic) + } + // tmsg.TapirFunctionStatus = tfs + outbox <- tapir.MqttPkgOut{ + Type: "raw", + Topic: mqtttopic, + RawData: tfs, + } + + // tmsg = tapir.TapirMsg{ + // Creator: "tapir-cli", + // MsgType: "status", + // TimeStamp: time.Now(), + // } + + } + } + respch := make(chan tapir.MqttEngineResponse, 2) + meng.CmdChan <- tapir.MqttEngineCmd{Cmd: "stop", Resp: respch} + r := <-respch + fmt.Printf("Response from MQTT Engine: %v\n", r) + }, +} + +var mqttTapirBootstrapCmd = &cobra.Command{ + Use: "bootstrap", + Short: "MQTT Bootstrap commands", +} + +var mqttTapirBootstrapStatusCmd = &cobra.Command{ + Use: "status", + Short: "Send send greylist-status request to MQTT Bootstrap Server", + Run: func(cmd *cobra.Command, args []string) { + srcs, err := ParseSources() + if err != nil { + log.Fatalf("Error parsing sources: %v", err) + } + + var src *SourceConf + for k, v := range srcs { + // fmt.Printf("Src: %s, Name: %s, Type: %s, Bootstrap: %v\n", k, v.Name, v.Type, v.Bootstrap) + if v.Name == mqttgreylist && v.Source == "mqtt" && v.Type == "greylist" { + src = &v + + PrintBootstrapMqttStatus(k, src) + } + } + + if src == nil { + fmt.Printf("Error: greylist source \"%s\" not found in sources", mqttgreylist) + os.Exit(1) + } + }, +} + +func init() { + MqttCmd.AddCommand(mqttEngineCmd, mqttTapirCmd) + mqttTapirCmd.AddCommand(mqttTapirObservationsCmd, mqttTapirConfigCmd, mqttTapirStatusCmd, mqttTapirBootstrapCmd) + mqttTapirBootstrapCmd.AddCommand(mqttTapirBootstrapStatusCmd) + + MqttCmd.PersistentFlags().StringVarP(&mqtttopic, "topic", "t", "", "MQTT topic, default from tapir-cli config") + + mqttclientid = "tapir-cli-" + uuid.New().String() + MqttCmd.PersistentFlags().StringVarP(&mqttclientid, "clientid", "", mqttclientid, "MQTT client id, default is a random string") + mqttEngineCmd.Flags().BoolVarP(&mqttpub, "pub", "", false, "Enable pub support") + mqttEngineCmd.Flags().BoolVarP(&mqttsub, "sub", "", false, "Enable sub support") + mqttTapirConfigCmd.Flags().BoolVarP(&mqttretain, "retain", "R", false, "Publish a retained message") + mqttTapirConfigCmd.Flags().BoolVarP(&mqttconfigclear, "clear", "C", false, "Clear retained config message") + mqttTapirConfigCmd.Flags().StringVarP(&gcfgfile, "cfgfile", "F", "", "Name of file containing global config to send") + mqttTapirBootstrapCmd.PersistentFlags().StringVarP(&mqttgreylist, "greylist", "G", "dns-tapir", "Greylist to inquire about") + + mqttTapirStatusCmd.Flags().StringVarP(&mqttfid, "functionid", "F", "tapir-cli debug tool", "Function ID to send status for") +} + +func PrintBootstrapMqttStatus(name string, src *SourceConf) error { + if len(src.Bootstrap) == 0 { + if len(src.Bootstrap) == 0 { + fmt.Printf("Note: greylist source %s (name \"%s\") has no bootstrap servers\n", name, src.Name) + return fmt.Errorf("no bootstrap servers") + } + } + + // Initialize the API client + api := &tapir.ApiClient{ + BaseUrl: fmt.Sprintf(src.BootstrapUrl, src.Bootstrap[0]), // Must specify a valid BaseUrl + ApiKey: src.BootstrapKey, + AuthMethod: "X-API-Key", + } + + cd := viper.GetString("certs.certdir") + if cd == "" { + log.Fatalf("Error: missing config key: certs.certdir") + } + // cert := cd + "/" + certname + cert := cd + "/" + "tapir-pop" + tlsConfig, err := tapir.NewClientConfig(viper.GetString("certs.cacertfile"), cert+".key", cert+".crt") + if err != nil { + log.Fatalf("BootstrapMqttSource: Error: Could not set up TLS: %v", err) + } + // XXX: Need to verify that the server cert is valid for the bootstrap server + tlsConfig.InsecureSkipVerify = true + err = api.SetupTLS(tlsConfig) + if err != nil { + return fmt.Errorf("error setting up TLS for the API client: %v", err) + } + + // out := []string{"Server|Uptime|Src|Name|MQTT Topic|Msgs|LastMsg"} + out := []string{"Server|Uptime|Src|Name|MQTT Topic|Pub Msgs|LastPub|Sub Msgs|LastSub"} + + // Iterate over the bootstrap servers + for _, server := range src.Bootstrap { + api.BaseUrl = fmt.Sprintf(src.BootstrapUrl, server) + + // Send an API ping command + pr, err := api.SendPing(0, false) + if err != nil { + fmt.Printf("Ping to MQTT bootstrap server %s failed: %v\n", server, err) + continue + } + + uptime := time.Since(pr.BootTime).Round(time.Second) + // fmt.Printf("MQTT bootstrap server %s uptime: %v. It has processed %d MQTT messages", server, uptime, 17) + + status, buf, err := api.RequestNG(http.MethodPost, "/bootstrap", tapir.BootstrapPost{ + Command: "greylist-status", + ListName: src.Name, + Encoding: "json", // XXX: This is our default, but we'll test other encodings later + }, true) + if err != nil { + fmt.Printf("Error from RequestNG: %v\n", err) + continue + } + + if status != http.StatusOK { + fmt.Printf("Bootstrap server %s responded with error: %s (instead of greylist status)\n", server, http.StatusText(status)) + continue + } + + var br tapir.BootstrapResponse + err = json.Unmarshal(buf, &br) + if err != nil { + fmt.Printf("Error decoding greylist-status response from %s: %v. Giving up.\n", server, err) + continue + } + if br.Error { + fmt.Printf("Bootstrap server %s responded with error: %s (instead of greylist status)\n", server, br.ErrorMsg) + } + if tapir.GlobalCF.Verbose && len(br.Msg) != 0 { + fmt.Printf("MQTT Bootstrap server %s responded with message: %s\n", server, br.Msg) + } + + // for topic, v := range br.MsgCounters { + // out = append(out, fmt.Sprintf("%s|%v|%s|%s|%s|%d|%s", server, uptime, name, src.Name, topic, v, br.MsgTimeStamps[topic].Format(time.RFC3339))) + // } + + for topic, topicdata := range br.TopicData { + out = append(out, fmt.Sprintf("%s|%v|%s|%s|%s|%d|%s|%d|%s", server, uptime, name, src.Name, topic, topicdata.PubMsgs, topicdata.LatestPub.Format(time.RFC3339), topicdata.SubMsgs, topicdata.LatestSub.Format(time.RFC3339))) + } + } + + fmt.Println(columnize.SimpleFormat(out)) + + return nil +} + +type SrcFoo struct { + Src struct { + Style string `yaml:"style"` + } `yaml:"src"` + Sources map[string]SourceConf `yaml:"sources"` +} + +type SourceConf struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Type string `yaml:"type"` + Topic string `yaml:"topic"` + Source string `yaml:"source"` + SrcFormat string `yaml:"src_format"` + Format string `yaml:"format"` + Datasource string `yaml:"datasource"` + Bootstrap []string + BootstrapUrl string + BootstrapKey string +} + +func ParseSources() (map[string]SourceConf, error) { + var srcfoo SrcFoo + configFile := filepath.Clean(tapir.PopSourcesCfgFile) + data, err := os.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("error reading config file: %v", err) + } + + err = yaml.Unmarshal(data, &srcfoo) + if err != nil { + return nil, fmt.Errorf("error unmarshalling YAML data: %v", err) + } + + srcs := srcfoo.Sources + // fmt.Printf("*** ParseSourcesNG: there are %d defined sources in config\n", len(srcs)) + return srcs, nil +} + +// func SetupSubPrinter(inbox chan tapir.MqttPkg) { +func SetupSubPrinter(inbox chan tapir.MqttPkgIn) { + fmt.Println("SetupSubPrinter: Starting") + go func() { + for pkg := range inbox { + + fmt.Printf("SetupSubPrinter: Received TAPIR MQTT Message on topic '%s'\n", pkg.Topic) + + switch { + case regexp.MustCompile(`^events/up/[^/]+/observations$`).MatchString(pkg.Topic), + regexp.MustCompile(`^events/down/[^/]+/general$`).MatchString(pkg.Topic): + parts := strings.Split(pkg.Topic, "/") + if len(parts) == 4 { + edgeId := parts[2] + edgeComponent := parts[3] + _ = edgeId // Avoid unused variable error + _ = edgeComponent // Avoid unused variable error + + fmt.Printf("SetupSubPrinter: Received TAPIR MQTT Message on topic '%s':\n%+v\n", pkg.Topic, string(pkg.Payload)) + + var tm tapir.TapirMsg + err := json.Unmarshal(pkg.Payload, &tm) + if err != nil { + fmt.Printf("MQTT: failed to decode json: %v", err) + continue + } + fmt.Printf("Received TAPIR Observation Message on topic %s\n", pkg.Topic) + var out []string + for _, a := range tm.Added { + out = append(out, fmt.Sprintf("ADD: %s|%032b", a.Name, a.TagMask)) + } + for _, a := range tm.Removed { + out = append(out, fmt.Sprintf("DEL: %s", a.Name)) + } + fmt.Println(columnize.SimpleFormat(out)) + } else { + fmt.Printf("Received TAPIR MQTT Message on unknown topic %s\n", pkg.Topic) + } + + case pkg.Topic == "status/up/axfr/tapir-pop": // payload is a tapir.TapirFunctionStatus + var tfs tapir.TapirFunctionStatus + err := json.Unmarshal(pkg.Payload, &tfs) + if err != nil { + fmt.Printf("MQTT: failed to decode json: %v", err) + } + + fmt.Printf("Received TAPIR MQTT Message of type: %s\n", tfs.FunctionID) + for _, comp := range tfs.ComponentStatus { + switch comp.Status { + case tapir.StatusFail: + fmt.Printf("TAPIR-POP %s Component: %s, Status: %s, Message: %s, Time of failure: %s\n", + tfs.FunctionID, comp.Component, tapir.StatusToString[comp.Status], comp.Msg, comp.LastFail.Format(time.RFC3339)) + case tapir.StatusWarn: + fmt.Printf("TAPIR-POP %s: Component: %s, Status: %s, Message: %s, Time of warning: %s\n", + tfs.FunctionID, comp.Component, tapir.StatusToString[comp.Status], comp.Msg, comp.LastWarn.Format(time.RFC3339)) + case tapir.StatusOK: + fmt.Printf("TAPIR-POP %s Component: %s, Status: %s, Message: %s, Time of success: %s\n", + tfs.FunctionID, comp.Component, tapir.StatusToString[comp.Status], comp.Msg, comp.LastSuccess.Format(time.RFC3339)) + } + } + + case strings.HasPrefix(pkg.Topic, "config/down/"): // payload is a tapir.GlobalConfig + var gc tapir.GlobalConfig + err := json.Unmarshal(pkg.Payload, &gc) + if err != nil { + fmt.Printf("MQTT: failed to decode json: %v", err) + } + yamlData, err := yaml.Marshal(gc) + if err != nil { + fmt.Printf("Error marshalling YAML data: %v\n", err) + } + fmt.Printf("Received TAPIR Global config update message:\n%s\n", string(yamlData)) + + case pkg.Topic == "pubkey/up/axfr/tapir-pop", pkg.Topic == "status/up/axfr/tapir-pop": // payload is a tapir.TapirPubkeyUpload + var tpk tapir.PubKeyUpload + err := json.Unmarshal(pkg.Payload, &tpk) + if err != nil { + fmt.Printf("MQTT: failed to decode json: %v", err) + } + yamlData, err := yaml.Marshal(tpk) + if err != nil { + fmt.Printf("Error marshalling YAML data: %v\n", err) + } + fmt.Printf("Received TAPIR PubKeyUpload message:\n%s\n", string(yamlData)) + + default: + fmt.Printf("Received TAPIR MQTT Message of unknown type on topic: %s\n", pkg.Topic) + } + + // case tapir.MqttPkgIn: + // p := pkg.(tapir.MqttPkgIn) + // var out []string + // fmt.Printf("Received TAPIR MQTT Message of type: %s\n", p.Data.MsgType) + // for _, a := range p.Data.Added { + // out = append(out, fmt.Sprintf("ADD: %s|%032b", a.Name, a.TagMask)) + // } + // for _, a := range p.Data.Removed { + // out = append(out, fmt.Sprintf("DEL: %s", a.Name)) + // } + // fmt.Println(columnize.SimpleFormat(out)) + // pretty, err := yaml.Marshal(p.Data) + // if err != nil { + // fmt.Printf("Error marshalling YAML data: %v\n", err) + // os.Exit(1) + // } + // fmt.Printf("Received TAPIR MQTT Message:\n%s\n", string(pretty)) + // } + } + }() +} + +func SetupInterruptHandler(cmnder chan tapir.MqttEngineCmd) { + respch := make(chan tapir.MqttEngineResponse, 2) + + ic := make(chan os.Signal, 1) + signal.Notify(ic, os.Interrupt, syscall.SIGTERM) + go func() { + for { + select { + + case <-ic: + fmt.Println("SIGTERM interrupt received, sending stop signal to MQTT Engine") + cmnder <- tapir.MqttEngineCmd{Cmd: "stop", Resp: respch} + r := <-respch + if r.Error { + fmt.Printf("Error: %s\n", r.ErrorMsg) + } else { + fmt.Printf("MQTT Engine: %s\n", r.Status) + } + os.Exit(1) + } + } + }() +} diff --git a/cmd/pop.go b/cmd/pop.go new file mode 100644 index 0000000..1db13c6 --- /dev/null +++ b/cmd/pop.go @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se + */ + +package cmd + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/dnstapir/tapir" + "github.com/ryanuber/columnize" + "github.com/spf13/cobra" +) + +const timelayout = "2006-01-02 15:04:05" + +var PopCmd = &cobra.Command{ + Use: "pop", + Short: "Prefix command, only usable via sub-commands", +} + +var PopMqttCmd = &cobra.Command{ + Use: "mqtt", + Short: "Prefix command, only usable via sub-commands", +} + +var PopPingCmd = &cobra.Command{ + Use: "ping", + Short: "Send an API ping request to TAPIR-POP and present the response", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + log.Fatal("ping must have no arguments") + } + + pr, err := tapir.GlobalCF.Api.SendPing(tapir.GlobalCF.PingCount, false) + if err != nil { + log.Fatalf("Error from SendPing: %v", err) + } + + uptime := time.Now().Sub(pr.BootTime).Round(time.Second) + if tapir.GlobalCF.Verbose { + fmt.Printf("%s from %s @ %s (version %s): pings: %d, pongs: %d, uptime: %v time: %s, client: %s\n", + pr.Msg, pr.Daemon, pr.ServerHost, pr.Version, pr.Pings, + pr.Pongs, uptime, pr.Time.Format(timelayout), pr.Client) + } else { + fmt.Printf("%s: pings: %d, pongs: %d, uptime: %v, time: %s\n", + pr.Msg, pr.Pings, pr.Pongs, uptime, pr.Time.Format(timelayout)) + } + }, +} + +var PopStatusCmd = &cobra.Command{ + Use: "status", + Short: "Get the status of TAPIR-POP", + Run: func(cmd *cobra.Command, args []string) { + resp := SendCommandCmd(tapir.CommandPost{ + Command: "status", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + + if len(resp.TapirFunctionStatus.ComponentStatus) != 0 { + tfs := resp.TapirFunctionStatus + fmt.Printf("TAPIR-POP Status. Reported components: %d Total errors (since last start): %d\n", len(tfs.ComponentStatus), tfs.NumFailures) + var out = []string{"Component|Status|Error msg|# Fails|# Warns|LastFailure|LastSuccess"} + for k, v := range tfs.ComponentStatus { + out = append(out, fmt.Sprintf("%s|%s|%s|%d|%d|%v|%v", k, tapir.StatusToString[v.Status], v.ErrorMsg, v.NumFails, v.NumWarnings, v.LastFail.Format(tapir.TimeLayout), v.LastSuccess.Format(tapir.TimeLayout))) + } + fmt.Printf("%s\n", columnize.SimpleFormat(out)) + } + }, +} + +var PopStopCmd = &cobra.Command{ + Use: "stop", + Short: "Instruct TAPIR-POP to stop", + Run: func(cmd *cobra.Command, args []string) { + resp := SendCommandCmd(tapir.CommandPost{ + Command: "stop", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + }, +} + +var PopMqttStartCmd = &cobra.Command{ + Use: "start", + Short: "Instruct TAPIR-POP MQTT Engine to start", + Run: func(cmd *cobra.Command, args []string) { + resp := SendCommandCmd(tapir.CommandPost{ + Command: "mqtt-start", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + }, +} + +var PopMqttStopCmd = &cobra.Command{ + Use: "stop", + Short: "Instruct TAPIR-POP MQTT Engine to stop", + Run: func(cmd *cobra.Command, args []string) { + resp := SendCommandCmd(tapir.CommandPost{ + Command: "mqtt-stop", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + }, +} + +var PopMqttRestartCmd = &cobra.Command{ + Use: "restart", + Short: "Instruct TAPIR-POP MQTT Engine to restart", + Run: func(cmd *cobra.Command, args []string) { + resp := SendCommandCmd(tapir.CommandPost{ + Command: "mqtt-restart", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + }, +} + +func init() { + PopCmd.AddCommand(PopStatusCmd, PopStopCmd, PopMqttCmd) + PopCmd.AddCommand(PopPingCmd) + + PopMqttCmd.AddCommand(PopMqttStartCmd, PopMqttStopCmd, PopMqttRestartCmd) + + PopPingCmd.Flags().IntVarP(&tapir.GlobalCF.PingCount, "count", "c", 0, "#pings to send") +} + +func SendCommandCmd(data tapir.CommandPost) tapir.CommandResponse { + _, buf, _ := tapir.GlobalCF.Api.RequestNG(http.MethodPost, "/command", data, true) + + var cr tapir.CommandResponse + + err := json.Unmarshal(buf, &cr) + if err != nil { + log.Fatalf("Error from json.Unmarshal: %v\n", err) + } + return cr +} diff --git a/cmd/rpz.go b/cmd/rpz.go new file mode 100644 index 0000000..bce6f0c --- /dev/null +++ b/cmd/rpz.go @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se + */ + +package cmd + +import ( + "fmt" + "os" + + "github.com/dnstapir/tapir" + "github.com/miekg/dns" + "github.com/spf13/cobra" +) + +var rpzname, rpztype, rpzaction, rpzpolicy string + +var RpzCmd = &cobra.Command{ + Use: "rpz", + Short: "Instruct TAPIR-POP to modify the RPZ zone; must use sub-command", + Long: `Known actions are: +drop send no response at all +nxdomain return an NXDOMAIN response +nodata return a NODATA response`, +} + +var RpzAddCmd = &cobra.Command{ + Use: "add", + Short: "Instruct TAPIR-POP to add a new rule to the RPZ zone", + Run: func(cmd *cobra.Command, args []string) { + if rpzname == "" { + fmt.Printf("Error: domain name for which to add new RPZ rule for not specified.\n") + os.Exit(1) + } + + if rpztype == "" { + fmt.Printf("Error: RPZ list type for domain name \"%s\" not specified.\n", rpzname) + fmt.Printf("Error: must be one of: whitelist, greylist or blacklist.\n") + os.Exit(1) + } + + if rpzpolicy == "" { + fmt.Printf("Error: desired RPZ policy for domain name \"%s\" not specified.\n", rpzname) + os.Exit(1) + } + + // if rpzaction == "" { + // fmt.Printf("Error: desired RPZ action for domain name \"%s\" not specified.\n", rpzname) + // os.Exit(1) + // } + + resp := SendCommandCmd(tapir.CommandPost{ + Command: "rpz-add", + Name: dns.Fqdn(rpzname), + ListType: rpztype, + Action: rpzaction, + Policy: rpzpolicy, + Zone: dns.Fqdn(tapir.GlobalCF.Zone), + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + if resp.Msg != "" { + fmt.Printf("%s\n", resp.Msg) + } + }, +} + +var RpzRemoveCmd = &cobra.Command{ + Use: "remove", + Short: "Instruct TAPIR-POP to remove a rule from the RPZ zone", + Run: func(cmd *cobra.Command, args []string) { + if rpzname == "" { + fmt.Printf("Error: domain name to add rule for not specified.\n") + os.Exit(1) + } + + resp := SendCommandCmd(tapir.CommandPost{ + Command: "rpz-remove", + Name: dns.Fqdn(rpzname), + Zone: dns.Fqdn(tapir.GlobalCF.Zone), + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + }, +} + +var RpzLookupCmd = &cobra.Command{ + Use: "lookup", + Short: "Instruct TAPIR-POP to remove a rule from the RPZ zone", + Run: func(cmd *cobra.Command, args []string) { + if rpzname == "" { + fmt.Printf("Error: domain name look up not specified.\n") + os.Exit(1) + } + + resp := SendCommandCmd(tapir.CommandPost{ + Command: "rpz-lookup", + Name: dns.Fqdn(rpzname), + Zone: dns.Fqdn(tapir.GlobalCF.Zone), + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + }, +} + +var RpzListCmd = &cobra.Command{ + Use: "list", + Short: "Instruct TAPIR-POP to remove a rule from the RPZ zone", + Run: func(cmd *cobra.Command, args []string) { + + resp := SendCommandCmd(tapir.CommandPost{ + Command: "rpz-list-sources", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + }, +} + +func init() { + RpzCmd.AddCommand(RpzAddCmd, RpzRemoveCmd, RpzLookupCmd, RpzListCmd) + + RpzAddCmd.Flags().StringVarP(&rpzname, "name", "", "", "Domain name to add rule for") + RpzLookupCmd.Flags().StringVarP(&rpzname, "name", "", "", "Domain name to look up") + RpzAddCmd.Flags().StringVarP(&rpztype, "type", "", "", "One of: whitelist, greylist or blacklist") + RpzAddCmd.Flags().StringVarP(&rpzaction, "action", "", "", "Desired action") + RpzAddCmd.Flags().StringVarP(&rpzpolicy, "policy", "", "", "Desired policy for this domain name") +} diff --git a/cmd/slogger.go b/cmd/slogger.go new file mode 100644 index 0000000..6f33ca0 --- /dev/null +++ b/cmd/slogger.go @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se + */ +package cmd + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/dnstapir/tapir" + "github.com/ryanuber/columnize" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var SloggerCmd = &cobra.Command{ + Use: "slogger", + Short: "Prefix command to TAPIR-Slogger, only usable in TAPIR Core, not in TAPIR Edge", +} + +var SloggerPopCmd = &cobra.Command{ + Use: "pop", + Short: "Prefix command, only usable via sub-commands", +} + +var SloggerEdmCmd = &cobra.Command{ + Use: "edm", + Short: "Prefix command, only usable via sub-commands", +} + +var onlyfails bool + +var SloggerPopStatusCmd = &cobra.Command{ + Use: "status", + Short: "Get the TAPIR-POP status report from TAPIR-Slogger", + Run: func(cmd *cobra.Command, args []string) { + resp := SendSloggerCommand(tapir.SloggerCmdPost{ + Command: "status", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + + if len(resp.PopStatus) == 0 { + fmt.Printf("No Status reports from any TAPIR-POP received\n") + os.Exit(0) + } + + showfails := "" + if onlyfails { + showfails = " (only fails)" + } + + var out []string + for functionid, ps := range resp.PopStatus { + fmt.Printf("Status for TAPIR-POP%s: %s\n", showfails, functionid) + out = []string{"Component|Status|Error msg|NumFailures|LastFailure|LastSuccess"} + for comp, v := range ps.ComponentStatus { + if !onlyfails || v.Status == tapir.StatusFail { + out = append(out, fmt.Sprintf("%s|%s|%s|%d|%s|%s", comp, tapir.StatusToString[v.Status], v.ErrorMsg, v.NumFails, + v.LastFail.Format(tapir.TimeLayout), v.LastSuccess.Format(tapir.TimeLayout))) + } + } + } + + fmt.Printf("%s\n", columnize.SimpleFormat(out)) + }, +} + +var SloggerEdmStatusCmd = &cobra.Command{ + Use: "status", + Short: "Get the TAPIR-EDM status report from TAPIR-Slogger", + Run: func(cmd *cobra.Command, args []string) { + resp := SendSloggerCommand(tapir.SloggerCmdPost{ + Command: "status", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + + fmt.Printf("%s\n", resp.Msg) + + if len(resp.EdmStatus) == 0 { + fmt.Printf("No Status reports from any TAPIR-EDM received\n") + os.Exit(0) + } + + showfails := "" + if onlyfails { + showfails = " (only fails)" + } + + var out []string + for functionid, ps := range resp.EdmStatus { + fmt.Printf("Status for TAPIR-EDM%s: %s\n", showfails, functionid) + out = []string{"Component|Status|Error msg|NumFailures|LastFailure|LastSuccess"} + for comp, v := range ps.ComponentStatus { + if !onlyfails || v.Status == tapir.StatusFail { + out = append(out, fmt.Sprintf("%s|%s|%s|%d|%s|%s", comp, tapir.StatusToString[v.Status], v.ErrorMsg, v.NumFails, + v.LastFail.Format(tapir.TimeLayout), v.LastSuccess.Format(tapir.TimeLayout))) + } + } + } + + fmt.Printf("%s\n", columnize.SimpleFormat(out)) + }, +} + +var SloggerPingCmd = &cobra.Command{ + Use: "ping", + Short: "Send an API ping request to TAPIR-Slogger and present the response", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + log.Fatal("ping must have no arguments") + } + + api, err := SloggerApi() + if err != nil { + log.Fatalf("Error: Could not set up API client to TAPIR-SLOGGER: %v", err) + } + + pr, err := api.SendPing(tapir.GlobalCF.PingCount, false) + if err != nil { + log.Fatalf("Error from SendPing: %v", err) + } + + uptime := time.Now().Sub(pr.BootTime).Round(time.Second) + if tapir.GlobalCF.Verbose { + fmt.Printf("%s from %s @ %s (version %s): pings: %d, pongs: %d, uptime: %v time: %s, client: %s\n", + pr.Msg, pr.Daemon, pr.ServerHost, pr.Version, pr.Pings, + pr.Pongs, uptime, pr.Time.Format(timelayout), pr.Client) + } else { + fmt.Printf("%s: pings: %d, pongs: %d, uptime: %v, time: %s\n", + pr.Msg, pr.Pings, pr.Pongs, uptime, pr.Time.Format(timelayout)) + } + }, +} + +func init() { + SloggerCmd.AddCommand(SloggerPopCmd, SloggerPingCmd, SloggerEdmCmd) + SloggerPopCmd.AddCommand(SloggerPopStatusCmd) + SloggerEdmCmd.AddCommand(SloggerEdmStatusCmd) + + SloggerCmd.PersistentFlags().BoolVarP(&tapir.GlobalCF.ShowHdr, "headers", "H", false, "Show column headers") + SloggerPopStatusCmd.Flags().BoolVarP(&onlyfails, "onlyfails", "f", false, "Show only components that currently fail") + SloggerEdmStatusCmd.Flags().BoolVarP(&onlyfails, "onlyfails", "f", false, "Show only components that currently fail") +} + +func SloggerApi() (*tapir.ApiClient, error) { + servername := "tapir-slogger" + var baseurl, urlkey string + var err error + + switch tapir.GlobalCF.UseTLS { + case true: + urlkey = "cli." + servername + ".tlsurl" + baseurl = viper.GetString(urlkey) + case false: + urlkey = "cli." + servername + ".url" + baseurl = viper.GetString(urlkey) + } + if baseurl == "" { + return nil, fmt.Errorf("Error: missing config key: %s", urlkey) + } + + api := &tapir.ApiClient{ + BaseUrl: baseurl, + ApiKey: viper.GetString("cli." + servername + ".apikey"), + AuthMethod: "X-API-Key", + Debug: tapir.GlobalCF.Debug, + Verbose: tapir.GlobalCF.Verbose, + } + + if tapir.GlobalCF.UseTLS { // default = true + cd := viper.GetString("certs.certdir") + if cd == "" { + return nil, fmt.Errorf("Error: missing config key: certs.certdir") + } + cert := cd + "/" + tapir.GlobalCF.Certname + tlsConfig, err := tapir.NewClientConfig(viper.GetString("certs.cacertfile"), + cert+".key", cert+".crt") + if err != nil { + return nil, fmt.Errorf("Error: Could not set up TLS: %v", err) + } + tlsConfig.InsecureSkipVerify = true + err = api.SetupTLS(tlsConfig) + } else { + err = api.Setup() + } + if err != nil { + return nil, fmt.Errorf("Error: Could not set up API client to TAPIR-SLOGGER at %s: %v", baseurl, err) + } + return api, nil +} + +func SendSloggerCommand(data tapir.SloggerCmdPost) tapir.SloggerCmdResponse { + + api, err := SloggerApi() + if err != nil { + log.Fatalf("Error: Could not set up API client to TAPIR-SLOGGER: %v", err) + } + + _, buf, _ := api.RequestNG(http.MethodPost, "/status", data, true) + + var cr tapir.SloggerCmdResponse + + err = json.Unmarshal(buf, &cr) + if err != nil { + log.Fatalf("Error from json.Unmarshal: %v\n", err) + } + return cr +} diff --git a/globals.go b/globals.go index 8fa619f..36ada62 100644 --- a/globals.go +++ b/globals.go @@ -12,6 +12,11 @@ type CliFlags struct { Api *ApiClient PingCount int Zone string + // TODO cleaner solution: + // Moved "certname" from slogger.go here so it can know what cert to look + // for. "certname" was previously declared globally in "root.go", but since + // the move to the tapir lib, slogger.go no longer sees that variable. + Certname string } var GlobalCF CliFlags From f15a5e3cbb1a4821cc5937aa4225534b9c1b5274 Mon Sep 17 00:00:00 2001 From: Leon Fernandez Date: Thu, 31 Oct 2024 17:25:46 +0100 Subject: [PATCH 2/2] Separate colourlists command file --- cmd/colourlists.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/debug.go | 39 +-------------------------------------- 2 files changed, 47 insertions(+), 38 deletions(-) create mode 100644 cmd/colourlists.go diff --git a/cmd/colourlists.go b/cmd/colourlists.go new file mode 100644 index 0000000..9c0751f --- /dev/null +++ b/cmd/colourlists.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/dnstapir/tapir" + "github.com/spf13/cobra" +) + + +var ColourlistsCmd = &cobra.Command{ + Use: "colourlists", + Short: "Return the white/black/greylists from the current data structures", + Run: func(cmd *cobra.Command, args []string) { + resp := SendDebugCmd(tapir.DebugPost{ + Command: "colourlists", + }) + if resp.Error { + fmt.Printf("%s\n", resp.ErrorMsg) + } + fmtstring := "%-35s|%-20s|%-10s|%-10s\n" + + // print the column headings + fmt.Printf(fmtstring, "Domain", "Source", "Src Fmt", "Colour") + fmt.Println(strings.Repeat("-", 78)) // A nice ruler over the data rows + + for _, l := range resp.Lists["whitelist"] { + for _, n := range l.Names { + fmt.Printf(fmtstring, n.Name, l.Name, "-", "white") + } + } + for _, l := range resp.Lists["blacklist"] { + for _, n := range l.Names { + fmt.Printf(fmtstring, n.Name, l.Name, "-", "black") + } + } + for _, l := range resp.Lists["greylist"] { + for _, n := range l.Names { + fmt.Printf(fmtstring, n.Name, l.Name, l.SrcFormat, "grey") + } + } + }, +} + + diff --git a/cmd/debug.go b/cmd/debug.go index 6b3d3ed..44cd8f7 100644 --- a/cmd/debug.go +++ b/cmd/debug.go @@ -11,7 +11,6 @@ import ( "log" "net/http" "os" - "strings" "time" "github.com/dnstapir/tapir" @@ -54,42 +53,6 @@ var debugZoneDataCmd = &cobra.Command{ }, } -var debugColourlistsCmd = &cobra.Command{ - Use: "colourlists", - Short: "Return the white/black/greylists from the current data structures", - Run: func(cmd *cobra.Command, args []string) { - resp := SendDebugCmd(tapir.DebugPost{ - Command: "colourlists", - }) - if resp.Error { - fmt.Printf("%s\n", resp.ErrorMsg) - } - fmtstring := "%-35s|%-20s|%-10s|%-10s\n" - - // fmt.Printf("Received %d bytes of data\n", len(resp.Msg)) - - // print the column headings - fmt.Printf(fmtstring, "Domain", "Source", "Src Fmt", "Colour") - fmt.Println(strings.Repeat("-", 78)) // A nice ruler over the data rows - - for _, l := range resp.Lists["whitelist"] { - for _, n := range l.Names { - fmt.Printf(fmtstring, n.Name, l.Name, "-", "white") - } - } - for _, l := range resp.Lists["blacklist"] { - for _, n := range l.Names { - fmt.Printf(fmtstring, n.Name, l.Name, "-", "black") - } - } - for _, l := range resp.Lists["greylist"] { - for _, n := range l.Names { - fmt.Printf(fmtstring, n.Name, l.Name, l.SrcFormat, "grey") - } - } - }, -} - var debugGenRpzCmd = &cobra.Command{ Use: "genrpz", Short: "Return the white/black/greylists from the current data structures", @@ -390,7 +353,7 @@ var debugImportGreylistCmd = &cobra.Command{ } func init() { - DebugCmd.AddCommand(debugSyncZoneCmd, debugZoneDataCmd, debugColourlistsCmd, debugGenRpzCmd) + DebugCmd.AddCommand(debugSyncZoneCmd, debugZoneDataCmd, debugGenRpzCmd) DebugCmd.AddCommand(debugMqttStatsCmd, debugReaperStatsCmd) DebugCmd.AddCommand(debugImportGreylistCmd, debugGreylistStatusCmd) DebugCmd.AddCommand(debugGenerateSchemaCmd, debugUpdatePopStatusCmd)