examples_SUITE.erl 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. %% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. -module(examples_SUITE).
  15. -compile(export_all).
  16. -compile(nowarn_export_all).
  17. -import(ct_helper, [config/2]).
  18. -import(ct_helper, [doc/1]).
  19. -import(cowboy_test, [gun_open/1]).
  20. %% ct.
  21. all() ->
  22. ct_helper:all(?MODULE).
  23. init_per_suite(Config) ->
  24. %% Remove environment variables inherited from Erlang.mk.
  25. os:unsetenv("ERLANG_MK_TMP"),
  26. os:unsetenv("APPS_DIR"),
  27. os:unsetenv("DEPS_DIR"),
  28. os:unsetenv("ERL_LIBS"),
  29. %% Clone and build Cowboy, Cowlib and Ranch only once and
  30. %% reuse the same build across all tests.
  31. Make = do_find_make_cmd(),
  32. CommonDir = config(priv_dir, Config),
  33. ct:log("~s~n", [os:cmd("git clone --depth 1 https://github.com/ninenines/cowboy "
  34. ++ CommonDir ++ "cowboy")]),
  35. ct:log("~s~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy distclean")]),
  36. ct:log("~s~n", [os:cmd(Make ++ " -C " ++ CommonDir ++ "cowboy DEPS_DIR=" ++ CommonDir)]),
  37. Config.
  38. end_per_suite(_) ->
  39. ok.
  40. %% Find GNU Make.
  41. do_find_make_cmd() ->
  42. case os:getenv("MAKE") of
  43. false ->
  44. case os:find_executable("gmake") of
  45. false -> "make";
  46. Cmd -> Cmd
  47. end;
  48. Cmd ->
  49. Cmd
  50. end.
  51. %% Compile, start and stop releases.
  52. do_get_paths(Example0) ->
  53. Example = atom_to_list(Example0),
  54. {ok, CWD} = file:get_cwd(),
  55. Dir = CWD ++ "/../../examples/" ++ Example,
  56. Rel = Dir ++ "/_rel/" ++ Example ++ "_example/bin/" ++ Example ++ "_example",
  57. Log = Dir ++ "/_rel/" ++ Example ++ "_example/log/erlang.log.1",
  58. {Dir, Rel, Log}.
  59. do_compile_and_start(Example, Config) ->
  60. Make = do_find_make_cmd(),
  61. {Dir, Rel, _} = do_get_paths(Example),
  62. ct:log("~s~n", [os:cmd(Make ++ " -C " ++ Dir ++ " distclean")]),
  63. %% We use a common build for Cowboy, Cowlib and Ranch to speed things up.
  64. CommonDir = config(priv_dir, Config),
  65. ct:log("~s~n", [os:cmd("mkdir " ++ Dir ++ "/deps")]),
  66. ct:log("~s~n", [os:cmd("ln -s " ++ CommonDir ++ "cowboy " ++ Dir ++ "/deps/cowboy")]),
  67. ct:log("~s~n", [os:cmd("ln -s " ++ CommonDir ++ "cowlib " ++ Dir ++ "/deps/cowlib")]),
  68. ct:log("~s~n", [os:cmd("ln -s " ++ CommonDir ++ "ranch " ++ Dir ++ "/deps/ranch")]),
  69. %% TERM=dumb disables relx coloring.
  70. ct:log("~s~n", [os:cmd(Make ++ " -C " ++ Dir ++ " TERM=dumb")]),
  71. ct:log("~s~n", [os:cmd(Rel ++ " stop")]),
  72. ct:log("~s~n", [os:cmd(Rel ++ " daemon")]),
  73. timer:sleep(2000),
  74. ok.
  75. do_stop(Example) ->
  76. {_, Rel, Log} = do_get_paths(Example),
  77. ct:log("~s~n", [os:cmd(Rel ++ " stop")]),
  78. ct:log("~s~n", [element(2, file:read_file(Log))]),
  79. ok.
  80. %% Fetch a response.
  81. do_get(Transport, Protocol, Path, Config) ->
  82. do_get(Transport, Protocol, Path, [], Config).
  83. do_get(Transport, Protocol, Path, ReqHeaders, Config) ->
  84. Port = case Transport of
  85. tcp -> 8080;
  86. ssl -> 8443
  87. end,
  88. ConnPid = gun_open([{port, Port}, {type, Transport}, {protocol, Protocol}|Config]),
  89. Ref = gun:get(ConnPid, Path, ReqHeaders),
  90. case gun:await(ConnPid, Ref) of
  91. {response, nofin, Status, RespHeaders} ->
  92. {ok, Body} = gun:await_body(ConnPid, Ref),
  93. {Status, RespHeaders, Body};
  94. {response, fin, Status, RespHeaders} ->
  95. {Status, RespHeaders, <<>>}
  96. end.
  97. %% TCP and SSL Hello World.
  98. hello_world(Config) ->
  99. doc("Hello World example."),
  100. try
  101. do_compile_and_start(hello_world, Config),
  102. do_hello_world(tcp, http, Config),
  103. do_hello_world(tcp, http2, Config)
  104. after
  105. do_stop(hello_world)
  106. end.
  107. ssl_hello_world(Config) ->
  108. doc("SSL Hello World example."),
  109. try
  110. do_compile_and_start(ssl_hello_world, Config),
  111. do_hello_world(ssl, http, Config),
  112. do_hello_world(ssl, http2, Config)
  113. after
  114. do_stop(ssl_hello_world)
  115. end.
  116. do_hello_world(Transport, Protocol, Config) ->
  117. {200, _, <<"Hello world!">>} = do_get(Transport, Protocol, "/", Config),
  118. ok.
  119. %% Chunked Hello World.
  120. chunked_hello_world(Config) ->
  121. doc("Chunked Hello World example."),
  122. try
  123. do_compile_and_start(chunked_hello_world, Config),
  124. do_chunked_hello_world(tcp, http, Config),
  125. do_chunked_hello_world(tcp, http2, Config)
  126. after
  127. do_stop(chunked_hello_world)
  128. end.
  129. do_chunked_hello_world(Transport, Protocol, Config) ->
  130. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  131. Ref = gun:get(ConnPid, "/"),
  132. {response, nofin, 200, _} = gun:await(ConnPid, Ref),
  133. %% We expect to receive a chunk every second, three total.
  134. {data, nofin, <<"Hello\r\n">>} = gun:await(ConnPid, Ref, 2000),
  135. {data, nofin, <<"World\r\n">>} = gun:await(ConnPid, Ref, 2000),
  136. {data, IsFin, <<"Chunked!\r\n">>} = gun:await(ConnPid, Ref, 2000),
  137. %% We may get an extra empty chunk (last chunk for HTTP/1.1,
  138. %% empty DATA frame with the FIN bit set for HTTP/2).
  139. case IsFin of
  140. fin -> ok;
  141. nofin ->
  142. {data, fin, <<>>} = gun:await(ConnPid, Ref, 500),
  143. ok
  144. end.
  145. %% Compressed responses.
  146. compress_response(Config) ->
  147. doc("Compressed response example."),
  148. try
  149. do_compile_and_start(compress_response, Config),
  150. do_compress_response(tcp, http, Config),
  151. do_compress_response(tcp, http2, Config)
  152. after
  153. do_stop(compress_response)
  154. end.
  155. do_compress_response(Transport, Protocol, Config) ->
  156. {200, Headers, Body} = do_get(Transport, Protocol, "/",
  157. [{<<"accept-encoding">>, <<"gzip">>}], Config),
  158. {_, <<"gzip">>} = lists:keyfind(<<"content-encoding">>, 1, Headers),
  159. _ = zlib:gunzip(Body),
  160. ok.
  161. %% Cookie.
  162. cookie(Config) ->
  163. doc("Cookie example."),
  164. try
  165. do_compile_and_start(cookie, Config),
  166. do_cookie(tcp, http, Config),
  167. do_cookie(tcp, http2, Config)
  168. after
  169. do_stop(cookie)
  170. end.
  171. do_cookie(Transport, Protocol, Config) ->
  172. {200, _, One} = do_get(Transport, Protocol, "/", Config),
  173. {200, _, Two} = do_get(Transport, Protocol, "/", [{<<"cookie">>, <<"server=abcdef">>}], Config),
  174. true = One =/= Two,
  175. ok.
  176. %% Echo GET.
  177. echo_get(Config) ->
  178. doc("GET parameter echo example."),
  179. try
  180. do_compile_and_start(echo_get, Config),
  181. do_echo_get(tcp, http, Config),
  182. do_echo_get(tcp, http2, Config)
  183. after
  184. do_stop(echo_get)
  185. end.
  186. do_echo_get(Transport, Protocol, Config) ->
  187. {200, _, <<"this is fun">>} = do_get(Transport, Protocol, "/?echo=this+is+fun", Config),
  188. {400, _, _} = do_get(Transport, Protocol, "/", Config),
  189. ok.
  190. %% Echo POST.
  191. echo_post(Config) ->
  192. doc("POST parameter echo example."),
  193. try
  194. do_compile_and_start(echo_post, Config),
  195. do_echo_post(tcp, http, Config),
  196. do_echo_post(tcp, http2, Config)
  197. after
  198. do_stop(echo_post)
  199. end.
  200. do_echo_post(Transport, Protocol, Config) ->
  201. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  202. Ref = gun:post(ConnPid, "/", [
  203. {<<"content-type">>, <<"application/octet-stream">>}
  204. ], <<"echo=this+is+fun">>),
  205. {response, nofin, 200, _} = gun:await(ConnPid, Ref),
  206. {ok, <<"this is fun">>} = gun:await_body(ConnPid, Ref),
  207. ok.
  208. %% Eventsource.
  209. eventsource(Config) ->
  210. doc("Eventsource example."),
  211. try
  212. do_compile_and_start(eventsource, Config),
  213. do_eventsource(tcp, http, Config),
  214. do_eventsource(tcp, http2, Config)
  215. after
  216. do_stop(eventsource)
  217. end.
  218. do_eventsource(Transport, Protocol, Config) ->
  219. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  220. Ref = gun:get(ConnPid, "/eventsource"),
  221. {response, nofin, 200, Headers} = gun:await(ConnPid, Ref),
  222. {_, <<"text/event-stream">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  223. %% Receive a few events.
  224. {data, nofin, << "id: ", _/bits >>} = gun:await(ConnPid, Ref, 2000),
  225. {data, nofin, << "id: ", _/bits >>} = gun:await(ConnPid, Ref, 2000),
  226. {data, nofin, << "id: ", _/bits >>} = gun:await(ConnPid, Ref, 2000),
  227. gun:close(ConnPid).
  228. %% REST Hello World.
  229. rest_hello_world(Config) ->
  230. doc("REST Hello World example."),
  231. try
  232. do_compile_and_start(rest_hello_world, Config),
  233. do_rest_hello_world(tcp, http, Config),
  234. do_rest_hello_world(tcp, http2, Config)
  235. after
  236. do_stop(rest_hello_world)
  237. end.
  238. do_rest_hello_world(Transport, Protocol, Config) ->
  239. << "<html>", _/bits >> = do_rest_get(Transport, Protocol,
  240. "/", undefined, undefined, Config),
  241. << "REST Hello World as text!" >> = do_rest_get(Transport, Protocol,
  242. "/", <<"text/plain">>, undefined, Config),
  243. << "{\"rest\": \"Hello World!\"}" >> = do_rest_get(Transport, Protocol,
  244. "/", <<"application/json">>, undefined, Config),
  245. not_acceptable = do_rest_get(Transport, Protocol,
  246. "/", <<"text/css">>, undefined, Config),
  247. ok.
  248. do_rest_get(Transport, Protocol, Path, Accept, Auth, Config) ->
  249. ReqHeaders0 = case Accept of
  250. undefined -> [];
  251. _ -> [{<<"accept">>, Accept}]
  252. end,
  253. ReqHeaders = case Auth of
  254. undefined -> ReqHeaders0;
  255. _ -> [{<<"authorization">>, [<<"Basic ">>, base64:encode(Auth)]}|ReqHeaders0]
  256. end,
  257. case do_get(Transport, Protocol, Path, ReqHeaders, Config) of
  258. {200, RespHeaders, Body} ->
  259. Accept = case Accept of
  260. undefined -> undefined;
  261. _ ->
  262. {_, ContentType} = lists:keyfind(<<"content-type">>, 1, RespHeaders),
  263. ContentType
  264. end,
  265. Body;
  266. {401, _, _} ->
  267. unauthorized;
  268. {406, _, _} ->
  269. not_acceptable
  270. end.
  271. %% REST basic auth.
  272. rest_basic_auth(Config) ->
  273. doc("REST basic authorization example."),
  274. try
  275. do_compile_and_start(rest_basic_auth, Config),
  276. do_rest_basic_auth(tcp, http, Config),
  277. do_rest_basic_auth(tcp, http2, Config)
  278. after
  279. do_stop(rest_basic_auth)
  280. end.
  281. do_rest_basic_auth(Transport, Protocol, Config) ->
  282. unauthorized = do_rest_get(Transport, Protocol, "/", undefined, undefined, Config),
  283. <<"Hello, Alladin!\n">> = do_rest_get(Transport, Protocol, "/", undefined, "Alladin:open sesame", Config),
  284. ok.
  285. %% REST pastebin.
  286. rest_pastebin(Config) ->
  287. doc("REST pastebin example."),
  288. try
  289. do_compile_and_start(rest_pastebin, Config),
  290. do_rest_pastebin(tcp, http, Config),
  291. do_rest_pastebin(tcp, http2, Config)
  292. after
  293. do_stop(rest_pastebin)
  294. end.
  295. do_rest_pastebin(Transport, Protocol, Config) ->
  296. %% Existing files.
  297. _ = do_rest_get(Transport, Protocol, "/", <<"text/html">>, undefined, Config),
  298. _ = do_rest_get(Transport, Protocol, "/", <<"text/plain">>, undefined, Config),
  299. %% Use POST to upload a new file and download it back.
  300. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  301. Ref = gun:post(ConnPid, "/", [
  302. {<<"content-type">>, <<"application/x-www-form-urlencoded">>}
  303. ], <<"paste=this+is+fun">>),
  304. %% @todo Not too happy about 303 here,
  305. %% will need to revisit this example.
  306. {response, _, 303, Headers} = gun:await(ConnPid, Ref),
  307. {_, Location} = lists:keyfind(<<"location">>, 1, Headers),
  308. <<"this is fun">> = do_rest_get(Transport, Protocol, Location, <<"text/plain">>, undefined, Config),
  309. << "<!DOCTYPE html><html>", _/bits >>
  310. = do_rest_get(Transport, Protocol, Location, <<"text/html">>, undefined, Config),
  311. ok.
  312. %% File server.
  313. file_server(Config) ->
  314. doc("File server example with directory listing."),
  315. try
  316. do_compile_and_start(file_server, Config),
  317. do_file_server(tcp, http, Config),
  318. do_file_server(tcp, http2, Config)
  319. after
  320. do_stop(file_server)
  321. end.
  322. do_file_server(Transport, Protocol, Config) ->
  323. %% Directory.
  324. {200, DirHeaders, <<"<!DOCTYPE html><html>", _/bits >>} = do_get(Transport, Protocol, "/", Config),
  325. {_, <<"text/html; charset=utf-8">>} = lists:keyfind(<<"content-type">>, 1, DirHeaders),
  326. _ = do_rest_get(Transport, Protocol, "/", <<"application/json">>, undefined, Config),
  327. %% Files.
  328. {200, _, _} = do_get(Transport, Protocol, "/small.mp4", Config),
  329. {200, _, _} = do_get(Transport, Protocol, "/small.ogv", Config),
  330. {200, _, _} = do_get(Transport, Protocol, "/test.txt", Config),
  331. {200, _, _} = do_get(Transport, Protocol, "/video.html", Config),
  332. {200, _, _} = do_get(Transport, Protocol,
  333. ["/", cow_uri:urlencode(<<"中文"/utf8>>), "/", cow_uri:urlencode(<<"中文.html"/utf8>>)],
  334. Config),
  335. ok.
  336. %% Markdown middleware.
  337. markdown_middleware(Config) ->
  338. doc("Markdown middleware example."),
  339. try
  340. do_compile_and_start(markdown_middleware, Config),
  341. do_markdown_middleware(tcp, http, Config),
  342. do_markdown_middleware(tcp, http2, Config)
  343. after
  344. do_stop(markdown_middleware)
  345. end.
  346. do_markdown_middleware(Transport, Protocol, Config) ->
  347. {200, Headers, <<"<h1>", _/bits >>} = do_get(Transport, Protocol, "/video.html", Config),
  348. {_, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers),
  349. ok.
  350. %% Upload.
  351. upload(Config) ->
  352. doc("Upload example."),
  353. try
  354. do_compile_and_start(upload, Config),
  355. do_upload(tcp, http, Config),
  356. do_upload(tcp, http2, Config)
  357. after
  358. do_stop(upload)
  359. end.
  360. do_upload(Transport, Protocol, Config) ->
  361. {200, _, << "<html>", _/bits >>} = do_get(Transport, Protocol, "/", Config),
  362. %% Use POST to upload a file using multipart.
  363. ConnPid = gun_open([{port, 8080}, {type, Transport}, {protocol, Protocol}|Config]),
  364. Ref = gun:post(ConnPid, "/upload", [
  365. {<<"content-type">>, <<"multipart/form-data;boundary=deadbeef">>}
  366. ], <<
  367. "--deadbeef\r\n"
  368. "Content-Disposition: form-data; name=\"inputfile\"; filename=\"test.txt\"\r\n"
  369. "Content-Type: text/plain\r\n"
  370. "\r\n"
  371. "Cowboy upload example!\r\n"
  372. "--deadbeef--">>),
  373. {response, fin, 204, _} = gun:await(ConnPid, Ref),
  374. ok.
  375. %% Websocket.
  376. websocket(Config) ->
  377. doc("Websocket example."),
  378. try
  379. do_compile_and_start(websocket, Config),
  380. %% We can only initiate a Websocket connection from HTTP/1.1.
  381. {ok, Pid} = gun:open("127.0.0.1", 8080, #{protocols => [http], retry => 0}),
  382. {ok, http} = gun:await_up(Pid),
  383. _ = monitor(process, Pid),
  384. StreamRef = gun:ws_upgrade(Pid, "/websocket", [], #{compress => true}),
  385. receive
  386. {gun_upgrade, Pid, StreamRef, _, _} ->
  387. ok;
  388. Msg1 ->
  389. exit({connection_failed, Msg1})
  390. end,
  391. %% Check that we receive the message sent on timer on init.
  392. receive
  393. {gun_ws, Pid, StreamRef, {text, <<"Hello!">>}} ->
  394. ok
  395. after 2000 ->
  396. exit(timeout)
  397. end,
  398. %% Check that we receive subsequent messages sent on timer.
  399. receive
  400. {gun_ws, Pid, StreamRef, {text, <<"How' you doin'?">>}} ->
  401. ok
  402. after 2000 ->
  403. exit(timeout)
  404. end,
  405. %% Check that we receive the echoed message.
  406. gun:ws_send(Pid, StreamRef, {text, <<"hello">>}),
  407. receive
  408. {gun_ws, Pid, StreamRef, {text, <<"That's what she said! hello">>}} ->
  409. ok
  410. after 500 ->
  411. exit(timeout)
  412. end,
  413. gun:ws_send(Pid, StreamRef, close)
  414. after
  415. do_stop(websocket)
  416. end.