github.com/go-chef/chef@v0.30.1/http_test.go (about) 1 package chef 2 3 import ( 4 "bytes" 5 "crypto/rsa" 6 "crypto/x509" 7 "encoding/pem" 8 "errors" 9 "fmt" 10 "github.com/hashicorp/go-retryablehttp" 11 "github.com/stretchr/testify/assert" 12 "io" 13 "math/big" 14 "net/http" 15 "net/http/httptest" 16 "net/url" 17 "regexp" 18 "strconv" 19 "strings" 20 "testing" 21 "time" 22 23 . "github.com/ctdk/goiardi/chefcrypto" 24 ) 25 26 type keyPair struct { 27 private, 28 public, 29 kind string 30 } 31 32 const ( 33 userid = "tester" 34 requestURL = "http://localhost:80" 35 36 // Generated from 37 // openssl genrsa -out privkey.pem 2048 38 // perl -pe 's/\n/\\n/g' privkey.pem 39 privateKeyPKCS1 = ` 40 -----BEGIN RSA PRIVATE KEY----- 41 MIIEpAIBAAKCAQEAx12nDxxOwSPHRSJEDz67a0folBqElzlu2oGMiUTS+dqtj3FU 42 h5lJc1MjcprRVxcDVwhsSSo9948XEkk39IdblUCLohucqNMzOnIcdZn8zblN7Cnp 43 W03UwRM0iWX1HuwHnGvm6PKeqKGqplyIXYO0qlDWCzC+VaxFTwOUk31MfOHJQn4y 44 fTrfuE7h3FTElLBu065SFp3dPICIEmWCl9DadnxbnZ8ASxYQ9xG7hmZduDgjNW5l 45 3x6/EFkpym+//D6AbWDcVJ1ovCsJL3CfH/NZC3ekeJ/aEeLxP/vaCSH1VYC5VsYK 46 5Qg7SIa6Nth3+RZz1hYOoBJulEzwljznwoZYRQIDAQABAoIBADPQol+qAsnty5er 47 PTcdHcbXLJp5feZz1dzSeL0gdxja/erfEJIhg9aGUBs0I55X69VN6h7l7K8PsHZf 48 MzzJhUL4QJJETOYP5iuVhtIF0I+DTr5Hck/5nYcEv83KAvgjbiL4ZE486IF5awnL 49 2OE9HtJ5KfhEleNcX7MWgiIHGb8G1jCqu/tH0GI8Z4cNgUrXMbczGwfbN/5Wc0zo 50 Dtpe0Tec/Fd0DLFwRiAuheakPjlVWb7AGMDX4TyzCXfMpS1ul2jk6nGFk77uQozF 51 PQUawCRp+mVS4qecgq/WqfTZZbBlW2L18/kpafvsxG8kJ7OREtrb0SloZNFHEc2Q 52 70GbgKECgYEA6c/eOrI3Uour1gKezEBFmFKFH6YS/NZNpcSG5PcoqF6AVJwXg574 53 Qy6RatC47e92be2TT1Oyplntj4vkZ3REv81yfz/tuXmtG0AylH7REbxubxAgYmUT 54 18wUAL4s3TST2AlK4R29KwBadwUAJeOLNW+Rc4xht1galsqQRb4pUzkCgYEA2kj2 55 vUhKAB7QFCPST45/5q+AATut8WeHnI+t1UaiZoK41Jre8TwlYqUgcJ16Q0H6KIbJ 56 jlEZAu0IsJxjQxkD4oJgv8n5PFXdc14HcSQ512FmgCGNwtDY/AT7SQP3kOj0Rydg 57 N02uuRb/55NJ07Bh+yTQNGA+M5SSnUyaRPIAMW0CgYBgVU7grDDzB60C/g1jZk/G 58 VKmYwposJjfTxsc1a0gLJvSE59MgXc04EOXFNr4a+oC3Bh2dn4SJ2Z9xd1fh8Bur 59 UwCLwVE3DBTwl2C/ogiN4C83/1L4d2DXlrPfInvloBYR+rIpUlFweDLNuve2pKvk 60 llU9YGeaXOiHnGoY8iKgsQKBgQDZKMOHtZYhHoZlsul0ylCGAEz5bRT0V8n7QJlw 61 12+TSjN1F4n6Npr+00Y9ov1SUh38GXQFiLq4RXZitYKu6wEJZCm6Q8YXd1jzgDUp 62 IyAEHNsrV7Y/fSSRPKd9kVvGp2r2Kr825aqQasg16zsERbKEdrBHmwPmrsVZhi7n 63 rlXw1QKBgQDBOyUJKQOgDE2u9EHybhCIbfowyIE22qn9a3WjQgfxFJ+aAL9Bg124 64 fJIEzz43fJ91fe5lTOgyMF5TtU5ClAOPGtlWnXU0e5j3L4LjbcqzEbeyxvP3sn1z 65 dYkX7NdNQ5E6tcJZuJCGq0HxIAQeKPf3x9DRKzMnLply6BEzyuAC4g== 66 -----END RSA PRIVATE KEY----- 67 ` 68 // Generated from 69 // openssl rsa -in privkey.pem -pubout -out pubkey.pem 70 // perl -pe 's/\n/\\n/g' pubkey.pem 71 publicKeyPKCS1 = ` 72 -----BEGIN PUBLIC KEY----- 73 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx12nDxxOwSPHRSJEDz67 74 a0folBqElzlu2oGMiUTS+dqtj3FUh5lJc1MjcprRVxcDVwhsSSo9948XEkk39Idb 75 lUCLohucqNMzOnIcdZn8zblN7CnpW03UwRM0iWX1HuwHnGvm6PKeqKGqplyIXYO0 76 qlDWCzC+VaxFTwOUk31MfOHJQn4yfTrfuE7h3FTElLBu065SFp3dPICIEmWCl9Da 77 dnxbnZ8ASxYQ9xG7hmZduDgjNW5l3x6/EFkpym+//D6AbWDcVJ1ovCsJL3CfH/NZ 78 C3ekeJ/aEeLxP/vaCSH1VYC5VsYK5Qg7SIa6Nth3+RZz1hYOoBJulEzwljznwoZY 79 RQIDAQAB 80 -----END PUBLIC KEY----- 81 ` 82 // Generated from 83 // openssl dsaparam -out dsaparam.pem 2048 84 // openssl gendsa -out privkey.pem dsaparam.pem 85 // perl -pe 's/\n/\\n/g' privkey.pem 86 badPrivateKeyPKCS1 = ` 87 -----BEGIN DSA PRIVATE KEY----- 88 MIIDVgIBAAKCAQEApv0SsaKRWyn0IrbI6i547c/gldLQ3vB5xoSuTkVOvmD3HfuE 89 EVPKMS+XKlhgHOJy677zYNKUOIR78vfDVr1M89w19NSic81UwGGaOkrjQWOkoHaA 90 BS4046AzYKWqHWQNn9dm7WdQlbMBcBv9u+J6EqlzstPwWVaRdbAzyPtwQZRF5WfC 91 OcrQr8XpXbKsPh55FzfvFpu4KEKTY+8ynLz9uDNW2iAxj9NtRlUHQNqKQvjQsr/8 92 4pVrEBh+CnzNrmPXQIbyxV0y8WukAo3I3ZXK5nsUcJhFoVCRx4aBlp9W96mYZ7OE 93 dPCkFsoVhUNFo0jlJhMPODR1NXy77c4v1Kh6xwIhAJwFm6CQBOWJxZdGo2luqExE 94 acUG9Hkr2qd0yccgs2tFAoIBAQCQJCwASD7X9l7nZyZvJpXMe6YreGaP3VbbHCz8 95 GHs1P5exOausfJXa9gRLx2qDW0sa1ZyFUDnd2Dt810tgAhY143lufNoV3a4IRHpS 96 Fm8jjDRMyBQ/BrLBBXgpwiZ9LHBuUSeoRKY0BdyRsULmcq2OaBq9J38NUblWSe2R 97 NjQ45X6SGgUdHy3CrQtLjCA9l8+VPg3l05IBbXIhVSllP5AUmMG4T9x6M7NHEoSr 98 c7ewKSJNvc1C8+G66Kfz8xcChKcKC2z1YzvxrlcDHF+BBLw1Ppp+yMBfhQDWIZfe 99 6tpiKEEyWoyi4GkzQ+vooFIriaaL+Nnggh+iJ7BEUByHBaHnAoIBAFUxSB3bpbbp 100 Vna0HN6b+svuTCFhYi9AcmI1dcyEFKycUvZjP/X07HvX2yrL8aGxMJgF6RzPob/F 101 +SZar3u9Fd8DUYLxis6/B5d/ih7GnfPdChrDOJM1nwlferTGHXd1TBDzugpAovCe 102 JAjXiPsGmcCi9RNyoGib/FgniT7IKA7s3yJAzYSeW3wtLToSNGFJHn+TzFDBuWV4 103 KH70bpEV84JIzWo0ejKzgMBQ0Zrjcsm4lGBtzaBqGSvOrlIVFuSWFYUxrSTTxthQ 104 /JYz4ch8+HsQC/0HBuJ48yALDCVKsWq4Y21LRRJIOC25DfjwEYWWaKNGlDDsJA1m 105 Y5WF0OX+ABcCIEXhrzI1NddyFwLnfDCQ+sy6HT8/xLKXfaipd2rpn3gL 106 -----END DSA PRIVATE KEY----- 107 ` 108 // Generated from 109 // openssl genpkey -out rsakey.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048 110 // openssl genrsa -out privkey.pem 2048 111 privateKeyPKCS8 = ` 112 -----BEGIN PRIVATE KEY----- 113 MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNjtxSUP5FjiD9 114 a0KXByeLPE1y5d7G1WpJOo6YgAJjFUFPYs8+EtF7MzWpxvcRQEuYgrR7K5E7ZmSk 115 uM3fg+kWessqrc8qZLx3LFVv7C2O2IT0s2riHjBbBOjLbM0Ps9uX5u5vgyIOlEGz 116 o1dw5AMDi52QjjfROMML7WqRLMY7jcRuK7IpL5UhnAtKnOrakHSzxMHqIC2ZQnsJ 117 Es2Rnj7ihgr6VZ66FEEUcIqbUwZDEHYsamkg4bCFHB+P925FeZfQtBDBGlFGeNSs 118 mDOKrw66I2wDdq/BZ7MN3y/tdpda0H+95qYRye2FeyL9uSoREWaAv5PemQYGt2wc 119 xmkNoImRAgMBAAECggEABFJ2q3xsfEXqx6lTsx1BZZoU/s96ia+/Fl8W1HoMkszF 120 nMe1F9cJdI+1FybJ1yEE9eX5qYVW/mq+vv/rxEFfy0s1rmYNLxUDKXZTLZFHu/Mt 121 iH+lRa/g0GkgA/b7sNLVUTJX3RxiwO+5Ge/bTNJehdqPq5Rx9AI/h6asUPUiDep5 122 gy22eGh8hNYXrDvZxQBe8stVw11PSItn5pgYTtlLW+AxdR5r17JvIsxbdX+nceEK 123 KWiS8YvkPJwlhIskMu8nBlc62efk6R8bVIRCrgbn87KNe/SmOTgUvgdw5zL5UxU7 124 m3IMdy7Cl9+0h7AYKUha2d05cAw5nEvmcJlOGjwygQKBgQD4vOuEJXcjjOYzOwEO 125 DbCfExCl9KnCCOJewq37AxBWreo3gWu4+S4RxSnsEN7NPQGj4JfePr/gSzcr0Zkb 126 wDZc1jVIUdh5eyE1ABvJWnyfYducKF1j5hO0XJNlHqg1+5DhtycsQRlsbiMDEUxk 127 1S/zMMg3Af/y87Su/wmnZdCo+QKBgQDTjzY2iaxhut3gi8vKzrS+YAAsjHw+ohT5 128 WVgFp+TP1lFEjV8hLhWWvnbfluFItzLawjYNpckNcHEA/cgTtsy2baEdrkhhFIj0 129 1FF2xIYJzHucHZT9e8hMU6FyoX/iqXSfA9bmc5LSV/Bi6nN8hneIcz/x/Vt1z3qd 130 EeUgHYnjWQKBgGwR2NnPVVYSz6mOh0TN2eEjbWZNSLxPE9tMBj8684xVf5+iEWWK 131 jeOWoEI6ijLtwJqs6A7dgIw44b2eEUGnX3cycm/7b2xIfQMECw6Oy/qLj9jnCLxw 132 qDsCxd93VGov5KDM7K4jkqIzr+6TQ3fD0FN+7F5J9iRekjA+Crm6WNAxAoGBAJkC 133 84rueCcXKHLHqVW9uywV8wpFcXc7c0AFRoyQqgVIVO7n8O3mjubASuncDoSxO67M 134 2Jt2VLvLn2/AHX1ksRsgn28AJolQeN3a0jC8YtWjd6OqIaBUbsIFmrd15zDgruBz 135 vnJfFMndoJdqSqy99KZT9OPpAsVqkpwX3UglFR3BAoGBAJLMwZ1bKqIH1BrZhSdx 136 dtDSoMoQsg+5mWVx5DXSyN4cgkykfbIqAPh8xe6hDFUwMBPniVj9D1c67YYPs/7/ 137 9UtZHPN4s55Li7gJ4tGIpRkcThMEbdBE9rBzgFdNSPloBzwJgC4/XgWR6ZQr6zXD 138 CD/2ADbs1OybuNTkDSiPdw9K 139 -----END PRIVATE KEY----- 140 ` 141 // Generated from 142 // openssl rsa -in privkey.pem -pubout -out pubkey.pem 143 // perl -pe 's/\n/\\n/g' pubkey.pem 144 publicKeyPKCS8 = ` 145 -----BEGIN PUBLIC KEY----- 146 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzY7cUlD+RY4g/WtClwcn 147 izxNcuXextVqSTqOmIACYxVBT2LPPhLRezM1qcb3EUBLmIK0eyuRO2ZkpLjN34Pp 148 FnrLKq3PKmS8dyxVb+wtjtiE9LNq4h4wWwToy2zND7Pbl+bub4MiDpRBs6NXcOQD 149 A4udkI430TjDC+1qkSzGO43EbiuyKS+VIZwLSpzq2pB0s8TB6iAtmUJ7CRLNkZ4+ 150 4oYK+lWeuhRBFHCKm1MGQxB2LGppIOGwhRwfj/duRXmX0LQQwRpRRnjUrJgziq8O 151 uiNsA3avwWezDd8v7XaXWtB/veamEcnthXsi/bkqERFmgL+T3pkGBrdsHMZpDaCJ 152 kQIDAQAB 153 -----END PUBLIC KEY----- 154 ` 155 ) 156 157 var ( 158 testRequiredHeaders = []string{ 159 "X-Ops-Timestamp", 160 "X-Ops-UserId", 161 "X-Ops-Sign", 162 "X-Ops-Content-Hash", 163 "X-Ops-Authorization-1", 164 "X-Ops-Request-Source", 165 } 166 167 mux *http.ServeMux 168 server *httptest.Server 169 client *Client 170 keyPairs = []keyPair{ 171 { 172 privateKeyPKCS1, 173 publicKeyPKCS1, 174 "PKCS1", 175 }, 176 { 177 privateKeyPKCS8, 178 publicKeyPKCS8, 179 "PKCS8", 180 }, 181 } 182 ) 183 184 // Gave up trying to implement this myself 185 // nopCloser came from https://groups.google.com/d/msg/golang-nuts/J-Y4LtdGNSw/wDSYbHWIKj0J 186 // yay for sharing 187 // nopCloser creates a io.ReadCloser to satisfy the request.Body input 188 type nopCloser struct { 189 io.Reader 190 } 191 192 func (nopCloser) Close() error { return nil } 193 194 func setup() { 195 mux = http.NewServeMux() 196 server = httptest.NewServer(mux) 197 client, _ = NewClient(&Config{ 198 Name: userid, 199 Key: privateKeyPKCS1, 200 BaseURL: server.URL, 201 AuthenticationVersion: "1.0", 202 }) 203 } 204 205 func setupWithKey(privateKey string) { 206 mux = http.NewServeMux() 207 server = httptest.NewServer(mux) 208 client, _ = NewClient(&Config{ 209 Name: userid, 210 Key: privateKey, 211 BaseURL: server.URL, 212 AuthenticationVersion: "1.0", 213 }) 214 } 215 216 func teardown() { 217 server.Close() 218 } 219 220 func createServer(key *keyPair) *httptest.Server { 221 return httptest.NewServer( 222 http.HandlerFunc( 223 func(rw http.ResponseWriter, req *http.Request) { 224 checkHeader(key, rw, req) 225 }, 226 ), 227 ) 228 } 229 230 func createTLSServer(key *keyPair) *httptest.Server { 231 return httptest.NewTLSServer( 232 http.HandlerFunc( 233 func(rw http.ResponseWriter, req *http.Request) { 234 checkHeader(key, rw, req) 235 }, 236 ), 237 ) 238 } 239 240 // publicKeyFromString parses an RSA public key from a string 241 func publicKeyFromString(key []byte) (*rsa.PublicKey, error) { 242 block, _ := pem.Decode(key) 243 if block == nil { 244 return nil, fmt.Errorf("block size invalid for '%s'", string(key)) 245 } 246 rsaKey, err := x509.ParsePKIXPublicKey(block.Bytes) 247 if err != nil { 248 return nil, err 249 } 250 251 return rsaKey.(*rsa.PublicKey), nil 252 } 253 254 func makeAuthConfig(privateKey string) (*AuthConfig, error) { 255 pk, err := PrivateKeyFromString([]byte(privateKey)) 256 if err != nil { 257 return nil, err 258 } 259 260 ac := &AuthConfig{ 261 PrivateKey: pk, 262 ClientName: userid, 263 } 264 return ac, nil 265 } 266 267 func TestAuthConfig(t *testing.T) { 268 for _, keys := range keyPairs { 269 _, err := makeAuthConfig(keys.private) 270 assert.Nil(t, err, "Failed to create AuthConfig struct from privatekeys and stuff") 271 } 272 } 273 274 func TestBase64BlockEncodeNoLimit(t *testing.T) { 275 for _, keys := range keyPairs { 276 ac, _ := makeAuthConfig(keys.private) 277 var content string 278 for _, key := range []string{"header1", "header2", "header3"} { 279 content += fmt.Sprintf("%s:blahblahblah\n", key) 280 } 281 content = strings.TrimSuffix(content, "\n") 282 283 signature, _ := GenerateSignature(ac.PrivateKey, content) 284 Base64BlockEncode(signature, 0) 285 } 286 // TODO: Test something 287 } 288 289 func TestSignRequestBadSignature(t *testing.T) { 290 for _, keys := range keyPairs { 291 ac, err := makeAuthConfig(keys.private) 292 request, err := http.NewRequest("GET", requestURL, nil) 293 ac.PrivateKey.PublicKey.N = big.NewInt(23234728432324) 294 295 err = ac.SignRequest(request) 296 assert.NotNil(t, err, "failed to generate failed signature") 297 } 298 } 299 300 func TestSignRequestNoBody(t *testing.T) { 301 for _, keys := range keyPairs { 302 func() { 303 setupWithKey(keys.private) 304 defer teardown() 305 ac, err := makeAuthConfig(keys.private) 306 request, err := client.NewRequest("GET", requestURL, nil) 307 308 err = ac.SignRequest(request) 309 assert.Nil(t, err, "Generate Request Headers") 310 count := 0 311 for _, requiredHeader := range testRequiredHeaders { 312 for header := range request.Header { 313 if strings.ToLower(requiredHeader) == strings.ToLower(header) { 314 count++ 315 break 316 } 317 } 318 } 319 assert.Equal(t, count, len(testRequiredHeaders), "All required headers returned") 320 }() 321 } 322 } 323 324 func TestSignRequestBody(t *testing.T) { 325 for _, keys := range keyPairs { 326 func() { 327 ac, err := makeAuthConfig(keys.private) 328 if err != nil { 329 t.Fatal(err) 330 } 331 setupWithKey(keys.private) 332 defer teardown() 333 334 // Gave up trying to implement this myself 335 // nopCloser came from https://groups.google.com/d/msg/golang-nuts/J-Y4LtdGNSw/wDSYbHWIKj0J 336 // yay for sharing 337 requestBody := strings.NewReader("somecoolbodytext") 338 request, err := client.NewRequest("GET", requestURL, requestBody) 339 340 err = ac.SignRequest(request) 341 if err != nil { 342 t.Fatal("failed to generate RequestHeaders") 343 } 344 count := 0 345 for _, requiredHeader := range testRequiredHeaders { 346 for header := range request.Header { 347 if strings.ToLower(requiredHeader) == strings.ToLower(header) { 348 count++ 349 break 350 } 351 } 352 } 353 assert.Equal(t, count, len(testRequiredHeaders), "Return all of the test required headers") 354 }() 355 } 356 } 357 358 // <3 goiardi 359 // Test our headers as goiardi would 360 // https://github.com/ctdk/goiardi/blob/master/authentication/authentication.go 361 // func checkHeader(user_id string, r *http.Request) string { 362 func checkHeader(key *keyPair, rw http.ResponseWriter, req *http.Request) { 363 user_id := req.Header.Get("X-OPS-USERID") 364 // Since we don't have a real client or user to check against, 365 // we'll just verify that input user = output user 366 // user, err := actor.GetReqUser(user_id) 367 // if err != nil { 368 if user_id != userid { 369 fmt.Fprintf(rw, "Failed to authenticate as %s with key standard %s", user_id, key.kind) 370 } 371 372 contentHash := req.Header.Get("X-OPS-CONTENT-HASH") 373 if contentHash == "" { 374 fmt.Fprintf(rw, "no content hash provided (%s)", key.kind) 375 } 376 377 authTimestamp := req.Header.Get("x-ops-timestamp") 378 if authTimestamp == "" { 379 fmt.Fprintf(rw, "no timestamp header provided (%s)", key.kind) 380 } 381 // TODO: Will want to implement this later 382 // else { 383 // // check the time stamp w/ allowed slew 384 // tok, terr := checkTimeStamp(authTimestamp, config.Config.TimeSlewDur) 385 // if !tok { 386 // return terr 387 // } 388 // } 389 390 // Eventually this may be put to some sort of use, but for now just 391 // make sure that it's there. Presumably eventually it would be used to 392 // use algorithms other than sha1 for hashing the body, or using a 393 // different version of the header signing algorithm. 394 xopssign := req.Header.Get("x-ops-sign") 395 var apiVer string 396 var hashChk []string 397 if xopssign == "" { 398 fmt.Fprintf(rw, "missing X-Ops-Sign header (%s)", key.kind) 399 } else { 400 re := regexp.MustCompile(`version=(\d+\.\d+)`) 401 shaRe := regexp.MustCompile(`algorithm=(\w+)`) 402 if verChk := re.FindStringSubmatch(xopssign); verChk != nil { 403 apiVer = verChk[1] 404 if apiVer != "1.0" && apiVer != "1.1" { 405 fmt.Fprintf(rw, "Bad version number '%s' in X-Ops-Header with crypto standard %s", apiVer, key.kind) 406 407 } 408 } else { 409 fmt.Fprintf(rw, "malformed version in X-Ops-Header with crypto standard %s", key.kind) 410 } 411 412 // if algorithm is missing, it uses sha1. Of course, no other 413 // hashing algorithm is supported yet... 414 if hashChk = shaRe.FindStringSubmatch(xopssign); hashChk != nil { 415 if hashChk[1] != "sha1" { 416 fmt.Fprintf(rw, "Unsupported hashing algorithm '%s' specified in X-Ops-Header with crypto standard %s", hashChk[1], key.kind) 417 } 418 } 419 } 420 421 signedHeaders, sherr := assembleSignedHeader(req) 422 if sherr != nil { 423 fmt.Fprintf(rw, sherr.Error()) 424 } 425 426 _, err := HeaderDecrypt(key.public, signedHeaders) 427 if err != nil { 428 fmt.Fprintf(rw, "unexpected header decryption error '%s' with crypto standard %s", err, key.kind) 429 } 430 // TODO: Test something 431 } 432 433 func TestRequest(t *testing.T) { 434 for _, keys := range keyPairs { 435 func() { 436 ac, err := makeAuthConfig(keys.private) 437 server := createServer(&keys) 438 defer server.Close() 439 setupWithKey(keys.private) 440 defer teardown() 441 442 request, err := client.NewRequest("GET", server.URL, nil) 443 444 err = ac.SignRequest(request) 445 assert.Nil(t, err, "Generate request headers") 446 447 client := &http.Client{} 448 response, err := client.Do(request) 449 assert.Nil(t, err, "Do error") 450 assert.Equal(t, http.StatusOK, response.StatusCode, "Response status") 451 452 buf := new(bytes.Buffer) 453 buf.ReadFrom(response.Body) 454 bodyStr := buf.String() 455 456 assert.Equal(t, "", bodyStr, "Expect empty string") 457 }() 458 } 459 } 460 461 func TestRequestToEndpoint(t *testing.T) { 462 for _, keys := range keyPairs { 463 func() { 464 ac, err := makeAuthConfig(keys.private) 465 assert.Nil(t, err, "Build auth config") 466 server := createServer(&keys) 467 defer server.Close() 468 469 requestBody := strings.NewReader("somecoolbodytext") 470 request, err := client.NewRequest("GET", server.URL+"/clients", requestBody) 471 472 err = ac.SignRequest(request) 473 assert.Nil(t, err, "Generate request headers") 474 475 client := &http.Client{} 476 response, err := client.Do(request) 477 assert.Nil(t, err, "Response from Do") 478 assert.Equal(t, http.StatusOK, response.StatusCode, "Status from Do") 479 480 buf := new(bytes.Buffer) 481 buf.ReadFrom(response.Body) 482 bodyStr := buf.String() 483 assert.Equal(t, "", bodyStr, "Expect an empty return") 484 }() 485 } 486 } 487 488 func TestTLSValidation(t *testing.T) { 489 for _, keys := range keyPairs { 490 func() { 491 ac, err := makeAuthConfig(keys.private) 492 if err != nil { 493 panic(err) 494 } 495 // Self-signed server 496 server := createTLSServer(&keys) 497 defer server.Close() 498 499 // Without RootCAs, TLS validation should fail 500 chefClient, _ := NewClient(&Config{ 501 Name: userid, 502 Key: keys.private, 503 BaseURL: server.URL, 504 }) 505 506 request, err := chefClient.NewRequest("GET", server.URL, nil) 507 err = ac.SignRequest(request) 508 assert.Nil(t, err, "Generate request headers") 509 510 client := chefClient.Client 511 response, err := client.Do(request) 512 assert.NotNil(t, err, "Invalid TLS certificate") 513 514 // Success with RootCAs containing the server's certificate 515 certPool := x509.NewCertPool() 516 certPool.AddCert(server.Certificate()) 517 chefClient, _ = NewClient(&Config{ 518 Name: userid, 519 Key: keys.private, 520 BaseURL: server.URL, 521 RootCAs: certPool, 522 }) 523 524 request, err = chefClient.NewRequest("GET", server.URL, nil) 525 err = ac.SignRequest(request) 526 assert.Nil(t, err, "generate request headers") 527 528 client = chefClient.Client 529 response, err = client.Do(request) 530 assert.Nil(t, err, "Do request should work") 531 assert.Equal(t, http.StatusOK, response.StatusCode, "Response code") 532 533 buf := new(bytes.Buffer) 534 buf.ReadFrom(response.Body) 535 bodyStr := buf.String() 536 assert.Equal(t, "", bodyStr, "Empty response body expected") 537 }() 538 } 539 } 540 541 // More Goiardi <3 542 func assembleSignedHeader(r *http.Request) (string, error) { 543 sHeadStore := make(map[int]string) 544 authHeader := regexp.MustCompile(`(?i)^X-Ops-Authorization-(\d+)`) 545 for k := range r.Header { 546 if c := authHeader.FindStringSubmatch(k); c != nil { 547 /* Have to put it into a map first, then sort, in case 548 * the headers don't come out in the right order */ 549 // skipping this error because we shouldn't even be 550 // able to get here with something that won't be an 551 // integer. Famous last words, I'm sure. 552 i, _ := strconv.Atoi(c[1]) 553 sHeadStore[i] = r.Header.Get(k) 554 } 555 } 556 if len(sHeadStore) == 0 { 557 return "", errors.New("no authentication headers found") 558 } 559 560 sH := make([]string, len(sHeadStore)) 561 sHlimit := len(sH) 562 for k, v := range sHeadStore { 563 if k > sHlimit { 564 return "", errors.New("malformed authentication headers") 565 } 566 sH[k-1] = v 567 } 568 signedHeaders := strings.Join(sH, "") 569 570 return signedHeaders, nil 571 } 572 573 func TestGenerateHash(t *testing.T) { 574 input, output := HashStr("hi"), "witfkXg0JglCjW9RssWvTAveakI=" 575 assert.Equal(t, input, output, "correctly hashes a given input string") 576 } 577 578 // BUG(fujin): @bradbeam: this doesn't make sense to me. 579 func TestGenerateSignatureError(t *testing.T) { 580 for _, keys := range keyPairs { 581 ac, _ := makeAuthConfig(keys.private) 582 583 // BUG(fujin): what about the 'hi' string is not meant to be signable? 584 sig, err := GenerateSignature(ac.PrivateKey, "hi") 585 assert.NotEqual(t, "", sig, "Generated sig should not be empty") 586 assert.Nil(t, err, "errors for an unknown reason to fujin") 587 } 588 } 589 590 func TestSignatureContent(t *testing.T) { 591 pk, _ := PrivateKeyFromString([]byte(privateKeyPKCS1)) 592 ac := &AuthConfig{ 593 PrivateKey: pk, 594 ClientName: userid, 595 AuthenticationVersion: "1.0", 596 } 597 vals := map[string]string{ 598 "Method": "GET", 599 "Accept": "application/json", 600 "Hashed Path": "FaX3AVJLlDDqHB7giEG/2EbBsR0=", 601 "X-Chef-Version": DefaultChefVersion, 602 "X-Ops-Server-API-Version": "1", 603 "X-Ops-Timestamp": "1990-12-31T15:59:60-08:00", 604 "X-Ops-UserId": ac.ClientName, 605 "X-Ops-Content-Hash": "Content-Hash", 606 } 607 expected := "Method:GET\nHashed Path:FaX3AVJLlDDqHB7giEG/2EbBsR0=\nX-Ops-Content-Hash:Content-Hash\nX-Ops-Timestamp:1990-12-31T15:59:60-08:00\nX-Ops-UserId:tester" 608 609 content := ac.SignatureContent(vals) 610 assert.Equal(t, expected, content, "Signature content") 611 } 612 613 func TestRequestError(t *testing.T) { 614 615 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 616 http.Error(w, `{"error":["Not Available"]}`, http.StatusServiceUnavailable) 617 })) 618 defer ts.Close() 619 620 resp, _ := http.Get(ts.URL) 621 err := CheckResponse(resp) 622 cerr, err := ChefError(err) 623 matched, err := regexp.MatchString(`^GET http://127.0.0.1:\d+: 503`, cerr.Error()) 624 assert.True(t, matched, "match request error 503") 625 assert.Equal(t, http.StatusServiceUnavailable, cerr.StatusCode(), "Status code for 503") 626 assert.Equal(t, "GET", cerr.StatusMethod(), "method used for 503") 627 assert.Equal(t, "Not Available", cerr.StatusMsg(), "message returned for 503)") 628 assert.Equal(t, `{"error":["Not Available"]}`, strings.TrimSpace(string(cerr.StatusText())), "message text returned for 503") 629 matched, err = regexp.MatchString(`http://127.0.0.1:\d+`, cerr.StatusURL().String()) 630 assert.True(t, matched, "Request Status returned URL with 503") 631 matched, err = regexp.MatchString(`http://127.0.0.1:\d+`, cerr.Error()) 632 assert.True(t, matched, "Request Error returned with 503") 633 } 634 635 func TestNewClient(t *testing.T) { 636 cfg := &Config{Name: "testclient", Key: privateKeyPKCS1, SkipSSL: false, Timeout: 1} 637 c, err := NewClient(cfg) 638 assert.Nil(t, err, "Make a valid client") 639 // simple validations on the created client 640 assert.Equal(t, "testclient", c.Auth.ClientName, "Valid client Name") 641 assert.Equal(t, time.Duration(1)*time.Second, c.Client.Timeout, "Valid timeout value") 642 643 // Bad PEM should be an error 644 cfg = &Config{Name: "blah", Key: "not a key", SkipSSL: false} 645 c, err = NewClient(cfg) 646 assert.NotNil(t, err, "Build a client from a bad key string, bad PEM") 647 648 // Not a proper key should be an error 649 cfg = &Config{Name: "blah", Key: badPrivateKeyPKCS1, SkipSSL: false} 650 c, err = NewClient(cfg) 651 assert.NotNil(t, err, "Build a client from a bad key string, bad key") 652 653 // Verify using a supplied http client works 654 crt := retryablehttp.NewClient() 655 crt.RetryMax = 10 656 cfg = &Config{Name: "testclient", Key: privateKeyPKCS1, SkipSSL: false, Timeout: 1, Client: crt.StandardClient()} 657 c, err = NewClient(cfg) 658 assert.Nil(t, err, "Build a client with a supplied http client") 659 assert.Equal(t, c.Client, crt.StandardClient(), "Client uses a supplied http client") 660 661 // Verify using a supplied RoundTripper factory works 662 cfg = &Config{Name: "testclient", Key: privateKeyPKCS1, SkipSSL: false, Timeout: 1, RoundTripper: newTestRt} 663 c, err = NewClient(cfg) 664 assert.Nil(t, err, "Build a client with a supplied RoundTripper factory") 665 assert.IsType(t, &http.Client{}, c.Client, "The inner client should be an *http.Client") 666 rt, correct_type := c.Client.Transport.(*testRt) 667 assert.True(t, correct_type, "Client Transport should be a *testRt") 668 assert.IsType(t, &http.Transport{}, rt.next, "Client Transport should wrap a *http.Transport") 669 670 cfg = &Config{Name: "testclient", Key: privateKeyPKCS1, Client: crt.StandardClient(), RoundTripper: newTestRt} 671 c, err = NewClient(cfg) 672 assert.NotNil(t, err, "Build a client with both Client and RoundTripper") 673 674 // ServerVersion tests 675 // Test value of authentication version. 676 // 1.0, 1.3, 4.0 => 1.0 677 cfg = &Config{AuthenticationVersion: "1.0", Name: "testclient", Key: privateKeyPKCS1, SkipSSL: false, Timeout: 1, Client: crt.StandardClient()} 678 c, err = NewClient(cfg) 679 assert.Nil(t, err, "Make a valid client authversion 1.0") 680 assert.Equal(t, c.Auth.AuthenticationVersion, "1.0", "AuthVersion 1.0") 681 // 682 cfg = &Config{AuthenticationVersion: "1.3", Name: "testclient", Key: privateKeyPKCS1, SkipSSL: false, Timeout: 1, Client: crt.StandardClient()} 683 c, err = NewClient(cfg) 684 assert.Nil(t, err, "Make a valid client authversion 1.3") 685 assert.Equal(t, c.Auth.AuthenticationVersion, "1.3", "AuthVersion 1.3") 686 // 687 cfg = &Config{AuthenticationVersion: "", Name: "testclient", Key: privateKeyPKCS1, SkipSSL: false, Timeout: 1, Client: crt.StandardClient()} 688 c, err = NewClient(cfg) 689 assert.Nil(t, err, "Make a valid client authversion blank") 690 assert.Equal(t, "1.0", c.Auth.AuthenticationVersion, "AuthVersion blank") 691 692 } 693 694 type testRt struct { 695 req_count int 696 err_count int 697 next http.RoundTripper 698 } 699 700 func newTestRt(next http.RoundTripper) http.RoundTripper { return &testRt{next: next} } 701 702 func (this *testRt) RoundTrip(req *http.Request) (*http.Response, error) { 703 this.req_count++ 704 res, err := this.next.RoundTrip(req) 705 if err != nil { 706 this.err_count++ 707 } 708 return res, err 709 } 710 711 func TestNewClientProxy(t *testing.T) { 712 // no proxy provided 713 cfg := &Config{Name: "testclient", Key: privateKeyPKCS1, SkipSSL: false, Timeout: 1} 714 chefClient, err := NewClient(cfg) 715 assert.Nil(t, err, "Create client") 716 request, err := chefClient.NewRequest("GET", "https://test.com", nil) 717 assert.Nil(t, err, "Create request") 718 trfunc, err := chefClient.Client.Transport.(*http.Transport).Proxy(request) 719 assert.Nil(t, trfunc, "no proxy") 720 721 // custom proxy provided 722 proxyFunc := func(req *http.Request) (*url.URL, error) { 723 url, _ := url.Parse("https://proxy.com:9000") 724 return url, nil 725 } 726 727 cfg = &Config{Name: "testclient", Key: privateKeyPKCS1, SkipSSL: false, Timeout: 1, Proxy: proxyFunc} 728 chefClient, err = NewClient(cfg) 729 assert.Nil(t, err, "Create client") 730 request, err = chefClient.NewRequest("GET", "https://test.com", nil) 731 assert.Nil(t, err, "Create request") 732 trurl, err := chefClient.Client.Transport.(*http.Transport).Proxy(request) 733 assert.Nil(t, err, "Proxy execution") 734 eurl := &url.URL{Scheme: "https", Host: "proxy.com:9000"} 735 assert.Equal(t, *eurl, *trurl, "Proxy set from supplied function") 736 } 737 738 func TestNewRequest(t *testing.T) { 739 for _, keys := range keyPairs { 740 var err error 741 server := createServer(&keys) 742 cfg := &Config{Name: "testclient", Key: keys.private, SkipSSL: false} 743 c, _ := NewClient(cfg) 744 defer server.Close() 745 746 request, err := c.NewRequest("GET", server.URL, nil) 747 assert.Nil(t, err, "New request created") 748 749 resp, err := c.Do(request, nil) 750 assert.Nil(t, err, "Do the request error return") 751 assert.Equal(t, http.StatusOK, resp.StatusCode, "Do the request status code") 752 753 // This should fail because we've got an invalid URI 754 _, err = c.NewRequest("GET", "%gh&%ij", nil) 755 assert.NotNil(t, err, "Create invalid request") 756 757 // This should fail because there is no TOODLES! method :D 758 request, err = c.NewRequest("TOODLES!", "", nil) 759 _, err = c.Do(request, nil) 760 assert.NotNil(t, err, "Request has invalid method") 761 } 762 } 763 764 func TestDo_badjson(t *testing.T) { 765 setup() 766 defer teardown() 767 768 mux.HandleFunc("/hashrocket", func(w http.ResponseWriter, r *http.Request) { 769 fmt.Fprintf(w, " pigthrusters => 100%% ") 770 }) 771 772 stupidData := struct{}{} 773 request, err := client.NewRequest("GET", "hashrocket", nil) 774 _, err = client.Do(request, &stupidData) 775 assert.NotNil(t, err, "Request a return struct that doesn't match the data") 776 } 777 778 // Add Content-Type tests 779 780 func TestDoText(t *testing.T) { 781 setup() 782 defer teardown() 783 784 pigText := " pigthrusters => 100 " 785 mux.HandleFunc("/hashrocket", func(w http.ResponseWriter, r *http.Request) { 786 w.Header().Add("Content-Type", "text/plain") 787 fmt.Fprintf(w, pigText) 788 }) 789 790 var getdata string 791 request, _ := client.NewRequest("GET", "hashrocket", nil) 792 res, err := client.Do(request, &getdata) 793 assert.Nil(t, err, "text request err") 794 assert.Equal(t, pigText, getdata, "Plain text returned in string") 795 resData, err := io.ReadAll(res.Body) 796 assert.Nil(t, err, "Read the response body") 797 assert.Equal(t, pigText, string(resData), "Plain text from the response body") 798 } 799 800 func TestDoJSON(t *testing.T) { 801 setup() 802 defer teardown() 803 804 jsonText := `{"key": "value"}` 805 mux.HandleFunc("/hashrocket", func(w http.ResponseWriter, r *http.Request) { 806 w.Header().Add("Content-Type", "application/json") 807 fmt.Fprintf(w, jsonText) 808 }) 809 810 getdata := map[string]string{} 811 wantdata := map[string]string{"key": "value"} 812 request, _ := client.NewRequest("GET", "hashrocket", nil) 813 res, err := client.Do(request, &getdata) 814 assert.Nil(t, err, "Json returned") 815 assert.Equal(t, getdata, wantdata, "Json returned data") 816 resData, err := io.ReadAll(res.Body) 817 assert.Nil(t, err, "Read the response body") 818 assert.Equal(t, jsonText, string(resData), "Plain text from the response body") 819 } 820 821 func TestDoDefaultParse(t *testing.T) { 822 setup() 823 defer teardown() 824 825 jsonText := `{"key": "value"}` 826 mux.HandleFunc("/hashrocket", func(w http.ResponseWriter, r *http.Request) { 827 // Note: deliberately using a non standard text type 828 w.Header().Add("Content-Type", "none/here") 829 fmt.Fprintf(w, jsonText) 830 }) 831 832 getdata := map[string]string{} 833 wantdata := map[string]string{"key": "value"} 834 request, _ := client.NewRequest("GET", "hashrocket", nil) 835 res, err := client.Do(request, &getdata) 836 assert.Nil(t, err, "Default parse err") 837 assert.Equal(t, getdata, wantdata, "Default parse of json data") 838 resData, err := io.ReadAll(res.Body) 839 assert.Nil(t, err, "Read the response body") 840 assert.Equal(t, jsonText, string(resData), "Default parse text from the response body") 841 } 842 843 func TestDoNoResponseInterface(t *testing.T) { 844 setup() 845 defer teardown() 846 847 jsonText := `{"key": "value"}` 848 mux.HandleFunc("/hashrocket", func(w http.ResponseWriter, r *http.Request) { 849 // Note: deliberately using a non standard text type 850 w.Header().Add("Content-Type", "none/here") 851 fmt.Fprintf(w, jsonText) 852 }) 853 854 request, _ := client.NewRequest("GET", "hashrocket", nil) 855 res, err := client.Do(request, nil) 856 assert.Nil(t, err, "No interface parse err") 857 resData, err := io.ReadAll(res.Body) 858 assert.Nil(t, err, "Read the response body") 859 assert.Equal(t, jsonText, string(resData), "No Interface from the response body") 860 } 861 862 func TestDoIOWriter(t *testing.T) { 863 setup() 864 defer teardown() 865 866 jsonText := `{"key": "value"}` 867 mux.HandleFunc("/hashrocket", func(w http.ResponseWriter, r *http.Request) { 868 // Note: deliberately using a non standard text type 869 w.Header().Add("Content-Type", "none/here") 870 fmt.Fprintf(w, jsonText) 871 }) 872 873 buf := new(bytes.Buffer) 874 request, _ := client.NewRequest("GET", "hashrocket", nil) 875 res, err := client.Do(request, buf) 876 assert.Nil(t, err, "No interface parse err") 877 byteData, err := io.ReadAll(buf) 878 wantdata := string(byteData) 879 assert.Nil(t, err, "Readable IO stream") 880 assert.Equal(t, jsonText, wantdata, "IO writer parse") 881 resData, err := io.ReadAll(res.Body) 882 assert.Nil(t, err, "Read the response body") 883 assert.Equal(t, jsonText, string(resData), "IO Writer from the response body") 884 } 885 886 func TestBasicAuthHeader(t *testing.T) { 887 setup() 888 defer teardown() 889 req, _ := client.NewRequest("GET", "http://dummy", nil) 890 basicAuthHeader(req, "stduser", "stdpassword") 891 basicHeader := req.Header.Get("Authorization") 892 assert.Equal(t, "Basic c3RkdXNlcjpzdGRwYXNzd29yZA==", basicHeader, "BasicAuthHeader") 893 } 894 895 func TestBasicAuth(t *testing.T) { 896 header := basicAuth("stduser", "stdpassword") 897 assert.Equal(t, "c3RkdXNlcjpzdGRwYXNzd29yZA==", header, "Basic auth credentials") 898 } 899 900 func TestHeaderValue(t *testing.T) { 901 for _, keys := range keyPairs { 902 func() { 903 ac, err := makeAuthConfig(keys.private) 904 if err != nil { 905 t.Fatal(err) 906 } 907 setupWithKey(keys.private) 908 defer teardown() 909 910 // add 'X-Ops-Request-Source' header value as web if IsWebuiKey is true 911 client.IsWebuiKey = true 912 913 requestBody := strings.NewReader("somecoolbodytext") 914 request, err := client.NewRequest("GET", requestURL, requestBody) 915 916 err = ac.SignRequest(request) 917 assert.Nil(t, err, "generate request headers") 918 assert.Equal(t, "web", request.Header.Get("X-Ops-Request-Source"), "Header source value") 919 920 // Should not add 'X-Ops-Request-Source' header value as web if IsWebuiKey is false 921 client.IsWebuiKey = false 922 923 requestBody = strings.NewReader("somecoolbodytext") 924 request, err = client.NewRequest("GET", requestURL, requestBody) 925 926 err = ac.SignRequest(request) 927 assert.Nil(t, err, "generate request headers") 928 assert.Equal(t, "", request.Header.Get("X-Ops-Request-Source"), "No X-Ops-Request-Source") 929 }() 930 } 931 }