diff --git a/README.md b/README.md index fdc749199..59744a159 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ - [Loading Configuration](#loading-configuration) - [Request Fields](#request-fields) - [Statistics](#statistics) +- [HTTP Port](#http-port) + - [/json endpoint](#json-endpoint) - [Debug Port](#debug-port) - [Local Cache](#local-cache) - [Redis](#redis) @@ -360,6 +362,29 @@ ratelimit.service.rate_limit.messaging.message_type_marketing.to_number.over_lim ratelimit.service.rate_limit.messaging.message_type_marketing.to_number.total_hits: 0 ``` +# HTTP Port + +The ratelimit service listens to HTTP 1.1 (by default on port 8080) with two endpoints: +1. /healthcheck → return a 200 if this service is healthy +1. /json → HTTP 1.1 endpoint for interacting with ratelimit service + +## /json endpoint + +Takes an HTTP POST with a JSON body of the form e.g. +```json +{ + "domain": "dummy", + "descriptors": [ + {"entries": [ + {"key": "one_per_day", + "value": "something"} + ]} + ] +} +``` +The service will return an http 200 if this request is allowed (if no ratelimits exceeded) or 429 if one or more +ratelimits were exceeded. Endpoint does not currently return detailed information on which limits were exceeded. + # Debug Port The debug port can be used to interact with the running process. diff --git a/docker-compose.yml b/docker-compose.yml index c4ab4443f..51360d361 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,14 @@ services: - .:/go/src/github.com/envoyproxy/ratelimit - binary:/usr/local/bin/ + ratelimit-client-build: + image: golang:1.14-alpine + working_dir: /go/src/github.com/envoyproxy/ratelimit + command: go build -o /usr/local/bin/ratelimit_client ./src/client_cmd/main.go + volumes: + - .:/go/src/github.com/envoyproxy/ratelimit + - binary:/usr/local/bin/ + ratelimit: image: alpine:3.6 command: /usr/local/bin/ratelimit @@ -29,6 +37,7 @@ services: depends_on: - redis - ratelimit-build + - ratelimit-client-build networks: - ratelimit-network volumes: diff --git a/src/server/server.go b/src/server/server.go index 820085744..38520092a 100644 --- a/src/server/server.go +++ b/src/server/server.go @@ -1,6 +1,7 @@ package server import ( + pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" "net/http" "github.com/lyft/goruntime/loader" @@ -25,6 +26,7 @@ type Server interface { * Add an HTTP endpoint to the local debug port. */ AddDebugHttpEndpoint(path string, help string, handler http.HandlerFunc) + AddJsonHandler(pb.RateLimitServiceServer) /** * Returns the embedded gRPC server to be used for registering gRPC endpoints. diff --git a/src/server/server_impl.go b/src/server/server_impl.go index 2b27f06cd..b0349594e 100644 --- a/src/server/server_impl.go +++ b/src/server/server_impl.go @@ -1,6 +1,7 @@ package server import ( + "encoding/json" "expvar" "fmt" "io" @@ -17,6 +18,7 @@ import ( "net" "github.com/coocood/freecache" + pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2" "github.com/envoyproxy/ratelimit/src/settings" "github.com/gorilla/mux" reuseport "github.com/kavu/go_reuseport" @@ -52,6 +54,37 @@ func (server *server) AddDebugHttpEndpoint(path string, help string, handler htt server.debugListener.endpoints[path] = help } +// add an http/1 handler at the /json endpoint which allows this ratelimit service to work with +// clients that cannot use the gRPC interface (e.g. lua) +// example usage from cURL with domain "dummy" and descriptor "perday": +// echo '{"domain": "dummy", "descriptors": [{"entries": [{"key": "perday"}]}]}' | curl -vvvXPOST --data @/dev/stdin localhost:8080/json +func (server *server) AddJsonHandler(svc pb.RateLimitServiceServer) { + handler := func(writer http.ResponseWriter, request *http.Request) { + var req pb.RateLimitRequest + + if err := json.NewDecoder(request.Body).Decode(&req); err != nil { + logger.Warnf("error: %s", err.Error()) + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + + resp, err := svc.ShouldRateLimit(nil, &req) + if err != nil { + logger.Warnf("error: %s", err.Error()) + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + logger.Debugf("resp:%s", resp) + if resp.OverallCode == pb.RateLimitResponse_OVER_LIMIT { + http.Error(writer, "over limit", http.StatusTooManyRequests) + } else if resp.OverallCode == pb.RateLimitResponse_UNKNOWN { + http.Error(writer, "unknown", http.StatusInternalServerError) + } + + } + server.router.HandleFunc("/json", handler) +} + func (server *server) GrpcServer() *grpc.Server { return server.grpcServer } diff --git a/src/service_cmd/runner/runner.go b/src/service_cmd/runner/runner.go index ee9da4b39..a6a5d0d8f 100644 --- a/src/service_cmd/runner/runner.go +++ b/src/service_cmd/runner/runner.go @@ -75,6 +75,8 @@ func (runner *Runner) Run() { io.WriteString(writer, service.GetCurrentConfig().Dump()) }) + srv.AddJsonHandler(service) + // Ratelimit is compatible with two proto definitions // 1. data-plane-api rls.proto: https://github.com/envoyproxy/data-plane-api/blob/master/envoy/service/ratelimit/v2/rls.proto pb.RegisterRateLimitServiceServer(srv.GrpcServer(), service) diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index b33683c8e..067a9a312 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -3,8 +3,10 @@ package integration_test import ( + "bytes" "fmt" "math/rand" + "net/http" "os" "strconv" "testing" @@ -344,6 +346,7 @@ func TestBasicConfigLegacy(t *testing.T) { assert := assert.New(t) conn, err := grpc.Dial("localhost:8083", grpc.WithInsecure()) + assert.NoError(err) defer conn.Close() c := pb_legacy.NewRateLimitServiceClient(conn) @@ -358,6 +361,30 @@ func TestBasicConfigLegacy(t *testing.T) { response) assert.NoError(err) + json_body := []byte(`{ + "domain": "basic", + "descriptors": [ + { + "entries": [ + { + "key": "one_per_minute" + } + ] + } + ] + }`) + http_resp, _ := http.Post("http://localhost:8082/json", "application/json", bytes.NewBuffer(json_body)) + assert.Equal(http_resp.StatusCode, 200) + http_resp.Body.Close() + + http_resp, _ = http.Post("http://localhost:8082/json", "application/json", bytes.NewBuffer(json_body)) + assert.Equal(http_resp.StatusCode, 429) + http_resp.Body.Close() + + invalid_json := []byte(`{"unclosed quote: []}`) + http_resp, _ = http.Post("http://localhost:8082/json", "application/json", bytes.NewBuffer(invalid_json)) + assert.Equal(http_resp.StatusCode, 400) + response, err = c.ShouldRateLimit( context.Background(), common.NewRateLimitRequestLegacy("basic_legacy", [][][2]string{{{"key1", "foo"}}}, 1)) diff --git a/test/integration/runtime/current/ratelimit/config/basic.yaml b/test/integration/runtime/current/ratelimit/config/basic.yaml index 41cd5a31a..843b98873 100644 --- a/test/integration/runtime/current/ratelimit/config/basic.yaml +++ b/test/integration/runtime/current/ratelimit/config/basic.yaml @@ -9,3 +9,8 @@ descriptors: rate_limit: unit: second requests_per_unit: 50 + + - key: one_per_minute + rate_limit: + unit: minute + requests_per_unit: 1 \ No newline at end of file