Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 213 additions & 68 deletions cmd/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"bytes"
"crypto/hmac"
"crypto/sha1"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"errors"
Expand Down Expand Up @@ -218,6 +219,195 @@
return buf.String()
}

func cloneRequestParams(params url.Values) url.Values {
cloned := make(url.Values)
for key, values := range params {
for _, value := range values {
cloned.Add(key, value)
}
}
return cloned
}

func buildAPIRequestParams(r *Request, api string, args []string) url.Values {
params := make(url.Values)
params.Add("command", api)
apiData := r.Config.GetCache()[api]
for _, arg := range args {
if apiData != nil {
skip := false
for _, fakeArg := range apiData.FakeArgs {
if strings.HasPrefix(arg, fakeArg) {
skip = true
break
}
}
if skip {
continue
}

}
parts := strings.SplitN(arg, "=", 2)
if len(parts) == 2 {
key := parts[0]
value := parts[1]
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
value = value[1 : len(value)-1]
}
if strings.HasPrefix(value, "@") {
possibleFileName := value[1:]
if fileInfo, err := os.Stat(possibleFileName); err == nil && !fileInfo.IsDir() {
bytes, err := ioutil.ReadFile(possibleFileName)
config.Debug()
if err == nil {
value = string(bytes)
config.Debug("Content for argument ", key, " read from file: ", possibleFileName, " is: ", value)
}
}
}
params.Add(key, value)
}
}
signatureversion := "3"
expiresKey := "expires"
params.Add("response", "json")
params.Add("signatureversion", signatureversion)
params.Add(expiresKey, time.Now().UTC().Add(15*time.Minute).Format(time.RFC3339))
return params
}

func signRequest(unsignedRequest, secretKey, algorithm string) (string, error) {
signatureAlgorithm, err := config.NormalizeSignatureAlgorithm(algorithm)
if err != nil {
return "", err
}

var signature []byte
switch signatureAlgorithm {
case config.SignatureAlgorithmHmacSHA1:
mac := hmac.New(sha1.New, []byte(secretKey))
mac.Write([]byte(strings.ToLower(unsignedRequest)))
signature = mac.Sum(nil)
case config.SignatureAlgorithmHmacSHA512:
mac := hmac.New(sha512.New, []byte(secretKey))
mac.Write([]byte(strings.ToLower(unsignedRequest)))
signature = mac.Sum(nil)
default:
return "", errors.New("signature algorithm must be concrete")
}
return base64.StdEncoding.EncodeToString(signature), nil
}

func executeSignedAPIRequest(r *Request, unsignedParams url.Values, algorithm string) (*http.Response, error) {
params := cloneRequestParams(unsignedParams)
encodedParams := encodeRequestParams(params)

signature, err := signRequest(encodedParams, r.Config.ActiveProfile.SecretKey, algorithm)
if err != nil {
return nil, err
}
if r.Config.Core.PostRequest {
params.Add("signature", signature)
} else {
encodedParams = encodedParams + fmt.Sprintf("&signature=%s", url.QueryEscape(signature))
params = nil
}

requestURL := fmt.Sprintf("%s?%s", r.Config.ActiveProfile.URL, encodedParams)
config.Debug("NewAPIRequest API request URL:", requestURL)
return executeRequest(r, requestURL, params)
}

func parseAPIResponse(body []byte) (map[string]interface{}, error) {
var data map[string]interface{}
if err := json.Unmarshal(body, &data); err != nil {
return nil, errors.New("failed to decode response")
}

if apiResponse := getResponseData(data); apiResponse != nil {
if _, ok := apiResponse["errorcode"]; ok {
return nil, fmt.Errorf("(HTTP %v, error code %v) %v", apiResponse["errorcode"], apiResponse["cserrorcode"], apiResponse["errortext"])
}
return apiResponse, nil
}

return nil, errors.New("failed to decode response")
}

func isAuthenticationFailure(statusCode int, err error) bool {
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
return true
}
if err == nil {
return false
}
errText := strings.ToLower(err.Error())
for _, marker := range []string{"signature", "authenticate", "authentication", "credential", "unauthoriz", "api key", "apikey"} {
if strings.Contains(errText, marker) {
return true
}
}
return false
}

func persistDetectedSignatureAlgorithm(r *Request, algorithm string) {
r.Config.ActiveProfile.SignatureAlgorithm = algorithm
if r.CredentialsSupplied {
config.Debug("Credentials supplied on command-line, not persisting detected signature algorithm")
return
}
r.Config.UpdateConfig("signaturealgorithm", algorithm, true)
}

func detectSignatureAlgorithm(r *Request) (string, error) {
attempts := []string{config.SignatureAlgorithmHmacSHA512, config.SignatureAlgorithmHmacSHA1}
var lastErr error

for _, algorithm := range attempts {
config.Debug("Trying API signature algorithm probe:", algorithm)
params := buildAPIRequestParams(r, "listApis", []string{"listall=true"})
params.Add("apiKey", r.Config.ActiveProfile.APIKey)
response, err := executeSignedAPIRequest(r, params, algorithm)
if err != nil {
config.Debug("API signature algorithm probe failed before response for ", algorithm, ": ", err)
lastErr = err
continue
}

body, _ := ioutil.ReadAll(response.Body)
config.Debug("Signature algorithm probe response body:", string(body))
if _, err := parseAPIResponse(body); err == nil {
Comment on lines +377 to +379
config.Debug("Selected API signature algorithm:", algorithm)
persistDetectedSignatureAlgorithm(r, algorithm)
return algorithm, nil
} else {

Check failure on line 383 in cmd/network.go

View workflow job for this annotation

GitHub Actions / Run make lint

if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)

Check failure on line 383 in cmd/network.go

View workflow job for this annotation

GitHub Actions / Run make lint

if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
lastErr = err
if isAuthenticationFailure(response.StatusCode, err) {
config.Debug("API signature algorithm probe failed authentication for ", algorithm, ": ", err)
} else {
config.Debug("API signature algorithm probe failed with non-authentication error for ", algorithm, ": ", err)
}
}
Comment on lines +383 to +390
}

