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  }