Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

THREESCALE-9976 Fix GRPC #1419

Merged
merged 5 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions dev-environments/grpc/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec
.DEFAULT_GOAL := gateway
MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
WORKDIR := $(patsubst %/,%,$(dir $(MKFILE_PATH)))
DOCKER ?= $(shell which docker 2> /dev/null || echo "docker")

gateway: ## run gateway configured to access upstream powered with TLS
$(DOCKER) compose -f docker-compose.yml run --service-ports gateway

clean:
$(DOCKER) compose down --volumes --remove-orphans
$(DOCKER) compose -f docker-compose.yml down --volumes --remove-orphans
- rm -rf gateway-cert
- rm -rf upstream-cert
- rm -rf bin

ca:
openssl genrsa -out rootCA.key 2048
openssl req -batch -new -x509 -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.pem

clientcerts:
openssl req -subj '/CN=$(DOMAIN)' -newkey rsa:4096 -nodes \
-sha256 \
-days 3650 \
-keyout $(DOMAIN).key \
-out $(DOMAIN).csr
chmod +r $(DOMAIN).key
openssl x509 -req -in $(DOMAIN).csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out $(DOMAIN).crt -days 500 -sha256

$(WORKDIR)/gateway-cert:
mkdir -p gateway-cert

.PHONY: gateway-certs
gateway-certs: $(WORKDIR)/gateway-cert
$(MAKE) ca -C $(WORKDIR)/gateway-cert -f $(WORKDIR)/Makefile
$(MAKE) clientcerts -C $(WORKDIR)/gateway-cert -f $(WORKDIR)/Makefile DOMAIN=gateway.example.com

$(WORKDIR)/upstream-cert:
mkdir -p upstream-cert

.PHONY: upstream-certs
upstream-certs: $(WORKDIR)/upstream-cert
$(MAKE) ca -C $(WORKDIR)/upstream-cert -f $(WORKDIR)/Makefile
$(MAKE) clientcerts -C $(WORKDIR)/upstream-cert -f $(WORKDIR)/Makefile DOMAIN=upstream.example.com
cat $(WORKDIR)/upstream-cert/upstream.example.com.key $(WORKDIR)/upstream-cert/upstream.example.com.crt >$(WORKDIR)/upstream-cert/upstream.example.com.pem

GRPCURL=$(WORKDIR)/bin/grpcurl
$(GRPCURL):
$(call go-install-tool,$(GRPCURL),github.com/fullstorydev/grpcurl/cmd/[email protected])

.PHONY: grpcurl
grpcurl: $(GRPCURL) ## Download grpcurl locally if necessary.

# go-install-tool will 'go install' any package $2 and install it to $1.
define go-install-tool
@[ -f $(1) ] || { \
set -e ;\
TMP_DIR=$$(mktemp -d) ;\
cd $$TMP_DIR ;\
go mod init tmp ;\
echo "Downloading $(2)" ;\
GOBIN=$(WORKDIR)/bin go install $(2) ;\
rm -rf $$TMP_DIR ;\
tkan145 marked this conversation as resolved.
Show resolved Hide resolved
}
endef
52 changes: 52 additions & 0 deletions dev-environments/grpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# APIcast GRPC endpoint

## Create the SSL Certificates

```sh
make gateway-certs
```

```sh
make upstream-certs
```

## Run the gateway

Running local `apicast-test` docker image

```sh
make gateway
```

Running custom apicast image

```sh
make gateway IMAGE_NAME=quay.io/3scale/apicast:latest
```

Traffic between the gateway and upstream can be inspected looking at logs from `one.upstream` service

```
docker compose -p grpc logs -f one.upstream
```

## Testing


Get `grpcurl`

```sh
make grpcurl
```

Run request

```sh
bin/grpcurl -vv -insecure -H "app_id: abc123" -H "app_key: abc123" -authority gateway.example.com 127.0.0.1:8443 main.HelloWorld/Greeting
```

## Clean env

