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  }