diff --git a/lib/router/backend_request_server.ex b/lib/router/backend_request_server.ex index 1924289..ea63be1 100644 --- a/lib/router/backend_request_server.ex +++ b/lib/router/backend_request_server.ex @@ -52,11 +52,10 @@ defmodule OpenAperture.Router.BackendRequestServer do hackney_options = [:async, {:stream_to, self}] hackney_options = get_hackney_options(url) ++ hackney_options - if has_request_body do - {time, result} = :timer.tc(:hackney, :request, [method, url, request_headers, :stream, hackney_options]) - else - {time, result} = :timer.tc(:hackney, :request, [method, url, request_headers, "", hackney_options]) - end + {time, result} = case has_request_body do + true -> :timer.tc(:hackney, :request, [method, url, request_headers, :stream, hackney_options]) + _ -> :timer.tc(:hackney, :request, [method, url, request_headers, "", hackney_options]) + end state = Map.put(state, :request_start, :os.timestamp) @@ -75,7 +74,7 @@ defmodule OpenAperture.Router.BackendRequestServer do def handle_call({:send_request_chunk, chunk, false}, _from, %{hackney_client: client} = state) do {time, result} = :timer.tc(:hackney, :send_body, [client, chunk]) case result do - :ok -> {:reply, {:ok, time}, state} + :ok -> {:reply, {:ok, time}, state} {:error, reason} -> {:reply, {:error, reason, time}, state} end end @@ -89,7 +88,7 @@ defmodule OpenAperture.Router.BackendRequestServer do :ok -> {response_time, result} = :timer.tc(:hackney, :start_response, [client]) case result do - {:ok, client} -> {:reply, {:ok, time + response_time}, %{state | hackney_client: client}} + {:ok, client} -> {:reply, {:ok, time + response_time}, %{state | hackney_client: client}} {:error, reason} -> {:reply, {:error, reason, time + response_time}, state} end {:error, reason} -> {:reply, {:error, reason, time}, state} @@ -142,4 +141,4 @@ defmodule OpenAperture.Router.BackendRequestServer do defp get_request_duration(start_timestamp) do :timer.now_diff(:os.timestamp, start_timestamp) end -end \ No newline at end of file +end diff --git a/lib/router/http_handler.ex b/lib/router/http_handler.ex index 54595e6..14a71ed 100644 --- a/lib/router/http_handler.ex +++ b/lib/router/http_handler.ex @@ -81,7 +81,7 @@ defmodule OpenAperture.Router.HttpHandler do def terminate(_reason, _req, {_transport, {start_time, req_time}}) do total_time = :timer.now_diff(:os.timestamp(), start_time) - total_time_ms = div(total_time, 1000) + total_time_ms = div(total_time, 1000) router_time_ms = div(total_time - req_time, 1000) Logger.info "Total request time (time in router): #{total_time_ms}ms (#{router_time_ms}ms)." @@ -132,9 +132,9 @@ defmodule OpenAperture.Router.HttpHandler do defp handle_request(req, path, transport) do proto = case transport do :ssl -> :https - _ -> :http + _ -> :http end ReverseProxy.proxy_request(req, path, proto) end -end \ No newline at end of file +end diff --git a/lib/router/http_request_util.ex b/lib/router/http_request_util.ex index 37f0611..d6a1fc4 100644 --- a/lib/router/http_request_util.ex +++ b/lib/router/http_request_util.ex @@ -18,14 +18,14 @@ defmodule OpenAperture.Router.HttpRequestUtil do {method, req} = :cowboy_req.method(req) method = case String.upcase(method) do - "DELETE" -> :delete - "GET" -> :get - "HEAD" -> :head + "DELETE" -> :delete + "GET" -> :get + "HEAD" -> :head "OPTIONS" -> :options - "PATCH" -> :patch - "POST" -> :post - "PUT" -> :put - _ -> + "PATCH" -> :patch + "POST" -> :post + "PUT" -> :put + _ -> # TODO: Figure out how we want to handle non-standard verbs. # We'll probably have to do something whitelist-based. We # **MUST NOT** just call `String.to_atom\1`, for reasons outlined @@ -41,7 +41,7 @@ defmodule OpenAperture.Router.HttpRequestUtil do The key feature of the router's reverse-proxying ability is taking a request to http://[public hostname]:[public port]/path?querystring and forwarding it to http://[backend hostname]:[backend port]/path?querystring. - This function takes the original request URL's host and port and replaces + This function takes the original request URL's host and port and replaces them with the backend's host and port, as well as specifying if the backend request needs to be made via https or http. """ @@ -50,11 +50,10 @@ defmodule OpenAperture.Router.HttpRequestUtil do {host_url, req} = :cowboy_req.host_url(req) {url, req} = :cowboy_req.url(req) - proto = if https? do - "https" - else - "http" - end + proto = case https? do + true -> "https" + _ -> "http" + end new_url = Regex.replace(~r/^#{host_url}/, url, "#{proto}://#{backend_host}:#{backend_port}") {new_url, req} @@ -67,7 +66,7 @@ defmodule OpenAperture.Router.HttpRequestUtil do headers |> Enum.any?(fn {key, value} -> # We have to do a case-insensitive check, because although the RFC states - # that headers should be all lowercase, many servers send it as + # that headers should be all lowercase, many servers send it as # "Transfer-Encoding". if String.downcase(key) == "transfer-encoding" do # If there is a transfer-encoding header, check if its value matches @@ -94,7 +93,7 @@ defmodule OpenAperture.Router.HttpRequestUtil do end @doc """ - Finds the first "content-length" header and returns it, Returns nil if + Finds the first "content-length" header and returns it, Returns nil if there isn't one. """ @spec get_content_length_header(Types.headers) :: {String.t, String.t} | nil @@ -113,9 +112,9 @@ defmodule OpenAperture.Router.HttpRequestUtil do def parse_content_length_header({_, val}) when is_binary(val) do case Integer.parse(val) do {num, _} -> num - _ -> nil + _ -> nil end end def parse_content_length_header(_), do: nil -end \ No newline at end of file +end diff --git a/lib/router/response_handler.ex b/lib/router/response_handler.ex index b727967..09282d5 100644 --- a/lib/router/response_handler.ex +++ b/lib/router/response_handler.ex @@ -20,12 +20,12 @@ defmodule OpenAperture.Router.ResponseHandler do defp message_loop(backend_request_pid, state \\ %{}) do receive do {:hackney_response, _client_ref, {:status, status_code, reason}} -> - state = Map.merge(state, + state = Map.merge(state, %{ - status_code: status_code, + status_code: status_code, status_reason: reason }) - + message_loop(backend_request_pid, state) {:hackney_response, _client_ref, {:headers, headers}} -> @@ -52,4 +52,4 @@ defmodule OpenAperture.Router.ResponseHandler do message_loop(backend_request_pid, state) end end -end \ No newline at end of file +end diff --git a/lib/router/reverse_proxy.ex b/lib/router/reverse_proxy.ex index c978053..3fae7d4 100644 --- a/lib/router/reverse_proxy.ex +++ b/lib/router/reverse_proxy.ex @@ -36,7 +36,7 @@ defmodule OpenAperture.Router.ReverseProxy do # We can't, hoever, process parts of the client request (using the # :cowboy_req module), in that other process, as the cowboy documentation # states: - # "It is highly discouraged to pass the Req object to another process. + # "It is highly discouraged to pass the Req object to another process. # Doing so and calling `cowboy_req` functions from it leads to undefined # behavior." (http://ninenines.eu/docs/en/cowboy/1.0/manual/cowboy_req/) # @@ -52,7 +52,7 @@ defmodule OpenAperture.Router.ReverseProxy do {req, _reply_time} = send_reply(req, "503 Service Unavailable", [], "") {:ok, req, 0} {backend_host, backend_port, is_https} -> - {method, req} = get_request_method(req) + {method, req} = get_request_method(req) {headers, req} = :cowboy_req.headers(req) # Add any custom headers needed... @@ -66,11 +66,7 @@ defmodule OpenAperture.Router.ReverseProxy do def proxy_request(method, req, host, port, is_https, headers) do {url, req} = get_backend_url(req, host, port, is_https) - has_body = Enum.any?(headers, fn {header, _value} -> - header = String.downcase(header) - header == "content-length" || header == "transfer-encoding" - end) - + has_body = has_body?(headers) # Start performing the backend request in a separate process case start_request(method, url, headers, has_body) do {:error, _reason, request_time} -> @@ -92,6 +88,14 @@ defmodule OpenAperture.Router.ReverseProxy do end end + @spec has_body?(Types.headers) :: boolean + defp has_body?(headers) do + Enum.any?(headers, fn {header, _value} -> + header = String.downcase(header) + header == "content-length" || header == "transfer-encoding" + end) + end + @spec handle_response(Types.cowboy_req, pid) :: {:ok, Types.cowboy_req, integer} | {:error, Types.cowboy_req, integer} defp handle_response(req, backend_request_server_pid) do case wait_for_response(req, backend_request_server_pid) do @@ -117,7 +121,7 @@ defmodule OpenAperture.Router.ReverseProxy do _ -> # Handling the streaming response is done in two parts. Here, we # initiate the reply, with our custom request object metadata of - # `:response_type` set to `:streaming`. In + # `:response_type` set to `:streaming`. In # `HttpHandler.onresponse/4`, we'll check that metadata, see that # it's set to `:streaming`, and call the streaming response body # handler. @@ -173,4 +177,4 @@ defmodule OpenAperture.Router.ReverseProxy do Integer.to_string(status_code) end end -end \ No newline at end of file +end diff --git a/lib/router/reverse_proxy/buffered_response_body_handler.ex b/lib/router/reverse_proxy/buffered_response_body_handler.ex index 740e64f..ad8847f 100644 --- a/lib/router/reverse_proxy/buffered_response_body_handler.ex +++ b/lib/router/reverse_proxy/buffered_response_body_handler.ex @@ -4,15 +4,10 @@ defmodule OpenAperture.Router.ReverseProxy.BufferedResponseBodyHandler do """ alias OpenAperture.Router.ReverseProxy.Client + alias OpenAperture.Router.Timeouts alias OpenAperture.Router.Types - # Read the timeouts info from the env config - @timeouts Application.get_env(:openaperture_router, :timeouts, [ - connecting: 5_000, - sending_request_body: 60_000, - waiting_for_response: 60_000, - receiving_response: 60_000 - ]) + @timeouts Timeouts.from_env @doc """ Handles buffering the entire response body from the backend server before @@ -47,4 +42,4 @@ defmodule OpenAperture.Router.ReverseProxy.BufferedResponseBodyHandler do {:error, :timeout} end end -end \ No newline at end of file +end diff --git a/lib/router/reverse_proxy/chunked_response_body_handler.ex b/lib/router/reverse_proxy/chunked_response_body_handler.ex index 9d1589a..76ee422 100644 --- a/lib/router/reverse_proxy/chunked_response_body_handler.ex +++ b/lib/router/reverse_proxy/chunked_response_body_handler.ex @@ -4,15 +4,11 @@ defmodule OpenAperture.Router.ReverseProxy.ChunkedResponseBodyHandler do """ alias OpenAperture.Router.ReverseProxy.Client + alias OpenAperture.Router.Timeouts alias OpenAperture.Router.Types # Read the timeouts info from the env config - @timeouts Application.get_env(:openaperture_router, :timeouts, [ - connecting: 5_000, - sending_request_body: 60_000, - waiting_for_response: 60_000, - receiving_response: 60_000 - ]) + @timeouts Timeouts.from_env @doc """ Handles chunks coming from the backend server and forwards them on to the @@ -47,4 +43,4 @@ defmodule OpenAperture.Router.ReverseProxy.ChunkedResponseBodyHandler do {:error, :timeout} end end -end \ No newline at end of file +end diff --git a/lib/router/reverse_proxy/client.ex b/lib/router/reverse_proxy/client.ex index 58757f7..04c503b 100644 --- a/lib/router/reverse_proxy/client.ex +++ b/lib/router/reverse_proxy/client.ex @@ -4,7 +4,7 @@ defmodule OpenAperture.Router.ReverseProxy.Client do functionality. """ require Logger - + alias OpenAperture.Router.BackendRequestServer alias OpenAperture.Router.Types @@ -15,7 +15,7 @@ defmodule OpenAperture.Router.ReverseProxy.Client do waiting_for_response: 60_000, receiving_response: 60_000 ]) - + @doc """ Sends the request body from the client to the backend server. """ @@ -97,7 +97,7 @@ defmodule OpenAperture.Router.ReverseProxy.Client do @doc """ Adds OpenAperture Router-specific request headers, which can be used by the - backend servers to get information regarding the original request. Because + backend servers to get information regarding the original request. Because the router proxies the request, many of the normal versions of these headers will be set with router-specific information, which might not be especially useful for the backend server. The added headers are: @@ -111,16 +111,16 @@ defmodule OpenAperture.Router.ReverseProxy.Client do @spec add_router_request_headers(Types.headers, String.t, integer, Types.cowboy_req, :http | :https) :: {Types.headers, Types.cowboy_req} def add_router_request_headers(headers, host, port, req, proto) when is_list(headers) do x_openaperture_request_id_header = List.keyfind(headers, "x-openaperture-request-id", 0) - x_forwarded_for_header = List.keyfind(headers, "x-forwarded-for", 0) - x_forwarded_host_header = List.keyfind(headers, "x-forwarded-host", 0) - x_forwarded_port_header = List.keyfind(headers, "x-forwarded-port", 0) + x_forwarded_for_header = List.keyfind(headers, "x-forwarded-for", 0) + x_forwarded_host_header = List.keyfind(headers, "x-forwarded-host", 0) + x_forwarded_port_header = List.keyfind(headers, "x-forwarded-port", 0) x_forwarded_proto_header = List.keyfind(headers, "x-forwarded-proto", 0) - headers = if x_openaperture_request_id_header == nil, do: add_request_id_header(headers), else: headers - {headers, req} = if x_forwarded_for_header == nil, do: add_forwarded_for_header(headers, req), else: {headers, req} - headers = if x_forwarded_host_header == nil, do: add_forwarded_host_header(headers, host), else: headers - headers = if x_forwarded_port_header == nil, do: add_forwarded_port_header(headers, port), else: headers - headers = if x_forwarded_proto_header == nil, do: add_forwarded_proto_header(headers, proto), else: headers + headers = if x_openaperture_request_id_header == nil, do: add_request_id_header(headers), else: headers + {headers, req} = if x_forwarded_for_header == nil, do: add_forwarded_for_header(headers, req), else: {headers, req} + headers = if x_forwarded_host_header == nil, do: add_forwarded_host_header(headers, host), else: headers + headers = if x_forwarded_port_header == nil, do: add_forwarded_port_header(headers, port), else: headers + headers = if x_forwarded_proto_header == nil, do: add_forwarded_proto_header(headers, proto), else: headers {headers, req} end @@ -162,4 +162,4 @@ defmodule OpenAperture.Router.ReverseProxy.Client do defp add_forwarded_proto_header(headers, proto) do [{"X-Forwarded-Proto", Atom.to_string(proto)}] ++ headers end -end \ No newline at end of file +end diff --git a/lib/router/reverse_proxy/streaming_response_body_handler.ex b/lib/router/reverse_proxy/streaming_response_body_handler.ex index a4cac7d..e9834e7 100644 --- a/lib/router/reverse_proxy/streaming_response_body_handler.ex +++ b/lib/router/reverse_proxy/streaming_response_body_handler.ex @@ -3,13 +3,9 @@ defmodule OpenAperture.Router.ReverseProxy.StreamingResponseBodyHandler do This module contains the handler for processing streaming response bodies. """ - # Read the timeouts info from the env config - @timeouts Application.get_env(:openaperture_router, :timeouts, [ - connecting: 5_000, - sending_request_body: 60_000, - waiting_for_response: 60_000, - receiving_response: 60_000 - ]) + alias OpenAperture.Router.Timeouts + + @timeouts Timeouts.from_env #@doc handle_streaming_response_body() def handle(socket, transport) do @@ -35,4 +31,4 @@ defmodule OpenAperture.Router.ReverseProxy.StreamingResponseBodyHandler do after timeout -> :timeout end end -end \ No newline at end of file +end diff --git a/lib/router/route_cache.ex b/lib/router/route_cache.ex index 08e05ec..bbe50ca 100644 --- a/lib/router/route_cache.ex +++ b/lib/router/route_cache.ex @@ -22,11 +22,11 @@ defmodule OpenAperture.Router.RouteCache do Logger.debug "Routes matching #{host}:#{port}: #{inspect routes}" case routes do - nil -> nil + nil -> nil [route | []] -> route routes -> index = :random.uniform(length(routes)) - 1 Enum.at(routes, index) end end -end \ No newline at end of file +end diff --git a/lib/router/route_server.ex b/lib/router/route_server.ex index 00fbc70..f8b5cab 100644 --- a/lib/router/route_server.ex +++ b/lib/router/route_server.ex @@ -78,10 +78,8 @@ defmodule OpenAperture.Router.RouteServer do timestamp -> case handle_deleted_routes(timestamp) do - :ok -> - handle_updated_routes(timestamp) - {:error, error} -> - Logger.error "Error handling deleted routes: #{inspect error}" + :ok -> handle_updated_routes(timestamp) + {:error, error} -> Logger.error "Error handling deleted routes: #{inspect error}" end end end @@ -149,8 +147,7 @@ defmodule OpenAperture.Router.RouteServer do case :hackney.body(client) do {:ok, body} -> case Poison.decode(body) do - {:ok, specs} -> - {:ok, specs} + {:ok, specs} -> {:ok, specs} {:error, err} -> Logger.error "Could not decode JSON response from #{url}: #{inspect err}" {:error, err} @@ -198,7 +195,7 @@ defmodule OpenAperture.Router.RouteServer do end end - @spec format_routes([{String.t, Map.t}]) :: [{String.t, {String.t, integer, boolean}}] + @spec format_routes([{String.t, map}]) :: [{String.t, {String.t, integer, boolean}}] defp format_routes(routes) do Enum.map(routes, fn {authority, authority_routes} -> tuples = Enum.map(authority_routes, fn route -> @@ -215,4 +212,4 @@ defmodule OpenAperture.Router.RouteServer do {"Authorization", "Bearer #{token}"} end -end \ No newline at end of file +end diff --git a/lib/router/timeouts.ex b/lib/router/timeouts.ex new file mode 100644 index 0000000..554108a --- /dev/null +++ b/lib/router/timeouts.ex @@ -0,0 +1,17 @@ +defmodule OpenAperture.Router.Timeouts do + @moduledoc """ + This module provides a from_env function to get timeouts information shared across multiple + Modules. + """ + + @spec from_env :: map + def from_env do + # Read the timeouts info from the env config + Application.get_env(:openaperture_router, :timeouts, [ + connecting: 5_000, + sending_request_body: 60_000, + waiting_for_response: 60_000, + receiving_response: 60_000 + ]) + end +end diff --git a/lib/router/util.ex b/lib/router/util.ex index 82f7ed6..7d0a4b4 100644 --- a/lib/router/util.ex +++ b/lib/router/util.ex @@ -6,6 +6,8 @@ defmodule OpenAperture.Router.Util do alias OpenAperture.Router.Types + @mega 1_000_000 + @doc """ Converts an erlang timestamp to a unix timestamp. Erlang timestamps are of the form {MegaSecs, Secs, MicroSecs}, of the elapsed time since the start of @@ -21,7 +23,7 @@ defmodule OpenAperture.Router.Util do # Unix time only counts seconds, disregard microseconds {mega, secs, _} = erlang_timestamp - mega * 1_000_000 + secs + mega * @mega + secs end @doc """ @@ -35,7 +37,7 @@ defmodule OpenAperture.Router.Util do """ @spec erlang_timestamp_to_microseconds(Types.erlang_timestamp) :: integer def erlang_timestamp_to_microseconds({megaSecs, secs, microSecs}) do - megaSecs * 1_000_000 * 1_000_000 + secs * 1_000_000 + microSecs + megaSecs * @mega * @mega + secs * @mega + microSecs end @doc """ @@ -49,11 +51,11 @@ defmodule OpenAperture.Router.Util do """ @spec microseconds_to_erlang_timestamp(integer) :: Types.erlang_timestamp def microseconds_to_erlang_timestamp(integer) do - megas = div(integer, 1_000_000 * 1_000_000) + megas = div(integer, @mega * @mega) secs = integer - |> div(1_000_000) - |> rem(1_000_000) - micros = rem(integer, 1_000_000) + |> div(@mega) + |> rem(@mega) + micros = rem(integer, @mega) {megas, secs, micros} end @@ -95,12 +97,12 @@ defmodule OpenAperture.Router.Util do # Check if we're hitting a local endpoint, in which case we can't use # the proxy cond do - String.starts_with?(url, "https") -> [] + String.starts_with?(url, "https") -> [] String.starts_with?(url, "http://localhost") -> [] String.starts_with?(url, "http://127.0.0.1") -> [] - String.starts_with?(url, "http://lvh.me") -> [] - true -> conf + String.starts_with?(url, "http://lvh.me") -> [] + true -> conf end end end -end \ No newline at end of file +end diff --git a/mix.lock b/mix.lock index 762404d..745fbfb 100644 --- a/mix.lock +++ b/mix.lock @@ -2,6 +2,8 @@ "conform": {:hex, :conform, "0.11.0"}, "cowboy": {:hex, :cowboy, "1.0.0"}, "cowlib": {:hex, :cowlib, "1.0.1"}, + "earmark": {:hex, :earmark, "0.1.17"}, + "ex_doc": {:hex, :ex_doc, "0.7.3"}, "exactor": {:hex, :exactor, "2.0.1"}, "excoveralls": {:hex, :excoveralls, "0.3.9"}, "exjsx": {:hex, :exjsx, "3.1.0"},