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