Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@
- [Rig] Possibility to set logging level with env var `LOG_LEVEL` - [#49](https://github.com/Accenture/reactive-interaction-gateway/pull/49)
- [Deploy] Variations of Dockerfiles - basic version and AWS version - [#44](https://github.com/Accenture/reactive-interaction-gateway/pull/44)
- [Deploy] Helm deployment chart - [#59](https://github.com/Accenture/reactive-interaction-gateway/pull/59)
- [Inbound] Proxy is now able to do request header transformations - [#76](https://github.com/Accenture/reactive-interaction-gateway/pull/76)

- Fixed
- [Inbound] Make presence channel respect `JWT_USER_FIELD` setting (currently hardcoded to "username")
- [Inbound] Set proper environment variable for Phoenix server `INBOUND_PORT` - [#38](https://github.com/Accenture/reactive-interaction-gateway/pull/38)
- [API] Set proper environment variable for Phoenix server `API_PORT` - [#38](https://github.com/Accenture/reactive-interaction-gateway/pull/38)
- [Examples] Channels example fixed to be compatible with version 2.0.0 [#40](https://github.com/Accenture/reactive-interaction-gateway/pull/40)
- [Inbound] User defined query auth values are no longer overridden by `JWT` auth type
- [Inbound] More strict regex match for routes in proxy - [#76](https://github.com/Accenture/reactive-interaction-gateway/pull/76)
- [Inbound] Downcased response headers to avoid duplicates in proxy - [#76](https://github.com/Accenture/reactive-interaction-gateway/pull/76)

- Deprecated

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule RigInboundGateway.ApiProxy.Router do
alias RigInboundGateway.RateLimit
alias RigInboundGateway.Proxy

@typep headers_list :: [{String.t, String.t}, ...]
@typep headers :: [{String.t, String.t}]
@typep map_string_upload :: %{required(String.t) => %Plug.Upload{}}

plug :match
Expand Down Expand Up @@ -80,7 +80,7 @@ defmodule RigInboundGateway.ApiProxy.Router do
defp match_path(path, request_path) do
# Replace wildcards with actual params
full_path = String.replace(path, "{id}", "[^/]+")
String.match?(request_path, ~r/#{full_path}$/)
String.match?(request_path, ~r/^#{full_path}$/)
end

# Match endpoint method against requested method
Expand All @@ -89,28 +89,57 @@ defmodule RigInboundGateway.ApiProxy.Router do

# Skip authentication if turned off
@spec check_auth_and_forward_request(
Proxy.endpoint, Proxy.api_definition, %Plug.Conn{}) :: %Plug.Conn{}
Proxy.endpoint, Proxy.api_definition, Plug.Conn.t) :: Plug.Conn.t
defp check_auth_and_forward_request(%{"not_secured" => true} = endpoint, api, conn) do
forward_request(endpoint, api, conn)
transform_req_headers(endpoint, api, conn)
end
# Skip authentication if no auth type is set
@spec check_auth_and_forward_request(
Proxy.endpoint, Proxy.api_definition, %Plug.Conn{}) :: %Plug.Conn{}
Proxy.endpoint, Proxy.api_definition, Plug.Conn.t) :: Plug.Conn.t
defp check_auth_and_forward_request(endpoint, %{"auth_type" => "none"} = api, conn) do
forward_request(endpoint, api, conn)
transform_req_headers(endpoint, api, conn)
end
# Authentication with JWT
@spec check_auth_and_forward_request(
Proxy.endpoint, Proxy.api_definition, %Plug.Conn{}) :: %Plug.Conn{}
Proxy.endpoint, Proxy.api_definition, Plug.Conn.t) :: Plug.Conn.t
defp check_auth_and_forward_request(%{"not_secured" => false} = endpoint, %{"auth_type" => "jwt"} = api, conn) do
tokens = Enum.concat(Auth.pick_query_token(conn, api), Auth.pick_header_token(conn, api))
case Auth.any_token_valid?(tokens) do
true -> forward_request(endpoint, api, conn)
true -> transform_req_headers(endpoint, api, conn)
false -> send_resp(conn, 401, Serializer.encode_error_message("Missing or invalid token"))
end
end

@spec forward_request(Proxy.endpoint, Proxy.api_definition, %Plug.Conn{}) :: %Plug.Conn{}
# Transform request headers
@spec transform_req_headers(Proxy.endpoint(), Proxy.api_definition(), Plug.Conn.t) ::
Plug.Conn.t
defp transform_req_headers(
%{"transform_request_headers" => true} = endpoint,
%{"versioned" => false} = api,
%{req_headers: req_headers} = conn
) do
%{"add_headers" => add_headers} =
Kernel.get_in(api, ["version_data", "default", "transform_request_headers"])

new_req_headers =
add_headers
|> Enum.to_list()
|> Serializer.add_headers(req_headers)

new_conn = conn |> Map.put(:req_headers, new_req_headers)
forward_request(endpoint, api, new_conn)
end

defp transform_req_headers(
%{"transform_request_headers" => true} = _endpoint,
%{"versioned" => true} = _api,
_conn
),
do: raise("Not implemented - to be done when API versioning has landed.")

defp transform_req_headers(endpoint, api, conn), do: forward_request(endpoint, api, conn)

@spec forward_request(Proxy.endpoint, Proxy.api_definition, Plug.Conn.t) :: Plug.Conn.t
defp forward_request(endpoint, api, conn) do
log_request(endpoint, api, conn)

Expand Down Expand Up @@ -139,7 +168,7 @@ defmodule RigInboundGateway.ApiProxy.Router do
end

# Format multipart body and set as POST HTTP method
@spec format_post_request(String.t, map_string_upload, headers_list) :: %Plug.Conn{}
@spec format_post_request(String.t, map_string_upload, headers) :: Plug.Conn.t
defp format_post_request(url, %{"qqfile" => %Plug.Upload{}} = params, headers) do
%{"qqfile" => file} = params
optional_params = params |> Map.delete("qqfile")
Expand All @@ -151,12 +180,12 @@ defmodule RigInboundGateway.ApiProxy.Router do
Base.post!(url, {:multipart, params_merged}, headers)
end

@spec format_post_request(String.t, map, headers_list) :: %Plug.Conn{}
@spec format_post_request(String.t, map, headers) :: Plug.Conn.t
defp format_post_request(url, params, headers) do
Base.post!(url, Poison.encode!(params), headers)
end

@spec log_request(Proxy.endpoint, Proxy.api_definition, %Plug.Conn{}) :: :ok
@spec log_request(Proxy.endpoint, Proxy.api_definition, Plug.Conn.t) :: :ok
defp log_request(endpoint, api, conn) do
conf = config()

Expand All @@ -171,27 +200,29 @@ defmodule RigInboundGateway.ApiProxy.Router do
end

# Send error message with unsupported HTTP method
@spec send_response({:ok, %Plug.Conn{}, nil}) :: %Plug.Conn{}
@spec send_response({:ok, Plug.Conn.t, nil}) :: Plug.Conn.t
defp send_response({:ok, conn, nil}) do
send_resp(conn, 405, Serializer.encode_error_message("Method is not supported"))
end

# Send fulfilled response back to client
@spec send_response({:ok, %Plug.Conn{}, map}) :: %Plug.Conn{}
@spec send_response({:ok, Plug.Conn.t, map}) :: Plug.Conn.t
defp send_response({:ok, conn, %{headers: headers, status_code: status_code, body: body}}) do
conn = %{conn | resp_headers: headers}
if Serializer.header_value?(conn, "transfer-encoding", "chunked") do
send_chunked_response(conn, headers, status_code, body)
downcased_headers = headers |> Serializer.downcase_headers
conn = %{conn | resp_headers: downcased_headers}

if Serializer.header_value?(downcased_headers, "transfer-encoding", "chunked") do
send_chunked_response(conn, status_code, body)
else
%{conn | resp_headers: headers} |> send_resp(status_code, body)
send_resp(conn, status_code, body)
end
end

# Send chunked response to client with body and set transfer-encoding
@spec send_chunked_response(%Plug.Conn{}, headers_list, integer, String.t) :: %Plug.Conn{}
defp send_chunked_response(conn, headers, status_code, body) do
conn = %{conn | resp_headers: headers} |> send_chunked(status_code)
conn |> chunk(body)
conn
@spec send_chunked_response(Plug.Conn.t, integer, String.t) :: Plug.Conn.t
defp send_chunked_response(conn, status_code, body) do
chunked_conn = send_chunked(conn, status_code)
chunked_conn |> chunk(body)
chunked_conn
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ defmodule RigInboundGateway.ApiProxy.Serializer do
alias Plug.Conn.Query
alias RigInboundGateway.Proxy

@typep headers :: [{String.t, String.t}]

# Encode error message to JSON
@spec encode_error_message(String.t) :: %{message: String.t}
def encode_error_message(message) do
Expand All @@ -33,18 +35,30 @@ defmodule RigInboundGateway.ApiProxy.Serializer do
end

# Search if header has key with given value
@spec header_value?(%Plug.Conn{}, String.t, String.t) :: boolean
def header_value?(conn, key, value) do
conn
|> Map.get(:resp_headers)
|> Enum.find({}, fn(headers_tuple) ->
key_downcase =
headers_tuple
|> elem(0)
|> String.downcase
key_downcase == key
@spec header_value?(headers, String.t, String.t) :: boolean
def header_value?(headers, key, value) do
headers
|> Enum.find(fn
{^key, ^value} -> true
_ -> false
end)
|> Tuple.to_list
|> Enum.member?(value)
|> case do
nil -> false
_ -> true
end
end

# Transform keys for headers to lower-case
@spec downcase_headers(headers) :: headers
def downcase_headers(headers) do
headers
|> Enum.map(fn({key, value}) -> {String.downcase(key), value} end)
end

# Add new headers and update existing ones
@spec add_headers(headers, headers) :: headers
def add_headers(new_headers, old_headers) do
Map.merge(Map.new(old_headers), Map.new(new_headers))
|> Enum.to_list()
end
end
6 changes: 6 additions & 0 deletions apps/rig_inbound_gateway/lib/rig_inbound_gateway/proxy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ defmodule RigInboundGateway.Proxy do

@type endpoint :: %{
optional(:not_secured) => boolean,
optional(:transform_request_headers) => boolean,
id: String.t,
path: String.t,
method: String.t,
Expand All @@ -31,6 +32,11 @@ defmodule RigInboundGateway.Proxy do
optional(:node_name) => atom,
optional(:ref_number) => integer,
optional(:timestamp) => DateTime,
optional(:transform_request_headers) => %{
optional(:add_headers) => %{
optional(String.t) => String.t
}
},
id: String.t,
name: String.t,
auth: %{
Expand Down
19 changes: 19 additions & 0 deletions apps/rig_inbound_gateway/priv/proxy/proxy.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
"versioned": false,
"version_data": {
"default": {
"transform_request_headers": {
"add_headers": {
"host": "different",
"john": "doe"
}
},
"endpoints": [
{
"id": "get-myapi-detail-id",
Expand Down Expand Up @@ -72,6 +78,19 @@
"path": "/myapi/books",
"method": "GET",
"not_secured": false
},
{
"id": "post-myapi-transform-headers",
"path": "/myapi/transform-headers",
"method": "POST",
"not_secured": true,
"transform_request_headers": true
},
{
"id": "post-myapi-no-transform-headers",
"path": "/myapi/no-transform-headers",
"method": "POST",
"not_secured": true
}
]
}
Expand Down
30 changes: 30 additions & 0 deletions apps/rig_inbound_gateway/test/rig/api_proxy/router_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,36 @@ defmodule RigInboundGateway.ApiProxy.RouterTest do
assert conn.resp_body =~ "{\"status\":\"ok\"}"
end

test "transform_req_headers should update existing and add new request headers, if requested",
%{first_service: first_service} do
Bypass.expect first_service, "POST", "/myapi/transform-headers", fn conn ->
Plug.Conn.resp(conn, 200, ~s<{"status":"ok"}>)
end

request = build_conn(:post, "/myapi/transform-headers") |> put_req_header("host", "original")
conn = call(Router, request)

assert get_req_header(conn, "host") == ["different"]
assert get_req_header(conn, "john") == ["doe"]
assert conn.status == 200
assert conn.resp_body =~ "{\"status\":\"ok\"}"
end

test "transform_req_headers shouldn\'t update existing and add new request headers, if not requested",
%{first_service: first_service} do
Bypass.expect first_service, "POST", "/myapi/no-transform-headers", fn conn ->
Plug.Conn.resp(conn, 200, ~s<{"status":"ok"}>)
end

request = build_conn(:post, "/myapi/no-transform-headers") |> put_req_header("host", "original")
conn = call(Router, request)

assert get_req_header(conn, "host") == ["original"]
assert get_req_header(conn, "john") == []
assert conn.status == 200
assert conn.resp_body =~ "{\"status\":\"ok\"}"
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'd be nice to have a test around how it behaves with versioned set or unset

Copy link
Copy Markdown
Collaborator Author

@mmacai mmacai May 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is no support for versioning yet, it would crash with ** (FunctionClauseError) no function clause matching. Once we decide to add versioning it definitely makes sense.


@tag :smoke
test "GET request should be correctly proxied to external service" do
conn = call(Router, build_conn(:get, "/api"))
Expand Down
32 changes: 22 additions & 10 deletions apps/rig_inbound_gateway/test/rig/api_proxy/serializer_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,34 @@ defmodule RigInboundGateway.ApiProxy.SerializerTest do
end

test "header_value? should return true if headers have key with given value" do
conn = conn_with_header("a", "b")
assert Serializer.header_value?(conn, "a", "b") == true
assert Serializer.header_value?([{"a", "b"}, {"d", "d"}], "a", "b") == true
end

test "header_value? should handle capital letters" do
conn = conn_with_header("A", "b")
assert Serializer.header_value?(conn, "a", "b") == true
test "header_value? should return false if headers don'\t have key with given value" do
assert Serializer.header_value?([{"a", "b"}, {"d", "d"}], "a", "bb") == false
end

test "header_value? should return false if headers don'\t have key with given value" do
conn = conn_with_header("a", "b")
assert Serializer.header_value?(conn, "a", "bb") == false
test "header_value? should not mix up searched key and value" do
assert Serializer.header_value?([{"a", "b"}], "a", "a") == false
end

defp conn_with_header(key, value) do
%Plug.Conn{} |> put_resp_header(key, value)
test "down_case_headers should down case keys for all headers" do
assert Serializer.downcase_headers([{"A", "b"}, {"C", "d"}]) == [{"a", "b"}, {"c", "d"}]
end

test "add_headers should add non-existing headers" do
assert Serializer.add_headers([{"c", "d"}, {"e", "f"}], [{"a", "b"}]) == [
{"a", "b"},
{"c", "d"},
{"e", "f"}
]
end

test "add_headers should replace existing headers" do
assert Serializer.add_headers([{"c", "d"}, {"e", "f"}], [{"a", "b"}, {"c", "X"}]) == [
{"a", "b"},
{"c", "d"},
{"e", "f"}
]
end
end