1defmodule Plug.Adapters.Cowboy.ConnTest do
2  use ExUnit.Case, async: true
3
4  alias Plug.Conn
5  import Plug.Conn
6  import ExUnit.CaptureLog
7
8  ## Cowboy setup for testing
9  #
10  # We use hackney to perform an HTTP request against the cowboy/plug running
11  # on port 8001. Plug then uses Kernel.apply/3 to dispatch based on the first
12  # element of the URI's path.
13  #
14  # e.g. `assert {204, _, _} = request :get, "/build/foo/bar"` will perform a
15  # GET http://127.0.0.1:8001/build/foo/bar and Plug will call build/1.
16
17  setup_all do
18    {:ok, _pid} = Plug.Adapters.Cowboy.http __MODULE__, [], port: 8001
19
20    on_exit fn ->
21      :ok = Plug.Adapters.Cowboy.shutdown(__MODULE__.HTTP)
22    end
23
24    :ok
25  end
26
27  @already_sent {:plug_conn, :sent}
28
29  def init(opts) do
30    opts
31  end
32
33  def call(conn, []) do
34    # Assert we never have a lingering @already_sent entry in the inbox
35    refute_received @already_sent
36
37    function = String.to_atom List.first(conn.path_info) || "root"
38    apply __MODULE__, function, [conn]
39  rescue
40    exception ->
41      receive do
42        {:plug_conn, :sent} ->
43          :erlang.raise(:error, exception, :erlang.get_stacktrace)
44      after
45        0 ->
46          send_resp(conn, 500, Exception.message(exception) <> "\n" <>
47                    Exception.format_stacktrace(System.stacktrace))
48      end
49  end
50
51  ## Tests
52
53  def root(%Conn{} = conn) do
54    assert conn.method == "HEAD"
55    assert conn.path_info == []
56    assert conn.query_string == "foo=bar&baz=bat"
57    assert conn.request_path == "/"
58    resp(conn, 200, "ok")
59  end
60
61  def build(%Conn{} = conn) do
62    assert {Plug.Adapters.Cowboy.Conn, _} = conn.adapter
63    assert conn.path_info == ["build", "foo", "bar"]
64    assert conn.query_string == ""
65    assert conn.scheme == :http
66    assert conn.host == "127.0.0.1"
67    assert conn.port == 8001
68    assert conn.method == "GET"
69    assert {{127, 0, 0, 1}, _} = conn.peer
70    assert conn.remote_ip == {127, 0, 0, 1}
71    resp(conn, 200, "ok")
72  end
73
74  test "builds a connection" do
75    assert {200, _, _} = request :head, "/?foo=bar&baz=bat"
76    assert {200, _, _} = request :get, "/build/foo/bar"
77    assert {200, _, _} = request :get, "//build//foo//bar"
78  end
79
80  def return_request_path(%Conn{} = conn) do
81    resp(conn, 200, conn.request_path)
82  end
83
84  test "request_path" do
85    assert {200, _, "/return_request_path/foo"} =
86           request :get, "/return_request_path/foo?barbat"
87    assert {200, _, "/return_request_path/foo/bar"} =
88           request :get, "/return_request_path/foo/bar?bar=bat"
89    assert {200, _, "/return_request_path/foo/bar/"} =
90           request :get, "/return_request_path/foo/bar/?bar=bat"
91    assert {200, _, "/return_request_path/foo//bar"} =
92           request :get, "/return_request_path/foo//bar"
93    assert {200, _, "//return_request_path//foo//bar//"} =
94           request :get, "//return_request_path//foo//bar//"
95  end
96
97  def headers(conn) do
98    assert get_req_header(conn, "foo") == ["bar"]
99    assert get_req_header(conn, "baz") == ["bat"]
100    resp(conn, 200, "ok")
101  end
102
103  test "stores request headers" do
104    assert {200, _, _} = request :get, "/headers", [{"foo", "bar"}, {"baz", "bat"}]
105  end
106
107  test "fails on large headers" do
108    assert capture_log(fn ->
109      cookie = "bar=" <> String.duplicate("a", 8_000_000)
110      response = request :get, "/headers", [{"cookie", cookie}]
111      assert match?({400, _, _}, response) or match?({:error, :closed}, response)
112      assert {200, _, _} = request :get, "/headers", [{"foo", "bar"}, {"baz", "bat"}]
113    end) =~ "Cowboy returned 400 and there are no headers in the connection"
114  end
115
116  def send_200(conn) do
117    assert conn.state == :unset
118    assert conn.resp_body == nil
119    conn = send_resp(conn, 200, "OK")
120    assert conn.state == :sent
121    assert conn.resp_body == nil
122    conn
123  end
124
125  def send_418(conn) do
126    send_resp(conn, 418, "")
127  end
128
129  def send_451(conn) do
130    send_resp(conn, 451, "")
131  end
132
133  def send_500(conn) do
134    conn
135    |> delete_resp_header("cache-control")
136    |> put_resp_header("x-sample", "value")
137    |> send_resp(500, ["ERR", ["OR"]])
138  end
139
140  test "sends a response with status, headers and body" do
141    assert {200, headers, "OK"} = request :get, "/send_200"
142    assert List.keyfind(headers, "cache-control", 0) ==
143           {"cache-control", "max-age=0, private, must-revalidate"}
144    assert {500, headers, "ERROR"} = request :get, "/send_500"
145    assert List.keyfind(headers, "cache-control", 0) == nil
146    assert List.keyfind(headers, "x-sample", 0) ==
147           {"x-sample", "value"}
148  end
149
150  test "allows customized statuses based on config" do
151    assert {451, _headers, ""} = request :get, "/send_451"
152    {:ok, ref} = :hackney.get("http://127.0.0.1:8001/send_451", [], "", async: :once)
153    assert_receive({:hackney_response, ^ref, {:status, 451, "Unavailable For Legal Reasons"}})
154    :hackney.close(ref)
155  end
156
157  test "existing statuses can be customized" do
158    assert {418, _headers, ""} = request :get, "/send_418"
159    {:ok, ref} = :hackney.get("http://127.0.0.1:8001/send_418", [], "", async: :once)
160    assert_receive({:hackney_response, ^ref, {:status, 418, "Totally not a teapot"}})
161    :hackney.close(ref)
162  end
163
164  test "skips body on head" do
165    assert {200, _, nil} = request :head, "/send_200"
166  end
167
168  def send_file(conn) do
169    conn = send_file(conn, 200, __ENV__.file)
170    assert conn.state == :file
171    assert conn.resp_body == nil
172    conn
173  end
174
175  test "sends a file with status and headers" do
176    assert {200, headers, body} = request :get, "/send_file"
177    assert body =~ "sends a file with status and headers"
178    assert List.keyfind(headers, "cache-control", 0) ==
179           {"cache-control", "max-age=0, private, must-revalidate"}
180    assert List.keyfind(headers, "content-length", 0) ==
181           {"content-length", __ENV__.file |> File.stat!() |> Map.fetch!(:size) |> Integer.to_string()}
182  end
183
184  test "skips file on head" do
185    assert {200, _, nil} = request :head, "/send_file"
186  end
187
188  def send_chunked(conn) do
189    conn = send_chunked(conn, 200)
190    assert conn.state == :chunked
191    {:ok, conn} = chunk(conn, "HELLO\n")
192    {:ok, conn} = chunk(conn, ["WORLD", ["\n"]])
193    conn
194  end
195
196  test "sends a chunked response with status and headers" do
197    assert {200, headers, "HELLO\nWORLD\n"} = request :get, "/send_chunked"
198    assert List.keyfind(headers, "cache-control", 0) ==
199           {"cache-control", "max-age=0, private, must-revalidate"}
200    assert List.keyfind(headers, "transfer-encoding", 0) ==
201           {"transfer-encoding", "chunked"}
202  end
203
204  def read_req_body(conn) do
205    expected = :binary.copy("abcdefghij", 100_000)
206    assert {:ok, ^expected, conn} = read_body(conn)
207    assert {:ok, "", conn} = read_body(conn)
208    resp(conn, 200, "ok")
209  end
210
211  def read_req_body_partial(conn) do
212    assert {:more, _body, conn} = read_body(conn, length: 5, read_length: 5)
213    resp(conn, 200, "ok")
214  end
215
216  test "reads body" do
217    body = :binary.copy("abcdefghij", 100_000)
218    assert {200, _, "ok"} = request :get, "/read_req_body", [], body
219    assert {200, _, "ok"} = request :post, "/read_req_body", [], body
220    assert {200, _, "ok"} = request :post, "/read_req_body_partial", [], body
221  end
222
223  def multipart(conn) do
224    opts = Plug.Parsers.init(parsers: [Plug.Parsers.MULTIPART], length: 8_000_000)
225    conn = Plug.Parsers.call(conn, opts)
226    assert conn.params["name"] == "hello"
227    assert conn.params["status"] == ["choice1", "choice2"]
228    assert conn.params["empty"] == nil
229
230    assert %Plug.Upload{} = file = conn.params["pic"]
231    assert File.read!(file.path) == "hello\n\n"
232    assert file.content_type == "text/plain"
233    assert file.filename == "foo.txt"
234
235    resp(conn, 200, "ok")
236  end
237
238  test "parses multipart requests" do
239    multipart = """
240    ------w58EW1cEpjzydSCq\r
241    Content-Disposition: form-data; name=\"name\"\r
242    \r
243    hello\r
244    ------w58EW1cEpjzydSCq\r
245    Content-Disposition: form-data; name=\"pic\"; filename=\"foo.txt\"\r
246    Content-Type: text/plain\r
247    \r
248    hello
249
250    \r
251    ------w58EW1cEpjzydSCq\r
252    Content-Disposition: form-data; name=\"empty\"; filename=\"\"\r
253    Content-Type: application/octet-stream\r
254    \r
255    \r
256    ------w58EW1cEpjzydSCq\r
257    Content-Disposition: form-data; name="status[]"\r
258    \r
259    choice1\r
260    ------w58EW1cEpjzydSCq\r
261    Content-Disposition: form-data; name="status[]"\r
262    \r
263    choice2\r
264    ------w58EW1cEpjzydSCq\r
265    Content-Disposition: form-data; name=\"commit\"\r
266    \r
267    Create User\r
268    ------w58EW1cEpjzydSCq--\r
269    """
270
271    headers =
272      [{"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"},
273       {"Content-Length", byte_size(multipart)}]
274
275    assert {200, _, _} = request :post, "/multipart", headers, multipart
276    assert {200, _, _} = request :post, "/multipart?name=overriden", headers, multipart
277  end
278
279  def file_too_big(conn) do
280    opts = Plug.Parsers.init(parsers: [Plug.Parsers.MULTIPART], length: 5)
281    conn = Plug.Parsers.call(conn, opts)
282
283    assert %Plug.Upload{} = file = conn.params["pic"]
284    assert File.read!(file.path) == "hello\n\n"
285    assert file.content_type == "text/plain"
286    assert file.filename == "foo.txt"
287
288    resp(conn, 200, "ok")
289  end
290
291  test "returns parse error when file pushed the boundaries in multipart requests" do
292    multipart = """
293    ------w58EW1cEpjzydSCq\r
294    Content-Disposition: form-data; name=\"pic\"; filename=\"foo.txt\"\r
295    Content-Type: text/plain\r
296    \r
297    hello
298
299    \r
300    ------w58EW1cEpjzydSCq--\r
301    """
302
303    headers =
304      [{"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"},
305       {"Content-Length", byte_size(multipart)}]
306
307    assert {500, _, body} = request :post, "/file_too_big", headers, multipart
308    assert body =~ "the request is too large"
309  end
310
311  test "validates utf-8 on multipart requests" do
312    multipart = """
313    ------w58EW1cEpjzydSCq\r
314    Content-Disposition: form-data; name=\"name\"\r
315    \r
316    #{<<139>>}\r
317    ------w58EW1cEpjzydSCq\r
318    """
319
320    headers =
321      [{"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"},
322       {"Content-Length", byte_size(multipart)}]
323
324    assert {500, _, body} = request :post, "/multipart", headers, multipart
325    assert body =~ "invalid UTF-8 on multipart body, got byte 139"
326  end
327
328  test "returns parse error when body is badly formatted in multipart requests" do
329    multipart = """
330    ------w58EW1cEpjzydSCq\r
331    Content-Disposition: form-data; name=\"name\"\r
332    ------w58EW1cEpjzydSCq\r
333    """
334
335    headers =
336      [{"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"},
337       {"Content-Length", byte_size(multipart)}]
338
339    assert {500, _, body} = request :post, "/multipart", headers, multipart
340    assert body =~ "malformed request, a RuntimeError exception was raised with message \"invalid multipart"
341
342    multipart = """
343    ------w58EW1cEpjzydSCq\r
344    Content-Disposition: form-data; name=\"name\"\r
345    \r
346    hello
347    """
348
349    headers =
350      [{"Content-Type", "multipart/form-data; boundary=----w58EW1cEpjzydSCq"},
351       {"Content-Length", byte_size(multipart)}]
352
353    assert {500, _, body} = request :post, "/multipart", headers, multipart
354    assert body =~ "malformed request, a RuntimeError exception was raised with message \"invalid multipart"
355  end
356
357  def https(conn) do
358    assert conn.scheme == :https
359    send_resp(conn, 200, "OK")
360  end
361
362  @https_options [
363    port: 8002, password: "cowboy",
364    keyfile: Path.expand("../../../fixtures/ssl/key.pem", __DIR__),
365    certfile: Path.expand("../../../fixtures/ssl/cert.pem", __DIR__)
366  ]
367
368  test "https" do
369    {:ok, _pid} = Plug.Adapters.Cowboy.https __MODULE__, [], @https_options
370    ssl_options = [ssl_options: [cacertfile: @https_options[:certfile], server_name_indication: 'localhost']]
371    assert {:ok, 200, _headers, client} = :hackney.get("https://127.0.0.1:8002/https", [], "", ssl_options)
372    assert {:ok, "OK"} = :hackney.body(client)
373    :hackney.close(client)
374  after
375    :ok = Plug.Adapters.Cowboy.shutdown __MODULE__.HTTPS
376  end
377
378  ## Helpers
379
380  defp request(:head = verb, path) do
381    {:ok, status, headers} =
382      :hackney.request(verb, "http://127.0.0.1:8001" <> path, [], "", [])
383    {status, headers, nil}
384  end
385  defp request(verb, path, headers \\ [], body \\ "") do
386    {:ok, status, headers, client} =
387      :hackney.request(verb, "http://127.0.0.1:8001" <> path, headers, body, [])
388    {:ok, body} = :hackney.body(client)
389    :hackney.close(client)
390    {status, headers, body}
391  end
392end
393