```sh
make clean
```
35 changes: 35 additions & 0 deletions dev-environments/grpc/apicast-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"services": [
{
"id": "1",
"backend_version": "2",
"proxy": {
"hosts": ["gateway.example.com"],
"credentials_location": "headers",
"api_backend": "https://one.upstream:443",
"backend": {
"endpoint": "http://127.0.0.1:8081",
"host": "backend"
},
"policy_chain": [
{
"name": "apicast.policy.grpc"
},
{
"name": "apicast.policy.apicast"
}
],
"proxy_rules": [
{
"http_method": "POST",
"pattern": "/",
"metric_system_name": "hits",
"delta": 1,
"parameters": [],
"querystring_parameters": {}
}
]
}
}
]
}
40 changes: 40 additions & 0 deletions dev-environments/grpc/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
version: '3.8'
services:
gateway:
image: ${IMAGE_NAME:-apicast-test}
depends_on:
- one.upstream
- two.upstream
environment:
APICAST_HTTPS_PORT: 8443
APICAST_HTTPS_CERTIFICATE: /var/run/secrets/apicast/gateway.example.com.crt
APICAST_HTTPS_CERTIFICATE_KEY: /var/run/secrets/apicast/gateway.example.com.key
THREESCALE_CONFIG_FILE: /tmp/config.json
THREESCALE_DEPLOYMENT_ENV: staging
APICAST_CONFIGURATION_LOADER: lazy
APICAST_WORKERS: 1
APICAST_LOG_LEVEL: debug
APICAST_CONFIGURATION_CACHE: "0"
expose:
- "8443"
- "8090"
ports:
- "8443:8443"
- "8090:8090"
volumes:
- ./apicast-config.json:/tmp/config.json
- ./gateway-cert:/var/run/secrets/apicast
one.upstream:
image: alpine/socat:1.7.4.4
container_name: one.upstream
command: "-v openssl-listen:443,reuseaddr,fork,cert=/etc/pki/upstream.example.com.pem,verify=0,openssl-max-proto-version=TLS1.3 ssl:two.upstream:8005,verify=0"
expose:
- "443"
restart: unless-stopped
volumes:
- ./upstream-cert/upstream.example.com.pem:/etc/pki/upstream.example.com.pem
two.upstream:
image: kalmhq/echoserver
expose:
- "8005"
42 changes: 40 additions & 2 deletions gateway/src/apicast/configuration/service.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
local rawget = rawget
local lower = string.lower
local gsub = string.gsub
local str_find = string.find
local type = type
local tbl_concat = table.concat
local select = select

local re = require 'ngx.re'
Expand All @@ -30,13 +33,42 @@
PATCH = true
}

local function is_http2_request()
return ngx.req.http_version == '2.0'
end

local function content_type_is_urlencoded(headers)
local ct = headers["Content-Type"]
tkan145 marked this conversation as resolved.
Show resolved Hide resolved
if not ct then
return false
end

-- Handle duplicate headers
-- This shouldn't happen but can in the real world
if type(ct) ~= "string" then
ct = tbl_concat(ct, ",")
end

return str_find(lower(ct), "application/x-www-form-urlencoded", 1, true) ~= nil
end


local function read_body_args(...)
local method = ngx.req.get_method()

ngx.log(ngx.WARN, "is_http2_request: ", is_http2_request())
tkan145 marked this conversation as resolved.
Show resolved Hide resolved
if is_http2_request() then
return {}, 'not supported'

Check warning on line 61 in gateway/src/apicast/configuration/service.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/configuration/service.lua#L61

Added line #L61 was not covered by tests
end

if not http_methods_with_body[method] then
return {}, 'not supported'
end

if not content_type_is_urlencoded(ngx.req.get_headers()) then
return {}, 'not supported'
end

ngx.req.read_body()

local args, err = ngx.req.get_post_args()
Expand Down Expand Up @@ -174,9 +206,13 @@
local function get_request_params(method)
local params = ngx.req.get_uri_args()

if method == "GET" then
ngx.log(ngx.WARN, "is_http2_request: ", is_http2_request())
tkan145 marked this conversation as resolved.
Show resolved Hide resolved
if is_http2_request() then

Check warning on line 210 in gateway/src/apicast/configuration/service.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/configuration/service.lua#L209-L210

Added lines #L209 - L210 were not covered by tests
return params
else
end

-- Only read request body when POST query arguments (of the MIME type application/x-www-form-urlencoded)
if http_methods_with_body[method] and content_type_is_urlencoded(ngx.req.get_headers()) then

Check warning on line 215 in gateway/src/apicast/configuration/service.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/configuration/service.lua#L215

