// WithParseAndValidate represent a middleware handler. For POST or // PUT requests, it also parses the request body as a form. The extracted valid // token will be added to the context. The extracted token will be checked // against the Blacklist. errHandler is an optional argument. Only the first // item in the slice will be considered. Default errHandler is: // // http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) // // ProTip: Instead of passing the token as an HTML Header you can also add the token // to a form (multipart/form-data) with an input name of access_token. If the // token cannot be found within the Header the fallback triggers the lookup within the form. func (s *Service) WithParseAndValidate(errHandler ...ctxhttp.Handler) ctxhttp.Middleware { var errH ctxhttp.Handler errH = ctxhttp.HandlerFunc(defaultTokenErrorHandler) if len(errHandler) == 1 && errHandler[0] != nil { errH = errHandler[0] } return func(h ctxhttp.Handler) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { token, err := jwt.ParseFromRequest(r, s.keyFunc) var inBL bool if token != nil { inBL = s.Blacklist.Has(token.Raw) } if token != nil && err == nil && token.Valid && !inBL { return h.ServeHTTPContext(NewContext(ctx, token), w, r) } if PkgLog.IsDebug() { PkgLog.Debug("ctxjwt.Service.Authenticate", "err", err, "token", token, "inBlacklist", inBL) } return errH.ServeHTTPContext(NewContextWithError(ctx, err), w, r) }) } }
func TestWithAccessLog(t *testing.T) { var buf bytes.Buffer defer buf.Reset() ctx := ctxlog.NewContext(context.Background(), log.NewStdLogger(log.SetStdWriter(&buf))) finalH := ctxhttp.Chain( ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { w.WriteHeader(http.StatusTeapot) _, err := w.Write([]byte{'1', '2', '3'}) time.Sleep(time.Millisecond) return err }), ctxhttp.WithAccessLog(), ) r, _ := http.NewRequest("GET", "/gopherine", nil) r.RemoteAddr = "127.0.0.1" r.Header.Set("User-Agent", "Mozilla") r.Header.Set("Referer", "http://rustlang.org") w := httptest.NewRecorder() if err := finalH.ServeHTTPContext(ctx, w, r); err != nil { t.Fatal(err) } assert.Exactly(t, `123`, w.Body.String()) assert.Exactly(t, http.StatusTeapot, w.Code) want1 := `request error: "" method: "GET" uri: "/gopherine" type: "access" status: "error" status_code: 418 duration:` want2 := `size: 3 remote_addr: "127.0.0.1" user_agent: "Mozilla" referer: "http://rustlang.org"` assert.Contains(t, buf.String(), want1) assert.Contains(t, buf.String(), want2) }
func TestWithParseAndValidateInBlackList(t *testing.T) { bl := &testRealBL{} jm, err := ctxjwt.NewService( ctxjwt.WithBlacklist(bl), ) assert.NoError(t, err) theToken, _, err := jm.GenerateToken(nil) bl.token = theToken assert.NoError(t, err) assert.NotEmpty(t, theToken) req, err := http.NewRequest("GET", "http://auth.xyz", nil) assert.NoError(t, err) ctxjwt.SetHeaderAuthorization(req, theToken) finalHandler := ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { w.WriteHeader(http.StatusTeapot) return nil }) authHandler := jm.WithParseAndValidate()(finalHandler) wRec := httptest.NewRecorder() assert.NoError(t, authHandler.ServeHTTPContext(context.Background(), wRec, req)) assert.NotEqual(t, http.StatusTeapot, wRec.Code) assert.Equal(t, http.StatusUnauthorized, wRec.Code) }
func TestWithParseAndValidateSuccess(t *testing.T) { jm, err := ctxjwt.NewService() assert.NoError(t, err) theToken, _, err := jm.GenerateToken(map[string]interface{}{ "xfoo": "bar", "zfoo": 4711, }) assert.NoError(t, err) assert.NotEmpty(t, theToken) req, err := http.NewRequest("GET", "http://auth.xyz", nil) assert.NoError(t, err) ctxjwt.SetHeaderAuthorization(req, theToken) finalHandler := ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { w.WriteHeader(http.StatusTeapot) fmt.Fprintf(w, "I'm more of a coffee pot") ctxToken, err := ctxjwt.FromContext(ctx) assert.NoError(t, err) assert.NotNil(t, ctxToken) assert.Exactly(t, "bar", ctxToken.Claims["xfoo"].(string)) return nil }) authHandler := jm.WithParseAndValidate()(finalHandler) wRec := httptest.NewRecorder() assert.NoError(t, authHandler.ServeHTTPContext(context.Background(), wRec, req)) assert.Equal(t, http.StatusTeapot, wRec.Code) assert.Equal(t, `I'm more of a coffee pot`, wRec.Body.String()) }
// WithIsCountryAllowedByIP a more advanced function. It expects from the context // the store.ManagerReader ... func (s *Service) WithIsCountryAllowedByIP() ctxhttp.Middleware { return func(h ctxhttp.Handler) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { _, requestedStore, err := store.FromContextReader(ctx) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("geoip.WithCountryByIP.FromContextManagerReader", "err", err) } return errgo.Mask(err) } var ipCountry *IPCountry ctx, ipCountry, err = s.newContextCountryByIP(ctx, r) if err != nil { ctx = NewContextWithError(ctx, err) return h.ServeHTTPContext(ctx, w, r) } allowedCountries, err := directory.AllowedCountries(requestedStore.Config) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("geoip.WithCountryByIP.directory.AllowedCountries", "err", err, "st.Config", requestedStore.Config) } return errgo.Mask(err) } if false == s.IsAllowed(requestedStore, ipCountry, allowedCountries, r) { h = s.altHandlerByID(requestedStore) } return h.ServeHTTPContext(ctx, w, r) }) } }
// Benchmark_WithValidateBaseUrl-4 3000 489089 ns/op 188333 B/op 272 allocs/op => with debug enabled // Benchmark_WithValidateBaseUrl-4 200000 8925 ns/op 2924 B/op 49 allocs/op => no debug func Benchmark_WithValidateBaseUrl(b *testing.B) { // todo: there is room for optimization with disabled debugging. too many allocs store.PkgLog.SetLevel(log.StdLevelInfo) req, err := http.NewRequest(httputils.MethodGet, "https://corestore.io/customer/comments/view?id=1916#tab=ratings", nil) if err != nil { b.Fatal(err) } finalHandler := store.WithValidateBaseURL(middlewareConfigReader)(ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return errors.New("This handler should not be called!") })) want := "https://www.corestore.io/customer/comments/view?id=1916#tab=ratings" rec := httptest.NewRecorder() b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { if err := finalHandler.ServeHTTPContext(middlewareCtxStoreService, rec, req); err != nil { b.Error(err) } if rec.HeaderMap.Get("Location") != want { b.Errorf("Have: %s\nWant: %s", rec.HeaderMap.Get("Location"), want) } rec.HeaderMap = nil } }
func finalHandlerWithValidateBaseURL(t *testing.T) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { assert.NotNil(t, ctx) assert.NotNil(t, w) assert.NotNil(t, r) assert.Empty(t, w.Header().Get("Location")) return nil }) }
func ipErrorFinalHandler(t *testing.T) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { ipc, err, ok := geoip.FromContextCountry(ctx) assert.Nil(t, ipc) assert.True(t, ok) assert.EqualError(t, err, geoip.ErrCannotGetRemoteAddr.Error()) return nil }) }
func finalInitStoreHandler(t *testing.T, wantStoreCode string) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { _, haveReqStore, err := store.FromContextReader(ctx) if err != nil { return err } assert.Exactly(t, wantStoreCode, haveReqStore.StoreCode()) return nil }) }
func TestCustomHTTPRateLimitHandlers(t *testing.T) { limiter := ctxthrottled.HTTPRateLimit{ RateLimiter: &stubLimiter{}, VaryBy: &pathGetter{}, DeniedHandler: ctxhttp.HandlerFunc(func(_ context.Context, w http.ResponseWriter, _ *http.Request) error { http.Error(w, "custom limit exceeded", 400) return nil }), } handler := limiter.WithRateLimit(nil, ctxhttp.HandlerFunc(func(_ context.Context, w http.ResponseWriter, _ *http.Request) error { w.WriteHeader(200) return nil })) runHTTPTestCases(t, handler, []httpTestCase{ {"limit", 400, map[string]string{}}, {"error", 500, map[string]string{}}, }) }
func finalHandlerFinland(t *testing.T) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { ipc, err, ok := geoip.FromContextCountry(ctx) assert.NotNil(t, ipc) assert.True(t, ok) assert.NoError(t, err) assert.Exactly(t, "2a02:d200::", ipc.IP.String()) assert.Exactly(t, "FI", ipc.Country.Country.IsoCode) return nil }) }
// WithValidateBaseURL is a middleware which checks if the request base URL // is equal to the one store in the configuration, if not // i.e. redirect from http://example.com/store/ to http://www.example.com/store/ // @see app/code/Magento/Store/App/FrontController/Plugin/RequestPreprocessor.php func WithValidateBaseURL(cr config.ReaderPubSuber) ctxhttp.Middleware { // Having the GetBool command here, means you must restart the app to take // changes in effect. @todo refactor and use pub/sub to automatically change // the isRedirectToBase value. checkBaseURL, err := cr.GetBool(config.Path(PathRedirectToBase)) // scope default if config.NotKeyNotFoundError(err) && PkgLog.IsDebug() { PkgLog.Debug("ctxhttp.WithValidateBaseUrl.GetBool", "err", err, "path", PathRedirectToBase) } redirectCode := http.StatusMovedPermanently if rc, err := cr.GetInt(config.Path(PathRedirectToBase)); rc != redirectCode && false == config.NotKeyNotFoundError(err) { redirectCode = http.StatusFound } return func(h ctxhttp.Handler) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { if checkBaseURL && r.Method != "POST" { _, requestedStore, err := FromContextReader(ctx) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("ctxhttp.WithValidateBaseUrl.FromContextServiceReader", "err", err, "ctx", ctx) } return errgo.Mask(err) } baseURL, err := requestedStore.BaseURL(config.URLTypeWeb, requestedStore.IsCurrentlySecure(r)) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("ctxhttp.WithValidateBaseUrl.requestedStore.BaseURL", "err", err, "ctx", ctx) } return errgo.Mask(err) } if err := httputils.IsBaseURLCorrect(r, &baseURL); err != nil { if PkgLog.IsDebug() { PkgLog.Debug("store.WithValidateBaseUrl.IsBaseUrlCorrect.error", "err", err, "baseURL", baseURL, "request", r) } baseURL.Path = r.URL.Path baseURL.RawPath = r.URL.RawPath baseURL.RawQuery = r.URL.RawQuery baseURL.Fragment = r.URL.Fragment http.Redirect(w, r, (&baseURL).String(), redirectCode) return nil } } return h.ServeHTTPContext(ctx, w, r) }) } }
// WithCountryByIP is a simple middleware which detects the country via an IP // address. With the detected country a new tree context.Context gets created. func (s *Service) WithCountryByIP() ctxhttp.Middleware { return func(h ctxhttp.Handler) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { var err error ctx, _, err = s.newContextCountryByIP(ctx, r) if err != nil { ctx = NewContextWithError(ctx, err) } return h.ServeHTTPContext(ctx, w, r) }) } }
func benchValidationHandler(b *testing.B, wantStoreCode string) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { _, haveReqStore, err := store.FromContextReader(ctx) if err != nil { return err } if wantStoreCode != haveReqStore.StoreCode() { b.Errorf("Want: %s\nHave: %s", wantStoreCode, haveReqStore.StoreCode()) } return nil }) }
func TestWithValidateBaseUrl_ActivatedAndShouldRedirectWithGETRequest(t *testing.T) { var newReq = func(urlStr string) *http.Request { req, err := http.NewRequest(httputils.MethodGet, urlStr, nil) if err != nil { t.Fatal(err) } return req } tests := []struct { rec *httptest.ResponseRecorder req *http.Request wantRedirectURL string }{ { httptest.NewRecorder(), newReq("http://corestore.io/catalog/product/view/"), "http://www.corestore.io/catalog/product/view/", }, { httptest.NewRecorder(), newReq("http://corestore.io/catalog/product/view"), "http://www.corestore.io/catalog/product/view", }, { httptest.NewRecorder(), newReq("http://corestore.io"), "http://www.corestore.io", }, { httptest.NewRecorder(), newReq("https://corestore.io/catalog/category/view?catid=1916"), "https://www.corestore.io/catalog/category/view?catid=1916", }, { httptest.NewRecorder(), newReq("https://corestore.io/customer/comments/view?id=1916#tab=ratings"), "https://www.corestore.io/customer/comments/view?id=1916#tab=ratings", }, } for i, test := range tests { mw := store.WithValidateBaseURL(middlewareConfigReader)(ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return fmt.Errorf("This handler should not be called! Iindex %d", i) })) assert.NoError(t, mw.ServeHTTPContext(middlewareCtxStoreService, test.rec, test.req), "Index %d", i) assert.Exactly(t, test.wantRedirectURL, test.rec.HeaderMap.Get("Location"), "Index %d", i) } }
func testAuth(t *testing.T, errH ctxhttp.Handler, opts ...ctxjwt.Option) (ctxhttp.Handler, string) { jm, err := ctxjwt.NewService(opts...) assert.NoError(t, err) theToken, _, err := jm.GenerateToken(map[string]interface{}{ "xfoo": "bar", "zfoo": 4711, }) assert.NoError(t, err) final := ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { w.WriteHeader(http.StatusOK) return nil }) authHandler := jm.WithParseAndValidate(errH)(final) return authHandler, theToken }
// WithInitStoreByToken is a middleware which initializes a request based store // via a JSON Web Token. // Extracts the store.Reader and jwt.Token from context.Context. If the requested // store is different than the initialized requested store than the new requested // store will be saved in the context. func WithInitStoreByToken() ctxhttp.Middleware { return func(h ctxhttp.Handler) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { storeService, requestedStore, err := FromContextReader(ctx) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("store.WithInitStoreByToken.FromContextServiceReader", "err", err, "ctx", ctx) } return errgo.Mask(err) } token, err := ctxjwt.FromContext(ctx) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("store.WithInitStoreByToken.ctxjwt.FromContext.err", "err", err, "ctx", ctx) } return errgo.Mask(err) } scopeOption, err := CodeFromClaim(token.Claims) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("store.WithInitStoreByToken.StoreCodeFromClaim", "err", err, "token", token, "ctx", ctx) } return errgo.Mask(err) } newRequestedStore, err := storeService.RequestedStore(scopeOption) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("store.WithInitStoreByToken.RequestedStore", "err", err, "token", token, "scopeOption", scopeOption, "ctx", ctx) } return errgo.Mask(err) } if newRequestedStore.StoreID() != requestedStore.StoreID() { // this may lead to a bug because the previously set storeService and requestedStore // will still exists and have not been removed. ctx = NewContextReader(ctx, storeService, newRequestedStore) } return h.ServeHTTPContext(ctx, w, r) }) } }
func TestHTTPRateLimit(t *testing.T) { limiter := ctxthrottled.HTTPRateLimit{ RateLimiter: &stubLimiter{}, VaryBy: &pathGetter{}, } handler := limiter.WithRateLimit(nil, ctxhttp.HandlerFunc(func(_ context.Context, w http.ResponseWriter, _ *http.Request) error { w.WriteHeader(200) return nil })) runHTTPTestCases(t, handler, []httpTestCase{ {"ok", 200, map[string]string{"X-Ratelimit-Limit": "1", "X-Ratelimit-Remaining": "2", "X-Ratelimit-Reset": "60"}}, {"error", 500, map[string]string{}}, {"limit", 429, map[string]string{"Retry-After": "60"}}, }) }
func TestWithParseAndValidateHTTPErrorHandler(t *testing.T) { authHandler, _ := testAuth(t, ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { tok, err := ctxjwt.FromContext(ctx) assert.Nil(t, tok) w.WriteHeader(http.StatusTeapot) _, err = w.Write([]byte(err.Error())) return err })) req, err := http.NewRequest("GET", "http://auth.xyz", nil) assert.NoError(t, err) w := httptest.NewRecorder() assert.NoError(t, authHandler.ServeHTTPContext(context.Background(), w, req)) assert.Equal(t, http.StatusTeapot, w.Code) assert.Equal(t, "no token present in request", w.Body.String()) }
// WithRateLimit wraps an ctxhttp.Handler to limit incoming requests. // Requests that are not limited will be passed to the handler // unchanged. Limited requests will be passed to the DeniedHandler. // X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and // Retry-After headers will be written to the response based on the // values in the RateLimitResult. func (t *HTTPRateLimit) WithRateLimit(rlStore throttled.GCRAStore, h ctxhttp.Handler) ctxhttp.Handler { if t.Config == nil { t.Config = config.DefaultManager } if t.DeniedHandler == nil { t.DeniedHandler = DefaultDeniedHandler } if t.RateLimiter == nil { if rlStore == nil { var err error rlStore, err = memstore.New(65536) if err != nil { panic(err) } } var err error t.RateLimiter, err = throttled.NewGCRARateLimiter(rlStore, t.quota()) if err != nil { panic(err) } } return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { var k string if t.VaryBy != nil { k = t.VaryBy.Key(r) } limited, context, err := t.RateLimiter.RateLimit(k, 1) if err != nil { return err } setRateLimitHeaders(w, context) if !limited { return h.ServeHTTPContext(ctx, w, r) } return t.DeniedHandler.ServeHTTPContext(ctx, w, r) }) }
func TestWithIsCountryAllowedByIPErrorStoreManager(t *testing.T) { s := mustGetTestService() defer deferClose(t, s.GeoIP) finalHandler := ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { ipc, err, ok := geoip.FromContextCountry(ctx) assert.Nil(t, ipc) assert.False(t, ok) assert.NoError(t, err) return nil }) countryHandler := s.WithIsCountryAllowedByIP()(finalHandler) rec := httptest.NewRecorder() req, err := http.NewRequest("GET", "http://corestore.io", nil) assert.NoError(t, err) assert.EqualError(t, countryHandler.ServeHTTPContext(context.Background(), rec, req), store.ErrContextServiceNotFound.Error()) }
func TestWithInitStoreByToken_EqualPointers(t *testing.T) { // this Test is related to Benchmark_WithInitStoreByToken // The returned pointers from store.FromContextReader must be the // same for each request with the same request pattern. ctx := newStoreServiceWithTokenCtx(scope.Option{Website: scope.MockID(2)}, "nz") rec := httptest.NewRecorder() req, err := http.NewRequest(httputils.MethodGet, "https://corestore.io/store/list", nil) if err != nil { t.Fatal(err) } var equalStorePointer *store.Store mw := store.WithInitStoreByToken()(ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { _, haveReqStore, err := store.FromContextReader(ctx) if err != nil { return err } if equalStorePointer == nil { equalStorePointer = haveReqStore } if "nz" != haveReqStore.StoreCode() { t.Errorf("Have: %s\nWant: nz", haveReqStore.StoreCode()) } wantP := reflect.ValueOf(equalStorePointer) haveP := reflect.ValueOf(haveReqStore) if wantP.Pointer() != haveP.Pointer() { t.Errorf("Expecting equal pointers for each request.\nWant: %p\nHave: %p", equalStorePointer, haveReqStore) } return nil })) for i := 0; i < 10; i++ { if err := mw.ServeHTTPContext(ctx, rec, req); err != nil { t.Error(err) } } }
func TestWithCountryByIPErrorGetCountryByIP(t *testing.T) { s := mustGetTestService() s.GeoIP = geoReaderMock{} defer deferClose(t, s.GeoIP) finalHandler := ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { ipc, err, ok := geoip.FromContextCountry(ctx) assert.Nil(t, ipc) assert.True(t, ok) assert.EqualError(t, err, "Failed to read country from MMDB") return nil }) countryHandler := s.WithCountryByIP()(finalHandler) rec := httptest.NewRecorder() req, err := http.NewRequest("GET", "http://corestore.io", nil) assert.NoError(t, err) req.Header.Set("X-Forwarded-For", "2a02:d200::") assert.NoError(t, countryHandler.ServeHTTPContext(context.Background(), rec, req)) }
func bmServeHTTP(b *testing.B, opts ...ctxjwt.Option) { service, err := ctxjwt.NewService(opts...) if err != nil { b.Error(err) } token, _, err := service.GenerateToken(map[string]interface{}{ "xfoo": "bar", "zfoo": 4711, }) if err != nil { b.Error(err) } final := ctxhttp.HandlerFunc(func(_ context.Context, w http.ResponseWriter, _ *http.Request) error { w.WriteHeader(http.StatusTeapot) return nil }) jwtHandler := service.WithParseAndValidate()(final) req, err := http.NewRequest("GET", "http://abc.xyz", nil) if err != nil { b.Error(err) } ctxjwt.SetHeaderAuthorization(req, token) w := httptest.NewRecorder() ctx := context.Background() b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { if err := jwtHandler.ServeHTTPContext(ctx, w, req); err != nil { b.Error(err) } if w.Code != http.StatusTeapot { b.Errorf("Response Code want %d; have %d", http.StatusTeapot, w.Code) } } }
func TestHTTPRateLimitConfig(t *testing.T) { cr := config.NewMockReader( config.WithMockValues(config.MockPV{ config.MockPathScopeDefault(ctxthrottled.PathRateLimitBurst): 0, config.MockPathScopeDefault(ctxthrottled.PathRateLimitRequests): 1, config.MockPathScopeDefault(ctxthrottled.PathRateLimitDuration): "i", }), ) limiter := ctxthrottled.HTTPRateLimit{ Config: cr, VaryBy: &pathGetter{}, } handler := limiter.WithRateLimit(nil, ctxhttp.HandlerFunc(func(_ context.Context, w http.ResponseWriter, _ *http.Request) error { w.WriteHeader(200) return nil })) runHTTPTestCases(t, handler, []httpTestCase{ {"xx", 200, map[string]string{"X-Ratelimit-Limit": "1", "X-Ratelimit-Remaining": "0", "X-Ratelimit-Reset": "60"}}, {"xx", 429, map[string]string{"X-Ratelimit-Limit": "1", "X-Ratelimit-Remaining": "0", "X-Ratelimit-Reset": "60", "Retry-After": "60"}}, }) }
return h.ServeHTTPContext(ctx, w, r) }) } } // DefaultAlternativeHandler gets called when detected IPCountry cannot be found // within the list of allowed countries. This handler can be overridden depending // on the overall scope (Website, Group or Store). This function gets called in // WithIsCountryAllowedByIP. // // Status is StatusServiceUnavailable var DefaultAlternativeHandler ctxhttp.Handler = defaultAlternativeHandler var defaultAlternativeHandler ctxhttp.Handler = ctxhttp.HandlerFunc(func(_ context.Context, w http.ResponseWriter, _ *http.Request) error { http.Error(w, http.StatusText(http.StatusServiceUnavailable), http.StatusServiceUnavailable) return nil }) // altHandlerByID searches in the hierarchical order of store -> group -> website // the next alternative handler IF a country is not allowed as defined in function // type IsAllowedFunc. func (s *Service) altHandlerByID(st *store.Store) ctxhttp.Handler { if s.storeIDs != nil && s.storeAltH != nil { return findHandlerByID(scope.StoreID, st.StoreID(), s.storeIDs, s.storeAltH) } if s.groupIDs != nil && s.groupAltH != nil { return findHandlerByID(scope.GroupID, st.Group.GroupID(), s.groupIDs, s.groupAltH) } if s.websiteIDs != nil && s.websiteAltH != nil { return findHandlerByID(scope.WebsiteID, st.Website.WebsiteID(), s.websiteIDs, s.websiteAltH)
func ExampleWithInitStoreByToken() { initStore() ctx := store.NewContextReader(context.Background(), testStoreService) jwtService, err := ctxjwt.NewService(ctxjwt.WithPassword([]byte(`GÒph3r`))) finalHandler := ctxhttp.Chain( ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { _, haveReqStore, err := store.FromContextReader(ctx) if err != nil { return err } // now we know that the current request depends on the store view DE. fmt.Fprintf(w, "StoreCode: %s\n", haveReqStore.StoreCode()) return nil }), // executed 3rd store.WithInitStoreByToken(), // executed 2nd jwtService.WithParseAndValidate(), // executed 1st ) ts := httptest.NewServer(ctxhttp.NewAdapter(ctx, finalHandler)) defer ts.Close() // Setup GET request token, _, err := jwtService.GenerateToken( map[string]interface{}{ // Despite default store for Website ID 1 is AT we are currently // in the store context of DE. store.ParamName: "de", }, ) if err != nil { log.Fatal("jwtService.GenerateToken", "err", err) } req, err := http.NewRequest("GET", ts.URL, nil) if err != nil { log.Fatal("http.Get", "err", err) } ctxjwt.SetHeaderAuthorization(req, token) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal("http.DefaultClient.Do", "err", err) } response, err := ioutil.ReadAll(res.Body) if errC := res.Body.Close(); errC != nil { log.Fatal("res.Body.Close", "err", errC) } if err != nil { log.Fatal("ioutil.ReadAll", "err", err) } fmt.Printf("Response: %s\n", response) fmt.Printf("Log: %s\n", testDebugLogBuf.String()) // Output: // Response: StoreCode: de // // Log: }
// WithInitStoreByFormCookie reads from a GET parameter or cookie the store code. // Checks if the store code is valid and allowed. If so it adjusts the context.Context // to provide the new requestedStore. // // It calls Reader.RequestedStore() to determine the correct store. // 1. check cookie store, always a string and the store code // 2. check for GET ___store variable, always a string and the store code func WithInitStoreByFormCookie() ctxhttp.Middleware { return func(h ctxhttp.Handler) ctxhttp.Handler { return ctxhttp.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error { storeService, requestedStore, err := FromContextReader(ctx) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("store.WithInitStoreByToken.FromContextServiceReader", "err", err, "ctx", ctx) } return errgo.Mask(err) } var reqSO scope.Option reqSO, err = CodeFromRequestGET(r) if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("store.WithInitStoreByFormCookie.StoreCodeFromRequestGET", "err", err, "req", r, "scope", reqSO) } reqSO, err = CodeFromCookie(r) if err != nil { // ignore further processing because all codes are invalid or not found if PkgLog.IsDebug() { PkgLog.Debug("store.WithInitStoreByFormCookie.StoreCodeFromCookie", "err", err, "req", r, "scope", reqSO) } return h.ServeHTTPContext(ctx, w, r) } } var newRequestedStore *Store if newRequestedStore, err = storeService.RequestedStore(reqSO); err != nil { if PkgLog.IsDebug() { PkgLog.Debug("store.WithInitStoreByFormCookie.storeService.RequestedStore", "err", err, "req", r, "scope", reqSO) } return errgo.Mask(err) } soStoreCode := reqSO.StoreCode() // delete and re-set a new cookie, adjust context.Context if newRequestedStore != nil && newRequestedStore.Data.Code.String == soStoreCode { wds, err := newRequestedStore.Website.DefaultStore() if err != nil { if PkgLog.IsDebug() { PkgLog.Debug("store.WithInitStoreByFormCookie.Website.DefaultStore", "err", err, "soStoreCode", soStoreCode) } return errgo.Mask(err) } if wds.Data.Code.String == soStoreCode { newRequestedStore.DeleteCookie(w) // cookie not needed anymore } else { newRequestedStore.SetCookie(w) // make sure we force set the new store if newRequestedStore.StoreID() != requestedStore.StoreID() { // this may lead to a bug because the previously set storeService and requestedStore // will still exists and have not been removed. ctx = NewContextReader(ctx, storeService, newRequestedStore) } } } return h.ServeHTTPContext(ctx, w, r) }) } }
// greater than or equal to zero. // Scope Global, Type Int. PathRateLimitBurst = `corestore/ctxthrottled/burst` // PathRateLimitRequests number of requests allowed per time period // Scope Global, Type Int. PathRateLimitRequests = `corestore/ctxthrottled/requests` // PathRateLimitDuration per second (s), minute (i), hour (h), day (d) // Scope Global, Type String. PathRateLimitDuration = `corestore/ctxthrottled/duration` ) // DefaultDeniedHandler is the default DeniedHandler for an // HTTPRateLimit. It returns a 429 status code with a generic // message. var DefaultDeniedHandler = ctxhttp.Handler(ctxhttp.HandlerFunc(func(_ context.Context, w http.ResponseWriter, _ *http.Request) error { http.Error(w, "limit exceeded", 429) return nil })) // DefaultBurst defines the number of requests that // will be allowed to exceed the rate in a single burst and must be // greater than or equal to zero. var DefaultBurst int = 5 // DefaultRequests number of requests allowed per time period var DefaultRequests int = 100 // DefaultDuration per second (s), minute (i), hour (h), day (d) var DefaultDuration string = "h" // HTTPRateLimit faciliates using a Limiter to limit HTTP requests. type HTTPRateLimit struct {