2
0
mirror of https://github.com/xcat2/confluent.git synced 2025-09-23 10:38:14 +00:00

Add a general utility for confluent in golang

This commit is contained in:
Jarrod Johnson
2025-03-07 17:16:13 -05:00
parent 28c929aec6
commit 13a6493100
5 changed files with 274 additions and 0 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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=

View File

@@ -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")
}
}