Added line #L215 was not covered by tests
ngx.req.read_body()
local body_params, err = ngx.req.get_post_args()

Expand All @@ -192,6 +228,8 @@

return body_params
end

return params

Check warning on line 232 in gateway/src/apicast/configuration/service.lua

View check run for this annotation

Codecov / codecov/patch

gateway/src/apicast/configuration/service.lua#L232

Added line #L232 was not covered by tests
end

-- This table can be used with `table.concat` to serialize
Expand Down
47 changes: 47 additions & 0 deletions spec/configuration/service_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,51 @@ describe('Service object', function()

ngx.var = {}
stub(ngx.req, 'get_method', function() return 'POST' end)
stub(ngx.req, 'get_headers', function() return {["Content-Type"] = 'application/x-www-form-urlencoded' } end)
stub(ngx.req, 'read_body')
stub(ngx.req, 'get_post_args', function() return { user_key = 'post' } end)

assert.same({ 'post', user_key = 'post' }, assert(service:extract_credentials()))
end)

it('unknown POST request returns empty', function()
local service = Service.new({
backend_version = 1,
credentials = { location = 'query' }
})

ngx.var = {}
stub(ngx.req, 'get_method', function() return 'POST' end)
-- No Content-Type header
stub(ngx.req, 'get_headers', function() return {} end)

assert.same({}, assert(service:extract_credentials()))
end)

it('urlencoded POST request without credentials', function()
local service = Service.new({
backend_version = 1,
credentials = { location = 'query' }
})

ngx.var = {}
stub(ngx.req, 'get_method', function() return 'POST' end)
stub(ngx.req, 'get_headers', function() return {["Content-Type"] = 'application/x-www-form-urlencoded' } end)
stub(ngx.req, 'read_body')
stub(ngx.req, 'get_post_args', function() return {} end)

assert.same({}, assert(service:extract_credentials()))
end)

it('urlencoded POST request with multiple Content-Type headers', function()
local service = Service.new({
backend_version = 1,
credentials = { location = 'query' }
})

ngx.var = {}
stub(ngx.req, 'get_method', function() return 'POST' end)
stub(ngx.req, 'get_headers', function() return {["Content-Type"] = {'other', 'application/x-www-form-urlencoded'} } end)
stub(ngx.req, 'read_body')
stub(ngx.req, 'get_post_args', function() return { user_key = 'post' } end)

Expand Down Expand Up @@ -74,6 +119,7 @@ describe('Service object', function()

ngx.var = {}
stub(ngx.req, 'get_method', function() return 'POST' end)
stub(ngx.req, 'get_headers', function() return {["Content-Type"] = 'application/x-www-form-urlencoded' } end)
stub(ngx.req, 'read_body')
stub(ngx.req, 'get_post_args', function() return { app_id = 'post' } end)

Expand Down Expand Up @@ -128,6 +174,7 @@ describe('Service object', function()

ngx.var = {}
stub(ngx.req, 'get_method', function() return 'POST' end)
stub(ngx.req, 'get_headers', function() return {["Content-Type"] = 'application/x-www-form-urlencoded' } end)
stub(ngx.req, 'read_body')
stub(ngx.req, 'get_post_args', function() return { access_token = 'post' } end)

Expand Down
2 changes: 2 additions & 0 deletions t/apicast-mapping-rules.t
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ request body params in a POST call are taken into account when matching mapping
POST /foo?user_key=somekey
bar=baz
--- more_headers
Content-Type: application/x-www-form-urlencoded
X-3scale-Debug: my-token
--- response_body
api response
Expand Down Expand Up @@ -193,6 +194,7 @@ precedence.
POST /foo?a_param=val3&another_param=val2&user_key=somekey
a_param=val1
--- more_headers
Content-Type: application/x-www-form-urlencoded
X-3scale-Debug: my-token
--- response_body
api response
Expand Down
2 changes: 2 additions & 0 deletions t/apicast-policy-upstream.t
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ GET /path_in_the_rule/some_path?user_key=uk&a_param=a_value HTTP/1.1
--- request
POST /a_path
user_key=uk&a_param=a_value
--- more_headers
Content-Type: application/x-www-form-urlencoded
--- response_body
yay, api backend
--- error_code: 200
Expand Down
Loading