| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| package cmd |
| |
| import ( |
| "fmt" |
| "net/http" |
| "net/http/httptest" |
| "net/url" |
| "path/filepath" |
| "strings" |
| "sync" |
| "testing" |
| |
| "github.com/apache/cloudstack-cloudmonkey/config" |
| ) |
| |
| const ( |
| testAPIKey = "api-key" |
| testSecretKey = "secret-key" |
| ) |
| |
| type signatureAttempt struct { |
| Command string |
| Algorithm string |
| } |
| |
| type signatureTestServer struct { |
| server *httptest.Server |
| attempts []signatureAttempt |
| mu sync.Mutex |
| } |
| |
| func newSignatureTestServer(t *testing.T, allowedAlgorithms map[string]bool) *signatureTestServer { |
| t.Helper() |
| testServer := &signatureTestServer{} |
| server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { |
| if err := req.ParseForm(); err != nil { |
| http.Error(w, "failed to parse request form", http.StatusBadRequest) |
| return |
| } |
| |
| command := req.Form.Get("command") |
| algorithm := identifySignatureAlgorithm(req.Form) |
| testServer.mu.Lock() |
| testServer.attempts = append(testServer.attempts, signatureAttempt{ |
| Command: command, |
| Algorithm: algorithm, |
| }) |
| testServer.mu.Unlock() |
| |
| w.Header().Set("Content-Type", "application/json") |
| if !allowedAlgorithms[algorithm] { |
| w.WriteHeader(http.StatusUnauthorized) |
| fmt.Fprint(w, `{"errorresponse":{"errorcode":401,"cserrorcode":9999,"errortext":"unable to verify request signature"}}`) |
| return |
| } |
| |
| fmt.Fprint(w, successResponse(command)) |
| })) |
| testServer.server = server |
| t.Cleanup(server.Close) |
| return testServer |
| } |
| |
| func identifySignatureAlgorithm(form url.Values) string { |
| unsigned := cloneRequestParams(form) |
| signature := unsigned.Get("signature") |
| unsigned.Del("signature") |
| |
| unsignedRequest := encodeRequestParams(unsigned) |
| for _, algorithm := range []string{config.SignatureAlgorithmHmacSHA512, config.SignatureAlgorithmHmacSHA1} { |
| expected, err := signRequest(unsignedRequest, testSecretKey, algorithm) |
| if err != nil { |
| return "unknown" |
| } |
| if signature == expected { |
| return algorithm |
| } |
| } |
| return "unknown" |
| } |
| |
| func successResponse(command string) string { |
| responseKey := strings.ToLower(command) + "response" |
| if strings.EqualFold(command, "listApis") { |
| return `{"listapisresponse":{"count":0,"api":[]}}` |
| } |
| return fmt.Sprintf(`{"%s":{"success":true}}`, responseKey) |
| } |
| |
| func newTestRequest(t *testing.T, serverURL string, signatureAlgorithm string) *Request { |
| t.Helper() |
| dir := t.TempDir() |
| cfg := &config.Config{ |
| Dir: dir, |
| ConfigFile: filepath.Join(dir, "config"), |
| HistoryFile: filepath.Join(dir, "history"), |
| Core: &config.Core{ |
| Prompt: "cmk", |
| AsyncBlock: true, |
| Timeout: 30, |
| Output: config.JSON, |
| VerifyCert: true, |
| ProfileName: "localcloud", |
| AutoComplete: true, |
| PostRequest: true, |
| }, |
| ActiveProfile: &config.ServerProfile{ |
| URL: serverURL, |
| Domain: "/", |
| APIKey: testAPIKey, |
| SecretKey: testSecretKey, |
| SignatureAlgorithm: signatureAlgorithm, |
| Client: http.DefaultClient, |
| }, |
| } |
| return NewRequest(GetAPIHandler(), cfg, nil, false) |
| } |
| |
| func (s *signatureTestServer) snapshotAttempts() []signatureAttempt { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| attempts := make([]signatureAttempt, len(s.attempts)) |
| copy(attempts, s.attempts) |
| return attempts |
| } |
| |
| func TestSignRequestHmacSHA1(t *testing.T) { |
| got, err := signRequest("apikey=abc&command=listZones&response=json", "secret", config.SignatureAlgorithmHmacSHA1) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got != "tcMI1Kpm20pLhrrVYtCCcualuBU=" { |
| t.Fatalf("HmacSHA1 signature = %q", got) |
| } |
| } |
| |
| func TestSignRequestHmacSHA512(t *testing.T) { |
| got, err := signRequest("apikey=abc&command=listZones&response=json", "secret", config.SignatureAlgorithmHmacSHA512) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got != "/JUUf9SPvNd0sjdbfIxsCxYItNUlGauI+T71cOhHd5fYffHuVAXIba9RzFBK+empezGCzlhw4+R9LFri3CG+oQ==" { |
| t.Fatalf("HmacSHA512 signature = %q", got) |
| } |
| } |
| |
| func TestExplicitHmacSHA512DoesNotTryHmacSHA1(t *testing.T) { |
| server := newSignatureTestServer(t, map[string]bool{ |
| config.SignatureAlgorithmHmacSHA1: true, |
| }) |
| request := newTestRequest(t, server.server.URL, config.SignatureAlgorithmHmacSHA512) |
| |
| if _, err := NewAPIRequest(request, "listZones", nil, false); err == nil { |
| t.Fatal("expected explicit HmacSHA512 request to fail") |
| } |
| |
| attempts := server.snapshotAttempts() |
| if len(attempts) != 1 { |
| t.Fatalf("attempt count = %d, want 1", len(attempts)) |
| } |
| if attempts[0].Algorithm != config.SignatureAlgorithmHmacSHA512 { |
| t.Fatalf("attempted algorithm = %q, want %q", attempts[0].Algorithm, config.SignatureAlgorithmHmacSHA512) |
| } |
| } |
| |
| func TestAutoPersistsHmacSHA512WhenProbeSucceeds(t *testing.T) { |
| server := newSignatureTestServer(t, map[string]bool{ |
| config.SignatureAlgorithmHmacSHA512: true, |
| }) |
| request := newTestRequest(t, server.server.URL, config.SignatureAlgorithmAuto) |
| |
| if _, err := NewAPIRequest(request, "listZones", nil, false); err != nil { |
| t.Fatal(err) |
| } |
| |
| if got := request.Config.ActiveProfile.SignatureAlgorithm; got != config.SignatureAlgorithmHmacSHA512 { |
| t.Fatalf("persisted signature algorithm = %q, want %q", got, config.SignatureAlgorithmHmacSHA512) |
| } |
| attempts := server.snapshotAttempts() |
| if len(attempts) != 2 { |
| t.Fatalf("attempt count = %d, want 2", len(attempts)) |
| } |
| if attempts[0] != (signatureAttempt{Command: "listApis", Algorithm: config.SignatureAlgorithmHmacSHA512}) { |
| t.Fatalf("first attempt = %+v, want listApis with HmacSHA512", attempts[0]) |
| } |
| if attempts[1] != (signatureAttempt{Command: "listZones", Algorithm: config.SignatureAlgorithmHmacSHA512}) { |
| t.Fatalf("second attempt = %+v, want listZones with HmacSHA512", attempts[1]) |
| } |
| } |
| |
| func TestAutoFallsBackAndPersistsHmacSHA1(t *testing.T) { |
| server := newSignatureTestServer(t, map[string]bool{ |
| config.SignatureAlgorithmHmacSHA1: true, |
| }) |
| request := newTestRequest(t, server.server.URL, config.SignatureAlgorithmAuto) |
| |
| if _, err := NewAPIRequest(request, "listZones", nil, false); err != nil { |
| t.Fatal(err) |
| } |
| |
| if got := request.Config.ActiveProfile.SignatureAlgorithm; got != config.SignatureAlgorithmHmacSHA1 { |
| t.Fatalf("persisted signature algorithm = %q, want %q", got, config.SignatureAlgorithmHmacSHA1) |
| } |
| attempts := server.snapshotAttempts() |
| if len(attempts) != 3 { |
| t.Fatalf("attempt count = %d, want 3", len(attempts)) |
| } |
| if attempts[0] != (signatureAttempt{Command: "listApis", Algorithm: config.SignatureAlgorithmHmacSHA512}) { |
| t.Fatalf("first attempt = %+v, want listApis with HmacSHA512", attempts[0]) |
| } |
| if attempts[1] != (signatureAttempt{Command: "listApis", Algorithm: config.SignatureAlgorithmHmacSHA1}) { |
| t.Fatalf("second attempt = %+v, want listApis with HmacSHA1", attempts[1]) |
| } |
| if attempts[2] != (signatureAttempt{Command: "listZones", Algorithm: config.SignatureAlgorithmHmacSHA1}) { |
| t.Fatalf("third attempt = %+v, want listZones with HmacSHA1", attempts[2]) |
| } |
| } |
| |
| func TestAutoDoesNotRetryUserCommandDirectly(t *testing.T) { |
| server := newSignatureTestServer(t, map[string]bool{ |
| config.SignatureAlgorithmHmacSHA1: true, |
| }) |
| request := newTestRequest(t, server.server.URL, config.SignatureAlgorithmAuto) |
| |
| if _, err := NewAPIRequest(request, "deployVirtualMachine", []string{"serviceofferingid=1"}, false); err != nil { |
| t.Fatal(err) |
| } |
| |
| userCommandAttempts := 0 |
| for _, attempt := range server.snapshotAttempts() { |
| if attempt.Command == "deployVirtualMachine" { |
| userCommandAttempts++ |
| } |
| } |
| if userCommandAttempts != 1 { |
| t.Fatalf("deployVirtualMachine attempt count = %d, want 1", userCommandAttempts) |
| } |
| } |