Skip to content
Open
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
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,22 @@ From [Wikipedia](https://wikipedia.org/wiki/Reverse_proxy):

Upstream servers can be listed per-domain in the following forms:

- List of remote nodes, e.g. `["host:4000", "host:4001"]`
- List of remote nodes, e.g. `["http://host:4000", "http://host:4001"]`
- A `{plug, options}` tuple, useful for umbrella applications

> Note: This structure may change in the future as the project progresses.

```elixir
config :reverse_proxy,
# ...
upstreams: %{ "api." => ["localhost:4000"],
"slogsdon.com" => ["localhost:4001"] }
upstreams: %{ "foobar.localhost" => ["http://www.example.com"],
"api." => ["http://localhost:4000"],
"slogsdon.com" => ["http://localhost:4001"] }
```

You might need to create `foobar.localhost in `/etc/hosts` and replace
example.com with an actual site.

### `:cache`

Enables the caching of the responses from the upstream server.
Expand Down
11 changes: 7 additions & 4 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use Mix.Config

config :reverse_proxy,
upstreams: %{ "api." => {ReverseProxyTest.SuccessPlug, []},
"example.com" => {ReverseProxyTest.SuccessPlug, []},
"badgateway.com" => ["localhost:1"] }
config(:reverse_proxy,
upstreams: %{
# You could add foobar.localhost to /etc/hosts to test this
"foobar.localhost" => ["http://www.example.com"],
"api." => {ReverseProxyTest.SuccessPlug, []},
"example.com" => {ReverseProxyTest.SuccessPlug, []},
"badgateway.com" => ["http://localhost:1"] })
44 changes: 34 additions & 10 deletions lib/reverse_proxy/runner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ defmodule ReverseProxy.Runner do

method
|> client.request(url, body, headers, timeout: 5_000)
|> process_response(conn)
|> process_response(conn, server)
end

@spec prepare_request(String.t, Conn.t) :: {Atom.t,
Expand All @@ -40,7 +40,7 @@ defmodule ReverseProxy.Runner do
"transfer-encoding"
)
method = conn.method |> String.downcase |> String.to_atom
url = "#{conn.scheme}://#{server}#{conn.request_path}?#{conn.query_string}"
url = "#{server}#{conn.request_path}?#{conn.query_string}"
headers = conn.req_headers
body = case Conn.read_body(conn) do
{:ok, body, _conn} ->
Expand Down Expand Up @@ -70,20 +70,33 @@ defmodule ReverseProxy.Runner do
{method, url, body, headers}
end

@spec process_response({Atom.t, Map.t}, Conn.t) :: Conn.t
defp process_response({:error, _}, conn) do
conn |> Conn.send_resp(502, "Bad Gateway")
@spec process_response({Atom.t, Map.t}, Plug.Conn.t, String.t) :: Plug.Conn.t
defp process_response({:error, _}, conn, _server) do
conn |> Plug.Conn.send_resp(502, "Bad Gateway")
end
defp process_response({:ok, response}, conn) do
defp process_response({:ok, response}, conn, server) do
conn = conn |> put_resp_headers(response.headers)

conn
|> put_resp_headers(response.headers)
|> Conn.delete_resp_header("transfer-encoding")
|> Conn.send_resp(response.status_code, response.body)
|> put_resp_headers(response.headers)
|> Conn.delete_resp_header("transfer-encoding")
|> Conn.send_resp(response.status_code, response.body |> process_body(conn, server))
end

@spec put_resp_headers(Conn.t, [{String.t, String.t}]) :: Conn.t
defp put_resp_headers(conn, []), do: conn
defp put_resp_headers(conn, [{header, value} | rest]) do
defp put_resp_headers(conn, [{header = "Location", value}|rest]) do
[host, port] = conn |> get_host() |> String.split(":")
value = URI.parse(value)
|> Map.put(:host, host)
|> Map.put(:port, port |> String.to_integer)
|> URI.to_string

conn
|> Plug.Conn.put_resp_header(header |> String.downcase, value)
|> put_resp_headers(rest)
end
defp put_resp_headers(conn, [{header, value}|rest]) do
conn
|> Conn.put_resp_header(header |> String.downcase, value)
|> put_resp_headers(rest)
Expand All @@ -92,4 +105,15 @@ defmodule ReverseProxy.Runner do
defp upstream_select(servers) do
servers |> hd
end

defp process_body(body, conn, server) do
body |> String.replace(server, conn |> get_host())
end

defp get_host(conn) do
case conn |> Plug.Conn.get_req_header("host") do
[head | _] -> head
_ -> ""
end
end
end
20 changes: 20 additions & 0 deletions test/reverse_proxy/runner_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,26 @@ defmodule ReverseProxy.RunnerTest do
assert conn.resp_headers == headers
end

test "retreive/3 - http - location header rewrite" do
conn = conn(:get, "/")
|> put_req_header("host", "localhost:4000")

conn = ReverseProxy.Runner.retreive(
conn,
["localhost:5001"],
ReverseProxyTest.RedirectHTTP
)

assert conn.status == 301
assert conn.resp_body == """
<html><body>You are being <a href="http://localhost:4000/foo">redirected</a>.</body></html>
"""
assert conn.resp_headers == [
{"cache-control", "max-age=0, private, must-revalidate"},
{"location", "http://localhost:4000/foo"},
]
end

test "retreive/3 - http - failure" do
conn = conn(:get, "/")

Expand Down
17 changes: 17 additions & 0 deletions test/support/redirect_http.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule ReverseProxyTest.RedirectHTTP do
def request(_method, _url, _body, _headers, _opts \\ []) do
{:ok, %{
headers: headers,
status_code: 301,
body: """
<html><body>You are being <a href="http://localhost:5001/foo">redirected</a>.</body></html>
"""
}}
end
def headers do
[
{"cache-control", "max-age=0, private, must-revalidate"},
{"Location", "http://localhost:5001/foo"},
]
end
end