istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/model/test/mockopenidserver.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package test 16 17 import ( 18 "crypto/tls" 19 "errors" 20 "fmt" 21 "net" 22 "net/http" 23 "strconv" 24 "sync" 25 "sync/atomic" 26 "time" 27 28 "github.com/gorilla/mux" 29 30 "istio.io/istio/pkg/log" 31 ) 32 33 var ( 34 cfgContent = "{\"jwks_uri\": \"%s\"}" 35 serverMutex = &sync.Mutex{} 36 ) 37 38 const ( 39 // JwtPubKey1 is the response to 1st call for JWT public key returned by mock server. 40 JwtPubKey1 = `{ "keys": [ { "kid": "fakeKey1_1", "alg": "RS256", "kty": "RSA", "n": "abc", "e": "def" }, 41 { "kid": "fakeKey1_2", "alg": "RS256", "kty": "RSA", "n": "123", "e": "456" } ] }` 42 43 // JwtPubKey1Reordered is the response to 1st call for JWT public key returned by mock server, but in a modified order of json elements. 44 JwtPubKey1Reordered = `{ "keys": [ { "alg": "RS256", "kid": "fakeKey1_2", "n": "123", "kty": "RSA", "e": "456" }, 45 { "n": "abc", "alg": "RS256", "kty": "RSA", "kid": "fakeKey1_1", "e": "def" } ] }` 46 47 // JwtPubKey2 is the response to later calls for JWT public key returned by mock server. 48 JwtPubKey2 = `{ "keys": [ { "kid": "fakeKey2_1", "alg": "RS256", "kty": "RSA", "n": "ghi", "e": "lmn" }, 49 { "kid": "fakeKey2_2", "alg": "RS256", "kty": "RSA", "n": "789", "e": "1234" } ] }` 50 51 JwtPubKeyNoKid = `{ "keys": [ { "alg": "RS256", "kty": "RSA", "n": "abc", "e": "def" }, 52 { "alg": "RS256", "kty": "RSA", "n": "123", "e": "456" } ] }` 53 54 JwtPubKeyNoKid2 = `{ "keys": [ { "alg": "RS256", "kty": "RSA", "n": "ghi", "e": "lmn" }, 55 { "alg": "RS256", "kty": "RSA", "n": "789", "e": "123" } ] }` 56 57 JwtPubKeyNoKeys = `{ "pub": [ { "kid": "fakeKey1_1", "alg": "RS256", "kty": "RSA", "n": "abc", "e": "def" }, 58 { "kid": "fakeKey1_2", "alg": "RS256", "kty": "RSA", "n": "123", "e": "456" } ] }` 59 60 JwtPubKeyNoKeys2 = `{ "pub": [ { "kid": "fakeKey1_3", "alg": "RS256", "kty": "RSA", "n": "abc", "e": "def" }, 61 { "kid": "fakeKey1_4", "alg": "RS256", "kty": "RSA", "n": "123", "e": "456" } ] }` 62 63 JwtPubKeyExtraElements = `{ "keys": [ { "kid": "fakeKey1_1", "alg": "RS256", "kty": "RSA", "n": "abc", "e": "def", "bla": "blah" }, 64 { "kid": "fakeKey1_2", "alg": "RS256", "kty": "RSA", "n": "123", "e": "456", "bla": "blah" } ] }` 65 ) 66 67 // Wrap the original handler with a delay 68 func withDelay(handler http.Handler, delay time.Duration) http.Handler { 69 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 70 time.Sleep(delay) 71 handler.ServeHTTP(w, r) 72 }) 73 } 74 75 // MockOpenIDDiscoveryServer is the in-memory openID discovery server. 76 type MockOpenIDDiscoveryServer struct { 77 Port int 78 URL string 79 server *http.Server 80 81 // How many times openIDCfg is called, use this number to verify cache takes effect. 82 OpenIDHitNum uint64 83 84 // How many times jwtPubKey is called, use this number to verify cache takes effect. 85 PubKeyHitNum uint64 86 87 // The mock server will return an error for the first number of hits for public key, this is used 88 // to simulate network errors and test the retry logic in jwks resolver for public key fetch. 89 ReturnErrorForFirstNumHits uint64 90 91 // The mock server will start to return an error after the first number of hits for public key, 92 // this is used to simulate network errors and test the refresh logic in jwks resolver. 93 ReturnErrorAfterFirstNumHits uint64 94 95 // The mock server will start to return a successful response after the first number of hits for public key, 96 // this is used to simulate network errors and test the refresh logic in jwks resolver. Note the idea is to 97 // use this in combination with ReturnErrorAfterFirstNumHits to simulate something like this: 98 // { success, success, error, error, success, success } 99 ReturnSuccessAfterFirstNumHits uint64 100 101 // The mock server will start to return an error after the first number of hits for public key, 102 // this is used to simulate network errors and test the refresh logic in jwks resolver. 103 ReturnReorderedKeyAfterFirstNumHits uint64 104 105 // If both TLSKeyFile and TLSCertFile are set, Start() will attempt to start a HTTPS server. 106 TLSKeyFile string 107 TLSCertFile string 108 109 // Artificious delay added by the mock server on handling requests 110 timeout time.Duration 111 } 112 113 // StartNewServer creates a mock openID discovery server and starts it 114 func StartNewServer() (*MockOpenIDDiscoveryServer, error) { 115 serverMutex.Lock() 116 defer serverMutex.Unlock() 117 118 server := &MockOpenIDDiscoveryServer{ 119 // 0 means the mock server always return the success result. 120 ReturnErrorForFirstNumHits: 0, 121 ReturnErrorAfterFirstNumHits: 0, 122 } 123 124 return server, server.Start() 125 } 126 127 // StartNewServer creates a mock openID discovery server with an artificious timeout on handling requests and starts it 128 func StartNewServerWithHandlerDelay(timeout time.Duration) (*MockOpenIDDiscoveryServer, error) { 129 serverMutex.Lock() 130 defer serverMutex.Unlock() 131 132 server := &MockOpenIDDiscoveryServer{ 133 // 0 means the mock server always return the success result. 134 ReturnErrorForFirstNumHits: 0, 135 ReturnErrorAfterFirstNumHits: 0, 136 timeout: timeout, 137 } 138 139 return server, server.Start() 140 } 141 142 // StartNewTLSServer creates a mock openID discovery server that serves HTTPS and starts it 143 func StartNewTLSServer(tlsCert, tlsKey string) (*MockOpenIDDiscoveryServer, error) { 144 serverMutex.Lock() 145 defer serverMutex.Unlock() 146 147 server := &MockOpenIDDiscoveryServer{ 148 // 0 means the mock server always return the success result. 149 ReturnErrorForFirstNumHits: 0, 150 ReturnErrorAfterFirstNumHits: 0, 151 152 TLSCertFile: tlsCert, 153 TLSKeyFile: tlsKey, 154 } 155 156 return server, server.Start() 157 } 158 159 // Start starts the mock server. 160 func (ms *MockOpenIDDiscoveryServer) Start() error { 161 var handler http.Handler 162 router := mux.NewRouter() 163 router.HandleFunc("/.well-known/openid-configuration", ms.openIDCfg).Methods("GET") 164 router.HandleFunc("/oauth2/v3/certs", ms.jwtPubKey).Methods("GET") 165 handler = router 166 if ms.timeout != 0 { 167 handler = withDelay(router, ms.timeout) 168 } 169 server := &http.Server{ 170 Addr: ":" + strconv.Itoa(ms.Port), 171 Handler: handler, 172 } 173 ln, err := net.Listen("tcp", ":0") 174 if err != nil { 175 log.Errorf("Server failed to listen %v", err) 176 return err 177 } 178 179 scheme := "http" 180 if ms.TLSCertFile != "" && ms.TLSKeyFile != "" { 181 scheme = "https" 182 } 183 184 port := ln.Addr().(*net.TCPAddr).Port 185 ms.URL = fmt.Sprintf("%s://localhost:%d", scheme, port) 186 server.Addr = ":" + strconv.Itoa(port) 187 188 // Starts the HTTP and waits for it to begin receiving requests. 189 // Returns an error if the server doesn't serve traffic within about 2 seconds. 190 go func() { 191 if scheme == "https" { 192 if err := server.ServeTLS(ln, ms.TLSCertFile, ms.TLSKeyFile); err != nil { 193 log.Errorf("Server failed to serve TLS in %q: %v", ms.URL, err) 194 } 195 return 196 } 197 if err := server.Serve(ln); err != nil { 198 log.Errorf("Server failed to serve in %q: %v", ms.URL, err) 199 } 200 }() 201 202 // nolint: gosec // test only code 203 httpClient := &http.Client{ 204 Transport: &http.Transport{ 205 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 206 }, 207 } 208 wait := 10 * time.Millisecond 209 for try := 0; try < 10; try++ { 210 // Try to call the server 211 res, err := httpClient.Get(fmt.Sprintf("%s/.well-known/openid-configuration", ms.URL)) 212 if err != nil { 213 log.Infof("Server not yet serving: %v", err) 214 // Retry after some sleep. 215 wait *= 2 216 time.Sleep(wait) 217 continue 218 } 219 res.Body.Close() 220 log.Infof("Successfully serving on %s", ms.URL) 221 atomic.StoreUint64(&ms.OpenIDHitNum, 0) 222 atomic.StoreUint64(&ms.PubKeyHitNum, 0) 223 ms.server = server 224 return nil 225 } 226 227 _ = ms.Stop() 228 return errors.New("server failed to start") 229 } 230 231 // Stop stops he mock server. 232 func (ms *MockOpenIDDiscoveryServer) Stop() error { 233 atomic.StoreUint64(&ms.OpenIDHitNum, 0) 234 atomic.StoreUint64(&ms.PubKeyHitNum, 0) 235 if ms.server == nil { 236 return nil 237 } 238 239 return ms.server.Close() 240 } 241 242 func (ms *MockOpenIDDiscoveryServer) openIDCfg(w http.ResponseWriter, req *http.Request) { 243 atomic.AddUint64(&ms.OpenIDHitNum, 1) 244 fmt.Fprintf(w, "%v", fmt.Sprintf(cfgContent, ms.URL+"/oauth2/v3/certs")) 245 } 246 247 func (ms *MockOpenIDDiscoveryServer) jwtPubKey(w http.ResponseWriter, req *http.Request) { 248 atomic.AddUint64(&ms.PubKeyHitNum, 1) 249 250 if ms.ReturnSuccessAfterFirstNumHits > 0 && atomic.LoadUint64(&ms.PubKeyHitNum) >= ms.ReturnSuccessAfterFirstNumHits { 251 fmt.Fprintf(w, "%v", JwtPubKey1) 252 return 253 } 254 255 if ms.ReturnErrorAfterFirstNumHits != 0 && atomic.LoadUint64(&ms.PubKeyHitNum) > ms.ReturnErrorAfterFirstNumHits { 256 w.WriteHeader(http.StatusForbidden) 257 fmt.Fprintf(w, "Mock server configured to return error after %d hits", ms.ReturnErrorAfterFirstNumHits) 258 return 259 } 260 261 if atomic.LoadUint64(&ms.PubKeyHitNum) <= ms.ReturnErrorForFirstNumHits { 262 w.WriteHeader(http.StatusForbidden) 263 fmt.Fprintf(w, "Mock server configured to return error until %d retries", ms.ReturnErrorForFirstNumHits) 264 return 265 } 266 267 if atomic.LoadUint64(&ms.PubKeyHitNum) == ms.ReturnErrorForFirstNumHits+1 { 268 fmt.Fprintf(w, "%v", JwtPubKey1) 269 return 270 } 271 272 if ms.ReturnReorderedKeyAfterFirstNumHits != 0 && atomic.LoadUint64(&ms.PubKeyHitNum) >= ms.ReturnReorderedKeyAfterFirstNumHits+1 { 273 fmt.Fprintf(w, "%v", JwtPubKey1Reordered) 274 return 275 } 276 277 fmt.Fprintf(w, "%v", JwtPubKey2) 278 }