@@ -21,6 +21,7 @@ import (
2121 "bytes"
2222 "crypto/hmac"
2323 "crypto/sha1"
24+ "crypto/sha512"
2425 "encoding/base64"
2526 "encoding/json"
2627 "errors"
@@ -218,6 +219,195 @@ func encodeRequestParams(params url.Values) string {
218219 return buf .String ()
219220}
220221
222+ func cloneRequestParams (params url.Values ) url.Values {
223+ cloned := make (url.Values )
224+ for key , values := range params {
225+ for _ , value := range values {
226+ cloned .Add (key , value )
227+ }
228+ }
229+ return cloned
230+ }
231+
232+ func buildAPIRequestParams (r * Request , api string , args []string ) url.Values {
233+ params := make (url.Values )
234+ params .Add ("command" , api )
235+ apiData := r .Config .GetCache ()[api ]
236+ for _ , arg := range args {
237+ if apiData != nil {
238+ skip := false
239+ for _ , fakeArg := range apiData .FakeArgs {
240+ if strings .HasPrefix (arg , fakeArg ) {
241+ skip = true
242+ break
243+ }
244+ }
245+ if skip {
246+ continue
247+ }
248+
249+ }
250+ parts := strings .SplitN (arg , "=" , 2 )
251+ if len (parts ) == 2 {
252+ key := parts [0 ]
253+ value := parts [1 ]
254+ if strings .HasPrefix (value , "\" " ) && strings .HasSuffix (value , "\" " ) {
255+ value = value [1 : len (value )- 1 ]
256+ }
257+ if strings .HasPrefix (value , "@" ) {
258+ possibleFileName := value [1 :]
259+ if fileInfo , err := os .Stat (possibleFileName ); err == nil && ! fileInfo .IsDir () {
260+ bytes , err := ioutil .ReadFile (possibleFileName )
261+ config .Debug ()
262+ if err == nil {
263+ value = string (bytes )
264+ config .Debug ("Content for argument " , key , " read from file: " , possibleFileName , " is: " , value )
265+ }
266+ }
267+ }
268+ params .Add (key , value )
269+ }
270+ }
271+ signatureversion := "3"
272+ expiresKey := "expires"
273+ params .Add ("response" , "json" )
274+ params .Add ("signatureversion" , signatureversion )
275+ params .Add (expiresKey , time .Now ().UTC ().Add (15 * time .Minute ).Format (time .RFC3339 ))
276+ return params
277+ }
278+
279+ func signRequest (unsignedRequest , secretKey , algorithm string ) (string , error ) {
280+ signatureAlgorithm , err := config .NormalizeSignatureAlgorithm (algorithm )
281+ if err != nil {
282+ return "" , err
283+ }
284+
285+ var signature []byte
286+ switch signatureAlgorithm {
287+ case config .SignatureAlgorithmHmacSHA1 :
288+ mac := hmac .New (sha1 .New , []byte (secretKey ))
289+ mac .Write ([]byte (strings .ToLower (unsignedRequest )))
290+ signature = mac .Sum (nil )
291+ case config .SignatureAlgorithmHmacSHA512 :
292+ mac := hmac .New (sha512 .New , []byte (secretKey ))
293+ mac .Write ([]byte (strings .ToLower (unsignedRequest )))
294+ signature = mac .Sum (nil )
295+ default :
296+ return "" , errors .New ("signature algorithm must be concrete" )
297+ }
298+ return base64 .StdEncoding .EncodeToString (signature ), nil
299+ }
300+
301+ func executeSignedAPIRequest (r * Request , unsignedParams url.Values , algorithm string ) (* http.Response , error ) {
302+ params := cloneRequestParams (unsignedParams )
303+ encodedParams := encodeRequestParams (params )
304+
305+ signature , err := signRequest (encodedParams , r .Config .ActiveProfile .SecretKey , algorithm )
306+ if err != nil {
307+ return nil , err
308+ }
309+ if r .Config .Core .PostRequest {
310+ params .Add ("signature" , signature )
311+ } else {
312+ encodedParams = encodedParams + fmt .Sprintf ("&signature=%s" , url .QueryEscape (signature ))
313+ params = nil
314+ }
315+
316+ requestURL := fmt .Sprintf ("%s?%s" , r .Config .ActiveProfile .URL , encodedParams )
317+ config .Debug ("NewAPIRequest API request URL:" , requestURL )
318+ return executeRequest (r , requestURL , params )
319+ }
320+
321+ func parseAPIResponse (body []byte ) (map [string ]interface {}, error ) {
322+ var data map [string ]interface {}
323+ if err := json .Unmarshal (body , & data ); err != nil {
324+ return nil , errors .New ("failed to decode response" )
325+ }
326+
327+ if apiResponse := getResponseData (data ); apiResponse != nil {
328+ if _ , ok := apiResponse ["errorcode" ]; ok {
329+ return nil , fmt .Errorf ("(HTTP %v, error code %v) %v" , apiResponse ["errorcode" ], apiResponse ["cserrorcode" ], apiResponse ["errortext" ])
330+ }
331+ return apiResponse , nil
332+ }
333+
334+ return nil , errors .New ("failed to decode response" )
335+ }
336+
337+ func isAuthenticationFailure (statusCode int , err error ) bool {
338+ if statusCode == http .StatusUnauthorized || statusCode == http .StatusForbidden {
339+ return true
340+ }
341+ if err == nil {
342+ return false
343+ }
344+ errText := strings .ToLower (err .Error ())
345+ for _ , marker := range []string {"signature" , "authenticate" , "authentication" , "credential" , "unauthoriz" , "api key" , "apikey" } {
346+ if strings .Contains (errText , marker ) {
347+ return true
348+ }
349+ }
350+ return false
351+ }
352+
353+ func persistDetectedSignatureAlgorithm (r * Request , algorithm string ) {
354+ r .Config .ActiveProfile .SignatureAlgorithm = algorithm
355+ if r .CredentialsSupplied {
356+ config .Debug ("Credentials supplied on command-line, not persisting detected signature algorithm" )
357+ return
358+ }
359+ r .Config .UpdateConfig ("signaturealgorithm" , algorithm , true )
360+ }
361+
362+ func detectSignatureAlgorithm (r * Request ) (string , error ) {
363+ attempts := []string {config .SignatureAlgorithmHmacSHA512 , config .SignatureAlgorithmHmacSHA1 }
364+ var lastErr error
365+
366+ for _ , algorithm := range attempts {
367+ config .Debug ("Trying API signature algorithm probe:" , algorithm )
368+ params := buildAPIRequestParams (r , "listApis" , []string {"listall=true" })
369+ params .Add ("apiKey" , r .Config .ActiveProfile .APIKey )
370+ response , err := executeSignedAPIRequest (r , params , algorithm )
371+ if err != nil {
372+ config .Debug ("API signature algorithm probe failed before response for " , algorithm , ": " , err )
373+ lastErr = err
374+ continue
375+ }
376+
377+ body , _ := ioutil .ReadAll (response .Body )
378+ config .Debug ("Signature algorithm probe response body:" , string (body ))
379+ if _ , err := parseAPIResponse (body ); err == nil {
380+ config .Debug ("Selected API signature algorithm:" , algorithm )
381+ persistDetectedSignatureAlgorithm (r , algorithm )
382+ return algorithm , nil
383+ } else {
384+ lastErr = err
385+ if isAuthenticationFailure (response .StatusCode , err ) {
386+ config .Debug ("API signature algorithm probe failed authentication for " , algorithm , ": " , err )
387+ } else {
388+ config .Debug ("API signature algorithm probe failed with non-authentication error for " , algorithm , ": " , err )
389+ }
390+ }
391+ }
392+
393+ config .Debug ("Signature algorithm autodetection failed; attempted algorithms:" , strings .Join (attempts , ", " ))
394+ if lastErr != nil {
395+ return "" , lastErr
396+ }
397+ return "" , errors .New ("failed to detect signature algorithm" )
398+ }
399+
400+ func activeSignatureAlgorithm (r * Request ) (string , error ) {
401+ signatureAlgorithm , err := config .NormalizeSignatureAlgorithm (r .Config .ActiveProfile .SignatureAlgorithm )
402+ if err != nil {
403+ return "" , err
404+ }
405+ if signatureAlgorithm == config .SignatureAlgorithmAuto {
406+ return detectSignatureAlgorithm (r )
407+ }
408+ return signatureAlgorithm , nil
409+ }
410+
221411func getResponseData (data map [string ]interface {}) map [string ]interface {} {
222412 for k := range data {
223413 if strings .HasSuffix (k , "response" ) {
@@ -276,72 +466,28 @@ func pollAsyncJob(r *Request, jobID string) (map[string]interface{}, error) {
276466
277467// NewAPIRequest makes an API request to configured management server
278468func NewAPIRequest (r * Request , api string , args []string , isAsync bool ) (map [string ]interface {}, error ) {
279- params := make (url.Values )
280- params .Add ("command" , api )
281- apiData := r .Config .GetCache ()[api ]
282- for _ , arg := range args {
283- if apiData != nil {
284- skip := false
285- for _ , fakeArg := range apiData .FakeArgs {
286- if strings .HasPrefix (arg , fakeArg ) {
287- skip = true
288- break
289- }
290- }
291- if skip {
292- continue
293- }
294-
295- }
296- parts := strings .SplitN (arg , "=" , 2 )
297- if len (parts ) == 2 {
298- key := parts [0 ]
299- value := parts [1 ]
300- if strings .HasPrefix (value , "\" " ) && strings .HasSuffix (value , "\" " ) {
301- value = value [1 : len (value )- 1 ]
302- }
303- if strings .HasPrefix (value , "@" ) {
304- possibleFileName := value [1 :]
305- if fileInfo , err := os .Stat (possibleFileName ); err == nil && ! fileInfo .IsDir () {
306- bytes , err := ioutil .ReadFile (possibleFileName )
307- config .Debug ()
308- if err == nil {
309- value = string (bytes )
310- config .Debug ("Content for argument " , key , " read from file: " , possibleFileName , " is: " , value )
311- }
312- }
313- }
314- params .Add (key , value )
315- }
316- }
317- signatureversion := "3"
318- expiresKey := "expires"
319- params .Add ("response" , "json" )
320- params .Add ("signatureversion" , signatureversion )
321- params .Add (expiresKey , time .Now ().UTC ().Add (15 * time .Minute ).Format (time .RFC3339 ))
469+ params := buildAPIRequestParams (r , api , args )
322470
323471 var encodedParams string
324472 var err error
473+ usingSessionAuth := false
325474
326475 if len (r .Config .ActiveProfile .APIKey ) > 0 && len (r .Config .ActiveProfile .SecretKey ) > 0 {
327476 apiKey := r .Config .ActiveProfile .APIKey
328- secretKey := r .Config .ActiveProfile .SecretKey
329-
330477 if len (apiKey ) > 0 {
331478 params .Add ("apiKey" , apiKey )
332479 }
333- encodedParams = encodeRequestParams (params )
334-
335- mac := hmac .New (sha1 .New , []byte (secretKey ))
336- mac .Write ([]byte (strings .ToLower (encodedParams )))
337- signature := base64 .StdEncoding .EncodeToString (mac .Sum (nil ))
338- if r .Config .Core .PostRequest {
339- params .Add ("signature" , signature )
340- } else {
341- encodedParams = encodedParams + fmt .Sprintf ("&signature=%s" , url .QueryEscape (signature ))
342- params = nil
480+ signatureAlgorithm , err := activeSignatureAlgorithm (r )
481+ if err != nil {
482+ return nil , err
343483 }
484+ response , err := executeSignedAPIRequest (r , params , signatureAlgorithm )
485+ if err != nil {
486+ return nil , err
487+ }
488+ return processAPIResponse (r , response , isAsync )
344489 } else if len (r .Config .ActiveProfile .Username ) > 0 && len (r .Config .ActiveProfile .Password ) > 0 {
490+ usingSessionAuth = true
345491 sessionKey , err := Login (r )
346492 if err != nil {
347493 return nil , err
@@ -367,7 +513,7 @@ func NewAPIRequest(r *Request, api string, args []string, isAsync bool) (map[str
367513 config .Debug ("Credentials supplied on command-line, not falling back to login" )
368514 }
369515
370- if response .StatusCode == http .StatusUnauthorized && ! r .CredentialsSupplied {
516+ if usingSessionAuth && response .StatusCode == http .StatusUnauthorized && ! r .CredentialsSupplied {
371517 r .Client ().Jar , _ = cookiejar .New (nil )
372518 sessionKey , err := Login (r )
373519 if err != nil {
@@ -384,27 +530,26 @@ func NewAPIRequest(r *Request, api string, args []string, isAsync bool) (map[str
384530 }
385531 }
386532
533+ return processAPIResponse (r , response , isAsync )
534+ }
535+
536+ func processAPIResponse (r * Request , response * http.Response , isAsync bool ) (map [string ]interface {}, error ) {
387537 body , _ := ioutil .ReadAll (response .Body )
388538 config .Debug ("NewAPIRequest response body:" , string (body ))
389539
390- var data map [string ]interface {}
391- _ = json .Unmarshal ([]byte (body ), & data )
540+ apiResponse , err := parseAPIResponse (body )
541+ if err != nil {
542+ return nil , err
543+ }
392544
393545 if isAsync && r .Config .Core .AsyncBlock {
394- if jobResponse := getResponseData ( data ); jobResponse != nil && jobResponse ["jobid" ] != nil {
395- jobID := jobResponse ["jobid" ].(string )
546+ if apiResponse ["jobid" ] != nil {
547+ jobID := apiResponse ["jobid" ].(string )
396548 return pollAsyncJob (r , jobID )
397549 }
398550 }
399551
400- if apiResponse := getResponseData (data ); apiResponse != nil {
401- if _ , ok := apiResponse ["errorcode" ]; ok {
402- return nil , fmt .Errorf ("(HTTP %v, error code %v) %v" , apiResponse ["errorcode" ], apiResponse ["cserrorcode" ], apiResponse ["errortext" ])
403- }
404- return apiResponse , nil
405- }
406-
407- return nil , errors .New ("failed to decode response" )
552+ return apiResponse , nil
408553}
409554
410555// we can implement further conditions to do POST or GET (or other http commands) here
0 commit comments