diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go new file mode 100644 index 0000000..da5285d --- /dev/null +++ b/pkg/auth/auth.go @@ -0,0 +1,37 @@ +package auth + +import ( + "fmt" + "os" + + "github.com/spf13/viper" +) + +// LoadAccessToken() tries to load a JWT string from an environment +// variable, file, or config in that order. If loading the token +// fails with one options, it will fallback to the next option until +// all options are exhausted. +// +// Returns a token as a string with no error if successful. +// Alternatively, returns an empty string with an error if a token is +// not able to be loaded. +func LoadAccessToken(path string) (string, error) { + // try to load token from env var + testToken := os.Getenv("ACCESS_TOKEN") + if testToken != "" { + return testToken, nil + } + + // try reading access token from a file + b, err := os.ReadFile(path) + if err == nil { + return string(b), nil + } + + // TODO: try to load token from config + testToken = viper.GetString("access-token") + if testToken != "" { + return testToken, nil + } + return "", fmt.Errorf("failed to load token from environment variable, file, or config") +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 739332a..0005254 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -9,8 +9,6 @@ import ( "net/http" "os" "time" - - "github.com/OpenCHAMI/magellan/internal/util" ) type Option[T Client] func(client T) @@ -24,8 +22,8 @@ type Client interface { RootEndpoint(endpoint string) string // functions needed to make request - Add(data util.HTTPBody, headers util.HTTPHeader) error - Update(data util.HTTPBody, headers util.HTTPHeader) error + Add(data HTTPBody, headers HTTPHeader) error + Update(data HTTPBody, headers HTTPHeader) error } // NewClient() creates a new client @@ -71,11 +69,11 @@ func WithSecureTLS[T Client](certPath string) func(T) { // Post() is a simplified wrapper function that packages all of the // that marshals a mapper into a JSON-formatted byte array, and then performs // a request to the specified URL. -func (c *MagellanClient) Post(url string, data map[string]any, header util.HTTPHeader) (*http.Response, util.HTTPBody, error) { +func (c *MagellanClient) Post(url string, data map[string]any, header HTTPHeader) (*http.Response, HTTPBody, error) { // serialize data into byte array body, err := json.Marshal(data) if err != nil { return nil, nil, fmt.Errorf("failed to marshal data for request: %v", err) } - return util.MakeRequest(c.Client, url, http.MethodPost, body, header) + return MakeRequest(c.Client, url, http.MethodPost, body, header) } diff --git a/pkg/client/default.go b/pkg/client/default.go index e466125..2830921 100644 --- a/pkg/client/default.go +++ b/pkg/client/default.go @@ -3,8 +3,6 @@ package client import ( "fmt" "net/http" - - "github.com/OpenCHAMI/magellan/internal/util" ) type MagellanClient struct { @@ -20,13 +18,13 @@ func (c *MagellanClient) Name() string { // the first argument with no data processing or manipulation. The function sends // the data to a set callback URL (which may be changed to use a configurable value // instead). -func (c *MagellanClient) Add(data util.HTTPBody, headers util.HTTPHeader) error { +func (c *MagellanClient) Add(data HTTPBody, headers HTTPHeader) error { if data == nil { return fmt.Errorf("no data found") } path := "/inventory/add" - res, body, err := util.MakeRequest(c.Client, path, http.MethodPost, data, headers) + res, body, err := MakeRequest(c.Client, path, http.MethodPost, data, headers) if res != nil { statusOk := res.StatusCode >= 200 && res.StatusCode < 300 if !statusOk { @@ -37,13 +35,13 @@ func (c *MagellanClient) Add(data util.HTTPBody, headers util.HTTPHeader) error return err } -func (c *MagellanClient) Update(data util.HTTPBody, headers util.HTTPHeader) error { +func (c *MagellanClient) Update(data HTTPBody, headers HTTPHeader) error { if data == nil { return fmt.Errorf("no data found") } path := "/inventory/update" - res, body, err := util.MakeRequest(c.Client, path, http.MethodPut, data, headers) + res, body, err := MakeRequest(c.Client, path, http.MethodPut, data, headers) if res != nil { statusOk := res.StatusCode >= 200 && res.StatusCode < 300 if !statusOk { diff --git a/pkg/client/net.go b/pkg/client/net.go new file mode 100644 index 0000000..af69a53 --- /dev/null +++ b/pkg/client/net.go @@ -0,0 +1,178 @@ +package client + +import ( + "bytes" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + + "github.com/rs/zerolog/log" +) + +// HTTP aliases for readibility +type HTTPHeader map[string]string +type HTTPBody []byte + +func (h HTTPHeader) Authorization(accessToken string) HTTPHeader { + if accessToken != "" { + h["Authorization"] = fmt.Sprintf("Bearer %s", accessToken) + } + return h +} + +func (h HTTPHeader) ContentType(contentType string) HTTPHeader { + h["Content-Type"] = contentType + return h +} + +// GetNextIP() returns the next IP address, but does not account +// for net masks. +func GetNextIP(ip *net.IP, inc uint) *net.IP { + if ip == nil { + return &net.IP{} + } + i := ip.To4() + v := uint(i[0])<<24 + uint(i[1])<<16 + uint(i[2])<<8 + uint(i[3]) + v += inc + v3 := byte(v & 0xFF) + v2 := byte((v >> 8) & 0xFF) + v1 := byte((v >> 16) & 0xFF) + v0 := byte((v >> 24) & 0xFF) + // return &net.IP{[]byte{v0, v1, v2, v3}} + r := net.IPv4(v0, v1, v2, v3) + return &r +} + +// MakeRequest() is a wrapper function that condenses simple HTTP +// requests done to a single call. It expects an optional HTTP client, +// URL, HTTP method, request body, and request headers. This function +// is useful when making many requests where only these few arguments +// are changing. +// +// Returns a HTTP response object, response body as byte array, and any +// error that may have occurred with making the request. +func MakeRequest(client *http.Client, url string, httpMethod string, body HTTPBody, header HTTPHeader) (*http.Response, HTTPBody, error) { + // use defaults if no client provided + if client == nil { + client = http.DefaultClient + client.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + } + req, err := http.NewRequest(httpMethod, url, bytes.NewBuffer(body)) + if err != nil { + return nil, nil, fmt.Errorf("failed to create new HTTP request: %v", err) + } + req.Header.Add("User-Agent", "magellan") + for k, v := range header { + req.Header.Add(k, v) + } + res, err := client.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("failed to make request: %v", err) + } + b, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %v", err) + } + return res, b, err +} + +// FormatHostUrls() takes a list of hosts and ports and builds full URLs in the +// form of scheme://host:port. If no scheme is provided, it will use "https" by +// default. +// +// Returns a 2D string slice where each slice contains URL host strings for each +// port. The intention is to have all of the URLs for a single host combined into +// a single slice to initiate one goroutine per host, but making request to multiple +// ports. +func FormatHostUrls(hosts []string, ports []int, scheme string, verbose bool) [][]string { + // format each positional arg as a complete URL + var formattedHosts [][]string + for _, host := range hosts { + uri, err := url.ParseRequestURI(host) + if err != nil { + if verbose { + log.Warn().Msgf("invalid URI parsed: %s", host) + } + continue + } + + // check if scheme is set, if not set it with flag or default value ('https' if flag is not set) + if uri.Scheme == "" { + if scheme != "" { + uri.Scheme = scheme + } else { + // hardcoded assumption + uri.Scheme = "https" + } + } + + // tidy up slashes and update arg with new value + uri.Path = strings.TrimSuffix(uri.Path, "/") + uri.Path = strings.ReplaceAll(uri.Path, "//", "/") + + // for hosts with unspecified ports, add ports to scan from flag + if uri.Port() == "" { + var tmp []string + for _, port := range ports { + uri.Host += fmt.Sprintf(":%d", port) + tmp = append(tmp, uri.String()) + } + formattedHosts = append(formattedHosts, tmp) + } else { + formattedHosts = append(formattedHosts, []string{uri.String()}) + } + + } + return formattedHosts +} + +// FormatIPUrls() takes a list of IP addresses and ports and builds full URLs in the +// form of scheme://host:port. If no scheme is provided, it will use "https" by +// default. +// +// Returns a 2D string slice where each slice contains URL host strings for each +// port. The intention is to have all of the URLs for a single host combined into +// a single slice to initiate one goroutine per host, but making request to multiple +// ports. +func FormatIPUrls(ips []string, ports []int, scheme string, verbose bool) [][]string { + // format each positional arg as a complete URL + var formattedHosts [][]string + for _, ip := range ips { + if scheme == "" { + scheme = "https" + } + // make an entirely new object since we're expecting just IPs + uri := &url.URL{ + Scheme: scheme, + Host: ip, + } + + // tidy up slashes and update arg with new value + uri.Path = strings.ReplaceAll(uri.Path, "//", "/") + uri.Path = strings.TrimSuffix(uri.Path, "/") + + // for hosts with unspecified ports, add ports to scan from flag + if uri.Port() == "" { + if len(ports) == 0 { + ports = append(ports, 443) + } + var tmp []string + for _, port := range ports { + uri.Host += fmt.Sprintf(":%d", port) + tmp = append(tmp, uri.String()) + } + formattedHosts = append(formattedHosts, tmp) + } else { + formattedHosts = append(formattedHosts, []string{uri.String()}) + } + + } + return formattedHosts +} diff --git a/pkg/client/smd.go b/pkg/client/smd.go index ca3865e..f39d3ce 100644 --- a/pkg/client/smd.go +++ b/pkg/client/smd.go @@ -6,8 +6,6 @@ package client import ( "fmt" "net/http" - - "github.com/OpenCHAMI/magellan/internal/util" ) type SmdClient struct { @@ -31,14 +29,14 @@ func (c SmdClient) GetClient() *http.Client { // Add() has a similar function definition to that of the default implementation, // but also allows further customization and data/header manipulation that would // be specific and/or unique to SMD's API. -func (c SmdClient) Add(data util.HTTPBody, headers util.HTTPHeader) error { +func (c SmdClient) Add(data HTTPBody, headers HTTPHeader) error { if data == nil { return fmt.Errorf("failed to add redfish endpoint: no data found") } // Add redfish endpoint via POST `/hsm/v2/Inventory/RedfishEndpoints` endpoint url := c.RootEndpoint("/Inventory/RedfishEndpoints") - res, body, err := util.MakeRequest(c.Client, url, http.MethodPost, data, headers) + res, body, err := MakeRequest(c.Client, url, http.MethodPost, data, headers) if res != nil { statusOk := res.StatusCode >= 200 && res.StatusCode < 300 if !statusOk { @@ -49,13 +47,13 @@ func (c SmdClient) Add(data util.HTTPBody, headers util.HTTPHeader) error { return err } -func (c SmdClient) Update(data util.HTTPBody, headers util.HTTPHeader) error { +func (c SmdClient) Update(data HTTPBody, headers HTTPHeader) error { if data == nil { return fmt.Errorf("failed to add redfish endpoint: no data found") } // Update redfish endpoint via PUT `/hsm/v2/Inventory/RedfishEndpoints` endpoint url := c.RootEndpoint("/Inventory/RedfishEndpoints/" + c.Xname) - res, body, err := util.MakeRequest(c.Client, url, http.MethodPut, data, headers) + res, body, err := MakeRequest(c.Client, url, http.MethodPut, data, headers) fmt.Printf("%v (%v)\n%s\n", url, res.Status, string(body)) if res != nil { statusOk := res.StatusCode >= 200 && res.StatusCode < 300