Packages

Rexy - Reverse Proxy in Elixir

Retired package: Deprecated

Current section

Files

Jump to
rexy lib reverse_proxy runner.ex
Raw

lib/reverse_proxy/runner.ex

defmodule ReverseProxy.Runner do
@moduledoc """
This module retreives everything from an upstream.
It provides the `retreive` function which takes
care of getting and sending a response to the
selected upstreams.
"""
alias Plug.Conn
@doc """
Retreive either takes a tupple of a plug, and it's options
if we want to route to other plugs as a middleware.
Or we can give it a connection and a list of servers from which to fetch
a response. (If we have multiple servers it rotates them in order to load balance)
Otherwise it skips this step and fetches the response dirrectly.
It sets the needed `x-forwarded-header` and deletes the unnecessary
headers set.
"""
def retreive(conn, upstream)
def retreive(conn, {plug, opts}) do
options = plug.init(opts)
plug.call(conn, options)
end
def retreive(conn, servers, client \\ HTTPoison)
def retreive(conn, [server | []], client) do
do_retrieve(conn, server, client)
end
def retreive(conn, _servers, client) do
server = upstream_select(conn.host)
do_retrieve(conn, server, client)
end
defp do_retrieve(conn, server, client) do
{method, url, body, headers} = prepare_request(server, conn)
method
|> client.request(url, body, headers, timeout: 5_000)
|> process_response(conn)
end
# This function sets the headers.
defp prepare_request(server, conn) do
conn = conn
|> Conn.put_req_header(
"x-forwarded-for",
conn.remote_ip |> ip_to_string
)
|> Conn.delete_req_header("host")
|> Conn.delete_req_header("transfer-encoding")
method = conn.method |> String.downcase |> String.to_atom
url = "#{extract_server(conn.scheme, server)}#{conn.request_path}?#{conn.query_string}"
headers = conn.req_headers
{:ok, body, _conn} = Conn.read_body(conn) #TODO: take care of big requests, maybe use streams
{method, url, body, headers}
end
# TODO: I think this breaks for ipv6 ips.
defp ip_to_string({a, b, c, d}) do
"#{a}.#{b}.#{c}.#{d}"
end
# Make sure that the scheme isn't twice in the url name
defp extract_server(scheme, server)
defp extract_server(_, "http" <> _ = server), do: server
defp extract_server(_, "https" <> _ = server), do: server
defp extract_server(scheme, server), do: "#{scheme}://#{server}"
#TODO: again, custom error response
# After we get a response, we process for an error
# Otherwise we send the response to the client.
defp process_response({:error, _}, conn) do
conn |> Conn.send_resp(502, "Bad Gateway")
end
defp process_response({:ok, response}, conn) do
conn
|> put_resp_headers(response.headers)
|> Conn.delete_resp_header("transfer-encoding")
|> Conn.send_resp(response.status_code, response.body)
end
# A function to set multiple response headers
defp put_resp_headers(conn, []), do: conn
defp put_resp_headers(conn, [{header, value} | rest]) do
header = String.downcase(header)
conn
|> Conn.put_resp_header(header, value)
|> put_resp_headers(rest)
end
# Selects the next server from the load balancer.
defp upstream_select(host) do
ReverseProxy.LoadBalancer.select(host)
end
end