From f9194fcb29cb20eea5d3e7ddbde4a13ecb76bdc5 Mon Sep 17 00:00:00 2001 From: Matthew R Date: Fri, 18 Dec 2020 20:25:22 -0500 Subject: [PATCH] Initial commit --- Makefile | 14 ++++++ cmd/apply.go | 107 +++++++++++++++++++++++++++++++++++++++++++++ config/keys.go | 51 +++++++++++++++++++++ go.mod | 12 +++++ go.sum | 28 ++++++++++++ internal/custom.go | 29 ++++++++++++ internal/daemon.go | 95 ++++++++++++++++++++++++++++++++++++++++ main.go | 42 ++++++++++++++++++ 8 files changed, 378 insertions(+) create mode 100644 Makefile create mode 100644 cmd/apply.go create mode 100644 config/keys.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/custom.go create mode 100644 internal/daemon.go create mode 100644 main.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..af35d67 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +commands := $(wildcard cmd/*.go) + +all: build + + +build: + @GO111Module=on go build -v -ldflags="-s -w" -buildmode=pie -o csctl ${commands} + @sudo chmod 2751 ./csctl + @file ./csctl + +install: + @sudo cp cscli /usr/local/bin/csctl + @sudo chown root:cscli /usr/local/hbin/csctl + @sudo chmod 2751 /usr/local/bin/csctl diff --git a/cmd/apply.go b/cmd/apply.go new file mode 100644 index 0000000..de23457 --- /dev/null +++ b/cmd/apply.go @@ -0,0 +1,107 @@ +package cmd + +import ( + "csctl/config" + "csctl/internal" + "encoding/json" + "fmt" + "net/http" + // "os/user" + "strings" + + "github.com/kataras/golog" + "github.com/logrusorgru/aurora/v3" + "github.com/olekukonko/tablewriter" + "github.com/urfave/cli/v2" +) + +type Response struct { + Status string `json:"status"` + Decision string `json:"decision"` + ApplicationID string `json:"id"` + ProcessedBy string `json:"processedBy"` +} + +func ApplyHard(ctx *cli.Context) error { + + account, err := internal.FetchAccount("matthew") + if err != nil { + internal.Error(err, "Unable to read account information.") + } + + resp, err := http.Get(fmt.Sprintf("https://eds.libraryofcode.org/cs/t2/?userID=%s&auth=%s", account.UserID, config.FetchInternalKey())) + if err != nil { + internal.Error(err, "Unable to read information from EDS.") + } + + tableString := &strings.Builder{} + table := tablewriter.NewWriter(tableString) + table.SetHeader([]string{"Status", "Application ID"}) + + if resp.StatusCode == 404 || resp.StatusCode == 400 || resp.StatusCode == 401 { + dataErr := [][]string{ + {aurora.Yellow("PRE-DECLINED").String(), "N/A"}, + } + table.AppendBulk(dataErr) + table.Render() + fmt.Println(tableString.String()) + return nil + } + + var response Response + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + internal.Error(err, "Unable to parse JSON from CSD.") + } + + data := [][]string{ + {response.Decision, response.ApplicationID}, + } + table.AppendBulk(data) + table.Render() + fmt.Println(tableString.String()) + return nil +} + +func ApplySoft(ctx *cli.Context) error { + golog.Debug("Fetching account information.") + account, err := internal.FetchAccount("matthew") + if err != nil { + internal.Error(err, "Unable to read account information.") + } + + golog.Info("Processing application, do not exit the CLI...") + + resp, err := http.Get(fmt.Sprintf("https://eds.libraryofcode.org/cs/t2pre/?userID=%s&auth=%s", account.UserID, config.FetchInternalKey())) + if err != nil { + internal.Error(err, "Unable to read response from EDS.") + } + + tableString := &strings.Builder{} + table := tablewriter.NewWriter(tableString) + table.SetHeader([]string{"Status", "Application ID"}) + + if resp.StatusCode == 404 || resp.StatusCode == 400 || resp.StatusCode == 401 { + dataErr := [][]string{ + {aurora.Yellow("PRE-DECLINED").String(), "N/A"}, + } + table.AppendBulk(dataErr) + table.Render() + fmt.Println(tableString.String()) + return nil + } + + var response Response + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + internal.Error(err, "Unable to parse JSON from CSD.") + } + + data := [][]string{ + {response.Decision, response.ApplicationID}, + } + table.AppendBulk(data) + table.Render() + fmt.Println(tableString.String()) + return nil +} diff --git a/config/keys.go b/config/keys.go new file mode 100644 index 0000000..395ba9d --- /dev/null +++ b/config/keys.go @@ -0,0 +1,51 @@ +package config + +import ( + "io/ioutil" + "strings" +) + +// FetchHMACKey is a configuration function that fetches the system HMAC key to interact with CSD +func FetchHMACKey() []byte { + read, err := ioutil.ReadFile("/etc/cscli.conf") + if err != nil { + panic(err) + } + return []byte(strings.TrimSpace(string(read))) +} + +// FetchConfigKey is a configuration function that fetches the ConfigCat SDK key +func FetchConfigKey() string { + read, err := ioutil.ReadFile("/etc/cscli-sdk.conf") + if err != nil { + panic(err) + } + return strings.TrimSpace(string(read)) +} + +// FetchIPKey is a configuration function that fetches the API key for IP address information +func FetchIPKey() string { + read, err := ioutil.ReadFile("/etc/cscli-ip.conf") + if err != nil { + panic(err) + } + return strings.TrimSpace(string(read)) +} + +// FetchVendorKey is a configuration function that fetches the Vendor key for Community Report data +func FetchVendorKey() string { + read, err := ioutil.ReadFile("/etc/cscli-vend.conf") + if err != nil { + panic(err) + } + return strings.TrimSpace(string(read)) +} + +// FetchInternalKey is a configuration function that fetches the inter-process internal communication key +func FetchInternalKey() string { + read, err := ioutil.ReadFile("/etc/csctl-internal.conf") + if err != nil { + panic(err) + } + return strings.TrimSpace(string(read)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cd363b5 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module csctl + +go 1.14 + +require ( + github.com/kataras/golog v0.1.6 + github.com/logrusorgru/aurora/v3 v3.0.0 + github.com/olekukonko/tablewriter v0.0.4 + github.com/urfave/cli/v2 v2.3.0 + golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 // indirect + gopkg.in/hlandau/service.v1 v1.0.7 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b58adf8 --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/kataras/golog v0.1.6 h1:jEqEQCm+4B4M4/CgzrEWNj9iUST0hGZXDqAPMVnxWMw= +github.com/kataras/golog v0.1.6/go.mod h1:jOSQ+C5fUqsNSwurB/oAHq1IFSb0KI3l6GMa7xB6dZA= +github.com/kataras/pio v0.0.10 h1:b0qtPUqOpM2O+bqa5wr2O6dN4cQNwSmFd6HQqgVae0g= +github.com/kataras/pio v0.0.10/go.mod h1:gS3ui9xSD+lAUpbYnjOGiQyY7sUMJO+EHpiRzhtZ5no= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= +github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ= +golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/hlandau/service.v1 v1.0.7 h1:16G5AJ1Cp8Vr65QItJXpyAIzf/FWAWCZBsTgsc6eyA8= +gopkg.in/hlandau/service.v1 v1.0.7/go.mod h1:sZw6ksxcoafC04GoZtw32UeqqEuPSABX35lVBaJP/bE= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/custom.go b/internal/custom.go new file mode 100644 index 0000000..8eba0fe --- /dev/null +++ b/internal/custom.go @@ -0,0 +1,29 @@ +package internal + +import ( + "fmt" + "github.com/kataras/golog" + "os" + "os/user" + "time" +) + +func Error(err error, msg string) { + currentUser, _ := user.Current() + path := fmt.Sprintf("%s/.csctl.err.log", currentUser.HomeDir) + file, errf := os.OpenFile(path, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) + if errf != nil { + golog.Error("An error has occurred while handling another error. Please alert a technician immediately.") + fmt.Printf("==ERROR==\nOccurred at: %s || Message: %s\n\n%s\n\n\n", time.Now().String(), "nil", errf.Error()) + panic(errf) + } + defer file.Close() + contents := fmt.Sprintf("==ERROR==\nOccurred at: %s || Message: %s\n\n%s\n\n\n", time.Now().String(), msg, err.Error()) + _, errf = file.WriteString(contents) + if errf != nil { + golog.Error("An error has occurred while handling another error. Please alert a technician immediately.") + fmt.Printf("==ERROR==\nOccurred at: %s || Message: %s\n\n%s\n\n\n", time.Now().String(), "nil", errf.Error()) + panic(errf) + } + golog.Fatal("An error has occurred while processing the executable. A log file has been created in %s, please alert a Technician and provide the contents of this file if needed.\n", path) +} diff --git a/internal/daemon.go b/internal/daemon.go new file mode 100644 index 0000000..9a93a60 --- /dev/null +++ b/internal/daemon.go @@ -0,0 +1,95 @@ +package internal + +import ( + "bufio" + "crypto/hmac" + "crypto/sha256" + "csctl/config" + + // "csctl/config" + "encoding/hex" + "encoding/json" + "net" + "strings" + "time" +) + +// TCPData is a struct of information to be sent over to the CS daemon +type TCPData struct { + Username, Type, Message string +} + +// Account struct for user account information +type Account struct { + Username string `json:"username"` + UserID string `json:"userID"` + EmailAddress string `json:"emailAddress"` + CreatedBy string `json:"createdBy"` + CreatedAt time.Time `json:"createdAt"` + Locked bool `json:"locked"` + Tier int `json:"tier"` + SupportKey string `json:"supportKey"` + ReferralCode string `json:"referralCode"` + TotalReferrals int `json:"totalReferrals"` + Root bool `json:"root"` +} + +// FetchAccount is a helper function that fetches account information from CSD +func FetchAccount(username string) (*Account, error) { + data, err := ContactSR(&TCPData{ + Username: username, + Type: "userinfo", + }) + if err != nil { + return nil, err + } + + var account Account + err = json.Unmarshal([]byte(data), &account) + if err != nil { + return nil, err + } + return &account, nil +} + +// Contact one way contact function, sends message to CSD. Doesn't expect response back. +func Contact(data *TCPData) error { + conn, err := net.Dial("tcp", "localhost:8124") + if err != nil { + return err + } + newHmac := hmac.New(sha256.New, config.FetchHMACKey()) + jsonMarshal, err := json.Marshal(data) + if err != nil { + return err + } + newHmac.Write(jsonMarshal) + encoded := hex.EncodeToString(newHmac.Sum(nil)) + write := string(jsonMarshal) + "$" + encoded + conn.Write([]byte(write)) + conn.Close() + return nil +} + +// ContactSR two-way communication with CSD +func ContactSR(data *TCPData) (string, error) { + conn, err := net.Dial("tcp", "localhost:8124") + if err != nil { + return "", err + } + newHmac := hmac.New(sha256.New, config.FetchHMACKey()) + jsonMarshal, err := json.Marshal(data) + if err != nil { + return "", err + } + newHmac.Write(jsonMarshal) + encoded := hex.EncodeToString(newHmac.Sum(nil)) + write := string(jsonMarshal) + "$" + encoded + conn.Write([]byte(write)) + message, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + return "", err + } + defer conn.Close() + return strings.ReplaceAll(message, "\n", ""), nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..483ed91 --- /dev/null +++ b/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "csctl/cmd" + "github.com/urfave/cli/v2" + "gopkg.in/hlandau/service.v1/daemon/setuid" + "os" +) + +func main() { + err := setuid.Setresgid(115, 115, 115) + + app := &cli.App{ + Authors: []*cli.Author{ + {Name: "Matthew R", Email: "matthew@staff.libraryofcode.org"}, + }, + Copyright: "Copyright (c) 2020 Library of Code sp-us | Board of Directors", + Description: "Command line interface containing various functions relating to your CS Account.", + Usage: "Cloud Services Account Control (csctl)", + + Commands: []*cli.Command{ + { + Name: "apply", + Usage: "", + Description: "Submits various applications to EDS for a decision.", + Subcommands: []*cli.Command{ + { + Name: "p-t2", + Description: "Application for Tier 2 pre-approval.", + Action: cmd.ApplySoft, + }, + { + Name: "t2", + Description: "Application for Tier 2.", + Action: cmd.ApplyHard, + }, + }, + }, + }, + } + app.Run(os.Args) +}