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