config.Debug("Signature algorithm autodetection failed; attempted algorithms:", strings.Join(attempts, ", "))
if lastErr != nil {
return "", lastErr
}
return "", errors.New("failed to detect signature algorithm")
}

func activeSignatureAlgorithm(r *Request) (string, error) {
signatureAlgorithm, err := config.NormalizeSignatureAlgorithm(r.Config.ActiveProfile.SignatureAlgorithm)
if err != nil {
return "", err
}
if signatureAlgorithm == config.SignatureAlgorithmAuto {
return detectSignatureAlgorithm(r)
}
return signatureAlgorithm, nil
}

func getResponseData(data map[string]interface{}) map[string]interface{} {
for k := range data {
if strings.HasSuffix(k, "response") {
Expand Down Expand Up @@ -276,72 +466,28 @@

// NewAPIRequest makes an API request to configured management server
func NewAPIRequest(r *Request, api string, args []string, isAsync bool) (map[string]interface{}, error) {
params := make(url.Values)
params.Add("command", api)
apiData := r.Config.GetCache()[api]
for _, arg := range args {
if apiData != nil {
skip := false
for _, fakeArg := range apiData.FakeArgs {
if strings.HasPrefix(arg, fakeArg) {
skip = true
break
}
}
if skip {
continue
}

}
parts := strings.SplitN(arg, "=", 2)
if len(parts) == 2 {
key := parts[0]
value := parts[1]
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
value = value[1 : len(value)-1]
}
if strings.HasPrefix(value, "@") {
possibleFileName := value[1:]
if fileInfo, err := os.Stat(possibleFileName); err == nil && !fileInfo.IsDir() {
bytes, err := ioutil.ReadFile(possibleFileName)
config.Debug()
if err == nil {
value = string(bytes)
config.Debug("Content for argument ", key, " read from file: ", possibleFileName, " is: ", value)
}
}
}
params.Add(key, value)
}
}
signatureversion := "3"
expiresKey := "expires"
params.Add("response", "json")
params.Add("signatureversion", signatureversion)
params.Add(expiresKey, time.Now().UTC().Add(15*time.Minute).Format(time.RFC3339))
params := buildAPIRequestParams(r, api, args)

var encodedParams string
var err error
usingSessionAuth := false

if len(r.Config.ActiveProfile.APIKey) > 0 && len(r.Config.ActiveProfile.SecretKey) > 0 {
apiKey := r.Config.ActiveProfile.APIKey
secretKey := r.Config.ActiveProfile.SecretKey

if len(apiKey) > 0 {
params.Add("apiKey", apiKey)
}
encodedParams = encodeRequestParams(params)

mac := hmac.New(sha1.New, []byte(secretKey))
mac.Write([]byte(strings.ToLower(encodedParams)))
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
if r.Config.Core.PostRequest {
params.Add("signature", signature)
} else {
encodedParams = encodedParams + fmt.Sprintf("&signature=%s", url.QueryEscape(signature))
params = nil
signatureAlgorithm, err := activeSignatureAlgorithm(r)
if err != nil {
return nil, err
}
response, err := executeSignedAPIRequest(r, params, signatureAlgorithm)
if err != nil {
return nil, err
}
return processAPIResponse(r, response, isAsync)
} else if len(r.Config.ActiveProfile.Username) > 0 && len(r.Config.ActiveProfile.Password) > 0 {
usingSessionAuth = true
sessionKey, err := Login(r)
if err != nil {
return nil, err
Expand All @@ -367,7 +513,7 @@
config.Debug("Credentials supplied on command-line, not falling back to login")
}

if response.StatusCode == http.StatusUnauthorized && !r.CredentialsSupplied {
if usingSessionAuth && response.StatusCode == http.StatusUnauthorized && !r.CredentialsSupplied {
r.Client().Jar, _ = cookiejar.New(nil)
sessionKey, err := Login(r)
if err != nil {
Expand All @@ -384,27 +530,26 @@
}
}

return processAPIResponse(r, response, isAsync)
}

func processAPIResponse(r *Request, response *http.Response, isAsync bool) (map[string]interface{}, error) {
body, _ := ioutil.ReadAll(response.Body)
config.Debug("NewAPIRequest response body:", string(body))
Comment on lines +536 to 538

var data map[string]interface{}
_ = json.Unmarshal([]byte(body), &data)
apiResponse, err := parseAPIResponse(body)
if err != nil {
return nil, err
}

if isAsync && r.Config.Core.AsyncBlock {
if jobResponse := getResponseData(data); jobResponse != nil && jobResponse["jobid"] != nil {
jobID := jobResponse["jobid"].(string)
if apiResponse["jobid"] != nil {
jobID := apiResponse["jobid"].(string)
return pollAsyncJob(r, jobID)
}
}

if apiResponse := getResponseData(data); apiResponse != nil {
if _, ok := apiResponse["errorcode"]; ok {
return nil, fmt.Errorf("(HTTP %v, error code %v) %v", apiResponse["errorcode"], apiResponse["cserrorcode"], apiResponse["errortext"])
}
return apiResponse, nil
}

return nil, errors.New("failed to decode response")
return apiResponse, nil
}

// we can implement further conditions to do POST or GET (or other http commands) here
Expand Down
Loading
Loading