blob: 6333dabee20b5d2fb4518117ad120876a7af6b24 [file]
// 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)
}
}