diff --git a/confluent_osdeploy/utils/confusebox/apiclient.go b/confluent_osdeploy/utils/confusebox/apiclient.go new file mode 100644 index 00000000..81360a0c --- /dev/null +++ b/confluent_osdeploy/utils/confusebox/apiclient.go @@ -0,0 +1,133 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "os" + "net/http" + "crypto/x509" + "crypto/tls" + "strings" + "errors" +) +type ApiClient struct { + server string + urlserver string + apikey string + nodename string + webclient *http.Client +} + +func NewApiClient(cafile string, keyfile string, nodename string, server string) (*ApiClient, error) { + currcacerts, err := os.ReadFile(cafile) + if err != nil { + return nil, err + } + cacerts := x509.NewCertPool() + cacerts.AppendCertsFromPEM(currcacerts) + apikey := []byte("") + if keyfile != "" { + apikey, err = os.ReadFile(keyfile) + if err != nil { + return nil, err + } + if apikey[len(apikey) - 1] == 0xa { + apikey = apikey[:len(apikey)-1] + } + } + if nodename == "" { + cinfo, err := os.ReadFile("/etc/confluent/confliuent.info") + if err != nil { + nodename, err = os.Hostname() + if err != nil { return nil, err } + } + cinfolines := bytes.Split(cinfo, []byte("\n")) + if bytes.Contains(cinfolines[0], []byte("NODENAME")) { + cnodebytes := bytes.Split(cinfolines[0], []byte(" ")) + nodename = string(cnodebytes[0]) + } + } + urlserver := server + if strings.Contains(server, ":") { + if strings.Contains(server, "%") && !strings.Contains(server, "%25") { + server = strings.Replace(server, "%", "%25", 1) + } + urlserver = fmt.Sprintf("[%s]", server) + if strings.Contains(server, "%") { + server = server[:strings.Index(server, "%")] + } + } + webclient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: cacerts, + ServerName: server, + }, + }, + } + vc := ApiClient{server, urlserver, string(apikey), nodename, webclient} + return &vc, nil +} + +func (apiclient *ApiClient) RegisterKey(crypted string, hmac string) (error) { + cryptbytes := []byte(crypted) + _, err := apiclient.request("/confluent-api/self/registerapikey", "", &cryptbytes, "", hmac) + return err +} + +func (apiclient *ApiClient) Fetch(url string, outputfile string, mime string) (error) { + outp, err := os.Create(outputfile) + if err != nil { return err } + defer outp.Close() + rsp, err := apiclient.request(url, mime, nil, "", "") + if err != nil { return err } + _, err = io.Copy(outp, rsp) + return err +} + +func (apiclient *ApiClient) GrabText(url string, mime string) (string, error){ + rsp, err := apiclient.request(url, mime, nil, "", "") + if err != nil { return "", err } + rspdata, err := io.ReadAll(rsp) + if err != nil { return "", err } + rsptxt := string(rspdata) + return rsptxt, nil +} + +func (apiclient *ApiClient) request(url string, mime string, body *[]byte, method string, hmac string) (io.ReadCloser, error) { + if ! strings.Contains(url, "https://") { + url = fmt.Sprintf("https://%s%s", apiclient.urlserver, url) + } + if method == "" { + if body != nil { + method = http.MethodPost + } else { + method = http.MethodGet + } + } + var err error + var rq *http.Request + if body == nil { + rq, err = http.NewRequest(method, url, nil) + } else { + rq, err = http.NewRequest(method, url, bytes.NewBuffer(*body)) + } + if err != nil { return nil, err } + if (mime != "") { rq.Header.Set("Accept", mime) } + rq.Header.Set("CONFLUENT_NODENAME", apiclient.nodename) + if len(hmac) > 0 { + rq.Header.Set("CONFLUENT_CRYPTHMAC", hmac) + } else { + + rq.Header.Set("CONFLUENT_APIKEY", apiclient.apikey) + } + rsp, err := apiclient.webclient.Do(rq) + if err != nil { return nil, err } + if rsp.StatusCode >= 300 { + err = errors.New(rsp.Status) + return nil, err + } + return rsp.Body, err +} + diff --git a/confluent_osdeploy/utils/confusebox/genpasshmac.go b/confluent_osdeploy/utils/confusebox/genpasshmac.go new file mode 100644 index 00000000..3e8fabf0 --- /dev/null +++ b/confluent_osdeploy/utils/confusebox/genpasshmac.go @@ -0,0 +1,44 @@ +package main + +import ( + "bytes" + "github.com/go-crypt/crypt/algorithm/shacrypt" + "os" + "crypto/rand" + "encoding/base64" + "crypto/hmac" + "crypto/sha256" +) + +func genpasshmac(hmackeyfile string) (string, string, string, error) { + randbytes := make([]byte, 36) + _, err := rand.Read(randbytes) + if err != nil { + panic(err) + } + password := base64.StdEncoding.EncodeToString(randbytes) + hasher, err := shacrypt.New(shacrypt.WithVariant(shacrypt.VariantSHA256), shacrypt.WithIterations(5000)) + if err != nil { + panic(err) + } + + digest, err := hasher.Hash(password) + if err != nil { + panic(err) + } + cryptpass := digest.Encode() + hmackey, err := os.ReadFile(hmackeyfile) + if err != nil { return "", "", "", err } + keylines := bytes.Split(hmackey, []byte("\n")) + if bytes.Contains(keylines[0], []byte("apitoken:")) { + keyparts := bytes.Split(keylines[0], []byte(" ")) + hmackey = keyparts[1] + } + + hmacer := hmac.New(sha256.New, hmackey) + hmacer.Write([]byte(cryptpass)) + hmacresult := hmacer.Sum(nil) + hmacout := base64.StdEncoding.EncodeToString(hmacresult) + return password, cryptpass, hmacout, nil +} + diff --git a/confluent_osdeploy/utils/confusebox/go.mod b/confluent_osdeploy/utils/confusebox/go.mod new file mode 100644 index 00000000..424bbb1c --- /dev/null +++ b/confluent_osdeploy/utils/confusebox/go.mod @@ -0,0 +1,7 @@ +module confusebox + +go 1.23.6 + +require github.com/go-crypt/crypt v0.3.3 + +require github.com/go-crypt/x v0.3.4 // indirect diff --git a/confluent_osdeploy/utils/confusebox/go.sum b/confluent_osdeploy/utils/confusebox/go.sum new file mode 100644 index 00000000..cae66a5c --- /dev/null +++ b/confluent_osdeploy/utils/confusebox/go.sum @@ -0,0 +1,4 @@ +github.com/go-crypt/crypt v0.3.3 h1:mBSh8U+vwDm3V+UHNMQqsxV0clzlvKbLcJXcafYFpCs= +github.com/go-crypt/crypt v0.3.3/go.mod h1:ex5C1b58/tzCW6/rJfcdf5Y2TjgzmWVtX57sjpN3pUQ= +github.com/go-crypt/x v0.3.4 h1:zgpaI55VOAbkkRup9+tLaZ02IWTV/xz63tohoY0t9+Y= +github.com/go-crypt/x v0.3.4/go.mod h1:+uHWqfzD3S6YWxm18/Qp+4VcuBb0Le9dGUhX0zaWicU= diff --git a/confluent_osdeploy/utils/confusebox/main.go b/confluent_osdeploy/utils/confusebox/main.go new file mode 100644 index 00000000..1cc87413 --- /dev/null +++ b/confluent_osdeploy/utils/confusebox/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "bytes" + "flag" + "os" + "fmt" +) + +func main() { + var nodename string + var cacerts string + var apikey string + var usejson bool + var confluentsrv string + hmacreg := flag.NewFlagSet("hmacregister", flag.ExitOnError) + hmacreg.StringVar(&apikey, "k", "/etc/confluent/apikey", "Output file for the api key") + hmacKey := hmacreg.String("i", "", "Identity yaml file") + hmacreg.StringVar(&cacerts, "c", "/etc/confluent/ca.pem", "Certeficate authorities to use in PEM") + hmacreg.StringVar(&nodename, "n", "", "Node name") + hmacreg.StringVar(&confluentsrv, "s", "", "Confluent server to request from") + + invokeapi := flag.NewFlagSet("invoke", flag.ExitOnError) + invokeapi.StringVar(&nodename, "n", "", "Node name") + + invokeapi.StringVar(&cacerts, "c", "/etc/confluent/ca.pem", "Certeficate authorities to use in PEM") + invokeapi.StringVar(&apikey, "k", "/etc/confluent/confluent.apikey", "File containing Confluent API key") + invokeapi.BoolVar(&usejson, "j", false, "Request JSON formatted reply") + outputfile := invokeapi.String("o", "", "Filename to store download to") + invokeapi.StringVar(&confluentsrv, "s", "", "Confluent server to request from") + + if confluentsrv == "" { + dcfg, err := os.ReadFile("/etc/confluent/confluent.deploycfg") + if err == nil { + dcfglines := bytes.Split(dcfg, []byte("\n")) + for _, dcfgline := range(dcfglines) { + dkeyval := bytes.Split(dcfgline, []byte(" ")) + if bytes.Contains(dkeyval[0], []byte("deploy_server")) && (bytes.Contains(dkeyval[1], []byte(".")) || bytes.Contains(dkeyval[1], []byte(":"))) { + confluentsrv = string(dkeyval[1]) + } + } + + + } else { + _, err := os.ReadFile("/etc/confluent/confluent.info") + if err != nil { + panic("Unable to determine Confluent server") + } + } + } + + if len(os.Args) < 2 { + panic("Insufficient arguments, no subcommand") + } + switch os.Args[1] { + case "hmacregister": + hmacreg.Parse(os.Args[2:]) + password, crypted, hmac, err := genpasshmac(*hmacKey) + if err != nil { panic(err) } + //apiclient(cacerts, "/confluent-api/self/registerapikey", apikey, nodename, usejson) + apiclient, err := NewApiClient(cacerts, "", nodename, confluentsrv) + if err != nil { panic(err) } + err = apiclient.RegisterKey(crypted, hmac) + if err != nil { panic(err) } + outp, err := os.Create(apikey) + if err != nil { panic(err) } + defer outp.Close() + outp.Write([]byte(password)) + case "invoke": + invokeapi.Parse(os.Args[2:]) + apiclient, err := NewApiClient(cacerts, apikey, nodename, confluentsrv) + if err != nil { panic(err) } + mime := "" + if usejson { + mime = "application/json" + } + if *outputfile != "" { + apiclient.Fetch(invokeapi.Arg(0), *outputfile, mime) + } + rsp, err := apiclient.GrabText(invokeapi.Arg(0), mime) + if err != nil { panic(err) } + fmt.Println(rsp) + default: + panic("Unrecognized subcommand") + } +}