| /* |
| Copyright 2017 Google Inc. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package mock |
| |
| import ( |
| "compress/gzip" |
| "encoding/json" |
| "fmt" |
| "net" |
| "net/http" |
| "sync" |
| "time" |
| |
| "github.com/SermoDigital/jose/crypto" |
| "github.com/SermoDigital/jose/jws" |
| "github.com/SermoDigital/jose/jwt" |
| "github.com/apid/istioApigeeAdapter/common" |
| "github.com/julienschmidt/httprouter" |
| ) |
| |
| var mockInit = &sync.Once{} |
| |
| const ( |
| ValidAPIKey1 = "12345" |
| ValidAPIKeyNoProducts = "23456" |
| ValidPublishKey = "aaaaaa" |
| ValidPublishSecret = "bbbbbb" |
| ) |
| |
| type MockServer struct { |
| listener net.Listener |
| analyticsRecords []common.AnalyticsRecord |
| } |
| |
| func StartMockServer(port int) (*MockServer, error) { |
| mockInit.Do(makeKeys) |
| |
| l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) |
| if err != nil { |
| return nil, err |
| } |
| |
| ms := &MockServer{ |
| listener: l, |
| } |
| |
| router := httprouter.New() |
| router.GET("/publicKey", ms.getPublicKey) |
| router.GET("/products", ms.getProducts) |
| router.POST("/verifyApiKey", ms.getAPIKey) |
| router.POST("/edgemicro/axpublisher/organization/:org/environment/:env", ms.publishAnalytics) |
| router.GET("/edgemicro/region/organization/:org/environment/:env", ms.getRegion) |
| |
| go func() { |
| http.Serve(l, router) |
| }() |
| |
| return ms, nil |
| } |
| |
| func (m *MockServer) Address() string { |
| return m.listener.Addr().String() |
| } |
| |
| func (m *MockServer) Stop() { |
| m.listener.Close() |
| } |
| |
| func (m *MockServer) GetAnalyticsRecords() []common.AnalyticsRecord { |
| return m.analyticsRecords |
| } |
| |
| func (m *MockServer) getPublicKey(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { |
| w.Header().Set("content-type", "text/plain") |
| w.Write(mockCertPEM) |
| } |
| |
| func (m *MockServer) getProducts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { |
| now := time.Now() |
| p := []common.APIProduct{ |
| { |
| Name: "TestProduct1", |
| DisplayName: "First Product", |
| Description: "First test product", |
| CreatedAt: now.UnixNano(), |
| CreatedBy: "foo@bar.com", |
| LastModifiedAt: now.UnixNano(), |
| LastModifiedBy: "foo@bar.com", |
| |
| APIResources: []string{"/**"}, |
| ApprovalType: "auto", |
| Attributes: []common.Attribute{ |
| { |
| Name: "access", |
| Value: "public", |
| }, |
| }, |
| Environments: []string{"test", "prod"}, |
| Proxies: []string{"ProxyOne"}, |
| Quota: "10", |
| QuotaInterval: "1", |
| QuotaTimeUnit: "minute", |
| }, |
| } |
| pl := common.APIProductResponse{ |
| Products: p, |
| } |
| |
| w.Header().Set("content-type", "application/json") |
| |
| enc := json.NewEncoder(w) |
| enc.Encode(pl) |
| } |
| |
| func (m *MockServer) getAPIKey(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { |
| if r.Header.Get("content-type") != "application/json" { |
| // That's what the existing service does |
| sendFault(w, 404, "Failed to resolve API Key variable apikey", |
| "steps.oauth.v2.FailedToResolveAPIKey") |
| return |
| } |
| |
| var request common.VerifyAPIKeyRequest |
| defer r.Body.Close() |
| dec := json.NewDecoder(r.Body) |
| err := dec.Decode(&request) |
| if err != nil { |
| // Again, what existing service does |
| sendFault(w, 500, "Failed to execute the ExtractVariables: Extract-API-Key", |
| "steps.extractvariables.ExecutionFailed") |
| return |
| } |
| |
| var returnedJwt jwt.JWT |
| |
| switch request.Key { |
| case ValidAPIKey1: |
| returnedJwt = makeJWT1() |
| case ValidAPIKeyNoProducts: |
| returnedJwt = makeJWT2() |
| default: |
| // We actually do return 401 in this case! |
| sendFault(w, 401, |
| fmt.Sprintf("API key %s is not valid", request.Key), |
| "steps.oauth.v2.FailedToResolveAPIKey") |
| return |
| } |
| |
| bod, err := returnedJwt.Serialize(mockKey) |
| if err != nil { |
| sendFault(w, 500, err.Error(), "JWT") |
| return |
| } |
| |
| tok := common.VerifyAPIKeyResponse{ |
| Token: string(bod), |
| } |
| w.Header().Set("content-type", "application/json") |
| enc := json.NewEncoder(w) |
| enc.Encode(&tok) |
| } |
| |
| func (m *MockServer) publishAnalytics(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { |
| user, pw, aok := r.BasicAuth() |
| if !aok || user != ValidPublishKey || pw != ValidPublishSecret { |
| w.WriteHeader(401) |
| return |
| } |
| if r.Header.Get("content-type") != "application/json" { |
| sendAnalyticsResponse(w, 0, 1) |
| return |
| } |
| |
| var req common.AnalyticsRequest |
| var err error |
| defer r.Body.Close() |
| |
| if r.Header.Get("content-encoding") == "" { |
| dec := json.NewDecoder(r.Body) |
| err = dec.Decode(&req) |
| } else if r.Header.Get("content-encoding") == "gzip" { |
| zr, _ := gzip.NewReader(r.Body) |
| dec := json.NewDecoder(zr) |
| err = dec.Decode(&req) |
| } |
| |
| if err != nil { |
| // This is what the existing service does |
| sendFault(w, 500, "Failed to execute JavaCallout. not a JSON Object", |
| "steps.javacallout.ExecutionError") |
| return |
| } |
| |
| // For now we just accept it all. We'll add some validation in the future. |
| m.analyticsRecords = append(m.analyticsRecords, req.Records...) |
| sendAnalyticsResponse(w, len(req.Records), 0) |
| } |
| |
| func sendAnalyticsResponse(w http.ResponseWriter, accepted, rejected int) { |
| resp := &common.AnalyticsResponse{ |
| Accepted: accepted, |
| Rejected: rejected, |
| } |
| w.Header().Set("content-type", "application/json") |
| enc := json.NewEncoder(w) |
| enc.Encode(resp) |
| } |
| |
| func (m *MockServer) getRegion(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { |
| user, pw, aok := r.BasicAuth() |
| if !aok || user != ValidPublishKey || pw != ValidPublishSecret { |
| w.WriteHeader(401) |
| return |
| } |
| |
| rr := common.RegionResponse{ |
| Region: "test", |
| Host: m.Address(), |
| } |
| |
| w.Header().Set("content-type", "application/json") |
| enc := json.NewEncoder(w) |
| enc.Encode(&rr) |
| } |
| |
| func sendFault(w http.ResponseWriter, errorCode int, fault, code string) { |
| w.Header().Set("content-type", "application/json") |
| w.WriteHeader(errorCode) |
| f := &common.APIFaultMessage{ |
| Fault: common.APIFault{ |
| FaultString: fault, |
| Detail: common.APIFaultDetail{ |
| ErrorCode: code, |
| }, |
| }, |
| } |
| |
| enc := json.NewEncoder(w) |
| enc.Encode(f) |
| } |
| |
| func makeJWT1() jwt.JWT { |
| now := time.Now() |
| c := jws.Claims{} |
| c.SetIssuedAt(now) |
| c.SetNotBefore(now) |
| c.Set("audience", "microgateway") |
| c.Set("jti", "52137037-6ce1-426e-a255-e471f94854e5") |
| c.Set("iss", "https://mock.foo.net/verifyApiKey") |
| c.Set("client_id", ValidAPIKey1) |
| c.Set("application_name", "TestApp1") |
| c.Set("api_product_list", []string{ |
| "TestProduct1", |
| }) |
| |
| return jws.NewJWT(c, crypto.SigningMethodRS256) |
| } |
| |
| func makeJWT2() jwt.JWT { |
| now := time.Now() |
| c := jws.Claims{} |
| c.SetIssuedAt(now) |
| c.SetNotBefore(now) |
| c.Set("audience", "microgateway") |
| c.Set("jti", "52137037-6ce1-426e-a255-e471f94854e5") |
| c.Set("iss", "https://mock.foo.net/verifyApiKey") |
| c.Set("client_id", ValidAPIKeyNoProducts) |
| c.Set("application_name", "TestApp2") |
| c.Set("api_product_list", []string{ |
| "UnknownProduct1", |
| }) |
| |
| return jws.NewJWT(c, crypto.SigningMethodRS256) |
| } |