Sfoglia il codice sorgente

ft: 新版本初始化

master
SisMaker 3 anni fa
parent
commit
c8aaf0dc28
93 ha cambiato i file con 3692 aggiunte e 32866 eliminazioni
  1. +0
    -582
      include/cow_inline.hrl
  2. +0
    -83
      include/cow_parse.hrl
  3. +18
    -0
      include/elli.hrl
  4. +4
    -0
      src/eWSrv.erl
  5. +0
    -9
      src/eWSrv_sup.erl
  6. +52
    -0
      src/test/elli_handover_tests.erl
  7. +27
    -0
      src/test/elli_http_tests.erl
  8. +34
    -0
      src/test/elli_metrics_middleware.erl
  9. +102
    -0
      src/test/elli_middleware_tests.erl
  10. +124
    -0
      src/test/elli_ssl_tests.erl
  11. +21
    -0
      src/test/elli_test.hrl
  12. +747
    -0
      src/test/elli_tests.erl
  13. +19
    -0
      src/test/server_cert.pem
  14. +27
    -0
      src/test/server_key.pem
  15. +0
    -81
      src/wsLib/cow_base64url.erl
  16. +0
    -428
      src/wsLib/cow_cookie.erl
  17. +0
    -434
      src/wsLib/cow_date.erl
  18. +0
    -1449
      src/wsLib/cow_hpack.erl
  19. +0
    -4132
      src/wsLib/cow_hpack_dec_huffman_lookup.hrl
  20. +0
    -426
      src/wsLib/cow_http.erl
  21. +0
    -483
      src/wsLib/cow_http2.erl
  22. +0
    -1647
      src/wsLib/cow_http2_machine.erl
  23. +0
    -3645
      src/wsLib/cow_http_hd.erl
  24. +0
    -420
      src/wsLib/cow_http_struct_hd.erl
  25. +0
    -373
      src/wsLib/cow_http_te.erl
  26. +0
    -95
      src/wsLib/cow_iolists.erl
  27. +0
    -445
      src/wsLib/cow_link.erl
  28. +0
    -1045
      src/wsLib/cow_mimetypes.erl
  29. +0
    -61
      src/wsLib/cow_mimetypes.erl.src
  30. +0
    -775
      src/wsLib/cow_multipart.erl
  31. +0
    -563
      src/wsLib/cow_qs.erl
  32. +0
    -313
      src/wsLib/cow_spdy.erl
  33. +0
    -181
      src/wsLib/cow_spdy.hrl
  34. +0
    -348
      src/wsLib/cow_sse.erl
  35. +0
    -339
      src/wsLib/cow_uri.erl
  36. +0
    -356
      src/wsLib/cow_uri_template.erl
  37. +0
    -741
      src/wsLib/cow_ws.erl
  38. +0
    -625
      src/wsNet/ranch.erl
  39. +0
    -72
      src/wsNet/ranch_acceptor.erl
  40. +0
    -103
      src/wsNet/ranch_acceptors_sup.erl
  41. +0
    -48
      src/wsNet/ranch_app.erl
  42. +0
    -508
      src/wsNet/ranch_conns_sup.erl
  43. +0
    -42
      src/wsNet/ranch_conns_sup_sup.erl
  44. +0
    -115
      src/wsNet/ranch_crc32c.erl
  45. +0
    -36
      src/wsNet/ranch_embedded_sup.erl
  46. +0
    -48
      src/wsNet/ranch_listener_sup.erl
  47. +0
    -23
      src/wsNet/ranch_protocol.erl
  48. +0
    -1007
      src/wsNet/ranch_proxy_header.erl
  49. +0
    -279
      src/wsNet/ranch_server.erl
  50. +0
    -67
      src/wsNet/ranch_server_proxy.erl
  51. +0
    -341
      src/wsNet/ranch_ssl.erl
  52. +0
    -39
      src/wsNet/ranch_sup.erl
  53. +0
    -287
      src/wsNet/ranch_tcp.erl
  54. +0
    -157
      src/wsNet/ranch_transport.erl
  55. +0
    -105
      src/wsSrv/cowboy.erl
  56. +0
    -27
      src/wsSrv/cowboy_app.erl
  57. +0
    -123
      src/wsSrv/cowboy_bstr.erl
  58. +0
    -192
      src/wsSrv/cowboy_children.erl
  59. +0
    -60
      src/wsSrv/cowboy_clear.erl
  60. +0
    -221
      src/wsSrv/cowboy_clock.erl
  61. +0
    -249
      src/wsSrv/cowboy_compress_h.erl
  62. +0
    -174
      src/wsSrv/cowboy_constraints.erl
  63. +0
    -57
      src/wsSrv/cowboy_handler.erl
  64. +0
    -1523
      src/wsSrv/cowboy_http.erl
  65. +0
    -1221
      src/wsSrv/cowboy_http2.erl
  66. +0
    -108
      src/wsSrv/cowboy_loop.erl
  67. +0
    -331
      src/wsSrv/cowboy_metrics_h.erl
  68. +0
    -24
      src/wsSrv/cowboy_middleware.erl
  69. +0
    -1016
      src/wsSrv/cowboy_req.erl
  70. +0
    -1637
      src/wsSrv/cowboy_rest.erl
  71. +0
    -603
      src/wsSrv/cowboy_router.erl
  72. +0
    -418
      src/wsSrv/cowboy_static.erl
  73. +0
    -193
      src/wsSrv/cowboy_stream.erl
  74. +0
    -324
      src/wsSrv/cowboy_stream_h.erl
  75. +0
    -24
      src/wsSrv/cowboy_sub_protocol.erl
  76. +0
    -30
      src/wsSrv/cowboy_sup.erl
  77. +0
    -56
      src/wsSrv/cowboy_tls.erl
  78. +0
    -192
      src/wsSrv/cowboy_tracer_h.erl
  79. +0
    -707
      src/wsSrv/cowboy_websocket.erl
  80. +285
    -0
      src/wsSrv/elli.erl
  81. +329
    -0
      src/wsSrv/elli_example_callback.erl
  82. +42
    -0
      src/wsSrv/elli_example_callback_handover.erl
  83. +26
    -0
      src/wsSrv/elli_example_middleware.erl
  84. +57
    -0
      src/wsSrv/elli_handler.erl
  85. +959
    -0
      src/wsSrv/elli_http.erl
  86. +153
    -0
      src/wsSrv/elli_middleware.erl
  87. +57
    -0
      src/wsSrv/elli_middleware_compress.erl
  88. +299
    -0
      src/wsSrv/elli_request.erl
  89. +78
    -0
      src/wsSrv/elli_sendfile.erl
  90. +97
    -0
      src/wsSrv/elli_tcp.erl
  91. +47
    -0
      src/wsSrv/elli_test.erl
  92. +71
    -0
      src/wsSrv/elli_util.erl
  93. +17
    -0
      src/wsSrv/elli_util.hrl

+ 0
- 582
include/cow_inline.hrl Vedi File

@ -1,582 +0,0 @@
%% Copyright (c) 2014-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-ifndef(COW_INLINE_HRL).
-define(COW_INLINE_HRL, 1).
%% LC(Character)
-define(LC(C), case C of
$A -> $a;
$B -> $b;
$C -> $c;
$D -> $d;
$E -> $e;
$F -> $f;
$G -> $g;
$H -> $h;
$I -> $i;
$J -> $j;
$K -> $k;
$L -> $l;
$M -> $m;
$N -> $n;
$O -> $o;
$P -> $p;
$Q -> $q;
$R -> $r;
$S -> $s;
$T -> $t;
$U -> $u;
$V -> $v;
$W -> $w;
$X -> $x;
$Y -> $y;
$Z -> $z;
_ -> C
end).
%% LOWER(Bin)
%%
%% Lowercase the entire binary string in a binary comprehension.
-define(LOWER(Bin), <<<<?LC(C)>> || <<C>> <= Bin>>).
%% LOWERCASE(Function, Rest, Acc, ...)
%%
%% To be included at the end of a case block.
%% Defined for up to 10 extra arguments.
-define(LOWER(Function, Rest, Acc), case C of
$A -> Function(Rest, <<Acc/binary, $a>>);
$B -> Function(Rest, <<Acc/binary, $b>>);
$C -> Function(Rest, <<Acc/binary, $c>>);
$D -> Function(Rest, <<Acc/binary, $d>>);
$E -> Function(Rest, <<Acc/binary, $e>>);
$F -> Function(Rest, <<Acc/binary, $f>>);
$G -> Function(Rest, <<Acc/binary, $g>>);
$H -> Function(Rest, <<Acc/binary, $h>>);
$I -> Function(Rest, <<Acc/binary, $i>>);
$J -> Function(Rest, <<Acc/binary, $j>>);
$K -> Function(Rest, <<Acc/binary, $k>>);
$L -> Function(Rest, <<Acc/binary, $l>>);
$M -> Function(Rest, <<Acc/binary, $m>>);
$N -> Function(Rest, <<Acc/binary, $n>>);
$O -> Function(Rest, <<Acc/binary, $o>>);
$P -> Function(Rest, <<Acc/binary, $p>>);
$Q -> Function(Rest, <<Acc/binary, $q>>);
$R -> Function(Rest, <<Acc/binary, $r>>);
$S -> Function(Rest, <<Acc/binary, $s>>);
$T -> Function(Rest, <<Acc/binary, $t>>);
$U -> Function(Rest, <<Acc/binary, $u>>);
$V -> Function(Rest, <<Acc/binary, $v>>);
$W -> Function(Rest, <<Acc/binary, $w>>);
$X -> Function(Rest, <<Acc/binary, $x>>);
$Y -> Function(Rest, <<Acc/binary, $y>>);
$Z -> Function(Rest, <<Acc/binary, $z>>);
C -> Function(Rest, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, Acc), case C of
$A -> Function(Rest, A0, <<Acc/binary, $a>>);
$B -> Function(Rest, A0, <<Acc/binary, $b>>);
$C -> Function(Rest, A0, <<Acc/binary, $c>>);
$D -> Function(Rest, A0, <<Acc/binary, $d>>);
$E -> Function(Rest, A0, <<Acc/binary, $e>>);
$F -> Function(Rest, A0, <<Acc/binary, $f>>);
$G -> Function(Rest, A0, <<Acc/binary, $g>>);
$H -> Function(Rest, A0, <<Acc/binary, $h>>);
$I -> Function(Rest, A0, <<Acc/binary, $i>>);
$J -> Function(Rest, A0, <<Acc/binary, $j>>);
$K -> Function(Rest, A0, <<Acc/binary, $k>>);
$L -> Function(Rest, A0, <<Acc/binary, $l>>);
$M -> Function(Rest, A0, <<Acc/binary, $m>>);
$N -> Function(Rest, A0, <<Acc/binary, $n>>);
$O -> Function(Rest, A0, <<Acc/binary, $o>>);
$P -> Function(Rest, A0, <<Acc/binary, $p>>);
$Q -> Function(Rest, A0, <<Acc/binary, $q>>);
$R -> Function(Rest, A0, <<Acc/binary, $r>>);
$S -> Function(Rest, A0, <<Acc/binary, $s>>);
$T -> Function(Rest, A0, <<Acc/binary, $t>>);
$U -> Function(Rest, A0, <<Acc/binary, $u>>);
$V -> Function(Rest, A0, <<Acc/binary, $v>>);
$W -> Function(Rest, A0, <<Acc/binary, $w>>);
$X -> Function(Rest, A0, <<Acc/binary, $x>>);
$Y -> Function(Rest, A0, <<Acc/binary, $y>>);
$Z -> Function(Rest, A0, <<Acc/binary, $z>>);
C -> Function(Rest, A0, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, A1, Acc), case C of
$A -> Function(Rest, A0, A1, <<Acc/binary, $a>>);
$B -> Function(Rest, A0, A1, <<Acc/binary, $b>>);
$C -> Function(Rest, A0, A1, <<Acc/binary, $c>>);
$D -> Function(Rest, A0, A1, <<Acc/binary, $d>>);
$E -> Function(Rest, A0, A1, <<Acc/binary, $e>>);
$F -> Function(Rest, A0, A1, <<Acc/binary, $f>>);
$G -> Function(Rest, A0, A1, <<Acc/binary, $g>>);
$H -> Function(Rest, A0, A1, <<Acc/binary, $h>>);
$I -> Function(Rest, A0, A1, <<Acc/binary, $i>>);
$J -> Function(Rest, A0, A1, <<Acc/binary, $j>>);
$K -> Function(Rest, A0, A1, <<Acc/binary, $k>>);
$L -> Function(Rest, A0, A1, <<Acc/binary, $l>>);
$M -> Function(Rest, A0, A1, <<Acc/binary, $m>>);
$N -> Function(Rest, A0, A1, <<Acc/binary, $n>>);
$O -> Function(Rest, A0, A1, <<Acc/binary, $o>>);
$P -> Function(Rest, A0, A1, <<Acc/binary, $p>>);
$Q -> Function(Rest, A0, A1, <<Acc/binary, $q>>);
$R -> Function(Rest, A0, A1, <<Acc/binary, $r>>);
$S -> Function(Rest, A0, A1, <<Acc/binary, $s>>);
$T -> Function(Rest, A0, A1, <<Acc/binary, $t>>);
$U -> Function(Rest, A0, A1, <<Acc/binary, $u>>);
$V -> Function(Rest, A0, A1, <<Acc/binary, $v>>);
$W -> Function(Rest, A0, A1, <<Acc/binary, $w>>);
$X -> Function(Rest, A0, A1, <<Acc/binary, $x>>);
$Y -> Function(Rest, A0, A1, <<Acc/binary, $y>>);
$Z -> Function(Rest, A0, A1, <<Acc/binary, $z>>);
C -> Function(Rest, A0, A1, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, A1, A2, Acc), case C of
$A -> Function(Rest, A0, A1, A2, <<Acc/binary, $a>>);
$B -> Function(Rest, A0, A1, A2, <<Acc/binary, $b>>);
$C -> Function(Rest, A0, A1, A2, <<Acc/binary, $c>>);
$D -> Function(Rest, A0, A1, A2, <<Acc/binary, $d>>);
$E -> Function(Rest, A0, A1, A2, <<Acc/binary, $e>>);
$F -> Function(Rest, A0, A1, A2, <<Acc/binary, $f>>);
$G -> Function(Rest, A0, A1, A2, <<Acc/binary, $g>>);
$H -> Function(Rest, A0, A1, A2, <<Acc/binary, $h>>);
$I -> Function(Rest, A0, A1, A2, <<Acc/binary, $i>>);
$J -> Function(Rest, A0, A1, A2, <<Acc/binary, $j>>);
$K -> Function(Rest, A0, A1, A2, <<Acc/binary, $k>>);
$L -> Function(Rest, A0, A1, A2, <<Acc/binary, $l>>);
$M -> Function(Rest, A0, A1, A2, <<Acc/binary, $m>>);
$N -> Function(Rest, A0, A1, A2, <<Acc/binary, $n>>);
$O -> Function(Rest, A0, A1, A2, <<Acc/binary, $o>>);
$P -> Function(Rest, A0, A1, A2, <<Acc/binary, $p>>);
$Q -> Function(Rest, A0, A1, A2, <<Acc/binary, $q>>);
$R -> Function(Rest, A0, A1, A2, <<Acc/binary, $r>>);
$S -> Function(Rest, A0, A1, A2, <<Acc/binary, $s>>);
$T -> Function(Rest, A0, A1, A2, <<Acc/binary, $t>>);
$U -> Function(Rest, A0, A1, A2, <<Acc/binary, $u>>);
$V -> Function(Rest, A0, A1, A2, <<Acc/binary, $v>>);
$W -> Function(Rest, A0, A1, A2, <<Acc/binary, $w>>);
$X -> Function(Rest, A0, A1, A2, <<Acc/binary, $x>>);
$Y -> Function(Rest, A0, A1, A2, <<Acc/binary, $y>>);
$Z -> Function(Rest, A0, A1, A2, <<Acc/binary, $z>>);
C -> Function(Rest, A0, A1, A2, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, A1, A2, A3, Acc), case C of
$A -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $a>>);
$B -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $b>>);
$C -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $c>>);
$D -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $d>>);
$E -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $e>>);
$F -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $f>>);
$G -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $g>>);
$H -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $h>>);
$I -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $i>>);
$J -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $j>>);
$K -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $k>>);
$L -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $l>>);
$M -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $m>>);
$N -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $n>>);
$O -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $o>>);
$P -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $p>>);
$Q -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $q>>);
$R -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $r>>);
$S -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $s>>);
$T -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $t>>);
$U -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $u>>);
$V -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $v>>);
$W -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $w>>);
$X -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $x>>);
$Y -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $y>>);
$Z -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, $z>>);
C -> Function(Rest, A0, A1, A2, A3, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, A1, A2, A3, A4, Acc), case C of
$A -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $a>>);
$B -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $b>>);
$C -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $c>>);
$D -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $d>>);
$E -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $e>>);
$F -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $f>>);
$G -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $g>>);
$H -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $h>>);
$I -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $i>>);
$J -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $j>>);
$K -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $k>>);
$L -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $l>>);
$M -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $m>>);
$N -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $n>>);
$O -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $o>>);
$P -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $p>>);
$Q -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $q>>);
$R -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $r>>);
$S -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $s>>);
$T -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $t>>);
$U -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $u>>);
$V -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $v>>);
$W -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $w>>);
$X -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $x>>);
$Y -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $y>>);
$Z -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, $z>>);
C -> Function(Rest, A0, A1, A2, A3, A4, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, A1, A2, A3, A4, A5, Acc), case C of
$A ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $a>>);
$B ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $b>>);
$C ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $c>>);
$D ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $d>>);
$E ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $e>>);
$F ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $f>>);
$G ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $g>>);
$H ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $h>>);
$I ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $i>>);
$J ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $j>>);
$K ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $k>>);
$L ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $l>>);
$M ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $m>>);
$N ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $n>>);
$O ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $o>>);
$P ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $p>>);
$Q ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $q>>);
$R ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $r>>);
$S ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $s>>);
$T ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $t>>);
$U ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $u>>);
$V ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $v>>);
$W ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $w>>);
$X ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $x>>);
$Y ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $y>>);
$Z ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, $z>>);
C ->
Function(Rest, A0, A1, A2, A3, A4, A5, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, A1, A2, A3, A4, A5, A6, Acc), case C of
$A ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $a>>);
$B ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $b>>);
$C ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $c>>);
$D ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $d>>);
$E ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $e>>);
$F ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $f>>);
$G ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $g>>);
$H ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $h>>);
$I ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $i>>);
$J ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $j>>);
$K ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $k>>);
$L ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $l>>);
$M ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $m>>);
$N ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $n>>);
$O ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $o>>);
$P ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $p>>);
$Q ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $q>>);
$R ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $r>>);
$S ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $s>>);
$T ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $t>>);
$U ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $u>>);
$V ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $v>>);
$W ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $w>>);
$X ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $x>>);
$Y ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $y>>);
$Z ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, $z>>);
C ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, A1, A2, A3, A4, A5, A6, A7, Acc), case C of
$A ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $a>>);
$B ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $b>>);
$C ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $c>>);
$D ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $d>>);
$E ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $e>>);
$F ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $f>>);
$G ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $g>>);
$H ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $h>>);
$I ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $i>>);
$J ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $j>>);
$K ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $k>>);
$L ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $l>>);
$M ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $m>>);
$N ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $n>>);
$O ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $o>>);
$P ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $p>>);
$Q ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $q>>);
$R ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $r>>);
$S ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $s>>);
$T ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $t>>);
$U ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $u>>);
$V ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $v>>);
$W ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $w>>);
$X ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $x>>);
$Y ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $y>>);
$Z ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, $z>>);
C ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, Acc), case C of
$A ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $a>>);
$B ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $b>>);
$C ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $c>>);
$D ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $d>>);
$E ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $e>>);
$F ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $f>>);
$G ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $g>>);
$H ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $h>>);
$I ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $i>>);
$J ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $j>>);
$K ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $k>>);
$L ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $l>>);
$M ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $m>>);
$N ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $n>>);
$O ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $o>>);
$P ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $p>>);
$Q ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $q>>);
$R ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $r>>);
$S ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $s>>);
$T ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $t>>);
$U ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $u>>);
$V ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $v>>);
$W ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $w>>);
$X ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $x>>);
$Y ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $y>>);
$Z ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, $z>>);
C ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, <<Acc/binary, C>>)
end).
-define(LOWER(Function, Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, Acc), case C of
$A ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $a>>);
$B ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $b>>);
$C ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $c>>);
$D ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $d>>);
$E ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $e>>);
$F ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $f>>);
$G ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $g>>);
$H ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $h>>);
$I ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $i>>);
$J ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $j>>);
$K ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $k>>);
$L ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $l>>);
$M ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $m>>);
$N ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $n>>);
$O ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $o>>);
$P ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $p>>);
$Q ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $q>>);
$R ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $r>>);
$S ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $s>>);
$T ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $t>>);
$U ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $u>>);
$V ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $v>>);
$W ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $w>>);
$X ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $x>>);
$Y ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $y>>);
$Z ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, $z>>);
C ->
Function(Rest, A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, <<Acc/binary, C>>)
end).
%% HEX(C)
-define(HEX(C), (?HEXHL(C bsr 4)), (?HEXHL(C band 16#0f))).
-define(HEXHL(HL),
case HL of
0 -> $0;
1 -> $1;
2 -> $2;
3 -> $3;
4 -> $4;
5 -> $5;
6 -> $6;
7 -> $7;
8 -> $8;
9 -> $9;
10 -> $A;
11 -> $B;
12 -> $C;
13 -> $D;
14 -> $E;
15 -> $F
end
).
%% UNHEX(H, L)
-define(UNHEX(H, L), (?UNHEX(H) bsl 4 bor ?UNHEX(L))).
-define(UNHEX(C),
case C of
$0 -> 0;
$1 -> 1;
$2 -> 2;
$3 -> 3;
$4 -> 4;
$5 -> 5;
$6 -> 6;
$7 -> 7;
$8 -> 8;
$9 -> 9;
$A -> 10;
$B -> 11;
$C -> 12;
$D -> 13;
$E -> 14;
$F -> 15;
$a -> 10;
$b -> 11;
$c -> 12;
$d -> 13;
$e -> 14;
$f -> 15
end
).
-endif.

+ 0
- 83
include/cow_parse.hrl Vedi File

@ -1,83 +0,0 @@
%% Copyright (c) 2015-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-ifndef(COW_PARSE_HRL).
-define(COW_PARSE_HRL, 1).
-define(IS_ALPHA(C),
(C =:= $a) or (C =:= $b) or (C =:= $c) or (C =:= $d) or (C =:= $e) or
(C =:= $f) or (C =:= $g) or (C =:= $h) or (C =:= $i) or (C =:= $j) or
(C =:= $k) or (C =:= $l) or (C =:= $m) or (C =:= $n) or (C =:= $o) or
(C =:= $p) or (C =:= $q) or (C =:= $r) or (C =:= $s) or (C =:= $t) or
(C =:= $u) or (C =:= $v) or (C =:= $w) or (C =:= $x) or (C =:= $y) or
(C =:= $z) or
(C =:= $A) or (C =:= $B) or (C =:= $C) or (C =:= $D) or (C =:= $E) or
(C =:= $F) or (C =:= $G) or (C =:= $H) or (C =:= $I) or (C =:= $J) or
(C =:= $K) or (C =:= $L) or (C =:= $M) or (C =:= $N) or (C =:= $O) or
(C =:= $P) or (C =:= $Q) or (C =:= $R) or (C =:= $S) or (C =:= $T) or
(C =:= $U) or (C =:= $V) or (C =:= $W) or (C =:= $X) or (C =:= $Y) or
(C =:= $Z)
).
-define(IS_ALPHANUM(C), ?IS_ALPHA(C) or ?IS_DIGIT(C)).
-define(IS_CHAR(C), C > 0, C < 128).
-define(IS_DIGIT(C),
(C =:= $0) or (C =:= $1) or (C =:= $2) or (C =:= $3) or (C =:= $4) or
(C =:= $5) or (C =:= $6) or (C =:= $7) or (C =:= $8) or (C =:= $9)).
-define(IS_ETAGC(C), C =:= 16#21; C >= 16#23, C =/= 16#7f).
-define(IS_HEX(C),
?IS_DIGIT(C) or
(C =:= $a) or (C =:= $b) or (C =:= $c) or
(C =:= $d) or (C =:= $e) or (C =:= $f) or
(C =:= $A) or (C =:= $B) or (C =:= $C) or
(C =:= $D) or (C =:= $E) or (C =:= $F)).
-define(IS_LHEX(C),
?IS_DIGIT(C) or
(C =:= $a) or (C =:= $b) or (C =:= $c) or
(C =:= $d) or (C =:= $e) or (C =:= $f)).
-define(IS_TOKEN(C),
?IS_ALPHA(C) or ?IS_DIGIT(C) or
(C =:= $!) or (C =:= $#) or (C =:= $$) or (C =:= $%) or (C =:= $&) or
(C =:= $') or (C =:= $*) or (C =:= $+) or (C =:= $-) or (C =:= $.) or
(C =:= $^) or (C =:= $_) or (C =:= $`) or (C =:= $|) or (C =:= $~)).
-define(IS_TOKEN68(C),
?IS_ALPHA(C) or ?IS_DIGIT(C) or
(C =:= $-) or (C =:= $.) or (C =:= $_) or
(C =:= $~) or (C =:= $+) or (C =:= $/)).
-define(IS_URI_UNRESERVED(C),
?IS_ALPHA(C) or ?IS_DIGIT(C) or
(C =:= $-) or (C =:= $.) or (C =:= $_) or (C =:= $~)).
-define(IS_URI_GEN_DELIMS(C),
(C =:= $:) or (C =:= $/) or (C =:= $?) or (C =:= $#) or
(C =:= $[) or (C =:= $]) or (C =:= $@)).
-define(IS_URI_SUB_DELIMS(C),
(C =:= $!) or (C =:= $$) or (C =:= $&) or (C =:= $') or
(C =:= $() or (C =:= $)) or (C =:= $*) or (C =:= $+) or
(C =:= $,) or (C =:= $;) or (C =:= $=)).
-define(IS_VCHAR(C), C =:= $\t; C > 31, C < 127).
-define(IS_VCHAR_OBS(C), C =:= $\t; C > 31, C =/= 127).
-define(IS_WS(C), (C =:= $\s) or (C =:= $\t)).
-define(IS_WS_COMMA(C), ?IS_WS(C) or (C =:= $,)).
-endif.

+ 18
- 0
include/elli.hrl Vedi File

@ -0,0 +1,18 @@
-record(req, {method :: elli:http_method(),
scheme :: undefined | binary(),
host :: undefined | binary(),
port :: undefined | 1..65535,
path :: [binary()],
args :: [{binary(), any()}],
raw_path :: binary(),
version :: elli_http:version(),
headers :: elli:headers(),
original_headers :: elli:headers(),
body :: elli:body(),
pid :: pid(),
socket :: undefined | elli_tcp:socket(),
callback :: elli_handler:callback()
}).
-define(EXAMPLE_CONF, [{callback, elli_example_callback},
{callback_args, []}]).

+ 4
- 0
src/eWSrv.erl Vedi File

@ -0,0 +1,4 @@
-module(eWSrv).
%% API
-export([]).

+ 0
- 9
src/eWSrv_sup.erl Vedi File

@ -11,15 +11,6 @@
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
%% sup_flags() = #{strategy => strategy(), % optional
%% intensity => non_neg_integer(), % optional
%% period => pos_integer()} % optional
%% child_spec() = #{id => child_id(), % mandatory
%% start => mfargs(), % mandatory
%% restart => restart(), % optional
%% shutdown => shutdown(), % optional
%% type => worker(), % optional
%% modules => modules()} % optional
init([]) ->
SupFlags = #{strategy => one_for_all,
intensity => 0,

+ 52
- 0
src/test/elli_handover_tests.erl Vedi File

@ -0,0 +1,52 @@
-module(elli_handover_tests).
-include_lib("eunit/include/eunit.hrl").
-include("elli_test.hrl").
elli_test_() ->
{setup,
fun setup/0, fun teardown/1,
[
?_test(hello_world()),
?_test(echo())
]}.
setup() ->
application:start(crypto),
application:start(public_key),
application:start(ssl),
{ok, _} = application:ensure_all_started(hackney),
Config = [
{mods, [
{elli_example_callback_handover, []}
]}
],
{ok, P} = elli:start_link([{callback, elli_middleware},
{callback_args, Config},
{port, 3003}]),
unlink(P),
[P].
teardown(Pids) ->
[elli:stop(P) || P <- Pids].
%%
%% INTEGRATION TESTS
%% Uses hackney to actually call Elli over the network
%%
hello_world() ->
Response = hackney:get("http://localhost:3003/hello/world"),
?assertMatch(200, status(Response)),
?assertMatch([{<<"Connection">>, <<"close">>},
{<<"Content-Length">>, <<"12">>}], headers(Response)),
?assertMatch(<<"Hello World!">>, body(Response)).
echo() ->
Response = hackney:get("http://localhost:3003/hello?name=knut"),
?assertMatch(200, status(Response)),
?assertMatch(<<"Hello knut">>, body(Response)).

+ 27
- 0
src/test/elli_http_tests.erl Vedi File

@ -0,0 +1,27 @@
-module(elli_http_tests).
-include_lib("eunit/include/eunit.hrl").
%% UNIT TESTS
chunk_loop_test_() ->
fun () ->
Here = self(),
Pid = spawn_link(chunk_loop_wrapper(Here)),
Pid ! {tcp_closed, some_socket},
Message = receive_message(),
?assertMatch({error, client_closed}, Message)
end.
chunk_loop_wrapper(Here) ->
fun () ->
Result = elli_http:chunk_loop({some_type, some_socket}),
Here ! Result,
ok
end.
receive_message() ->
receive
X -> X
after
1 -> fail
end.

+ 34
- 0
src/test/elli_metrics_middleware.erl Vedi File

@ -0,0 +1,34 @@
-module(elli_metrics_middleware).
-export([init/2, preprocess/2, handle/2, postprocess/3, handle_event/3]).
-behaviour(elli_handler).
%%
%% ELLI
%%
init(_Req, _Args) ->
ignore.
preprocess(Req, _Args) ->
Req.
handle(_Req, _Args) ->
ignore.
postprocess(_Req, Res, _Args) ->
Res.
%%
%% ELLI EVENT CALLBACKS
%%
handle_event(request_complete, [_Req,_C,_Hs,_B, {Timings, Sizes}], _) ->
ets:insert(elli_stat_table, {timings, Timings}),
ets:insert(elli_stat_table, {sizes, Sizes});
handle_event(chunk_complete, [_Req,_C,_Hs,_B, {Timings, Sizes}], _) ->
ets:insert(elli_stat_table, {timings, Timings}),
ets:insert(elli_stat_table, {sizes, Sizes});
handle_event(_Event, _Data, _Args) ->
ok.

+ 102
- 0
src/test/elli_middleware_tests.erl Vedi File

@ -0,0 +1,102 @@
-module(elli_middleware_tests).
-include_lib("eunit/include/eunit.hrl").
-include("elli_test.hrl").
elli_test_() ->
{setup,
fun setup/0, fun teardown/1,
[
?_test(hello_world()),
?_test(short_circuit()),
?_test(compress()),
?_test(no_callbacks())
]}.
%%
%% TESTS
%%
short_circuit() ->
URL = "http://localhost:3002/middleware/short-circuit",
Response = hackney:get(URL),
?assertMatch(<<"short circuit!">>, body(Response)).
hello_world() ->
URL = "http://localhost:3002/hello/world",
Response = hackney:get(URL),
?assertMatch(<<"Hello World!">>, body(Response)).
compress() ->
Url = "http://localhost:3002/compressed",
Headers = [{<<"Accept-Encoding">>, <<"gzip">>}],
Response = hackney:get(Url, Headers),
?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>},
{<<"Content-Encoding">>, <<"gzip">>},
{<<"Content-Length">>, <<"41">>}],
headers(Response)),
?assertEqual(binary:copy(<<"Hello World!">>, 86),
zlib:gunzip(body(Response))),
Response1 = hackney:get("http://localhost:3002/compressed"),
?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>},
{<<"Content-Length">>, <<"1032">>}],
headers(Response1)),
?assertEqual(iolist_to_binary(lists:duplicate(86, "Hello World!")),
body(Response1)),
Url2 = "http://localhost:3002/compressed-io_list",
Headers2 = [{<<"Accept-Encoding">>, <<"gzip">>}],
Response2 = hackney:get(Url2, Headers2),
?assertMatch(200, status(Response2)),
?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>},
{<<"Content-Encoding">>, <<"gzip">>},
{<<"Content-Length">>, <<"41">>}],
headers(Response2)),
?assertEqual(binary:copy(<<"Hello World!">>, 86),
zlib:gunzip(body(Response2))),
Response3 = hackney:request("http://localhost:3002/compressed-io_list"),
?assertMatch(200, status(Response3)),
?assertHeadersEqual([{<<"Connection">>, <<"Keep-Alive">>},
{<<"Content-Length">>, <<"1032">>}],
headers(Response3)),
?assertEqual(iolist_to_binary(lists:duplicate(86, "Hello World!")),
body(Response3)).
no_callbacks() ->
Response = hackney:get("http://localhost:3004/whatever"),
?assertMatch(404, status(Response)),
?assertMatch(<<"Not Found">>, body(Response)).
%%
%% HELPERS
%%
setup() ->
application:start(crypto),
application:start(public_key),
application:start(ssl),
{ok, _} = application:ensure_all_started(hackney),
Config = [
{mods, [
{elli_access_log, [{name, elli_syslog},
{ip, "127.0.0.1"},
{port, 514}]},
{elli_example_middleware, []},
{elli_middleware_compress, []},
{elli_example_callback, []}
]}
],
{ok, P1} = elli:start_link([{callback, elli_middleware},
{callback_args, Config},
{port, 3002}]),
unlink(P1),
{ok, P2} = elli:start_link([{callback, elli_middleware},
{callback_args, [{mods, []}]},
{port, 3004}]),
unlink(P2),
[P1, P2].
teardown(Pids) ->
[elli:stop(P) || P <- Pids].

+ 124
- 0
src/test/elli_ssl_tests.erl Vedi File

@ -0,0 +1,124 @@
-module(elli_ssl_tests).
-include_lib("eunit/include/eunit.hrl").
-include("elli_test.hrl").
-define(README, "README.md").
elli_ssl_test_() ->
{setup,
fun setup/0, fun teardown/1,
[{foreach,
fun init_stats/0, fun clear_stats/1,
[
?_test(hello_world()),
?_test(chunked()),
?_test(sendfile()),
?_test(acceptor_leak_regression())
]}
]}.
get_size_value(Key) ->
[{sizes, Sizes}] = ets:lookup(elli_stat_table, sizes),
proplists:get_value(Key, Sizes).
get_timing_value(Key) ->
[{timings, Timings}] = ets:lookup(elli_stat_table, timings),
proplists:get_value(Key, Timings).
%%% Tests
hello_world() ->
Response = hackney:get("https://localhost:3443/hello/world",
[], <<>>, [insecure]),
?assertMatch(200, status(Response)),
?assertMatch({ok, 200, _, _}, Response).
chunked() ->
Expected = <<"chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1">>,
Response = hackney:get("https://localhost:3443/chunked", [], <<>>, [insecure]),
?assertMatch(200, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-type">>, <<"text/event-stream">>},
{<<"transfer-encoding">>,<<"chunked">>}], headers(Response)),
?assertMatch(Expected, body(Response)).
sendfile() ->
Response = hackney:get("https://localhost:3443/sendfile", [], <<>>, [insecure]),
F = ?README,
{ok, Expected} = file:read_file(F),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, ?I2B(size(Expected))}],
headers(Response)),
?assertEqual(Expected, body(Response)),
%% sizes
?assertEqual(size(Expected), get_size_value(file)),
?assertMatch(65, get_size_value(resp_headers)),
%% timings
?assertNotMatch(undefined, get_timing_value(request_start)),
?assertNotMatch(undefined, get_timing_value(headers_start)),
?assertNotMatch(undefined, get_timing_value(headers_end)),
?assertNotMatch(undefined, get_timing_value(body_start)),
?assertNotMatch(undefined, get_timing_value(body_end)),
?assertNotMatch(undefined, get_timing_value(user_start)),
?assertNotMatch(undefined, get_timing_value(user_end)),
?assertNotMatch(undefined, get_timing_value(send_start)),
?assertNotMatch(undefined, get_timing_value(send_end)),
?assertNotMatch(undefined, get_timing_value(request_end)).
acceptor_leak_regression() ->
{ok, Before} = elli:get_acceptors(elli_under_test),
Opts = [{verify, verify_peer},
{verify_fun, {fun(_,_) -> {fail, 23} end, []}},
{reuse_sessions, false}],
{error, _} = ssl:connect("localhost", 3443, Opts),
{ok, After} = elli:get_acceptors(elli_under_test),
?assertEqual(length(Before), length(After)).
%%% Internal helpers
setup() ->
application:start(asn1),
application:start(crypto),
application:start(public_key),
application:start(ssl),
{ok, _} = application:ensure_all_started(hackney),
EbinDir = filename:dirname(code:which(?MODULE)),
CertDir = filename:join([EbinDir, "..", "test"]),
CertFile = filename:join(CertDir, "server_cert.pem"),
KeyFile = filename:join(CertDir, "server_key.pem"),
Config = [
{mods, [
{elli_metrics_middleware, []},
{elli_middleware_compress, []},
{elli_example_callback, []}
]}
],
{ok, P} = elli:start_link([
{port, 3443},
ssl,
{keyfile, KeyFile},
{certfile, CertFile},
{callback, elli_middleware},
{callback_args, Config}
]),
unlink(P),
erlang:register(elli_under_test, P),
[P].
teardown(Pids) ->
[elli:stop(P) || P <- Pids],
application:stop(ssl),
application:stop(public_key),
application:stop(crypto).
init_stats() ->
ets:new(elli_stat_table, [set, named_table, public]).
clear_stats(_) ->
ets:delete(elli_stat_table).

+ 21
- 0
src/test/elli_test.hrl Vedi File

@ -0,0 +1,21 @@
-define(I2B(I), list_to_binary(integer_to_list(I))).
-define(assertHeadersEqual(H1, H2), ?assertEqual(lists:sort([{string:casefold(K), V} || {K, V} <- H1]),
lists:sort([{string:casefold(K), V} || {K, V} <- H2]))).
status({ok, Status, _Headers, _ClientRef}) ->
Status;
status({ok, Status, _Headers}) ->
Status.
body({ok, _Status, _Headers, ClientRef}) ->
{ok, Body} = hackney:body(ClientRef),
Body.
headers({ok, _Status, Headers, _ClientRef}) ->
lists:sort(Headers);
headers({ok, _Status, Headers}) ->
lists:sort(Headers).

+ 747
- 0
src/test/elli_tests.erl Vedi File

@ -0,0 +1,747 @@
-module(elli_tests).
-include_lib("eunit/include/eunit.hrl").
-include("elli.hrl").
-include("elli_test.hrl").
-define(README, "README.md").
-define(VTB(T1, T2, LB, UB),
time_diff_to_micro_seconds(T1, T2) >= LB andalso
time_diff_to_micro_seconds(T1, T2) =< UB).
-ifdef(OTP_RELEASE).
-include_lib("kernel/include/logger.hrl").
-else.
-define(LOG_ERROR(Str), error_logger:error_msg(Str)).
-define(LOG_ERROR(Format,Data), error_logger:error_msg(Format, Data)).
-define(LOG_INFO(Format,Data), error_logger:info_msg(Format, Data)).
-endif.
time_diff_to_micro_seconds(T1, T2) ->
erlang:convert_time_unit(
get_timing_value(T2) -
get_timing_value(T1),
native,
micro_seconds).
elli_test_() ->
{setup,
fun setup/0, fun teardown/1,
[{foreach,
fun init_stats/0, fun clear_stats/1,
[?_test(hello_world()),
?_test(keep_alive_timings()),
?_test(not_found()),
?_test(crash()),
?_test(invalid_return()),
?_test(no_compress()),
?_test(gzip()),
?_test(deflate()),
?_test(exception_flow()),
?_test(hello_iolist()),
?_test(accept_content_type()),
?_test(user_connection()),
?_test(get_args()),
?_test(decoded_get_args()),
?_test(decoded_get_args_list()),
?_test(post_args()),
?_test(shorthand()),
?_test(ip()),
?_test(found()),
?_test(too_many_headers()),
?_test(too_big_body()),
?_test(way_too_big_body()),
?_test(bad_request_line()),
?_test(content_length()),
?_test(user_content_length()),
?_test(headers()),
?_test(chunked()),
?_test(sendfile()),
?_test(send_no_file()),
?_test(sendfile_error()),
?_test(sendfile_range()),
?_test(slow_client()),
?_test(post_pipeline()),
?_test(get_pipeline()),
?_test(head()),
?_test(no_body()),
?_test(sends_continue())
]}
]}.
get_timing_value(Key) ->
[{timings, Timings}] = ets:lookup(elli_stat_table, timings),
proplists:get_value(Key, Timings).
get_size_value(Key) ->
[{sizes, Sizes}] = ets:lookup(elli_stat_table, sizes),
proplists:get_value(Key, Sizes).
setup() ->
application:start(crypto),
application:start(public_key),
application:start(ssl),
{ok, _} = application:ensure_all_started(hackney),
Config = [
{mods, [
{elli_metrics_middleware, []},
{elli_middleware_compress, []},
{elli_example_callback, []}
]}
],
{ok, P} = elli:start_link([{callback, elli_middleware},
{callback_args, Config},
{port, 3001}]),
unlink(P),
[P].
teardown(Pids) ->
[elli:stop(P) || P <- Pids].
init_stats() ->
ets:new(elli_stat_table, [set, named_table, public]).
clear_stats(_) ->
ets:delete(elli_stat_table).
accessors_test_() ->
RawPath = <<"/foo/bar">>,
Headers = [{<<"content-type">>, <<"application/x-www-form-urlencoded">>}],
Method = 'POST',
Body = <<"name=knut%3D">>,
Name = <<"knut=">>,
Req1 = #req{raw_path = RawPath,
original_headers = Headers,
headers = Headers,
method = Method,
body = Body},
Args = [{<<"name">>, Name}],
Req2 = #req{original_headers = Headers, headers = Headers, args = Args, body = <<>>},
[
%% POST /foo/bar
?_assertMatch(RawPath, elli_request:raw_path(Req1)),
?_assertMatch(Headers, elli_request:headers(Req1)),
?_assertMatch(Method, elli_request:method(Req1)),
?_assertMatch(Body, elli_request:body(Req1)),
?_assertMatch(Args, elli_request:post_args_decoded(Req1)),
?_assertMatch(undefined, elli_request:post_arg(<<"foo">>, Req1)),
?_assertMatch(undefined, elli_request:post_arg_decoded(<<"foo">>, Req1)),
?_assertMatch(Name, elli_request:post_arg_decoded(<<"name">>, Req1)),
%% GET /foo/bar
?_assertMatch(Headers, elli_request:headers(Req2)),
?_assertMatch(Args, elli_request:get_args(Req2)),
?_assertMatch(undefined, elli_request:get_arg_decoded(<<"foo">>, Req2)),
?_assertMatch(Name, elli_request:get_arg_decoded(<<"name">>, Req2)),
?_assertMatch([], elli_request:post_args(Req2)),
?_assertMatch({error, not_supported}, elli_request:chunk_ref(#req{}))
].
%%% Integration tests
%%% Use hackney to actually call Elli over the network.
hello_world() ->
Response = hackney:get("http://localhost:3001/hello/world"),
?assertMatch(200, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"12">>}], headers(Response)),
?assertMatch(<<"Hello World!">>, body(Response)),
%% sizes
?assertMatch(63, get_size_value(resp_headers)),
?assertMatch(12, get_size_value(resp_body)),
?assertMatch(undefined, get_size_value(req_body)),
%% timings
?assertNotMatch(undefined, get_timing_value(request_start)),
?assertNotMatch(undefined, get_timing_value(headers_start)),
?assertNotMatch(undefined, get_timing_value(headers_end)),
?assertNotMatch(undefined, get_timing_value(body_start)),
?assertNotMatch(undefined, get_timing_value(body_end)),
?assertNotMatch(undefined, get_timing_value(user_start)),
?assertNotMatch(undefined, get_timing_value(user_end)),
?assertNotMatch(undefined, get_timing_value(send_start)),
?assertNotMatch(undefined, get_timing_value(send_end)),
?assertNotMatch(undefined, get_timing_value(request_end)),
%% check timings
?assert(?VTB(request_start, request_end, 1000000, 1200000)),
?assert(?VTB(headers_start, headers_end, 1, 100)),
?assert(?VTB(body_start, body_end, 1, 100)),
?assert(?VTB(user_start, user_end, 1000000, 1200000)),
?assert(?VTB(send_start, send_end, 1, 2000)).
keep_alive_timings() ->
Transport = hackney_tcp,
Host = <<"localhost">>,
Port = 3001,
Options = [],
{ok, ConnRef} = hackney:connect(Transport, Host, Port, Options),
ReqBody = <<>>,
ReqHeaders = [],
ReqPath = <<"/hello/world">>,
ReqMethod = get,
Req = {ReqMethod, ReqPath, ReqHeaders, ReqBody},
{ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req),
keep_alive_timings(Status, Headers, HCRef),
%% pause between keep-alive requests,
%% request_start is a timestamp of
%% the first bytes of the second request
timer:sleep(1000),
{ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req),
keep_alive_timings(Status, Headers, HCRef),
hackney:close(ConnRef).
keep_alive_timings(Status, Headers, HCRef) ->
?assertMatch(200, Status),
?assertHeadersEqual([{<<"connection">>,<<"Keep-Alive">>},
{<<"content-length">>,<<"12">>}], Headers),
?assertMatch({ok, <<"Hello World!">>}, hackney:body(HCRef)),
%% sizes
?assertMatch(63, get_size_value(resp_headers)),
?assertMatch(12, get_size_value(resp_body)),
%% timings
?assertNotMatch(undefined, get_timing_value(request_start)),
?assertNotMatch(undefined, get_timing_value(headers_start)),
?assertNotMatch(undefined, get_timing_value(headers_end)),
?assertNotMatch(undefined, get_timing_value(body_start)),
?assertNotMatch(undefined, get_timing_value(body_end)),
?assertNotMatch(undefined, get_timing_value(user_start)),
?assertNotMatch(undefined, get_timing_value(user_end)),
?assertNotMatch(undefined, get_timing_value(send_start)),
?assertNotMatch(undefined, get_timing_value(send_end)),
?assertNotMatch(undefined, get_timing_value(request_end)),
%% check timings
?assert(?VTB(request_start, request_end, 1000000, 1200000)),
?assert(?VTB(headers_start, headers_end, 1, 100)),
?assert(?VTB(body_start, body_end, 1, 100)),
?assert(?VTB(user_start, user_end, 1000000, 1200000)),
?assert(?VTB(send_start, send_end, 1, 2000)).
not_found() ->
Response = hackney:get("http://localhost:3001/foobarbaz"),
?assertMatch(404, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"9">>}], headers(Response)),
?assertMatch(<<"Not Found">>, body(Response)).
crash() ->
Response = hackney:get("http://localhost:3001/crash"),
?assertMatch(500, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"21">>}], headers(Response)),
?assertMatch(<<"Internal server error">>, body(Response)).
invalid_return() ->
%% Elli should return 500 for handlers returning bogus responses.
Response = hackney:get("http://localhost:3001/invalid_return"),
?assertMatch(500, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"21">>}], headers(Response)),
?assertMatch(<<"Internal server error">>, body(Response)).
no_compress() ->
Response = hackney:get("http://localhost:3001/compressed"),
?assertMatch(200, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"1032">>}], headers(Response)),
?assertEqual(binary:copy(<<"Hello World!">>, 86),
body(Response)).
compress(Encoding, Length) ->
Response = hackney:get("http://localhost:3001/compressed",
[{<<"Accept-Encoding">>, Encoding}]),
?assertMatch(200, status(Response)),
?assertHeadersEqual([{<<"Content-Encoding">>, Encoding},
{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, Length}], headers(Response)),
?assertEqual(binary:copy(<<"Hello World!">>, 86),
uncompress(Encoding, body(Response))).
uncompress(<<"gzip">>, Data) -> zlib:gunzip(Data);
uncompress(<<"deflate">>, Data) -> zlib:uncompress(Data).
gzip() -> compress(<<"gzip">>, <<"41">>).
deflate() -> compress(<<"deflate">>, <<"29">>).
exception_flow() ->
Response = hackney:get("http://localhost:3001/403"),
?assertMatch(403, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"9">>}], headers(Response)),
?assertMatch(<<"Forbidden">>, body(Response)).
hello_iolist() ->
Url = "http://localhost:3001/hello/iolist?name=knut",
Response = hackney:get(Url),
?assertMatch(<<"Hello knut">>, body(Response)).
accept_content_type() ->
Json = hackney:get("http://localhost:3001/type?name=knut",
[{"Accept", "application/json"}]),
?assertMatch(<<"{\"name\" : \"knut\"}">>, body(Json)),
Text = hackney:get("http://localhost:3001/type?name=knut",
[{"Accept", "text/plain"}]),
?assertMatch(<<"name: knut">>, body(Text)).
user_connection() ->
Url = "http://localhost:3001/user/defined/behaviour",
Response = hackney:get(Url),
?assertMatch(304, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"close">>},
{<<"content-length">>, <<"123">>}], headers(Response)),
?assertMatch(<<>>, body(Response)).
get_args() ->
Response = hackney:get("http://localhost:3001/hello?name=knut"),
?assertMatch(<<"Hello knut">>, body(Response)).
decoded_get_args() ->
Url = "http://localhost:3001/decoded-hello?name=knut%3D",
Response = hackney:get(Url),
?assertMatch(<<"Hello knut=">>, body(Response)).
decoded_get_args_list() ->
Url = "http://localhost:3001/decoded-list?name=knut%3D&foo",
Response = hackney:get(Url),
?assertMatch(<<"Hello knut=">>, body(Response)).
post_args() ->
Body = <<"name=foo&city=New%20York">>,
ContentType = <<"application/x-www-form-urlencoded">>,
Response = hackney:post("http://localhost:3001/hello",
[{<<"content-type">>, ContentType}],
Body),
?assertMatch(200, status(Response)),
?assertMatch(<<"Hello foo of New York">>, body(Response)).
shorthand() ->
Response = hackney:get("http://localhost:3001/shorthand"),
?assertMatch(200, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"5">>}], headers(Response)),
?assertMatch(<<"hello">>, body(Response)).
ip() ->
Response = hackney:get("http://localhost:3001/ip"),
?assertMatch(200, status(Response)),
?assertMatch(<<"127.0.0.1">>, body(Response)).
found() ->
Response = hackney:get("http://localhost:3001/302"),
?assertMatch(302, status(Response)),
?assertHeadersEqual([{<<"Location">>, <<"/hello/world">>},
{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"0">>}], headers(Response)),
?assertMatch(<<>>, body(Response)).
too_many_headers() ->
Headers = lists:duplicate(100, {<<"X-Foo">>, <<"Bar">>}),
Response = hackney:get("http://localhost:3001/foo", Headers),
?assertMatch(400, status(Response)).
too_big_body() ->
Body = binary:copy(<<"x">>, (1024 * 1000) + 1),
Response = hackney:post("http://localhost:3001/foo", [], Body),
?assertMatch(413, status(Response)).
way_too_big_body() ->
Body = binary:copy(<<"x">>, (1024 * 2000) + 1),
?assertMatch({error, closed},
hackney:post("http://localhost:3001/foo", [], Body)).
bad_request_line() ->
{ok, Socket} = gen_tcp:connect("127.0.0.1", 3001,
[{active, false}, binary]),
Req = <<"FOO BAR /hello HTTP/1.1\r\n">>,
gen_tcp:send(Socket, <<Req/binary, Req/binary>>),
?assertMatch({ok, <<"HTTP/1.1 400 Bad Request\r\n"
"content-length: 11\r\n\r\nBad Request">>},
gen_tcp:recv(Socket, 0)).
content_length() ->
Response = hackney:get("http://localhost:3001/304"),
?assertMatch(304, status(Response)),
?assertHeadersEqual([{<<"Etag">>, <<"foobar">>},
{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"7">>}], headers(Response)),
?assertMatch(<<>>, body(Response)).
user_content_length() ->
Headers = <<"Foo: bar\n\n">>,
Client = start_slow_client(3001, "/user/content-length"),
send(Client, Headers, 128),
?assertMatch({ok, <<"HTTP/1.1 200 OK\r\n"
"connection: Keep-Alive\r\n"
"Content-Length: 123\r\n"
"\r\n"
"foobar">>},
gen_tcp:recv(Client, 0)).
headers() ->
Response = hackney:get("http://localhost:3001/headers.html"),
Headers = headers(Response),
?assert(proplists:is_defined(<<"X-Custom">>, Headers)),
?assertMatch(<<"foobar">>, proplists:get_value(<<"X-Custom">>, Headers)).
chunked() ->
Expected = <<"chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1">>,
Response = hackney:get("http://localhost:3001/chunked"),
?assertMatch(200, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-type">>, <<"text/event-stream">>},
{<<"transfer-encoding">>, <<"chunked">>}], headers(Response)),
?assertMatch(Expected, body(Response)),
%% sizes
?assertMatch(104, get_size_value(resp_headers)),
?assertMatch(111, get_size_value(chunks)),
%% timings
?assertNotMatch(undefined, get_timing_value(request_start)),
?assertNotMatch(undefined, get_timing_value(headers_start)),
?assertNotMatch(undefined, get_timing_value(headers_end)),
?assertNotMatch(undefined, get_timing_value(body_start)),
?assertNotMatch(undefined, get_timing_value(body_end)),
?assertNotMatch(undefined, get_timing_value(user_start)),
?assertNotMatch(undefined, get_timing_value(user_end)),
?assertNotMatch(undefined, get_timing_value(send_start)),
?assertNotMatch(undefined, get_timing_value(send_end)),
?assertNotMatch(undefined, get_timing_value(request_end)).
sendfile() ->
Response = hackney:get("http://localhost:3001/sendfile"),
F = ?README,
{ok, Expected} = file:read_file(F),
?assertMatch(200, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, ?I2B(size(Expected))}],
headers(Response)),
?assertEqual(Expected, body(Response)),
%% sizes
?assertEqual(size(Expected), get_size_value(file)),
?assertMatch(65, get_size_value(resp_headers)),
%% timings
?assertNotMatch(undefined, get_timing_value(request_start)),
?assertNotMatch(undefined, get_timing_value(headers_start)),
?assertNotMatch(undefined, get_timing_value(headers_end)),
?assertNotMatch(undefined, get_timing_value(body_start)),
?assertNotMatch(undefined, get_timing_value(body_end)),
?assertNotMatch(undefined, get_timing_value(user_start)),
?assertNotMatch(undefined, get_timing_value(user_end)),
?assertNotMatch(undefined, get_timing_value(send_start)),
?assertNotMatch(undefined, get_timing_value(send_end)),
?assertNotMatch(undefined, get_timing_value(request_end)).
send_no_file() ->
Response = hackney:get("http://localhost:3001/send_no_file"),
?assertMatch(500, status(Response)),
?assertHeadersEqual([{<<"content-length">>, <<"12">>}],
headers(Response)),
?assertMatch(<<"Server Error">>, body(Response)).
sendfile_error() ->
Response = hackney:get("http://localhost:3001/sendfile/error"),
?assertMatch(500, status(Response)),
?assertHeadersEqual([{<<"content-length">>, <<"12">>}],
headers(Response)),
?assertMatch(<<"Server Error">>, body(Response)).
sendfile_range() ->
Url = "http://localhost:3001/sendfile/range",
Headers = [{"Range", "bytes=300-699"}],
Response = hackney:get(Url, Headers),
F = ?README,
{ok, Fd} = file:open(F, [read, raw, binary]),
{ok, Expected} = file:pread(Fd, 300, 400),
file:close(Fd),
Size = elli_util:file_size(F),
?assertMatch(206, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"400">>},
{<<"Content-Range">>, iolist_to_binary(["bytes 300-699/", integer_to_binary(Size)])}],
headers(Response)),
?assertEqual(Expected, body(Response)).
slow_client() ->
Body = <<"name=foobarbaz">>,
Headers = <<"content-length: ", (?I2B(size(Body)))/binary, "\r\n\r\n">>,
Client = start_slow_client(3001, "/hello"),
send(Client, Headers, 1),
send(Client, Body, size(Body)),
?assertMatch({ok, <<"HTTP/1.1 200 OK\r\n"
"connection: Keep-Alive\r\n"
"content-length: 15\r\n"
"\r\n"
"Hello undefined">>},
gen_tcp:recv(Client, 0)),
%% check timings
?assert(?VTB(request_start, request_end, 30000, 70000)),
?assert(?VTB(headers_start, headers_end, 30000, 70000)),
?assert(?VTB(body_start, body_end, 1, 3000)),
?assert(?VTB(user_start, user_end, 1, 100)),
?assert(?VTB(send_start, send_end, 1, 200)).
post_pipeline() ->
Body = <<"name=elli&city=New%20York">>,
Headers = <<"content-length: ", (?I2B(size(Body)))/binary, "\r\n",
"Content-Type: application/x-www-form-urlencoded", "\r\n",
"\r\n">>,
{ok, Socket} = gen_tcp:connect("127.0.0.1", 3001,
[{active, false}, binary]),
Req = <<"POST /hello HTTP/1.1\r\n",
Headers/binary,
Body/binary>>,
gen_tcp:send(Socket, <<Req/binary, Req/binary>>),
ExpectedResponse = <<"HTTP/1.1 200 OK\r\n"
"connection: Keep-Alive\r\n"
"content-length: 22\r\n"
"\r\n"
"Hello elli of New York">>,
{ok, Res} = gen_tcp:recv(Socket, size(ExpectedResponse) * 2),
Size = size(Body),
?assertMatch(Size, get_size_value(req_body)),
?assertEqual(binary:copy(ExpectedResponse, 2),
Res).
get_pipeline() ->
Headers = <<"User-Agent: sloow\r\n\r\n">>,
Req = <<"GET /hello?name=elli HTTP/1.1\r\n",
Headers/binary>>,
{ok, Socket} = gen_tcp:connect("127.0.0.1", 3001,
[{active, false}, binary]),
gen_tcp:send(Socket, <<Req/binary, Req/binary>>),
ExpectedResponse = <<"HTTP/1.1 200 OK\r\n"
"connection: Keep-Alive\r\n"
"content-length: 10\r\n"
"\r\n"
"Hello elli">>,
{ok, Res} = gen_tcp:recv(Socket, size(ExpectedResponse) * 2),
case binary:copy(ExpectedResponse, 2) =:= Res of
true ->
ok;
false ->
?LOG_INFO("Expected: ~p~nResult: ~p~n",
[binary:copy(ExpectedResponse, 2), Res])
end,
?assertEqual(binary:copy(ExpectedResponse, 2),
Res).
head() ->
Response = hackney:head("http://localhost:3001/head"),
?assertMatch(200, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"20">>}], headers(Response)).
no_body() ->
Response = hackney:get("http://localhost:3001/304"),
?assertMatch(304, status(Response)),
?assertHeadersEqual([{<<"connection">>, <<"Keep-Alive">>},
{<<"content-length">>, <<"7">>},
{<<"Etag">>, <<"foobar">>}], headers(Response)),
?assertMatch(<<>>, body(Response)).
sends_continue() ->
{ok, Socket} = gen_tcp:connect("127.0.0.1", 3001, [{active, false}, binary]),
Body = <<"name=elli&city=New%20York">>,
Length = ?I2B(size(Body)),
Req = <<"POST /hello HTTP/1.1\r\n",
"Host: localhost\r\n",
"Content-Type: application/x-www-form-urlencoded\r\n",
"content-length: ", Length/binary, "\r\n",
"Expect: 100-continue\r\n\r\n">>,
gen_tcp:send(Socket, Req),
?assertMatch({ok, <<"HTTP/1.1 100 Continue\r\n"
"content-length: 0\r\n\r\n">>},
gen_tcp:recv(Socket, 0)),
% Send Result of the body
gen_tcp:send(Socket, Body),
ExpectedResponse = <<"HTTP/1.1 200 OK\r\n"
"connection: Keep-Alive\r\n"
"content-length: 22\r\n"
"\r\n"
"Hello elli of New York">>,
?assertMatch({ok, ExpectedResponse},
gen_tcp:recv(Socket, size(ExpectedResponse))).
%%% Slow client, sending only the specified byte size every millisecond
start_slow_client(Port, Url) ->
case gen_tcp:connect("127.0.0.1", Port, [{active, false}, binary]) of
{ok, Socket} ->
gen_tcp:send(Socket, "GET " ++ Url ++ " HTTP/1.1\r\n"),
Socket;
{error, Reason} ->
throw({slow_client_error, Reason})
end.
send(_Socket, <<>>, _) ->
ok;
send(Socket, B, ChunkSize) ->
{Part, Rest} = case B of
<<P:ChunkSize/binary, R/binary>> -> {P, R};
P -> {P, <<>>}
end,
%%?LOG_INFO("~p~n", [Part]),
gen_tcp:send(Socket, Part),
timer:sleep(1),
send(Socket, Rest, ChunkSize).
%%% Unit tests
body_qs_test() ->
Expected = [{<<"foo">>, <<"bar">>},
{<<"baz">>, <<"bang">>},
{<<"found">>, true}],
Body = <<"foo=bar&baz=bang&found">>,
Headers = [{<<"content-type">>, <<"application/x-www-form-urlencoded">>}],
?assertMatch(Expected, elli_request:body_qs(#req{body = Body,
original_headers = Headers,
headers = Headers})).
to_proplist_test() ->
Req = #req{method = 'GET',
path = [<<"crash">>],
args = [],
version = {1, 1},
raw_path = <<"/crash">>,
original_headers = [{<<"Host">>, <<"localhost:3001">>}],
headers = [{<<"host">>, <<"localhost:3001">>}],
body = <<>>,
pid = self(),
socket = socket,
callback = {mod, []}},
Prop = [{method, 'GET'},
{scheme, undefined},
{host, undefined},
{port, undefined},
{path, [<<"crash">>]},
{args, []},
{raw_path, <<"/crash">>},
{version, {1, 1}},
{headers, [{<<"host">>, <<"localhost:3001">>}]},
{original_headers, [{<<"Host">>, <<"localhost:3001">>}]},
{body, <<>>},
{pid, self()},
{socket, socket},
{callback, {mod, []}}],
?assertEqual(Prop, elli_request:to_proplist(Req)).
is_request_test() ->
?assert(elli_request:is_request(#req{})),
?assertNot(elli_request:is_request({req, foobar})).
query_str_test_() ->
MakeReq = fun(Path) -> #req{raw_path = Path} end,
[
%% For empty query strings, expect `query_str` to return an empty binary.
?_assertMatch(<<>>, elli_request:query_str(MakeReq(<<"/foo">>))),
?_assertMatch(<<>>, elli_request:query_str(MakeReq(<<"/foo?">>))),
%% Otherwise it should return everything to the right hand side of `?`.
?_assertMatch(<<"bar=baz&baz=bang">>,
elli_request:query_str(MakeReq(<<"/foo?bar=baz&baz=bang">>)))
].
get_range_test_() ->
Req = #req{headers = [{<<"range">>,
<<"bytes=0-99 ,500-999 , -800">>}]},
OffsetReq = #req{headers = [{<<"range">>, <<"bytes=200-">>}]},
UndefReq = #req{headers = []},
BadReq = #req{headers = [{<<"range">>, <<"bytes=--99,hallo-world">>}]},
ByteRangeSet = [{bytes, 0, 99}, {bytes, 500, 999}, {suffix, 800}],
[?_assertMatch(ByteRangeSet, elli_request:get_range(Req)),
?_assertMatch([{offset, 200}], elli_request:get_range(OffsetReq)),
?_assertMatch([], elli_request:get_range(UndefReq)),
?_assertMatch(parse_error, elli_request:get_range(BadReq))].
normalize_range_test_() ->
Size = 1000,
Bytes1 = {bytes, 200, 400},
Bytes2 = {bytes, 0, 1000000},
Suffix = {suffix, 303},
Offset = {offset, 42},
Normal = {200, 400},
Set = [{bytes, 0, 999}],
EmptySet = [],
Invalid1 = {bytes, 400, 200},
Invalid2 = {bytes, 1200, 2000},
Invalid3 = {offset, -10},
Invalid4 = {offset, 2000},
Invalid5 = parse_error,
Invalid6 = [{bytes, 0, 100}, {suffix, 42}],
[?_assertMatch({200, 201}, elli_util:normalize_range(Bytes1, Size)),
?_assertMatch({0, Size}, elli_util:normalize_range(Bytes2, Size)),
?_assertEqual({Size - 303, 303}, elli_util:normalize_range(Suffix, Size)),
?_assertEqual({42, Size - 42}, elli_util:normalize_range(Offset, Size)),
?_assertMatch({200, 400}, elli_util:normalize_range(Normal, Size)),
?_assertMatch({0, 1000}, elli_util:normalize_range(Set, Size)),
?_assertMatch(undefined, elli_util:normalize_range(EmptySet, Size)),
?_assertMatch(invalid_range, elli_util:normalize_range(Invalid1, Size)),
?_assertMatch(invalid_range, elli_util:normalize_range(Invalid2, Size)),
?_assertMatch(invalid_range, elli_util:normalize_range(Invalid3, Size)),
?_assertMatch(invalid_range, elli_util:normalize_range(Invalid4, Size)),
?_assertMatch(invalid_range, elli_util:normalize_range(Invalid5, Size)),
?_assertMatch(invalid_range, elli_util:normalize_range(Invalid6, Size))].
encode_range_test() ->
Expected = [<<"bytes ">>,<<"*">>,<<"/">>,<<"42">>],
?assertMatch(Expected, elli_util:encode_range(invalid_range, 42)).
register_test() ->
?assertMatch(undefined, whereis(elli)),
Config = [
{name, {local, elli}},
{callback, elli_middleware},
{callback_args, [{mods, [
elli_example_callback,
elli_metrics_middleware
]}]}
],
{ok, Pid} = elli:start_link(Config),
?assertMatch(Pid, whereis(elli)),
ok.
invalid_callback_test() ->
case catch elli:start_link([{callback, elli}]) of
E ->
?assertMatch(invalid_callback, E)
end.

+ 19
- 0
src/test/server_cert.pem Vedi File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDIDCCAgigAwIBAgIJAJLkNZzERPIUMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
BAMTCWxvY2FsaG9zdDAeFw0xMDAzMTgxOTM5MThaFw0yMDAzMTUxOTM5MThaMBQx
EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAJeUCOZxbmtngF4S5lXckjSDLc+8C+XjMBYBPyy5eKdJY20AQ1s9/hhp3ulI
8pAvl+xVo4wQ+iBSvOzcy248Q+Xi6+zjceF7UNRgoYPgtJjKhdwcHV3mvFFrS/fp
9ggoAChaJQWDO1OCfUgTWXImhkw+vcDR11OVMAJ/h73dqzJPI9mfq44PTTHfYtgr
v4LAQAOlhXIAa2B+a6PlF6sqDqJaW5jLTcERjsBwnRhUGi7JevQzkejujX/vdA+N
jRBjKH/KLU5h3Q7wUchvIez0PXWVTCnZjpA9aR4m7YV05nKQfxtGd71czYDYk+j8
hd005jetT4ir7JkAWValBybJVksCAwEAAaN1MHMwHQYDVR0OBBYEFJl9s51SnjJt
V/wgKWqV5Q6jnv1ZMEQGA1UdIwQ9MDuAFJl9s51SnjJtV/wgKWqV5Q6jnv1ZoRik
FjAUMRIwEAYDVQQDEwlsb2NhbGhvc3SCCQCS5DWcxETyFDAMBgNVHRMEBTADAQH/
MA0GCSqGSIb3DQEBBQUAA4IBAQB2ldLeLCc+lxK5i0EZquLamMBJwDIjGpT0JMP9
b4XQOK2JABIu54BQIZhwcjk3FDJz/uOW5vm8k1kYni8FCjNZAaRZzCUfiUYTbTKL
Rq9LuIAODyP2dnTqyKaQOOJHvrx9MRZ3XVecXPS0Tib4aO57vCaAbIkmhtYpTWmw
e3t8CAIDVtgvjR6Se0a1JA4LktR7hBu22tDImvCSJn1nVAaHpani6iPBPPdMuMsP
TBoeQfj8VpqBUjCStqJGa8ytjDFX73YaxV2mgrtGwPNme1x3YNRR11yTu7tksyMO
GrmgxNriqYRchBhNEf72AKF0LR1ByKwfbDB9rIsV00HtCgOp
-----END CERTIFICATE-----

+ 27
- 0
src/test/server_key.pem Vedi File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAl5QI5nFua2eAXhLmVdySNIMtz7wL5eMwFgE/LLl4p0ljbQBD
Wz3+GGne6UjykC+X7FWjjBD6IFK87NzLbjxD5eLr7ONx4XtQ1GChg+C0mMqF3Bwd
Xea8UWtL9+n2CCgAKFolBYM7U4J9SBNZciaGTD69wNHXU5UwAn+Hvd2rMk8j2Z+r
jg9NMd9i2Cu/gsBAA6WFcgBrYH5ro+UXqyoOolpbmMtNwRGOwHCdGFQaLsl69DOR
6O6Nf+90D42NEGMof8otTmHdDvBRyG8h7PQ9dZVMKdmOkD1pHibthXTmcpB/G0Z3
vVzNgNiT6PyF3TTmN61PiKvsmQBZVqUHJslWSwIDAQABAoIBACI8Ky5xHDFh9RpK
Rn/KC7OUlTpADKflgizWJ0Cgu2F9L9mkn5HyFHvLHa+u7CootbWJOiEejH/UcBtH
WyMQtX0snYCpdkUpJv5wvMoebGu+AjHOn8tfm9T/2O6rhwgckLyMb6QpGbMo28b1
p9QiY17BJPZx7qJQJcHKsAvwDwSThlb7MFmWf42LYWlzybpeYQvwpd+UY4I0WXLu
/dqJIS9Npq+5Y5vbo2kAEAssb2hSCvhCfHmwFdKmBzlvgOn4qxgZ1iHQgfKI6Z3Y
J0573ZgOVTuacn+lewtdg5AaHFcl/zIYEr9SNqRoPNGbPliuv6k6N2EYcufWL5lR
sCmmmHECgYEAxm+7OpepGr++K3+O1e1MUhD7vSPkKJrCzNtUxbOi2NWj3FFUSPRU
adWhuxvUnZgTcgM1+KuQ0fB2VmxXe9IDcrSFS7PKFGtd2kMs/5mBw4UgDZkOQh+q
kDiBEV3HYYJWRq0w3NQ/9Iy1jxxdENHtGmG9aqamHxNtuO608wGW2S8CgYEAw4yG
ZyAic0Q/U9V2OHI0MLxLCzuQz17C2wRT1+hBywNZuil5YeTuIt2I46jro6mJmWI2
fH4S/geSZzg2RNOIZ28+aK79ab2jWBmMnvFCvaru+odAuser4N9pfAlHZvY0pT+S
1zYX3f44ygiio+oosabLC5nWI0zB2gG8pwaJlaUCgYEAgr7poRB+ZlaCCY0RYtjo
mYYBKD02vp5BzdKSB3V1zeLuBWM84pjB6b3Nw0fyDig+X7fH3uHEGN+USRs3hSj6
BqD01s1OT6fyfbYXNw5A1r+nP+5h26Wbr0zblcKxdQj4qbbBZC8hOJNhqTqqA0Qe
MmzF7jiBaiZV/Cyj4x1f9BcCgYEAhjL6SeuTuOctTqs/5pz5lDikh6DpUGcH8qaV
o6aRAHHcMhYkZzpk8yh1uUdD7516APmVyvn6rrsjjhLVq4ZAJjwB6HWvE9JBN0TR
bILF+sREHUqU8Zn2Ku0nxyfXCKIOnxlx/J/y4TaGYqBqfXNFWiXNUrjQbIlQv/xR
K48g/MECgYBZdQlYbMSDmfPCC5cxkdjrkmAl0EgV051PWAi4wR+hLxIMRjHBvAk7
IweobkFvT4TICulgroLkYcSa5eOZGxB/DHqcQCbWj3reFV0VpzmTDoFKG54sqBRl
vVntGt0pfA40fF17VoS7riAdHF53ippTtsovHEsg5tq5NrBl5uKm2g==
-----END RSA PRIVATE KEY-----

+ 0
- 81
src/wsLib/cow_base64url.erl Vedi File

@ -1,81 +0,0 @@
%% Copyright (c) 2017-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% This module implements "base64url" following the algorithm
%% found in Appendix C of RFC7515. The option #{padding => false}
%% must be given to reproduce this variant exactly. The default
%% will leave the padding characters.
-module(cow_base64url).
-export([decode/1]).
-export([decode/2]).
-export([encode/1]).
-export([encode/2]).
-ifdef(TEST).
-include_lib("proper/include/proper.hrl").
-endif.
decode(Enc) ->
decode(Enc, #{}).
decode(Enc0, Opts) ->
Enc1 = <<<<case C of
$- -> $+;
$_ -> $/;
_ -> C
end>> || <<C>> <= Enc0>>,
Enc = case Opts of
#{padding := false} ->
case byte_size(Enc1) rem 4 of
0 -> Enc1;
2 -> <<Enc1/binary, "==">>;
3 -> <<Enc1/binary, "=">>
end;
_ ->
Enc1
end,
base64:decode(Enc).
encode(Dec) ->
encode(Dec, #{}).
encode(Dec, Opts) ->
encode(base64:encode(Dec), Opts, <<>>).
encode(<<$+, R/bits>>, Opts, Acc) -> encode(R, Opts, <<Acc/binary, $->>);
encode(<<$/, R/bits>>, Opts, Acc) -> encode(R, Opts, <<Acc/binary, $_>>);
encode(<<$=, _/bits>>, #{padding := false}, Acc) -> Acc;
encode(<<C, R/bits>>, Opts, Acc) -> encode(R, Opts, <<Acc/binary, C>>);
encode(<<>>, _, Acc) -> Acc.
-ifdef(TEST).
rfc7515_test() ->
Dec = <<3, 236, 255, 224, 193>>,
Enc = <<"A-z_4ME">>,
Pad = <<"A-z_4ME=">>,
Dec = decode(<<Enc/binary, $=>>),
Dec = decode(Enc, #{padding => false}),
Pad = encode(Dec),
Enc = encode(Dec, #{padding => false}),
ok.
prop_identity() ->
?FORALL(B, binary(), B =:= decode(encode(B))).
prop_identity_no_padding() ->
?FORALL(B, binary(), B =:= decode(encode(B, #{padding => false}), #{padding => false})).
-endif.

+ 0
- 428
src/wsLib/cow_cookie.erl Vedi File

@ -1,428 +0,0 @@
%% Copyright (c) 2013-2020, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_cookie).
-export([parse_cookie/1]).
-export([parse_set_cookie/1]).
-export([cookie/1]).
-export([setcookie/3]).
-type cookie_attrs() :: #{
expires => calendar:datetime(),
max_age => calendar:datetime(),
domain => binary(),
path => binary(),
secure => true,
http_only => true,
same_site => strict | lax | none
}.
-export_type([cookie_attrs/0]).
-type cookie_opts() :: #{
domain => binary(),
http_only => boolean(),
max_age => non_neg_integer(),
path => binary(),
same_site => strict | lax | none,
secure => boolean()
}.
-export_type([cookie_opts/0]).
-include("cow_inline.hrl").
%% Cookie header.
-spec parse_cookie(binary()) -> [{binary(), binary()}].
parse_cookie(Cookie) ->
parse_cookie(Cookie, []).
parse_cookie(<<>>, Acc) ->
lists:reverse(Acc);
parse_cookie(<<$\s, Rest/binary>>, Acc) ->
parse_cookie(Rest, Acc);
parse_cookie(<<$\t, Rest/binary>>, Acc) ->
parse_cookie(Rest, Acc);
parse_cookie(<<$,, Rest/binary>>, Acc) ->
parse_cookie(Rest, Acc);
parse_cookie(<<$;, Rest/binary>>, Acc) ->
parse_cookie(Rest, Acc);
parse_cookie(Cookie, Acc) ->
parse_cookie_name(Cookie, Acc, <<>>).
parse_cookie_name(<<>>, Acc, Name) ->
lists:reverse([{<<>>, parse_cookie_trim(Name)} | Acc]);
parse_cookie_name(<<$=, _/binary>>, _, <<>>) ->
error(badarg);
parse_cookie_name(<<$=, Rest/binary>>, Acc, Name) ->
parse_cookie_value(Rest, Acc, Name, <<>>);
parse_cookie_name(<<$,, _/binary>>, _, _) ->
error(badarg);
parse_cookie_name(<<$;, Rest/binary>>, Acc, Name) ->
parse_cookie(Rest, [{<<>>, parse_cookie_trim(Name)} | Acc]);
parse_cookie_name(<<$\t, _/binary>>, _, _) ->
error(badarg);
parse_cookie_name(<<$\r, _/binary>>, _, _) ->
error(badarg);
parse_cookie_name(<<$\n, _/binary>>, _, _) ->
error(badarg);
parse_cookie_name(<<$\013, _/binary>>, _, _) ->
error(badarg);
parse_cookie_name(<<$\014, _/binary>>, _, _) ->
error(badarg);
parse_cookie_name(<<C, Rest/binary>>, Acc, Name) ->
parse_cookie_name(Rest, Acc, <<Name/binary, C>>).
parse_cookie_value(<<>>, Acc, Name, Value) ->
lists:reverse([{Name, parse_cookie_trim(Value)} | Acc]);
parse_cookie_value(<<$;, Rest/binary>>, Acc, Name, Value) ->
parse_cookie(Rest, [{Name, parse_cookie_trim(Value)} | Acc]);
parse_cookie_value(<<$\t, _/binary>>, _, _, _) ->
error(badarg);
parse_cookie_value(<<$\r, _/binary>>, _, _, _) ->
error(badarg);
parse_cookie_value(<<$\n, _/binary>>, _, _, _) ->
error(badarg);
parse_cookie_value(<<$\013, _/binary>>, _, _, _) ->
error(badarg);
parse_cookie_value(<<$\014, _/binary>>, _, _, _) ->
error(badarg);
parse_cookie_value(<<C, Rest/binary>>, Acc, Name, Value) ->
parse_cookie_value(Rest, Acc, Name, <<Value/binary, C>>).
parse_cookie_trim(Value = <<>>) ->
Value;
parse_cookie_trim(Value) ->
case binary:last(Value) of
$\s ->
Size = byte_size(Value) - 1,
<<Value2:Size/binary, _>> = Value,
parse_cookie_trim(Value2);
_ ->
Value
end.
-ifdef(TEST).
parse_cookie_test_() ->
%% {Value, Result}.
Tests = [
{<<"name=value; name2=value2">>, [
{<<"name">>, <<"value">>},
{<<"name2">>, <<"value2">>}
]},
%% Space in value.
{<<"foo=Thu Jul 11 2013 15:38:43 GMT+0400 (MSK)">>,
[{<<"foo">>, <<"Thu Jul 11 2013 15:38:43 GMT+0400 (MSK)">>}]},
%% Comma in value. Google Analytics sets that kind of cookies.
{<<"refk=sOUZDzq2w2; sk=B602064E0139D842D620C7569640DBB4C81C45080651"
"9CC124EF794863E10E80; __utma=64249653.825741573.1380181332.1400"
"015657.1400019557.703; __utmb=64249653.1.10.1400019557; __utmc="
"64249653; __utmz=64249653.1400019557.703.13.utmcsr=bluesky.chic"
"agotribune.com|utmccn=(referral)|utmcmd=referral|utmcct=/origin"
"als/chi-12-indispensable-digital-tools-bsi,0,0.storygallery">>, [
{<<"refk">>, <<"sOUZDzq2w2">>},
{<<"sk">>, <<"B602064E0139D842D620C7569640DBB4C81C45080651"
"9CC124EF794863E10E80">>},
{<<"__utma">>, <<"64249653.825741573.1380181332.1400"
"015657.1400019557.703">>},
{<<"__utmb">>, <<"64249653.1.10.1400019557">>},
{<<"__utmc">>, <<"64249653">>},
{<<"__utmz">>, <<"64249653.1400019557.703.13.utmcsr=bluesky.chic"
"agotribune.com|utmccn=(referral)|utmcmd=referral|utmcct=/origin"
"als/chi-12-indispensable-digital-tools-bsi,0,0.storygallery">>}
]},
%% Potential edge cases (initially from Mochiweb).
{<<"foo=\\x">>, [{<<"foo">>, <<"\\x">>}]},
{<<"foo=;bar=">>, [{<<"foo">>, <<>>}, {<<"bar">>, <<>>}]},
{<<"foo=\\\";;bar=good ">>,
[{<<"foo">>, <<"\\\"">>}, {<<"bar">>, <<"good">>}]},
{<<"foo=\"\\\";bar=good">>,
[{<<"foo">>, <<"\"\\\"">>}, {<<"bar">>, <<"good">>}]},
{<<>>, []}, %% Flash player.
{<<"foo=bar , baz=wibble ">>, [{<<"foo">>, <<"bar , baz=wibble">>}]},
%% Technically invalid, but seen in the wild
{<<"foo">>, [{<<>>, <<"foo">>}]},
{<<"foo ">>, [{<<>>, <<"foo">>}]},
{<<"foo;">>, [{<<>>, <<"foo">>}]},
{<<"bar;foo=1">>, [{<<>>, <<"bar">>}, {<<"foo">>, <<"1">>}]}
],
[{V, fun() -> R = parse_cookie(V) end} || {V, R} <- Tests].
parse_cookie_error_test_() ->
%% Value.
Tests = [
<<"=">>
],
[{V, fun() -> {'EXIT', {badarg, _}} = (catch parse_cookie(V)) end} || V <- Tests].
-endif.
%% Set-Cookie header.
-spec parse_set_cookie(binary())
-> {ok, binary(), binary(), cookie_attrs()}
| ignore.
parse_set_cookie(SetCookie) ->
{NameValuePair, UnparsedAttrs} = take_until_semicolon(SetCookie, <<>>),
{Name, Value} = case binary:split(NameValuePair, <<$=>>) of
[Value0] -> {<<>>, trim(Value0)};
[Name0, Value0] -> {trim(Name0), trim(Value0)}
end,
case {Name, Value} of
{<<>>, <<>>} ->
ignore;
_ ->
Attrs = parse_set_cookie_attrs(UnparsedAttrs, #{}),
{ok, Name, Value, Attrs}
end.
parse_set_cookie_attrs(<<>>, Attrs) ->
Attrs;
parse_set_cookie_attrs(<<$;, Rest0/bits>>, Attrs) ->
{Av, Rest} = take_until_semicolon(Rest0, <<>>),
{Name, Value} = case binary:split(Av, <<$=>>) of
[Name0] -> {trim(Name0), <<>>};
[Name0, Value0] -> {trim(Name0), trim(Value0)}
end,
case parse_set_cookie_attr(?LOWER(Name), Value) of
{ok, AttrName, AttrValue} ->
parse_set_cookie_attrs(Rest, Attrs#{AttrName => AttrValue});
{ignore, AttrName} ->
parse_set_cookie_attrs(Rest, maps:remove(AttrName, Attrs));
ignore ->
parse_set_cookie_attrs(Rest, Attrs)
end.
take_until_semicolon(Rest = <<$;, _/bits>>, Acc) -> {Acc, Rest};
take_until_semicolon(<<C, R/bits>>, Acc) -> take_until_semicolon(R, <<Acc/binary, C>>);
take_until_semicolon(<<>>, Acc) -> {Acc, <<>>}.
trim(String) ->
string:trim(String, both, [$\s, $\t]).
parse_set_cookie_attr(<<"expires">>, Value) ->
try cow_date:parse_date(Value) of
DateTime ->
{ok, expires, DateTime}
catch _:_ ->
ignore
end;
parse_set_cookie_attr(<<"max-age">>, Value) ->
try binary_to_integer(Value) of
MaxAge when MaxAge =< 0 ->
%% Year 0 corresponds to 1 BC.
{ok, max_age, {{0, 1, 1}, {0, 0, 0}}};
MaxAge ->
CurrentTime = erlang:universaltime(),
{ok, max_age, calendar:gregorian_seconds_to_datetime(
calendar:datetime_to_gregorian_seconds(CurrentTime) + MaxAge)}
catch _:_ ->
ignore
end;
parse_set_cookie_attr(<<"domain">>, Value) ->
case Value of
<<>> ->
ignore;
<<".", Rest/bits>> ->
{ok, domain, ?LOWER(Rest)};
_ ->
{ok, domain, ?LOWER(Value)}
end;
parse_set_cookie_attr(<<"path">>, Value) ->
case Value of
<<"/", _/bits>> ->
{ok, path, Value};
%% When the path is not absolute, or the path is empty, the default-path will be used.
%% Note that the default-path is also used when there are no path attributes,
%% so we are simply ignoring the attribute here.
_ ->
{ignore, path}
end;
parse_set_cookie_attr(<<"secure">>, _) ->
{ok, secure, true};
parse_set_cookie_attr(<<"httponly">>, _) ->
{ok, http_only, true};
parse_set_cookie_attr(<<"samesite">>, Value) ->
case ?LOWER(Value) of
<<"strict">> ->
{ok, same_site, strict};
<<"lax">> ->
{ok, same_site, lax};
%% Clients may have different defaults than "None".
<<"none">> ->
{ok, same_site, none};
%% Unknown values and lack of value are equivalent.
_ ->
ignore
end;
parse_set_cookie_attr(_, _) ->
ignore.
-ifdef(TEST).
parse_set_cookie_test_() ->
Tests = [
{<<"a=b">>, {ok, <<"a">>, <<"b">>, #{}}},
{<<"a=b; Secure">>, {ok, <<"a">>, <<"b">>, #{secure => true}}},
{<<"a=b; HttpOnly">>, {ok, <<"a">>, <<"b">>, #{http_only => true}}},
{<<"a=b; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Expires=Wed, 21 Oct 2015 07:29:00 GMT">>,
{ok, <<"a">>, <<"b">>, #{expires => {{2015, 10, 21}, {7, 29, 0}}}}},
{<<"a=b; Max-Age=999; Max-Age=0">>,
{ok, <<"a">>, <<"b">>, #{max_age => {{0, 1, 1}, {0, 0, 0}}}}},
{<<"a=b; Domain=example.org; Domain=foo.example.org">>,
{ok, <<"a">>, <<"b">>, #{domain => <<"foo.example.org">>}}},
{<<"a=b; Path=/path/to/resource; Path=/">>,
{ok, <<"a">>, <<"b">>, #{path => <<"/">>}}},
{<<"a=b; SameSite=Lax; SameSite=Strict">>,
{ok, <<"a">>, <<"b">>, #{same_site => strict}}}
],
[{SetCookie, fun() -> Res = parse_set_cookie(SetCookie) end}
|| {SetCookie, Res} <- Tests].
-endif.
%% Build a cookie header.
-spec cookie([{iodata(), iodata()}]) -> iolist().
cookie([]) ->
[];
cookie([{<<>>, Value}]) ->
[Value];
cookie([{Name, Value}]) ->
[Name, $=, Value];
cookie([{<<>>, Value} | Tail]) ->
[Value, $;, $\s | cookie(Tail)];
cookie([{Name, Value} | Tail]) ->
[Name, $=, Value, $;, $\s | cookie(Tail)].
-ifdef(TEST).
cookie_test_() ->
Tests = [
{[], <<>>},
{[{<<"a">>, <<"b">>}], <<"a=b">>},
{[{<<"a">>, <<"b">>}, {<<"c">>, <<"d">>}], <<"a=b; c=d">>},
{[{<<>>, <<"b">>}, {<<"c">>, <<"d">>}], <<"b; c=d">>},
{[{<<"a">>, <<"b">>}, {<<>>, <<"d">>}], <<"a=b; d">>}
],
[{Res, fun() -> Res = iolist_to_binary(cookie(Cookies)) end}
|| {Cookies, Res} <- Tests].
-endif.
%% Convert a cookie name, value and options to its iodata form.
%%
%% Initially from Mochiweb:
%% * Copyright 2007 Mochi Media, Inc.
%% Initial binary implementation:
%% * Copyright 2011 Thomas Burdick <thomas.burdick@gmail.com>
%%
%% @todo Rename the function to set_cookie eventually.
-spec setcookie(iodata(), iodata(), cookie_opts()) -> iolist().
setcookie(Name, Value, Opts) ->
nomatch = binary:match(iolist_to_binary(Name), [<<$=>>, <<$,>>, <<$;>>,
<<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]),
nomatch = binary:match(iolist_to_binary(Value), [<<$,>>, <<$;>>,
<<$\s>>, <<$\t>>, <<$\r>>, <<$\n>>, <<$\013>>, <<$\014>>]),
[Name, <<"=">>, Value, <<"; Version=1">>, attributes(maps:to_list(Opts))].
attributes([]) -> [];
attributes([{domain, Domain} | Tail]) -> [<<"; Domain=">>, Domain | attributes(Tail)];
attributes([{http_only, false} | Tail]) -> attributes(Tail);
attributes([{http_only, true} | Tail]) -> [<<"; HttpOnly">> | attributes(Tail)];
%% MSIE requires an Expires date in the past to delete a cookie.
attributes([{max_age, 0} | Tail]) ->
[<<"; Expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0">> | attributes(Tail)];
attributes([{max_age, MaxAge} | Tail]) when is_integer(MaxAge), MaxAge > 0 ->
Secs = calendar:datetime_to_gregorian_seconds(calendar:universal_time()),
Expires = cow_date:rfc2109(calendar:gregorian_seconds_to_datetime(Secs + MaxAge)),
[<<"; Expires=">>, Expires, <<"; Max-Age=">>, integer_to_list(MaxAge) | attributes(Tail)];
attributes([Opt = {max_age, _} | _]) ->
error({badarg, Opt});
attributes([{path, Path} | Tail]) -> [<<"; Path=">>, Path | attributes(Tail)];
attributes([{secure, false} | Tail]) -> attributes(Tail);
attributes([{secure, true} | Tail]) -> [<<"; Secure">> | attributes(Tail)];
attributes([{same_site, lax} | Tail]) -> [<<"; SameSite=Lax">> | attributes(Tail)];
attributes([{same_site, strict} | Tail]) -> [<<"; SameSite=Strict">> | attributes(Tail)];
attributes([{same_site, none} | Tail]) -> [<<"; SameSite=None">> | attributes(Tail)];
%% Skip unknown options.
attributes([_ | Tail]) -> attributes(Tail).
-ifdef(TEST).
setcookie_test_() ->
%% {Name, Value, Opts, Result}
Tests = [
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{http_only => true, domain => <<"acme.com">>},
<<"Customer=WILE_E_COYOTE; Version=1; "
"Domain=acme.com; HttpOnly">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{path => <<"/acme">>},
<<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{secure => true},
<<"Customer=WILE_E_COYOTE; Version=1; Secure">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{secure => false, http_only => false},
<<"Customer=WILE_E_COYOTE; Version=1">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{same_site => lax},
<<"Customer=WILE_E_COYOTE; Version=1; SameSite=Lax">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{same_site => strict},
<<"Customer=WILE_E_COYOTE; Version=1; SameSite=Strict">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{path => <<"/acme">>, badoption => <<"negatory">>},
<<"Customer=WILE_E_COYOTE; Version=1; Path=/acme">>}
],
[{R, fun() -> R = iolist_to_binary(setcookie(N, V, O)) end}
|| {N, V, O, R} <- Tests].
setcookie_max_age_test() ->
F = fun(N, V, O) ->
binary:split(iolist_to_binary(
setcookie(N, V, O)), <<";">>, [global])
end,
[<<"Customer=WILE_E_COYOTE">>,
<<" Version=1">>,
<<" Expires=", _/binary>>,
<<" Max-Age=111">>,
<<" Secure">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
#{max_age => 111, secure => true}),
case catch F(<<"Customer">>, <<"WILE_E_COYOTE">>, #{max_age => -111}) of
{'EXIT', {{badarg, {max_age, -111}}, _}} -> ok
end,
[<<"Customer=WILE_E_COYOTE">>,
<<" Version=1">>,
<<" Expires=", _/binary>>,
<<" Max-Age=86417">>] = F(<<"Customer">>, <<"WILE_E_COYOTE">>,
#{max_age => 86417}),
ok.
setcookie_failures_test_() ->
F = fun(N, V) ->
try setcookie(N, V, #{}) of
_ ->
false
catch _:_ ->
true
end
end,
Tests = [
{<<"Na=me">>, <<"Value">>},
{<<"Name;">>, <<"Value">>},
{<<"\r\name">>, <<"Value">>},
{<<"Name">>, <<"Value;">>},
{<<"Name">>, <<"\value">>}
],
[{iolist_to_binary(io_lib:format("{~p, ~p} failure", [N, V])),
fun() -> true = F(N, V) end}
|| {N, V} <- Tests].
-endif.

+ 0
- 434
src/wsLib/cow_date.erl Vedi File

@ -1,434 +0,0 @@
%% Copyright (c) 2013-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_date).
-export([parse_date/1]).
-export([rfc1123/1]).
-export([rfc2109/1]).
-export([rfc7231/1]).
-ifdef(TEST).
-include_lib("proper/include/proper.hrl").
-endif.
%% @doc Parse the HTTP date (IMF-fixdate, rfc850, asctime).
-define(DIGITS(A, B), ((A - $0) * 10 + (B - $0))).
-define(DIGITS(A, B, C, D), ((A - $0) * 1000 + (B - $0) * 100 + (C - $0) * 10 + (D - $0))).
-spec parse_date(binary()) -> calendar:datetime().
parse_date(DateBin) ->
Date = {{_, _, D}, {H, M, S}} = http_date(DateBin),
true = D >= 0 andalso D =< 31,
true = H >= 0 andalso H =< 23,
true = M >= 0 andalso M =< 59,
true = S >= 0 andalso S =< 60, %% Leap second.
Date.
http_date(<<"Mon, ", D1, D2, " ", R/bits>>) -> fixdate(R, ?DIGITS(D1, D2));
http_date(<<"Tue, ", D1, D2, " ", R/bits>>) -> fixdate(R, ?DIGITS(D1, D2));
http_date(<<"Wed, ", D1, D2, " ", R/bits>>) -> fixdate(R, ?DIGITS(D1, D2));
http_date(<<"Thu, ", D1, D2, " ", R/bits>>) -> fixdate(R, ?DIGITS(D1, D2));
http_date(<<"Fri, ", D1, D2, " ", R/bits>>) -> fixdate(R, ?DIGITS(D1, D2));
http_date(<<"Sat, ", D1, D2, " ", R/bits>>) -> fixdate(R, ?DIGITS(D1, D2));
http_date(<<"Sun, ", D1, D2, " ", R/bits>>) -> fixdate(R, ?DIGITS(D1, D2));
http_date(<<"Monday, ", D1, D2, "-", R/bits>>) -> rfc850_date(R, ?DIGITS(D1, D2));
http_date(<<"Tuesday, ", D1, D2, "-", R/bits>>) -> rfc850_date(R, ?DIGITS(D1, D2));
http_date(<<"Wednesday, ", D1, D2, "-", R/bits>>) -> rfc850_date(R, ?DIGITS(D1, D2));
http_date(<<"Thursday, ", D1, D2, "-", R/bits>>) -> rfc850_date(R, ?DIGITS(D1, D2));
http_date(<<"Friday, ", D1, D2, "-", R/bits>>) -> rfc850_date(R, ?DIGITS(D1, D2));
http_date(<<"Saturday, ", D1, D2, "-", R/bits>>) -> rfc850_date(R, ?DIGITS(D1, D2));
http_date(<<"Sunday, ", D1, D2, "-", R/bits>>) -> rfc850_date(R, ?DIGITS(D1, D2));
http_date(<<"Mon ", R/bits>>) -> asctime_date(R);
http_date(<<"Tue ", R/bits>>) -> asctime_date(R);
http_date(<<"Wed ", R/bits>>) -> asctime_date(R);
http_date(<<"Thu ", R/bits>>) -> asctime_date(R);
http_date(<<"Fri ", R/bits>>) -> asctime_date(R);
http_date(<<"Sat ", R/bits>>) -> asctime_date(R);
http_date(<<"Sun ", R/bits>>) -> asctime_date(R).
fixdate(<<"Jan ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 1, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Feb ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 2, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Mar ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 3, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Apr ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 4, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"May ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 5, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Jun ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 6, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Jul ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 7, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Aug ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 8, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Sep ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 9, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Oct ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 10, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Nov ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 11, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
fixdate(<<"Dec ", Y1, Y2, Y3, Y4, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 12, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}.
rfc850_date(<<"Jan-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 1, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Feb-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 2, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Mar-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 3, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Apr-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 4, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"May-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 5, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Jun-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 6, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Jul-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 7, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Aug-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 8, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Sep-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 9, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Oct-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 10, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Nov-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 11, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
rfc850_date(<<"Dec-", Y1, Y2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " GMT">>, Day) ->
{{rfc850_year(?DIGITS(Y1, Y2)), 12, Day}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}.
rfc850_year(Y) when Y > 50 -> Y + 1900;
rfc850_year(Y) -> Y + 2000.
asctime_date(<<"Jan ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 1, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Feb ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 2, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Mar ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 3, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Apr ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 4, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"May ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 5, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Jun ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 6, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Jul ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 7, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Aug ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 8, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Sep ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 9, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Oct ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 10, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Nov ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 11, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}};
asctime_date(<<"Dec ", D1, D2, " ", H1, H2, ":", M1, M2, ":", S1, S2, " ", Y1, Y2, Y3, Y4>>) ->
{{?DIGITS(Y1, Y2, Y3, Y4), 12, asctime_day(D1, D2)}, {?DIGITS(H1, H2), ?DIGITS(M1, M2), ?DIGITS(S1, S2)}}.
asctime_day($\s, D2) -> (D2 - $0);
asctime_day(D1, D2) -> (D1 - $0) * 10 + (D2 - $0).
-ifdef(TEST).
day_name() -> oneof(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]).
day_name_l() -> oneof(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]).
year() -> integer(1951, 2050).
month() -> integer(1, 12).
day() -> integer(1, 31).
hour() -> integer(0, 23).
minute() -> integer(0, 59).
second() -> integer(0, 60).
fixdate_gen() ->
?LET({DayName, Y, Mo, D, H, Mi, S},
{day_name(), year(), month(), day(), hour(), minute(), second()},
{{{Y, Mo, D}, {H, Mi, S}},
list_to_binary([DayName, ", ", pad_int(D), " ", month(Mo), " ", integer_to_binary(Y),
" ", pad_int(H), ":", pad_int(Mi), ":", pad_int(S), " GMT"])}).
rfc850_gen() ->
?LET({DayName, Y, Mo, D, H, Mi, S},
{day_name_l(), year(), month(), day(), hour(), minute(), second()},
{{{Y, Mo, D}, {H, Mi, S}},
list_to_binary([DayName, ", ", pad_int(D), "-", month(Mo), "-", pad_int(Y rem 100),
" ", pad_int(H), ":", pad_int(Mi), ":", pad_int(S), " GMT"])}).
asctime_gen() ->
?LET({DayName, Y, Mo, D, H, Mi, S},
{day_name(), year(), month(), day(), hour(), minute(), second()},
{{{Y, Mo, D}, {H, Mi, S}},
list_to_binary([DayName, " ", month(Mo), " ",
if D < 10 -> <<$\s, (D + $0)>>; true -> integer_to_binary(D) end,
" ", pad_int(H), ":", pad_int(Mi), ":", pad_int(S), " ", integer_to_binary(Y)])}).
prop_http_date() ->
?FORALL({Date, DateBin},
oneof([fixdate_gen(), rfc850_gen(), asctime_gen()]),
Date =:= parse_date(DateBin)).
http_date_test_() ->
Tests = [
{<<"Sun, 06 Nov 1994 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}},
{<<"Sunday, 06-Nov-94 08:49:37 GMT">>, {{1994, 11, 6}, {8, 49, 37}}},
{<<"Sun Nov 6 08:49:37 1994">>, {{1994, 11, 6}, {8, 49, 37}}}
],
[{V, fun() -> R = http_date(V) end} || {V, R} <- Tests].
horse_http_date_fixdate() ->
horse:repeat(200000,
http_date(<<"Sun, 06 Nov 1994 08:49:37 GMT">>)
).
horse_http_date_rfc850() ->
horse:repeat(200000,
http_date(<<"Sunday, 06-Nov-94 08:49:37 GMT">>)
).
horse_http_date_asctime() ->
horse:repeat(200000,
http_date(<<"Sun Nov 6 08:49:37 1994">>)
).
-endif.
%% @doc Return the date formatted according to RFC1123.
-spec rfc1123(calendar:datetime()) -> binary().
rfc1123(DateTime) ->
rfc7231(DateTime).
%% @doc Return the date formatted according to RFC2109.
-spec rfc2109(calendar:datetime()) -> binary().
rfc2109({Date = {Y, Mo, D}, {H, Mi, S}}) ->
Wday = calendar:day_of_the_week(Date),
<<(weekday(Wday))/binary, ", ",
(pad_int(D))/binary, "-",
(month(Mo))/binary, "-",
(year(Y))/binary, " ",
(pad_int(H))/binary, ":",
(pad_int(Mi))/binary, ":",
(pad_int(S))/binary, " GMT">>.
-ifdef(TEST).
rfc2109_test_() ->
Tests = [
{<<"Sat, 14-May-2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}}},
{<<"Sun, 01-Jan-2012 00:00:00 GMT">>, {{2012, 1, 1}, {0, 0, 0}}}
],
[{R, fun() -> R = rfc2109(D) end} || {R, D} <- Tests].
horse_rfc2109_20130101_000000() ->
horse:repeat(100000,
rfc2109({{2013, 1, 1}, {0, 0, 0}})
).
horse_rfc2109_20131231_235959() ->
horse:repeat(100000,
rfc2109({{2013, 12, 31}, {23, 59, 59}})
).
horse_rfc2109_12340506_070809() ->
horse:repeat(100000,
rfc2109({{1234, 5, 6}, {7, 8, 9}})
).
-endif.
%% @doc Return the date formatted according to RFC7231.
-spec rfc7231(calendar:datetime()) -> binary().
rfc7231({Date = {Y, Mo, D}, {H, Mi, S}}) ->
Wday = calendar:day_of_the_week(Date),
<<(weekday(Wday))/binary, ", ",
(pad_int(D))/binary, " ",
(month(Mo))/binary, " ",
(year(Y))/binary, " ",
(pad_int(H))/binary, ":",
(pad_int(Mi))/binary, ":",
(pad_int(S))/binary, " GMT">>.
-ifdef(TEST).
rfc7231_test_() ->
Tests = [
{<<"Sat, 14 May 2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}}},
{<<"Sun, 01 Jan 2012 00:00:00 GMT">>, {{2012, 1, 1}, {0, 0, 0}}}
],
[{R, fun() -> R = rfc7231(D) end} || {R, D} <- Tests].
horse_rfc7231_20130101_000000() ->
horse:repeat(100000,
rfc7231({{2013, 1, 1}, {0, 0, 0}})
).
horse_rfc7231_20131231_235959() ->
horse:repeat(100000,
rfc7231({{2013, 12, 31}, {23, 59, 59}})
).
horse_rfc7231_12340506_070809() ->
horse:repeat(100000,
rfc7231({{1234, 5, 6}, {7, 8, 9}})
).
-endif.
%% Internal.
-spec pad_int(0..59) -> <<_:16>>.
pad_int(0) -> <<"00">>;
pad_int(1) -> <<"01">>;
pad_int(2) -> <<"02">>;
pad_int(3) -> <<"03">>;
pad_int(4) -> <<"04">>;
pad_int(5) -> <<"05">>;
pad_int(6) -> <<"06">>;
pad_int(7) -> <<"07">>;
pad_int(8) -> <<"08">>;
pad_int(9) -> <<"09">>;
pad_int(10) -> <<"10">>;
pad_int(11) -> <<"11">>;
pad_int(12) -> <<"12">>;
pad_int(13) -> <<"13">>;
pad_int(14) -> <<"14">>;
pad_int(15) -> <<"15">>;
pad_int(16) -> <<"16">>;
pad_int(17) -> <<"17">>;
pad_int(18) -> <<"18">>;
pad_int(19) -> <<"19">>;
pad_int(20) -> <<"20">>;
pad_int(21) -> <<"21">>;
pad_int(22) -> <<"22">>;
pad_int(23) -> <<"23">>;
pad_int(24) -> <<"24">>;
pad_int(25) -> <<"25">>;
pad_int(26) -> <<"26">>;
pad_int(27) -> <<"27">>;
pad_int(28) -> <<"28">>;
pad_int(29) -> <<"29">>;
pad_int(30) -> <<"30">>;
pad_int(31) -> <<"31">>;
pad_int(32) -> <<"32">>;
pad_int(33) -> <<"33">>;
pad_int(34) -> <<"34">>;
pad_int(35) -> <<"35">>;
pad_int(36) -> <<"36">>;
pad_int(37) -> <<"37">>;
pad_int(38) -> <<"38">>;
pad_int(39) -> <<"39">>;
pad_int(40) -> <<"40">>;
pad_int(41) -> <<"41">>;
pad_int(42) -> <<"42">>;
pad_int(43) -> <<"43">>;
pad_int(44) -> <<"44">>;
pad_int(45) -> <<"45">>;
pad_int(46) -> <<"46">>;
pad_int(47) -> <<"47">>;
pad_int(48) -> <<"48">>;
pad_int(49) -> <<"49">>;
pad_int(50) -> <<"50">>;
pad_int(51) -> <<"51">>;
pad_int(52) -> <<"52">>;
pad_int(53) -> <<"53">>;
pad_int(54) -> <<"54">>;
pad_int(55) -> <<"55">>;
pad_int(56) -> <<"56">>;
pad_int(57) -> <<"57">>;
pad_int(58) -> <<"58">>;
pad_int(59) -> <<"59">>;
pad_int(60) -> <<"60">>;
pad_int(Int) -> integer_to_binary(Int).
-spec weekday(1..7) -> <<_:24>>.
weekday(1) -> <<"Mon">>;
weekday(2) -> <<"Tue">>;
weekday(3) -> <<"Wed">>;
weekday(4) -> <<"Thu">>;
weekday(5) -> <<"Fri">>;
weekday(6) -> <<"Sat">>;
weekday(7) -> <<"Sun">>.
-spec month(1..12) -> <<_:24>>.
month(1) -> <<"Jan">>;
month(2) -> <<"Feb">>;
month(3) -> <<"Mar">>;
month(4) -> <<"Apr">>;
month(5) -> <<"May">>;
month(6) -> <<"Jun">>;
month(7) -> <<"Jul">>;
month(8) -> <<"Aug">>;
month(9) -> <<"Sep">>;
month(10) -> <<"Oct">>;
month(11) -> <<"Nov">>;
month(12) -> <<"Dec">>.
-spec year(pos_integer()) -> <<_:32>>.
year(1970) -> <<"1970">>;
year(1971) -> <<"1971">>;
year(1972) -> <<"1972">>;
year(1973) -> <<"1973">>;
year(1974) -> <<"1974">>;
year(1975) -> <<"1975">>;
year(1976) -> <<"1976">>;
year(1977) -> <<"1977">>;
year(1978) -> <<"1978">>;
year(1979) -> <<"1979">>;
year(1980) -> <<"1980">>;
year(1981) -> <<"1981">>;
year(1982) -> <<"1982">>;
year(1983) -> <<"1983">>;
year(1984) -> <<"1984">>;
year(1985) -> <<"1985">>;
year(1986) -> <<"1986">>;
year(1987) -> <<"1987">>;
year(1988) -> <<"1988">>;
year(1989) -> <<"1989">>;
year(1990) -> <<"1990">>;
year(1991) -> <<"1991">>;
year(1992) -> <<"1992">>;
year(1993) -> <<"1993">>;
year(1994) -> <<"1994">>;
year(1995) -> <<"1995">>;
year(1996) -> <<"1996">>;
year(1997) -> <<"1997">>;
year(1998) -> <<"1998">>;
year(1999) -> <<"1999">>;
year(2000) -> <<"2000">>;
year(2001) -> <<"2001">>;
year(2002) -> <<"2002">>;
year(2003) -> <<"2003">>;
year(2004) -> <<"2004">>;
year(2005) -> <<"2005">>;
year(2006) -> <<"2006">>;
year(2007) -> <<"2007">>;
year(2008) -> <<"2008">>;
year(2009) -> <<"2009">>;
year(2010) -> <<"2010">>;
year(2011) -> <<"2011">>;
year(2012) -> <<"2012">>;
year(2013) -> <<"2013">>;
year(2014) -> <<"2014">>;
year(2015) -> <<"2015">>;
year(2016) -> <<"2016">>;
year(2017) -> <<"2017">>;
year(2018) -> <<"2018">>;
year(2019) -> <<"2019">>;
year(2020) -> <<"2020">>;
year(2021) -> <<"2021">>;
year(2022) -> <<"2022">>;
year(2023) -> <<"2023">>;
year(2024) -> <<"2024">>;
year(2025) -> <<"2025">>;
year(2026) -> <<"2026">>;
year(2027) -> <<"2027">>;
year(2028) -> <<"2028">>;
year(2029) -> <<"2029">>;
year(Year) -> integer_to_binary(Year).

+ 0
- 1449
src/wsLib/cow_hpack.erl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 4132
src/wsLib/cow_hpack_dec_huffman_lookup.hrl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 426
src/wsLib/cow_http.erl Vedi File

@ -1,426 +0,0 @@
%% Copyright (c) 2013-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_http).
-export([parse_request_line/1]).
-export([parse_status_line/1]).
-export([status_to_integer/1]).
-export([parse_headers/1]).
-export([parse_fullpath/1]).
-export([parse_version/1]).
-export([request/4]).
-export([response/3]).
-export([headers/1]).
-export([version/1]).
-type version() :: 'HTTP/1.0' | 'HTTP/1.1'.
-export_type([version/0]).
-type status() :: 100..999.
-export_type([status/0]).
-type headers() :: [{binary(), iodata()}].
-export_type([headers/0]).
-include("cow_inline.hrl").
%% @doc Parse the request line.
-spec parse_request_line(binary()) -> {binary(), binary(), version(), binary()}.
parse_request_line(Data) ->
{Pos, _} = binary:match(Data, <<"\r">>),
<<RequestLine:Pos/binary, "\r\n", Rest/bits>> = Data,
[Method, Target, Version0] = binary:split(RequestLine, <<$\s>>, [trim_all, global]),
Version = case Version0 of
<<"HTTP/1.1">> -> 'HTTP/1.1';
<<"HTTP/1.0">> -> 'HTTP/1.0'
end,
{Method, Target, Version, Rest}.
-ifdef(TEST).
parse_request_line_test_() ->
Tests = [
{<<"GET /path HTTP/1.0\r\nRest">>,
{<<"GET">>, <<"/path">>, 'HTTP/1.0', <<"Rest">>}},
{<<"GET /path HTTP/1.1\r\nRest">>,
{<<"GET">>, <<"/path">>, 'HTTP/1.1', <<"Rest">>}},
{<<"CONNECT proxy.example.org:1080 HTTP/1.1\r\nRest">>,
{<<"CONNECT">>, <<"proxy.example.org:1080">>, 'HTTP/1.1', <<"Rest">>}}
],
[{V, fun() -> R = parse_request_line(V) end}
|| {V, R} <- Tests].
parse_request_line_error_test_() ->
Tests = [
<<>>,
<<"GET">>,
<<"GET /path\r\n">>,
<<"GET /path HTTP/1.1">>,
<<"GET /path HTTP/1.1\r">>,
<<"GET /path HTTP/1.1\n">>,
<<"GET /path HTTP/0.9\r\n">>,
<<"content-type: text/plain\r\n">>,
<<0:80, "\r\n">>
],
[{V, fun() -> {'EXIT', _} = (catch parse_request_line(V)) end}
|| V <- Tests].
horse_parse_request_line_get_path() ->
horse:repeat(200000,
parse_request_line(<<"GET /path HTTP/1.1\r\n">>)
).
-endif.
%% @doc Parse the status line.
-spec parse_status_line(binary()) -> {version(), status(), binary(), binary()}.
parse_status_line(<<"HTTP/1.1 200 OK\r\n", Rest/bits>>) ->
{'HTTP/1.1', 200, <<"OK">>, Rest};
parse_status_line(<<"HTTP/1.1 404 Not Found\r\n", Rest/bits>>) ->
{'HTTP/1.1', 404, <<"Not Found">>, Rest};
parse_status_line(<<"HTTP/1.1 500 Internal Server Error\r\n", Rest/bits>>) ->
{'HTTP/1.1', 500, <<"Internal Server Error">>, Rest};
parse_status_line(<<"HTTP/1.1 ", Status/bits>>) ->
parse_status_line(Status, 'HTTP/1.1');
parse_status_line(<<"HTTP/1.0 ", Status/bits>>) ->
parse_status_line(Status, 'HTTP/1.0').
parse_status_line(<<H, T, U, " ", Rest/bits>>, Version) ->
Status = status_to_integer(H, T, U),
{Pos, _} = binary:match(Rest, <<"\r">>),
<<StatusStr:Pos/binary, "\r\n", Rest2/bits>> = Rest,
{Version, Status, StatusStr, Rest2}.
-spec status_to_integer(status() | binary()) -> status().
status_to_integer(Status) when is_integer(Status) ->
Status;
status_to_integer(Status) ->
case Status of
<<H, T, U>> ->
status_to_integer(H, T, U);
<<H, T, U, " ", _/bits>> ->
status_to_integer(H, T, U)
end.
status_to_integer(H, T, U)
when $0 =< H, H =< $9, $0 =< T, T =< $9, $0 =< U, U =< $9 ->
(H - $0) * 100 + (T - $0) * 10 + (U - $0).
-ifdef(TEST).
parse_status_line_test_() ->
Tests = [
{<<"HTTP/1.1 200 OK\r\nRest">>,
{'HTTP/1.1', 200, <<"OK">>, <<"Rest">>}},
{<<"HTTP/1.0 404 Not Found\r\nRest">>,
{'HTTP/1.0', 404, <<"Not Found">>, <<"Rest">>}},
{<<"HTTP/1.1 500 Something very funny here\r\nRest">>,
{'HTTP/1.1', 500, <<"Something very funny here">>, <<"Rest">>}},
{<<"HTTP/1.1 200 \r\nRest">>,
{'HTTP/1.1', 200, <<>>, <<"Rest">>}}
],
[{V, fun() -> R = parse_status_line(V) end}
|| {V, R} <- Tests].
parse_status_line_error_test_() ->
Tests = [
<<>>,
<<"HTTP/1.1">>,
<<"HTTP/1.1 200\r\n">>,
<<"HTTP/1.1 200 OK">>,
<<"HTTP/1.1 200 OK\r">>,
<<"HTTP/1.1 200 OK\n">>,
<<"HTTP/0.9 200 OK\r\n">>,
<<"HTTP/1.1 42 Answer\r\n">>,
<<"HTTP/1.1 999999999 More than OK\r\n">>,
<<"content-type: text/plain\r\n">>,
<<0:80, "\r\n">>
],
[{V, fun() -> {'EXIT', _} = (catch parse_status_line(V)) end}
|| V <- Tests].
horse_parse_status_line_200() ->
horse:repeat(200000,
parse_status_line(<<"HTTP/1.1 200 OK\r\n">>)
).
horse_parse_status_line_404() ->
horse:repeat(200000,
parse_status_line(<<"HTTP/1.1 404 Not Found\r\n">>)
).
horse_parse_status_line_500() ->
horse:repeat(200000,
parse_status_line(<<"HTTP/1.1 500 Internal Server Error\r\n">>)
).
horse_parse_status_line_other() ->
horse:repeat(200000,
parse_status_line(<<"HTTP/1.1 416 Requested range not satisfiable\r\n">>)
).
-endif.
%% @doc Parse the list of headers.
-spec parse_headers(binary()) -> {[{binary(), binary()}], binary()}.
parse_headers(Data) ->
parse_header(Data, []).
parse_header(<<$\r, $\n, Rest/bits>>, Acc) ->
{lists:reverse(Acc), Rest};
parse_header(Data, Acc) ->
parse_hd_name(Data, Acc, <<>>).
parse_hd_name(<<C, Rest/bits>>, Acc, SoFar) ->
case C of
$: -> parse_hd_before_value(Rest, Acc, SoFar);
$\s -> parse_hd_name_ws(Rest, Acc, SoFar);
$\t -> parse_hd_name_ws(Rest, Acc, SoFar);
_ -> ?LOWER(parse_hd_name, Rest, Acc, SoFar)
end.
parse_hd_name_ws(<<C, Rest/bits>>, Acc, Name) ->
case C of
$: -> parse_hd_before_value(Rest, Acc, Name);
$\s -> parse_hd_name_ws(Rest, Acc, Name);
$\t -> parse_hd_name_ws(Rest, Acc, Name)
end.
parse_hd_before_value(<<$\s, Rest/bits>>, Acc, Name) ->
parse_hd_before_value(Rest, Acc, Name);
parse_hd_before_value(<<$\t, Rest/bits>>, Acc, Name) ->
parse_hd_before_value(Rest, Acc, Name);
parse_hd_before_value(Data, Acc, Name) ->
parse_hd_value(Data, Acc, Name, <<>>).
parse_hd_value(<<$\r, Rest/bits>>, Acc, Name, SoFar) ->
case Rest of
<<$\n, C, Rest2/bits>> when C =:= $\s; C =:= $\t ->
parse_hd_value(Rest2, Acc, Name, <<SoFar/binary, C>>);
<<$\n, Rest2/bits>> ->
Value = clean_value_ws_end(SoFar, byte_size(SoFar) - 1),
parse_header(Rest2, [{Name, Value} | Acc])
end;
parse_hd_value(<<C, Rest/bits>>, Acc, Name, SoFar) ->
parse_hd_value(Rest, Acc, Name, <<SoFar/binary, C>>).
%% This function has been copied from cowboy_http.
clean_value_ws_end(_, -1) ->
<<>>;
clean_value_ws_end(Value, N) ->
case binary:at(Value, N) of
$\s -> clean_value_ws_end(Value, N - 1);
$\t -> clean_value_ws_end(Value, N - 1);
_ ->
S = N + 1,
<<Value2:S/binary, _/bits>> = Value,
Value2
end.
-ifdef(TEST).
parse_headers_test_() ->
Tests = [
{<<"\r\nRest">>,
{[], <<"Rest">>}},
{<<"Server: Erlang/R17 \r\n\r\n">>,
{[{<<"server">>, <<"Erlang/R17">>}], <<>>}},
{<<"Server: Erlang/R17\r\n"
"Date: Sun, 23 Feb 2014 09:30:39 GMT\r\n"
"Multiline-Header: why hello!\r\n"
" I didn't see you all the way over there!\r\n"
"Content-Length: 12\r\n"
"Content-Type: text/plain\r\n"
"\r\nRest">>,
{[{<<"server">>, <<"Erlang/R17">>},
{<<"date">>, <<"Sun, 23 Feb 2014 09:30:39 GMT">>},
{<<"multiline-header">>,
<<"why hello! I didn't see you all the way over there!">>},
{<<"content-length">>, <<"12">>},
{<<"content-type">>, <<"text/plain">>}],
<<"Rest">>}}
],
[{V, fun() -> R = parse_headers(V) end}
|| {V, R} <- Tests].
parse_headers_error_test_() ->
Tests = [
<<>>,
<<"\r">>,
<<"Malformed\r\n\r\n">>,
<<"content-type: text/plain\r\nMalformed\r\n\r\n">>,
<<"HTTP/1.1 200 OK\r\n\r\n">>,
<<0:80, "\r\n\r\n">>,
<<"content-type: text/plain\r\ncontent-length: 12\r\n">>
],
[{V, fun() -> {'EXIT', _} = (catch parse_headers(V)) end}
|| V <- Tests].
horse_parse_headers() ->
horse:repeat(50000,
parse_headers(<<"Server: Erlang/R17\r\n"
"Date: Sun, 23 Feb 2014 09:30:39 GMT\r\n"
"Multiline-Header: why hello!\r\n"
" I didn't see you all the way over there!\r\n"
"Content-Length: 12\r\n"
"Content-Type: text/plain\r\n"
"\r\nRest">>)
).
-endif.
%% @doc Extract path and query string from a binary,
%% removing any fragment component.
-spec parse_fullpath(binary()) -> {binary(), binary()}.
parse_fullpath(Fullpath) ->
parse_fullpath(Fullpath, <<>>).
parse_fullpath(<<>>, Path) -> {Path, <<>>};
parse_fullpath(<<$#, _/bits>>, Path) -> {Path, <<>>};
parse_fullpath(<<$?, Qs/bits>>, Path) -> parse_fullpath_query(Qs, Path, <<>>);
parse_fullpath(<<C, Rest/bits>>, SoFar) -> parse_fullpath(Rest, <<SoFar/binary, C>>).
parse_fullpath_query(<<>>, Path, Query) -> {Path, Query};
parse_fullpath_query(<<$#, _/bits>>, Path, Query) -> {Path, Query};
parse_fullpath_query(<<C, Rest/bits>>, Path, SoFar) ->
parse_fullpath_query(Rest, Path, <<SoFar/binary, C>>).
-ifdef(TEST).
parse_fullpath_test() ->
{<<"*">>, <<>>} = parse_fullpath(<<"*">>),
{<<"/">>, <<>>} = parse_fullpath(<<"/">>),
{<<"/path/to/resource">>, <<>>} = parse_fullpath(<<"/path/to/resource#fragment">>),
{<<"/path/to/resource">>, <<>>} = parse_fullpath(<<"/path/to/resource">>),
{<<"/">>, <<>>} = parse_fullpath(<<"/?">>),
{<<"/">>, <<"q=cowboy">>} = parse_fullpath(<<"/?q=cowboy#fragment">>),
{<<"/">>, <<"q=cowboy">>} = parse_fullpath(<<"/?q=cowboy">>),
{<<"/path/to/resource">>, <<"q=cowboy">>}
= parse_fullpath(<<"/path/to/resource?q=cowboy">>),
ok.
-endif.
%% @doc Convert an HTTP version to atom.
-spec parse_version(binary()) -> version().
parse_version(<<"HTTP/1.1">>) -> 'HTTP/1.1';
parse_version(<<"HTTP/1.0">>) -> 'HTTP/1.0'.
-ifdef(TEST).
parse_version_test() ->
'HTTP/1.1' = parse_version(<<"HTTP/1.1">>),
'HTTP/1.0' = parse_version(<<"HTTP/1.0">>),
{'EXIT', _} = (catch parse_version(<<"HTTP/1.2">>)),
ok.
-endif.
%% @doc Return formatted request-line and headers.
%% @todo Add tests when the corresponding reverse functions are added.
-spec request(binary(), iodata(), version(), headers()) -> iodata().
request(Method, Path, Version, Headers) ->
[Method, <<" ">>, Path, <<" ">>, version(Version), <<"\r\n">>,
[[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers],
<<"\r\n">>].
-spec response(status() | binary(), version(), headers()) -> iodata().
response(Status, Version, Headers) ->
[version(Version), <<" ">>, status(Status), <<"\r\n">>,
headers(Headers), <<"\r\n">>].
-spec headers(headers()) -> iodata().
headers(Headers) ->
[[N, <<": ">>, V, <<"\r\n">>] || {N, V} <- Headers].
%% @doc Return the version as a binary.
-spec version(version()) -> binary().
version('HTTP/1.1') -> <<"HTTP/1.1">>;
version('HTTP/1.0') -> <<"HTTP/1.0">>.
-ifdef(TEST).
version_test() ->
<<"HTTP/1.1">> = version('HTTP/1.1'),
<<"HTTP/1.0">> = version('HTTP/1.0'),
{'EXIT', _} = (catch version('HTTP/1.2')),
ok.
-endif.
%% @doc Return the status code and string as binary.
-spec status(status() | binary()) -> binary().
status(100) -> <<"100 Continue">>;
status(101) -> <<"101 Switching Protocols">>;
status(102) -> <<"102 Processing">>;
status(103) -> <<"103 Early Hints">>;
status(200) -> <<"200 OK">>;
status(201) -> <<"201 Created">>;
status(202) -> <<"202 Accepted">>;
status(203) -> <<"203 Non-Authoritative Information">>;
status(204) -> <<"204 No Content">>;
status(205) -> <<"205 Reset Content">>;
status(206) -> <<"206 Partial Content">>;
status(207) -> <<"207 Multi-Status">>;
status(208) -> <<"208 Already Reported">>;
status(226) -> <<"226 IM Used">>;
status(300) -> <<"300 Multiple Choices">>;
status(301) -> <<"301 Moved Permanently">>;
status(302) -> <<"302 Found">>;
status(303) -> <<"303 See Other">>;
status(304) -> <<"304 Not Modified">>;
status(305) -> <<"305 Use Proxy">>;
status(306) -> <<"306 Switch Proxy">>;
status(307) -> <<"307 Temporary Redirect">>;
status(308) -> <<"308 Permanent Redirect">>;
status(400) -> <<"400 Bad Request">>;
status(401) -> <<"401 Unauthorized">>;
status(402) -> <<"402 Payment Required">>;
status(403) -> <<"403 Forbidden">>;
status(404) -> <<"404 Not Found">>;
status(405) -> <<"405 Method Not Allowed">>;
status(406) -> <<"406 Not Acceptable">>;
status(407) -> <<"407 Proxy Authentication Required">>;
status(408) -> <<"408 Request Timeout">>;
status(409) -> <<"409 Conflict">>;
status(410) -> <<"410 Gone">>;
status(411) -> <<"411 Length Required">>;
status(412) -> <<"412 Precondition Failed">>;
status(413) -> <<"413 Request Entity Too Large">>;
status(414) -> <<"414 Request-URI Too Long">>;
status(415) -> <<"415 Unsupported Media Type">>;
status(416) -> <<"416 Requested Range Not Satisfiable">>;
status(417) -> <<"417 Expectation Failed">>;
status(418) -> <<"418 I'm a teapot">>;
status(421) -> <<"421 Misdirected Request">>;
status(422) -> <<"422 Unprocessable Entity">>;
status(423) -> <<"423 Locked">>;
status(424) -> <<"424 Failed Dependency">>;
status(425) -> <<"425 Unordered Collection">>;
status(426) -> <<"426 Upgrade Required">>;
status(428) -> <<"428 Precondition Required">>;
status(429) -> <<"429 Too Many Requests">>;
status(431) -> <<"431 Request Header Fields Too Large">>;
status(451) -> <<"451 Unavailable For Legal Reasons">>;
status(500) -> <<"500 Internal Server Error">>;
status(501) -> <<"501 Not Implemented">>;
status(502) -> <<"502 Bad Gateway">>;
status(503) -> <<"503 Service Unavailable">>;
status(504) -> <<"504 Gateway Timeout">>;
status(505) -> <<"505 HTTP Version Not Supported">>;
status(506) -> <<"506 Variant Also Negotiates">>;
status(507) -> <<"507 Insufficient Storage">>;
status(508) -> <<"508 Loop Detected">>;
status(510) -> <<"510 Not Extended">>;
status(511) -> <<"511 Network Authentication Required">>;
status(B) when is_binary(B) -> B.

+ 0
- 483
src/wsLib/cow_http2.erl Vedi File

@ -1,483 +0,0 @@
%% Copyright (c) 2015-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_http2).
%% Parsing.
-export([parse_sequence/1]).
-export([parse/1]).
-export([parse/2]).
-export([parse_settings_payload/1]).
%% Building.
-export([data/3]).
-export([data_header/3]).
-export([headers/3]).
-export([priority/4]).
-export([rst_stream/2]).
-export([settings/1]).
-export([settings_payload/1]).
-export([settings_ack/0]).
-export([push_promise/3]).
-export([ping/1]).
-export([ping_ack/1]).
-export([goaway/3]).
-export([window_update/1]).
-export([window_update/2]).
-type streamid() :: pos_integer().
-export_type([streamid/0]).
-type fin() :: fin | nofin.
-export_type([fin/0]).
-type head_fin() :: head_fin | head_nofin.
-export_type([head_fin/0]).
-type exclusive() :: exclusive | shared.
-type weight() :: 1..256.
-type settings() :: map().
-type error() :: no_error
| protocol_error
| internal_error
| flow_control_error
| settings_timeout
| stream_closed
| frame_size_error
| refused_stream
| cancel
| compression_error
| connect_error
| enhance_your_calm
| inadequate_security
| http_1_1_required
| unknown_error.
-export_type([error/0]).
-type frame() :: {data, streamid(), fin(), binary()}
| {headers, streamid(), fin(), head_fin(), binary()}
| {headers, streamid(), fin(), head_fin(), exclusive(), streamid(), weight(), binary()}
| {priority, streamid(), exclusive(), streamid(), weight()}
| {rst_stream, streamid(), error()}
| {settings, settings()}
| settings_ack
| {push_promise, streamid(), head_fin(), streamid(), binary()}
| {ping, integer()}
| {ping_ack, integer()}
| {goaway, streamid(), error(), binary()}
| {window_update, non_neg_integer()}
| {window_update, streamid(), non_neg_integer()}
| {continuation, streamid(), head_fin(), binary()}.
-export_type([frame/0]).
%% Parsing.
-spec parse_sequence(binary())
-> {ok, binary()} | more | {connection_error, error(), atom()}.
parse_sequence(<<"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", Rest/bits>>) ->
{ok, Rest};
parse_sequence(Data) when byte_size(Data) >= 24 ->
{connection_error, protocol_error,
'The connection preface was invalid. (RFC7540 3.5)'};
parse_sequence(Data) ->
Len = byte_size(Data),
<<Preface:Len/binary, _/bits>> = <<"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n">>,
case Data of
Preface ->
more;
_ ->
{connection_error, protocol_error,
'The connection preface was invalid. (RFC7540 3.5)'}
end.
parse(<<Len:24, _/bits>>, MaxFrameSize) when Len > MaxFrameSize ->
{connection_error, frame_size_error, 'The frame size exceeded SETTINGS_MAX_FRAME_SIZE. (RFC7540 4.2)'};
parse(Data, _) ->
parse(Data).
%%
%% DATA frames.
%%
parse(<<_:24, 0:8, _:9, 0:31, _/bits>>) ->
{connection_error, protocol_error, 'DATA frames MUST be associated with a stream. (RFC7540 6.1)'};
parse(<<0:24, 0:8, _:4, 1:1, _:35, _/bits>>) ->
{connection_error, frame_size_error, 'DATA frames with padding flag MUST have a length > 0. (RFC7540 6.1)'};
parse(<<Len0:24, 0:8, _:4, 1:1, _:35, PadLen:8, _/bits>>) when PadLen >= Len0 ->
{connection_error, protocol_error, 'Length of padding MUST be less than length of payload. (RFC7540 6.1)'};
%% No padding.
parse(<<Len:24, 0:8, _:4, 0:1, _:2, FlagEndStream:1, _:1, StreamID:31, Data:Len/binary, Rest/bits>>) ->
{ok, {data, StreamID, parse_fin(FlagEndStream), Data}, Rest};
%% Padding.
parse(<<Len0:24, 0:8, _:4, 1:1, _:2, FlagEndStream:1, _:1, StreamID:31, PadLen:8, Rest0/bits>>)
when byte_size(Rest0) >= Len0 - 1 ->
Len = Len0 - PadLen - 1,
case Rest0 of
<<Data:Len/binary, 0:PadLen/unit:8, Rest/bits>> ->
{ok, {data, StreamID, parse_fin(FlagEndStream), Data}, Rest};
_ ->
{connection_error, protocol_error, 'Padding octets MUST be set to zero. (RFC7540 6.1)'}
end;
%%
%% HEADERS frames.
%%
parse(<<_:24, 1:8, _:9, 0:31, _/bits>>) ->
{connection_error, protocol_error, 'HEADERS frames MUST be associated with a stream. (RFC7540 6.2)'};
parse(<<0:24, 1:8, _:4, 1:1, _:35, _/bits>>) ->
{connection_error, frame_size_error, 'HEADERS frames with padding flag MUST have a length > 0. (RFC7540 6.1)'};
parse(<<Len:24, 1:8, _:2, 1:1, _:37, _/bits>>) when Len < 5 ->
{connection_error, frame_size_error, 'HEADERS frames with priority flag MUST have a length >= 5. (RFC7540 6.1)'};
parse(<<Len:24, 1:8, _:2, 1:1, _:1, 1:1, _:35, _/bits>>) when Len < 6 ->
{connection_error, frame_size_error, 'HEADERS frames with padding and priority flags MUST have a length >= 6. (RFC7540 6.1)'};
parse(<<Len0:24, 1:8, _:4, 1:1, _:35, PadLen:8, _/bits>>) when PadLen >= Len0 ->
{connection_error, protocol_error, 'Length of padding MUST be less than length of payload. (RFC7540 6.2)'};
parse(<<Len0:24, 1:8, _:2, 1:1, _:1, 1:1, _:35, PadLen:8, _/bits>>) when PadLen >= Len0 - 5 ->
{connection_error, protocol_error, 'Length of padding MUST be less than length of payload. (RFC7540 6.2)'};
%% No padding, no priority.
parse(<<Len:24, 1:8, _:2, 0:1, _:1, 0:1, FlagEndHeaders:1, _:1, FlagEndStream:1, _:1, StreamID:31,
HeaderBlockFragment:Len/binary, Rest/bits>>) ->
{ok, {headers, StreamID, parse_fin(FlagEndStream), parse_head_fin(FlagEndHeaders), HeaderBlockFragment}, Rest};
%% Padding, no priority.
parse(<<Len0:24, 1:8, _:2, 0:1, _:1, 1:1, FlagEndHeaders:1, _:1, FlagEndStream:1, _:1, StreamID:31,
PadLen:8, Rest0/bits>>) when byte_size(Rest0) >= Len0 - 1 ->
Len = Len0 - PadLen - 1,
case Rest0 of
<<HeaderBlockFragment:Len/binary, 0:PadLen/unit:8, Rest/bits>> ->
{ok, {headers, StreamID, parse_fin(FlagEndStream), parse_head_fin(FlagEndHeaders), HeaderBlockFragment}, Rest};
_ ->
{connection_error, protocol_error, 'Padding octets MUST be set to zero. (RFC7540 6.2)'}
end;
%% No padding, priority.
parse(<<_:24, 1:8, _:2, 1:1, _:1, 0:1, _:4, StreamID:31, _:1, StreamID:31, _/bits>>) ->
{connection_error, protocol_error,
'HEADERS frames cannot define a stream that depends on itself. (RFC7540 5.3.1)'};
parse(<<Len0:24, 1:8, _:2, 1:1, _:1, 0:1, FlagEndHeaders:1, _:1, FlagEndStream:1, _:1, StreamID:31,
E:1, DepStreamID:31, Weight:8, Rest0/bits>>) when byte_size(Rest0) >= Len0 - 5 ->
Len = Len0 - 5,
<<HeaderBlockFragment:Len/binary, Rest/bits>> = Rest0,
{ok, {headers, StreamID, parse_fin(FlagEndStream), parse_head_fin(FlagEndHeaders),
parse_exclusive(E), DepStreamID, Weight + 1, HeaderBlockFragment}, Rest};
%% Padding, priority.
parse(<<_:24, 1:8, _:2, 1:1, _:1, 1:1, _:4, StreamID:31, _:9, StreamID:31, _/bits>>) ->
{connection_error, protocol_error,
'HEADERS frames cannot define a stream that depends on itself. (RFC7540 5.3.1)'};
parse(<<Len0:24, 1:8, _:2, 1:1, _:1, 1:1, FlagEndHeaders:1, _:1, FlagEndStream:1, _:1, StreamID:31,
PadLen:8, E:1, DepStreamID:31, Weight:8, Rest0/bits>>) when byte_size(Rest0) >= Len0 - 6 ->
Len = Len0 - PadLen - 6,
case Rest0 of
<<HeaderBlockFragment:Len/binary, 0:PadLen/unit:8, Rest/bits>> ->
{ok, {headers, StreamID, parse_fin(FlagEndStream), parse_head_fin(FlagEndHeaders),
parse_exclusive(E), DepStreamID, Weight + 1, HeaderBlockFragment}, Rest};
_ ->
{connection_error, protocol_error, 'Padding octets MUST be set to zero. (RFC7540 6.2)'}
end;
%%
%% PRIORITY frames.
%%
parse(<<5:24, 2:8, _:9, 0:31, _/bits>>) ->
{connection_error, protocol_error, 'PRIORITY frames MUST be associated with a stream. (RFC7540 6.3)'};
parse(<<5:24, 2:8, _:9, StreamID:31, _:1, StreamID:31, _:8, Rest/bits>>) ->
{stream_error, StreamID, protocol_error,
'PRIORITY frames cannot make a stream depend on itself. (RFC7540 5.3.1)', Rest};
parse(<<5:24, 2:8, _:9, StreamID:31, E:1, DepStreamID:31, Weight:8, Rest/bits>>) ->
{ok, {priority, StreamID, parse_exclusive(E), DepStreamID, Weight + 1}, Rest};
%% @todo figure out how to best deal with frame size errors; if we have everything fine
%% if not we might want to inform the caller how much he should expect so that it can
%% decide if it should just close the connection
parse(<<BadLen:24, 2:8, _:9, StreamID:31, _:BadLen/binary, Rest/bits>>) ->
{stream_error, StreamID, frame_size_error, 'PRIORITY frames MUST be 5 bytes wide. (RFC7540 6.3)', Rest};
%%
%% RST_STREAM frames.
%%
parse(<<4:24, 3:8, _:9, 0:31, _/bits>>) ->
{connection_error, protocol_error, 'RST_STREAM frames MUST be associated with a stream. (RFC7540 6.4)'};
parse(<<4:24, 3:8, _:9, StreamID:31, ErrorCode:32, Rest/bits>>) ->
{ok, {rst_stream, StreamID, parse_error_code(ErrorCode)}, Rest};
%% @todo same as priority
parse(<<_:24, 3:8, _:9, _:31, _/bits>>) ->
{connection_error, frame_size_error, 'RST_STREAM frames MUST be 4 bytes wide. (RFC7540 6.4)'};
%%
%% SETTINGS frames.
%%
parse(<<0:24, 4:8, _:7, 1:1, _:1, 0:31, Rest/bits>>) ->
{ok, settings_ack, Rest};
parse(<<_:24, 4:8, _:7, 1:1, _:1, 0:31, _/bits>>) ->
{connection_error, frame_size_error, 'SETTINGS frames with the ACK flag set MUST have a length of 0. (RFC7540 6.5)'};
parse(<<Len:24, 4:8, _:7, 0:1, _:1, 0:31, _/bits>>) when Len rem 6 =/= 0 ->
{connection_error, frame_size_error, 'SETTINGS frames MUST have a length multiple of 6. (RFC7540 6.5)'};
parse(<<Len:24, 4:8, _:7, 0:1, _:1, 0:31, Rest/bits>>) when byte_size(Rest) >= Len ->
parse_settings_payload(Rest, Len, #{});
parse(<<_:24, 4:8, _:8, _:1, StreamID:31, _/bits>>) when StreamID =/= 0 ->
{connection_error, protocol_error, 'SETTINGS frames MUST NOT be associated with a stream. (RFC7540 6.5)'};
%%
%% PUSH_PROMISE frames.
%%
parse(<<Len:24, 5:8, _:40, _/bits>>) when Len < 4 ->
{connection_error, frame_size_error, 'PUSH_PROMISE frames MUST have a length >= 4. (RFC7540 4.2, RFC7540 6.6)'};
parse(<<Len:24, 5:8, _:4, 1:1, _:35, _/bits>>) when Len < 5 ->
{connection_error, frame_size_error, 'PUSH_PROMISE frames with padding flag MUST have a length >= 5. (RFC7540 4.2, RFC7540 6.6)'};
parse(<<_:24, 5:8, _:9, 0:31, _/bits>>) ->
{connection_error, protocol_error, 'PUSH_PROMISE frames MUST be associated with a stream. (RFC7540 6.6)'};
parse(<<Len0:24, 5:8, _:4, 1:1, _:35, PadLen:8, _/bits>>) when PadLen >= Len0 - 4 ->
{connection_error, protocol_error, 'Length of padding MUST be less than length of payload. (RFC7540 6.6)'};
parse(<<Len0:24, 5:8, _:4, 0:1, FlagEndHeaders:1, _:3, StreamID:31, _:1, PromisedStreamID:31, Rest0/bits>>)
when byte_size(Rest0) >= Len0 - 4 ->
Len = Len0 - 4,
<<HeaderBlockFragment:Len/binary, Rest/bits>> = Rest0,
{ok, {push_promise, StreamID, parse_head_fin(FlagEndHeaders), PromisedStreamID, HeaderBlockFragment}, Rest};
parse(<<Len0:24, 5:8, _:4, 1:1, FlagEndHeaders:1, _:2, StreamID:31, PadLen:8, _:1, PromisedStreamID:31, Rest0/bits>>)
when byte_size(Rest0) >= Len0 - 5 ->
Len = Len0 - 5,
case Rest0 of
<<HeaderBlockFragment:Len/binary, 0:PadLen/unit:8, Rest/bits>> ->
{ok, {push_promise, StreamID, parse_head_fin(FlagEndHeaders), PromisedStreamID, HeaderBlockFragment}, Rest};
_ ->
{connection_error, protocol_error, 'Padding octets MUST be set to zero. (RFC7540 6.6)'}
end;
%%
%% PING frames.
%%
parse(<<8:24, 6:8, _:7, 1:1, _:1, 0:31, Opaque:64, Rest/bits>>) ->
{ok, {ping_ack, Opaque}, Rest};
parse(<<8:24, 6:8, _:7, 0:1, _:1, 0:31, Opaque:64, Rest/bits>>) ->
{ok, {ping, Opaque}, Rest};
parse(<<8:24, 6:8, _:104, _/bits>>) ->
{connection_error, protocol_error, 'PING frames MUST NOT be associated with a stream. (RFC7540 6.7)'};
parse(<<Len:24, 6:8, _/bits>>) when Len =/= 8 ->
{connection_error, frame_size_error, 'PING frames MUST be 8 bytes wide. (RFC7540 6.7)'};
%%
%% GOAWAY frames.
%%
parse(<<Len0:24, 7:8, _:9, 0:31, _:1, LastStreamID:31, ErrorCode:32, Rest0/bits>>) when byte_size(Rest0) >= Len0 - 8 ->
Len = Len0 - 8,
<<DebugData:Len/binary, Rest/bits>> = Rest0,
{ok, {goaway, LastStreamID, parse_error_code(ErrorCode), DebugData}, Rest};
parse(<<Len:24, 7:8, _:40, _/bits>>) when Len < 8 ->
{connection_error, frame_size_error, 'GOAWAY frames MUST have a length >= 8. (RFC7540 4.2, RFC7540 6.8)'};
parse(<<_:24, 7:8, _:40, _/bits>>) ->
{connection_error, protocol_error, 'GOAWAY frames MUST NOT be associated with a stream. (RFC7540 6.8)'};
%%
%% WINDOW_UPDATE frames.
%%
parse(<<4:24, 8:8, _:9, 0:31, _:1, 0:31, _/bits>>) ->
{connection_error, protocol_error, 'WINDOW_UPDATE frames MUST have a non-zero increment. (RFC7540 6.9)'};
parse(<<4:24, 8:8, _:9, 0:31, _:1, Increment:31, Rest/bits>>) ->
{ok, {window_update, Increment}, Rest};
parse(<<4:24, 8:8, _:9, StreamID:31, _:1, 0:31, Rest/bits>>) ->
{stream_error, StreamID, protocol_error, 'WINDOW_UPDATE frames MUST have a non-zero increment. (RFC7540 6.9)', Rest};
parse(<<4:24, 8:8, _:9, StreamID:31, _:1, Increment:31, Rest/bits>>) ->
{ok, {window_update, StreamID, Increment}, Rest};
parse(<<Len:24, 8:8, _/bits>>) when Len =/= 4 ->
{connection_error, frame_size_error, 'WINDOW_UPDATE frames MUST be 4 bytes wide. (RFC7540 6.9)'};
%%
%% CONTINUATION frames.
%%
parse(<<_:24, 9:8, _:9, 0:31, _/bits>>) ->
{connection_error, protocol_error, 'CONTINUATION frames MUST be associated with a stream. (RFC7540 6.10)'};
parse(<<Len:24, 9:8, _:5, FlagEndHeaders:1, _:3, StreamID:31, HeaderBlockFragment:Len/binary, Rest/bits>>) ->
{ok, {continuation, StreamID, parse_head_fin(FlagEndHeaders), HeaderBlockFragment}, Rest};
%%
%% Unknown frames are ignored.
%%
parse(<<Len:24, Type:8, _:40, _:Len/binary, Rest/bits>>) when Type > 9 ->
{ignore, Rest};
%%
%% Incomplete frames.
%%
parse(_) ->
more.
-ifdef(TEST).
parse_ping_test() ->
Ping = ping(1234567890),
_ = [more = parse(binary:part(Ping, 0, I)) || I <- lists:seq(1, byte_size(Ping) - 1)],
{ok, {ping, 1234567890}, <<>>} = parse(Ping),
{ok, {ping, 1234567890}, <<42>>} = parse(<<Ping/binary, 42>>),
ok.
parse_windows_update_test() ->
WindowUpdate = <<4:24, 8:8, 0:9, 0:31, 0:1, 12345:31>>,
_ = [more = parse(binary:part(WindowUpdate, 0, I)) || I <- lists:seq(1, byte_size(WindowUpdate) - 1)],
{ok, {window_update, 12345}, <<>>} = parse(WindowUpdate),
{ok, {window_update, 12345}, <<42>>} = parse(<<WindowUpdate/binary, 42>>),
ok.
parse_settings_test() ->
more = parse(<<0:24, 4:8, 1:8, 0:8>>),
{ok, settings_ack, <<>>} = parse(<<0:24, 4:8, 1:8, 0:32>>),
{connection_error, protocol_error, _} = parse(<<0:24, 4:8, 1:8, 0:1, 1:31>>),
ok.
-endif.
parse_fin(0) -> nofin;
parse_fin(1) -> fin.
parse_head_fin(0) -> head_nofin;
parse_head_fin(1) -> head_fin.
parse_exclusive(0) -> shared;
parse_exclusive(1) -> exclusive.
parse_error_code(0) -> no_error;
parse_error_code(1) -> protocol_error;
parse_error_code(2) -> internal_error;
parse_error_code(3) -> flow_control_error;
parse_error_code(4) -> settings_timeout;
parse_error_code(5) -> stream_closed;
parse_error_code(6) -> frame_size_error;
parse_error_code(7) -> refused_stream;
parse_error_code(8) -> cancel;
parse_error_code(9) -> compression_error;
parse_error_code(10) -> connect_error;
parse_error_code(11) -> enhance_your_calm;
parse_error_code(12) -> inadequate_security;
parse_error_code(13) -> http_1_1_required;
parse_error_code(_) -> unknown_error.
parse_settings_payload(SettingsPayload) ->
{ok, {settings, Settings}, <<>>}
= parse_settings_payload(SettingsPayload, byte_size(SettingsPayload), #{}),
Settings.
parse_settings_payload(Rest, 0, Settings) ->
{ok, {settings, Settings}, Rest};
%% SETTINGS_HEADER_TABLE_SIZE.
parse_settings_payload(<<1:16, Value:32, Rest/bits>>, Len, Settings) ->
parse_settings_payload(Rest, Len - 6, Settings#{header_table_size => Value});
%% SETTINGS_ENABLE_PUSH.
parse_settings_payload(<<2:16, 0:32, Rest/bits>>, Len, Settings) ->
parse_settings_payload(Rest, Len - 6, Settings#{enable_push => false});
parse_settings_payload(<<2:16, 1:32, Rest/bits>>, Len, Settings) ->
parse_settings_payload(Rest, Len - 6, Settings#{enable_push => true});
parse_settings_payload(<<2:16, _:32, _/bits>>, _, _) ->
{connection_error, protocol_error, 'The SETTINGS_ENABLE_PUSH value MUST be 0 or 1. (RFC7540 6.5.2)'};
%% SETTINGS_MAX_CONCURRENT_STREAMS.
parse_settings_payload(<<3:16, Value:32, Rest/bits>>, Len, Settings) ->
parse_settings_payload(Rest, Len - 6, Settings#{max_concurrent_streams => Value});
%% SETTINGS_INITIAL_WINDOW_SIZE.
parse_settings_payload(<<4:16, Value:32, _/bits>>, _, _) when Value > 16#7fffffff ->
{connection_error, flow_control_error, 'The maximum SETTINGS_INITIAL_WINDOW_SIZE value is 0x7fffffff. (RFC7540 6.5.2)'};
parse_settings_payload(<<4:16, Value:32, Rest/bits>>, Len, Settings) ->
parse_settings_payload(Rest, Len - 6, Settings#{initial_window_size => Value});
%% SETTINGS_MAX_FRAME_SIZE.
parse_settings_payload(<<5:16, Value:32, _/bits>>, _, _) when Value =< 16#3fff ->
{connection_error, protocol_error, 'The SETTINGS_MAX_FRAME_SIZE value must be > 0x3fff. (RFC7540 6.5.2)'};
parse_settings_payload(<<5:16, Value:32, Rest/bits>>, Len, Settings) when Value =< 16#ffffff ->
parse_settings_payload(Rest, Len - 6, Settings#{max_frame_size => Value});
parse_settings_payload(<<5:16, _:32, _/bits>>, _, _) ->
{connection_error, protocol_error, 'The SETTINGS_MAX_FRAME_SIZE value must be =< 0xffffff. (RFC7540 6.5.2)'};
%% SETTINGS_MAX_HEADER_LIST_SIZE.
parse_settings_payload(<<6:16, Value:32, Rest/bits>>, Len, Settings) ->
parse_settings_payload(Rest, Len - 6, Settings#{max_header_list_size => Value});
%% SETTINGS_ENABLE_CONNECT_PROTOCOL.
parse_settings_payload(<<8:16, 0:32, Rest/bits>>, Len, Settings) ->
parse_settings_payload(Rest, Len - 6, Settings#{enable_connect_protocol => false});
parse_settings_payload(<<8:16, 1:32, Rest/bits>>, Len, Settings) ->
parse_settings_payload(Rest, Len - 6, Settings#{enable_connect_protocol => true});
parse_settings_payload(<<8:16, _:32, _/bits>>, _, _) ->
{connection_error, protocol_error, 'The SETTINGS_ENABLE_CONNECT_PROTOCOL value MUST be 0 or 1. (draft-h2-websockets-01 3)'};
%% Ignore unknown settings.
parse_settings_payload(<<_:48, Rest/bits>>, Len, Settings) ->
parse_settings_payload(Rest, Len - 6, Settings).
%% Building.
data(StreamID, IsFin, Data) ->
[data_header(StreamID, IsFin, iolist_size(Data)), Data].
data_header(StreamID, IsFin, Len) ->
FlagEndStream = flag_fin(IsFin),
<<Len:24, 0:15, FlagEndStream:1, 0:1, StreamID:31>>.
%% @todo Check size of HeaderBlock and use CONTINUATION frames if needed.
headers(StreamID, IsFin, HeaderBlock) ->
Len = iolist_size(HeaderBlock),
FlagEndStream = flag_fin(IsFin),
FlagEndHeaders = 1,
[<<Len:24, 1:8, 0:5, FlagEndHeaders:1, 0:1, FlagEndStream:1, 0:1, StreamID:31>>, HeaderBlock].
priority(StreamID, E, DepStreamID, Weight) ->
FlagExclusive = exclusive(E),
<<5:24, 2:8, 0:9, StreamID:31, FlagExclusive:1, DepStreamID:31, Weight:8>>.
rst_stream(StreamID, Reason) ->
ErrorCode = error_code(Reason),
<<4:24, 3:8, 0:9, StreamID:31, ErrorCode:32>>.
settings(Settings) ->
Payload = settings_payload(Settings),
Len = iolist_size(Payload),
[<<Len:24, 4:8, 0:40>>, Payload].
settings_payload(Settings) ->
[case Key of
header_table_size -> <<1:16, Value:32>>;
enable_push when Value -> <<2:16, 1:32>>;
enable_push -> <<2:16, 0:32>>;
max_concurrent_streams when Value =:= infinity -> <<>>;
max_concurrent_streams -> <<3:16, Value:32>>;
initial_window_size -> <<4:16, Value:32>>;
max_frame_size -> <<5:16, Value:32>>;
max_header_list_size when Value =:= infinity -> <<>>;
max_header_list_size -> <<6:16, Value:32>>;
enable_connect_protocol when Value -> <<8:16, 1:32>>;
enable_connect_protocol -> <<8:16, 0:32>>
end || {Key, Value} <- maps:to_list(Settings)].
settings_ack() ->
<<0:24, 4:8, 1:8, 0:32>>.
%% @todo Check size of HeaderBlock and use CONTINUATION frames if needed.
push_promise(StreamID, PromisedStreamID, HeaderBlock) ->
Len = iolist_size(HeaderBlock) + 4,
FlagEndHeaders = 1,
[<<Len:24, 5:8, 0:5, FlagEndHeaders:1, 0:3, StreamID:31, 0:1, PromisedStreamID:31>>, HeaderBlock].
ping(Opaque) ->
<<8:24, 6:8, 0:40, Opaque:64>>.
ping_ack(Opaque) ->
<<8:24, 6:8, 0:7, 1:1, 0:32, Opaque:64>>.
goaway(LastStreamID, Reason, DebugData) ->
ErrorCode = error_code(Reason),
Len = iolist_size(DebugData) + 8,
[<<Len:24, 7:8, 0:41, LastStreamID:31, ErrorCode:32>>, DebugData].
window_update(Increment) ->
window_update(0, Increment).
window_update(StreamID, Increment) when Increment =< 16#7fffffff ->
<<4:24, 8:8, 0:8, StreamID:32, 0:1, Increment:31>>.
flag_fin(nofin) -> 0;
flag_fin(fin) -> 1.
exclusive(shared) -> 0;
exclusive(exclusive) -> 1.
error_code(no_error) -> 0;
error_code(protocol_error) -> 1;
error_code(internal_error) -> 2;
error_code(flow_control_error) -> 3;
error_code(settings_timeout) -> 4;
error_code(stream_closed) -> 5;
error_code(frame_size_error) -> 6;
error_code(refused_stream) -> 7;
error_code(cancel) -> 8;
error_code(compression_error) -> 9;
error_code(connect_error) -> 10;
error_code(enhance_your_calm) -> 11;
error_code(inadequate_security) -> 12;
error_code(http_1_1_required) -> 13.

+ 0
- 1647
src/wsLib/cow_http2_machine.erl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 3645
src/wsLib/cow_http_hd.erl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 420
src/wsLib/cow_http_struct_hd.erl Vedi File

@ -1,420 +0,0 @@
%% Copyright (c) 2019, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% The mapping between Erlang and structured headers types is as follow:
%%
%% List: list()
%% Dictionary: map()
%% Bare item: one bare_item() that can be of type:
%% Integer: integer()
%% Float: float()
%% String: {string, binary()}
%% Token: {token, binary()}
%% Byte sequence: {binary, binary()}
%% Boolean: boolean()
%% And finally:
%% Type with Parameters: {with_params, Type, Parameters}
%% Parameters: [{binary(), bare_item()}]
-module(cow_http_struct_hd).
-export([parse_dictionary/1]).
-export([parse_item/1]).
-export([parse_list/1]).
-export([dictionary/1]).
-export([item/1]).
-export([list/1]).
-include("cow_parse.hrl").
-type sh_list() :: [sh_item() | sh_inner_list()].
-type sh_inner_list() :: sh_with_params([sh_item()]).
-type sh_params() :: #{binary() => sh_bare_item() | undefined}.
-type sh_dictionary() :: {#{binary() => sh_item() | sh_inner_list()}, [binary()]}.
-type sh_item() :: sh_with_params(sh_bare_item()).
-type sh_bare_item() :: integer() | float() | boolean()
| {string | token | binary, binary()}.
-type sh_with_params(Type) :: {with_params, Type, sh_params()}.
-define(IS_LC_ALPHA(C),
(C =:= $a) or (C =:= $b) or (C =:= $c) or (C =:= $d) or (C =:= $e) or
(C =:= $f) or (C =:= $g) or (C =:= $h) or (C =:= $i) or (C =:= $j) or
(C =:= $k) or (C =:= $l) or (C =:= $m) or (C =:= $n) or (C =:= $o) or
(C =:= $p) or (C =:= $q) or (C =:= $r) or (C =:= $s) or (C =:= $t) or
(C =:= $u) or (C =:= $v) or (C =:= $w) or (C =:= $x) or (C =:= $y) or
(C =:= $z)
).
%% Parsing.
-spec parse_dictionary(binary()) -> sh_dictionary().
parse_dictionary(<<>>) ->
{#{}, []};
parse_dictionary(<<C, R/bits>>) when ?IS_LC_ALPHA(C) ->
{Dict, Order, <<>>} = parse_dict_key(R, #{}, [], <<C>>),
{Dict, Order}.
parse_dict_key(<<$=, $(, R0/bits>>, Acc, Order, K) ->
false = maps:is_key(K, Acc),
{Item, R} = parse_inner_list(R0, []),
parse_dict_before_sep(R, Acc#{K => Item}, [K | Order]);
parse_dict_key(<<$=, R0/bits>>, Acc, Order, K) ->
false = maps:is_key(K, Acc),
{Item, R} = parse_item1(R0),
parse_dict_before_sep(R, Acc#{K => Item}, [K | Order]);
parse_dict_key(<<C, R/bits>>, Acc, Order, K)
when ?IS_LC_ALPHA(C) or ?IS_DIGIT(C)
or (C =:= $_) or (C =:= $-) or (C =:= $*) ->
parse_dict_key(R, Acc, Order, <<K/binary, C>>).
parse_dict_before_sep(<<C, R/bits>>, Acc, Order) when ?IS_WS(C) ->
parse_dict_before_sep(R, Acc, Order);
parse_dict_before_sep(<<C, R/bits>>, Acc, Order) when C =:= $, ->
parse_dict_before_member(R, Acc, Order);
parse_dict_before_sep(<<>>, Acc, Order) ->
{Acc, lists:reverse(Order), <<>>}.
parse_dict_before_member(<<C, R/bits>>, Acc, Order) when ?IS_WS(C) ->
parse_dict_before_member(R, Acc, Order);
parse_dict_before_member(<<C, R/bits>>, Acc, Order) when ?IS_LC_ALPHA(C) ->
parse_dict_key(R, Acc, Order, <<C>>).
-spec parse_item(binary()) -> sh_item().
parse_item(Bin) ->
{Item, <<>>} = parse_item1(Bin),
Item.
parse_item1(Bin) ->
case parse_bare_item(Bin) of
{Item, <<$;, R/bits>>} ->
{Params, Rest} = parse_before_param(R, #{}),
{{with_params, Item, Params}, Rest};
{Item, Rest} ->
{{with_params, Item, #{}}, Rest}
end.
-spec parse_list(binary()) -> sh_list().
parse_list(<<>>) ->
[];
parse_list(Bin) ->
parse_list_before_member(Bin, []).
parse_list_member(<<$(, R0/bits>>, Acc) ->
{Item, R} = parse_inner_list(R0, []),
parse_list_before_sep(R, [Item | Acc]);
parse_list_member(R0, Acc) ->
{Item, R} = parse_item1(R0),
parse_list_before_sep(R, [Item | Acc]).
parse_list_before_sep(<<C, R/bits>>, Acc) when ?IS_WS(C) ->
parse_list_before_sep(R, Acc);
parse_list_before_sep(<<$,, R/bits>>, Acc) ->
parse_list_before_member(R, Acc);
parse_list_before_sep(<<>>, Acc) ->
lists:reverse(Acc).
parse_list_before_member(<<C, R/bits>>, Acc) when ?IS_WS(C) ->
parse_list_before_member(R, Acc);
parse_list_before_member(R, Acc) ->
parse_list_member(R, Acc).
%% Internal.
parse_inner_list(<<C, R/bits>>, Acc) when ?IS_WS(C) ->
parse_inner_list(R, Acc);
parse_inner_list(<<$), $;, R0/bits>>, Acc) ->
{Params, R} = parse_before_param(R0, #{}),
{{with_params, lists:reverse(Acc), Params}, R};
parse_inner_list(<<$), R/bits>>, Acc) ->
{{with_params, lists:reverse(Acc), #{}}, R};
parse_inner_list(R0, Acc) ->
{Item, R = <<C, _/bits>>} = parse_item1(R0),
true = (C =:= $\s) orelse (C =:= $)),
parse_inner_list(R, [Item | Acc]).
parse_before_param(<<C, R/bits>>, Acc) when ?IS_WS(C) ->
parse_before_param(R, Acc);
parse_before_param(<<C, R/bits>>, Acc) when ?IS_LC_ALPHA(C) ->
parse_param(R, Acc, <<C>>).
parse_param(<<$;, R/bits>>, Acc, K) ->
parse_before_param(R, Acc#{K => undefined});
parse_param(<<$=, R0/bits>>, Acc, K) ->
case parse_bare_item(R0) of
{Item, <<$;, R/bits>>} ->
false = maps:is_key(K, Acc),
parse_before_param(R, Acc#{K => Item});
{Item, R} ->
false = maps:is_key(K, Acc),
{Acc#{K => Item}, R}
end;
parse_param(<<C, R/bits>>, Acc, K)
when ?IS_LC_ALPHA(C) or ?IS_DIGIT(C)
or (C =:= $_) or (C =:= $-) or (C =:= $*) ->
parse_param(R, Acc, <<K/binary, C>>);
parse_param(R, Acc, K) ->
false = maps:is_key(K, Acc),
{Acc#{K => undefined}, R}.
%% Integer or float.
parse_bare_item(<<$-, R/bits>>) -> parse_number(R, 0, <<$->>);
parse_bare_item(<<C, R/bits>>) when ?IS_DIGIT(C) -> parse_number(R, 1, <<C>>);
%% String.
parse_bare_item(<<$", R/bits>>) -> parse_string(R, <<>>);
%% Token.
parse_bare_item(<<C, R/bits>>) when ?IS_ALPHA(C) -> parse_token(R, <<C>>);
%% Byte sequence.
parse_bare_item(<<$*, R/bits>>) -> parse_binary(R, <<>>);
%% Boolean.
parse_bare_item(<<"?0", R/bits>>) -> {false, R};
parse_bare_item(<<"?1", R/bits>>) -> {true, R}.
parse_number(<<C, R/bits>>, L, Acc) when ?IS_DIGIT(C) ->
parse_number(R, L + 1, <<Acc/binary, C>>);
parse_number(<<C, R/bits>>, L, Acc) when C =:= $. ->
parse_float(R, L, 0, <<Acc/binary, C>>);
parse_number(R, L, Acc) when L =< 15 ->
{binary_to_integer(Acc), R}.
parse_float(<<C, R/bits>>, L1, L2, Acc) when ?IS_DIGIT(C) ->
parse_float(R, L1, L2 + 1, <<Acc/binary, C>>);
parse_float(R, L1, L2, Acc) when
L1 =< 9, L2 =< 6;
L1 =< 10, L2 =< 5;
L1 =< 11, L2 =< 4;
L1 =< 12, L2 =< 3;
L1 =< 13, L2 =< 2;
L1 =< 14, L2 =< 1 ->
{binary_to_float(Acc), R}.
parse_string(<<$\\, $", R/bits>>, Acc) ->
parse_string(R, <<Acc/binary, $">>);
parse_string(<<$\\, $\\, R/bits>>, Acc) ->
parse_string(R, <<Acc/binary, $\\>>);
parse_string(<<$", R/bits>>, Acc) ->
{{string, Acc}, R};
parse_string(<<C, R/bits>>, Acc) when
C >= 16#20, C =< 16#21;
C >= 16#23, C =< 16#5b;
C >= 16#5d, C =< 16#7e ->
parse_string(R, <<Acc/binary, C>>).
parse_token(<<C, R/bits>>, Acc) when ?IS_TOKEN(C) or (C =:= $:) or (C =:= $/) ->
parse_token(R, <<Acc/binary, C>>);
parse_token(R, Acc) ->
{{token, Acc}, R}.
parse_binary(<<$*, R/bits>>, Acc) ->
{{binary, base64:decode(Acc)}, R};
parse_binary(<<C, R/bits>>, Acc) when ?IS_ALPHANUM(C) or (C =:= $+) or (C =:= $/) or (C =:= $=) ->
parse_binary(R, <<Acc/binary, C>>).
-ifdef(TEST).
parse_struct_hd_test_() ->
Files = filelib:wildcard("deps/structured-header-tests/*.json"),
lists:flatten([begin
{ok, JSON} = file:read_file(File),
Tests = jsx:decode(JSON, [return_maps]),
[
{iolist_to_binary(io_lib:format("~s: ~s", [filename:basename(File), Name])), fun() ->
%% The implementation is strict. We fail whenever we can.
CanFail = maps:get(<<"can_fail">>, Test, false),
MustFail = maps:get(<<"must_fail">>, Test, false),
Expected = case MustFail of
true -> undefined;
false -> expected_to_term(maps:get(<<"expected">>, Test))
end,
Raw = raw_to_binary(Raw0),
case HeaderType of
<<"dictionary">> when MustFail; CanFail ->
{'EXIT', _} = (catch parse_dictionary(Raw));
%% The test "binary.json: non-zero pad bits" does not fail
%% due to our reliance on Erlang/OTP's base64 module.
<<"item">> when CanFail ->
case (catch parse_item(Raw)) of
{'EXIT', _} -> ok;
Expected -> ok
end;
<<"item">> when MustFail ->
{'EXIT', _} = (catch parse_item(Raw));
<<"list">> when MustFail; CanFail ->
{'EXIT', _} = (catch parse_list(Raw));
<<"dictionary">> ->
{Expected, _Order} = (catch parse_dictionary(Raw));
<<"item">> ->
Expected = (catch parse_item(Raw));
<<"list">> ->
Expected = (catch parse_list(Raw))
end
end}
|| Test = #{
<<"name">> := Name,
<<"header_type">> := HeaderType,
<<"raw">> := Raw0
} <- Tests]
end || File <- Files]).
%% Item.
expected_to_term(E = [_, Params]) when is_map(Params) ->
e2t(E);
%% Outer list.
expected_to_term(Expected) when is_list(Expected) ->
[e2t(E) || E <- Expected];
expected_to_term(Expected) ->
e2t(Expected).
%% Dictionary.
e2t(Dict) when is_map(Dict) ->
maps:map(fun(_, V) -> e2t(V) end, Dict);
%% Inner list.
e2t([List, Params]) when is_list(List) ->
{with_params, [e2t(E) || E <- List],
maps:map(fun(_, P) -> e2tb(P) end, Params)};
%% Item.
e2t([Bare, Params]) ->
{with_params, e2tb(Bare),
maps:map(fun(_, P) -> e2tb(P) end, Params)}.
%% Bare item.
e2tb(#{<<"__type">> := <<"token">>, <<"value">> := V}) ->
{token, V};
e2tb(#{<<"__type">> := <<"binary">>, <<"value">> := V}) ->
{binary, base32:decode(V)};
e2tb(V) when is_binary(V) ->
{string, V};
e2tb(null) ->
undefined;
e2tb(V) ->
V.
%% The Cowlib parsers currently do not support resuming parsing
%% in the case of multiple headers. To make tests work we modify
%% the raw value the same way Cowboy does when encountering
%% multiple headers: by adding a comma and space in between.
%%
%% Similarly, the Cowlib parsers expect the leading and trailing
%% whitespace to be removed before calling the parser.
raw_to_binary(RawList) ->
trim_ws(iolist_to_binary(lists:join(<<", ">>, RawList))).
trim_ws(<<C, R/bits>>) when ?IS_WS(C) -> trim_ws(R);
trim_ws(R) -> trim_ws_end(R, byte_size(R) - 1).
trim_ws_end(_, -1) ->
<<>>;
trim_ws_end(Value, N) ->
case binary:at(Value, N) of
$\s -> trim_ws_end(Value, N - 1);
$\t -> trim_ws_end(Value, N - 1);
_ ->
S = N + 1,
<<Value2:S/binary, _/bits>> = Value,
Value2
end.
-endif.
%% Building.
-spec dictionary(#{binary() => sh_item() | sh_inner_list()}
| [{binary(), sh_item() | sh_inner_list()}])
-> iolist().
%% @todo Also accept this? dictionary({Map, Order}) ->
dictionary(Map) when is_map(Map) ->
dictionary(maps:to_list(Map));
dictionary(KVList) when is_list(KVList) ->
lists:join(<<", ">>, [
[Key, $=, item_or_inner_list(Value)]
|| {Key, Value} <- KVList]).
-spec item(sh_item()) -> iolist().
item({with_params, BareItem, Params}) ->
[bare_item(BareItem), params(Params)].
-spec list(sh_list()) -> iolist().
list(List) ->
lists:join(<<", ">>, [item_or_inner_list(Value) || Value <- List]).
item_or_inner_list(Value = {with_params, List, _}) when is_list(List) ->
inner_list(Value);
item_or_inner_list(Value) ->
item(Value).
inner_list({with_params, List, Params}) ->
[$(, lists:join($\s, [item(Value) || Value <- List]), $), params(Params)].
bare_item({string, String}) ->
[$", escape_string(String, <<>>), $"];
bare_item({token, Token}) ->
Token;
bare_item({binary, Binary}) ->
[$*, base64:encode(Binary), $*];
bare_item(Integer) when is_integer(Integer) ->
integer_to_binary(Integer);
%% In order to properly reproduce the float as a string we
%% must first determine how many decimals we want in the
%% fractional component, otherwise rounding errors may occur.
bare_item(Float) when is_float(Float) ->
Decimals = case trunc(Float) of
I when I >= 10000000000000 -> 1;
I when I >= 1000000000000 -> 2;
I when I >= 100000000000 -> 3;
I when I >= 10000000000 -> 4;
I when I >= 1000000000 -> 5;
_ -> 6
end,
float_to_binary(Float, [{decimals, Decimals}, compact]);
bare_item(true) ->
<<"?1">>;
bare_item(false) ->
<<"?0">>.
escape_string(<<>>, Acc) -> Acc;
escape_string(<<$\\, R/bits>>, Acc) -> escape_string(R, <<Acc/binary, $\\, $\\>>);
escape_string(<<$", R/bits>>, Acc) -> escape_string(R, <<Acc/binary, $\\, $">>);
escape_string(<<C, R/bits>>, Acc) -> escape_string(R, <<Acc/binary, C>>).
params(Params) ->
maps:fold(fun
(Key, undefined, Acc) ->
[[$;, Key] | Acc];
(Key, Value, Acc) ->
[[$;, Key, $=, bare_item(Value)] | Acc]
end, [], Params).
-ifdef(TEST).
struct_hd_identity_test_() ->
Files = filelib:wildcard("deps/structured-header-tests/*.json"),
lists:flatten([begin
{ok, JSON} = file:read_file(File),
Tests = jsx:decode(JSON, [return_maps]),
[
{iolist_to_binary(io_lib:format("~s: ~s", [filename:basename(File), Name])), fun() ->
Expected = expected_to_term(Expected0),
case HeaderType of
<<"dictionary">> ->
{Expected, _Order} = parse_dictionary(iolist_to_binary(dictionary(Expected)));
<<"item">> ->
Expected = parse_item(iolist_to_binary(item(Expected)));
<<"list">> ->
Expected = parse_list(iolist_to_binary(list(Expected)))
end
end}
|| #{
<<"name">> := Name,
<<"header_type">> := HeaderType,
%% We only run tests that must not fail.
<<"expected">> := Expected0
} <- Tests]
end || File <- Files]).
-endif.

+ 0
- 373
src/wsLib/cow_http_te.erl Vedi File

@ -1,373 +0,0 @@
%% Copyright (c) 2014-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_http_te).
%% Identity.
-export([stream_identity/2]).
-export([identity/1]).
%% Chunked.
-export([stream_chunked/2]).
-export([chunk/1]).
-export([last_chunk/0]).
%% The state type is the same for both identity and chunked.
-type state() :: {non_neg_integer(), non_neg_integer()}.
-export_type([state/0]).
-type decode_ret() :: more
| {more, Data :: binary(), state()}
| {more, Data :: binary(), RemLen :: non_neg_integer(), state()}
| {more, Data :: binary(), Rest :: binary(), state()}
| {done, HasTrailers :: trailers | no_trailers, Rest :: binary()}
| {done, Data :: binary(), HasTrailers :: trailers | no_trailers, Rest :: binary()}.
-export_type([decode_ret/0]).
-include("cow_parse.hrl").
-ifdef(TEST).
dripfeed(<<C, Rest/bits>>, Acc, State, F) ->
case F(<<Acc/binary, C>>, State) of
more ->
dripfeed(Rest, <<Acc/binary, C>>, State, F);
{more, _, State2} ->
dripfeed(Rest, <<>>, State2, F);
{more, _, Length, State2} when is_integer(Length) ->
dripfeed(Rest, <<>>, State2, F);
{more, _, Acc2, State2} ->
dripfeed(Rest, Acc2, State2, F);
{done, _, <<>>} ->
ok;
{done, _, _, <<>>} ->
ok
end.
-endif.
%% Identity.
%% @doc Decode an identity stream.
-spec stream_identity(Data, State)
-> {more, Data, Len, State} | {done, Data, Len, Data}
when Data :: binary(), State :: state(), Len :: non_neg_integer().
stream_identity(Data, {Streamed, Total}) ->
Streamed2 = Streamed + byte_size(Data),
if
Streamed2 < Total ->
{more, Data, Total - Streamed2, {Streamed2, Total}};
true ->
Size = Total - Streamed,
<<Data2:Size/binary, Rest/bits>> = Data,
{done, Data2, Total, Rest}
end.
-spec identity(Data) -> Data when Data :: iodata().
identity(Data) ->
Data.
-ifdef(TEST).
stream_identity_test() ->
{done, <<>>, 0, <<>>}
= stream_identity(identity(<<>>), {0, 0}),
{done, <<"\r\n">>, 2, <<>>}
= stream_identity(identity(<<"\r\n">>), {0, 2}),
{done, <<0:80000>>, 10000, <<>>}
= stream_identity(identity(<<0:80000>>), {0, 10000}),
ok.
stream_identity_parts_test() ->
{more, <<0:8000>>, 1999, S1}
= stream_identity(<<0:8000>>, {0, 2999}),
{more, <<0:8000>>, 999, S2}
= stream_identity(<<0:8000>>, S1),
{done, <<0:7992>>, 2999, <<>>}
= stream_identity(<<0:7992>>, S2),
ok.
%% Using the same data as the chunked one for comparison.
horse_stream_identity() ->
horse:repeat(10000,
stream_identity(<<
"4\r\n"
"Wiki\r\n"
"5\r\n"
"pedia\r\n"
"e\r\n"
" in\r\n\r\nchunks.\r\n"
"0\r\n"
"\r\n">>, {0, 43})
).
horse_stream_identity_dripfeed() ->
horse:repeat(10000,
dripfeed(<<
"4\r\n"
"Wiki\r\n"
"5\r\n"
"pedia\r\n"
"e\r\n"
" in\r\n\r\nchunks.\r\n"
"0\r\n"
"\r\n">>, <<>>, {0, 43}, fun stream_identity/2)
).
-endif.
%% Chunked.
%% @doc Decode a chunked stream.
-spec stream_chunked(Data, State)
-> more | {more, Data, State} | {more, Data, non_neg_integer(), State}
| {more, Data, Data, State}
| {done, HasTrailers, Data} | {done, Data, HasTrailers, Data}
when Data :: binary(), State :: state(), HasTrailers :: trailers | no_trailers.
stream_chunked(Data, State) ->
stream_chunked(Data, State, <<>>).
%% New chunk.
stream_chunked(Data = <<C, _/bits>>, {0, Streamed}, Acc) when C =/= $\r ->
case chunked_len(Data, Streamed, Acc, 0) of
{next, Rest, State, Acc2} ->
stream_chunked(Rest, State, Acc2);
{more, State, Acc2} ->
{more, Acc2, Data, State};
Ret ->
Ret
end;
%% Trailing \r\n before next chunk.
stream_chunked(<<"\r\n", Rest/bits>>, {2, Streamed}, Acc) ->
stream_chunked(Rest, {0, Streamed}, Acc);
%% Trailing \r before next chunk.
stream_chunked(<<"\r">>, {2, Streamed}, Acc) ->
{more, Acc, {1, Streamed}};
%% Trailing \n before next chunk.
stream_chunked(<<"\n", Rest/bits>>, {1, Streamed}, Acc) ->
stream_chunked(Rest, {0, Streamed}, Acc);
%% More data needed.
stream_chunked(<<>>, State = {Rem, _}, Acc) ->
{more, Acc, Rem, State};
%% Chunk data.
stream_chunked(Data, {Rem, Streamed}, Acc) when Rem > 2 ->
DataSize = byte_size(Data),
RemSize = Rem - 2,
case Data of
<<Chunk:RemSize/binary, "\r\n", Rest/bits>> ->
stream_chunked(Rest, {0, Streamed + RemSize}, <<Acc/binary, Chunk/binary>>);
<<Chunk:RemSize/binary, "\r">> ->
{more, <<Acc/binary, Chunk/binary>>, {1, Streamed + RemSize}};
%% Everything in Data is part of the chunk. If we have more
%% data than the chunk accepts, then this is an error and we crash.
_ when DataSize =< RemSize ->
Rem2 = Rem - DataSize,
{more, <<Acc/binary, Data/binary>>, Rem2, {Rem2, Streamed + DataSize}}
end.
chunked_len(<<$0, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16);
chunked_len(<<$1, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 1);
chunked_len(<<$2, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 2);
chunked_len(<<$3, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 3);
chunked_len(<<$4, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 4);
chunked_len(<<$5, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 5);
chunked_len(<<$6, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 6);
chunked_len(<<$7, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 7);
chunked_len(<<$8, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 8);
chunked_len(<<$9, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 9);
chunked_len(<<$A, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 10);
chunked_len(<<$B, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 11);
chunked_len(<<$C, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 12);
chunked_len(<<$D, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 13);
chunked_len(<<$E, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 14);
chunked_len(<<$F, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 15);
chunked_len(<<$a, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 10);
chunked_len(<<$b, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 11);
chunked_len(<<$c, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 12);
chunked_len(<<$d, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 13);
chunked_len(<<$e, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 14);
chunked_len(<<$f, R/bits>>, S, A, Len) -> chunked_len(R, S, A, Len * 16 + 15);
%% Chunk extensions.
%%
%% Note that we currently skip the first character we encounter here,
%% and not in the skip_chunk_ext function. If we latter implement
%% chunk extensions (unlikely) we will need to change this clause too.
chunked_len(<<C, R/bits>>, S, A, Len) when ?IS_WS(C); C =:= $; -> skip_chunk_ext(R, S, A, Len, 0);
%% Final chunk.
%%
%% When trailers are following we simply return them as the Rest.
%% Then the user code can decide to call the stream_trailers function
%% to parse them. The user can therefore ignore trailers as necessary
%% if they do not wish to handle them.
chunked_len(<<"\r\n\r\n", R/bits>>, _, <<>>, 0) -> {done, no_trailers, R};
chunked_len(<<"\r\n\r\n", R/bits>>, _, A, 0) -> {done, A, no_trailers, R};
chunked_len(<<"\r\n", R/bits>>, _, <<>>, 0) when byte_size(R) > 2 -> {done, trailers, R};
chunked_len(<<"\r\n", R/bits>>, _, A, 0) when byte_size(R) > 2 -> {done, A, trailers, R};
chunked_len(_, _, _, 0) -> more;
%% Normal chunk. Add 2 to Len for the trailing \r\n.
chunked_len(<<"\r\n", R/bits>>, S, A, Len) -> {next, R, {Len + 2, S}, A};
chunked_len(<<"\r">>, _, <<>>, _) -> more;
chunked_len(<<"\r">>, S, A, _) -> {more, {0, S}, A};
chunked_len(<<>>, _, <<>>, _) -> more;
chunked_len(<<>>, S, A, _) -> {more, {0, S}, A}.
skip_chunk_ext(R = <<"\r", _/bits>>, S, A, Len, _) -> chunked_len(R, S, A, Len);
skip_chunk_ext(R = <<>>, S, A, Len, _) -> chunked_len(R, S, A, Len);
%% We skip up to 128 characters of chunk extensions. The value
%% is hardcoded: chunk extensions are very rarely seen in the
%% wild and Cowboy doesn't do anything with them anyway.
%%
%% Line breaks are not allowed in the middle of chunk extensions.
skip_chunk_ext(<<C, R/bits>>, S, A, Len, Skipped) when C =/= $\n, Skipped < 128 ->
skip_chunk_ext(R, S, A, Len, Skipped + 1).
%% @doc Encode a chunk.
-spec chunk(D) -> D when D :: iodata().
chunk(Data) ->
[integer_to_list(iolist_size(Data), 16), <<"\r\n">>,
Data, <<"\r\n">>].
%% @doc Encode the last chunk of a chunked stream.
-spec last_chunk() -> <<_:40>>.
last_chunk() ->
<<"0\r\n\r\n">>.
-ifdef(TEST).
stream_chunked_identity_test() ->
{done, <<"Wikipedia in\r\n\r\nchunks.">>, no_trailers, <<>>}
= stream_chunked(iolist_to_binary([
chunk("Wiki"),
chunk("pedia"),
chunk(" in\r\n\r\nchunks."),
last_chunk()
]), {0, 0}),
ok.
stream_chunked_one_pass_test() ->
{done, no_trailers, <<>>} = stream_chunked(<<"0\r\n\r\n">>, {0, 0}),
{done, <<"Wikipedia in\r\n\r\nchunks.">>, no_trailers, <<>>}
= stream_chunked(<<
"4\r\n"
"Wiki\r\n"
"5\r\n"
"pedia\r\n"
"e\r\n"
" in\r\n\r\nchunks.\r\n"
"0\r\n"
"\r\n">>, {0, 0}),
%% Same but with extra spaces or chunk extensions.
{done, <<"Wikipedia in\r\n\r\nchunks.">>, no_trailers, <<>>}
= stream_chunked(<<
"4 \r\n"
"Wiki\r\n"
"5 ; ext = abc\r\n"
"pedia\r\n"
"e;ext=abc\r\n"
" in\r\n\r\nchunks.\r\n"
"0;ext\r\n"
"\r\n">>, {0, 0}),
%% Same but with trailers.
{done, <<"Wikipedia in\r\n\r\nchunks.">>, trailers, Rest}
= stream_chunked(<<
"4\r\n"
"Wiki\r\n"
"5\r\n"
"pedia\r\n"
"e\r\n"
" in\r\n\r\nchunks.\r\n"
"0\r\n"
"x-foo-bar: bar foo\r\n"
"\r\n">>, {0, 0}),
{[{<<"x-foo-bar">>, <<"bar foo">>}], <<>>} = cow_http:parse_headers(Rest),
ok.
stream_chunked_n_passes_test() ->
S0 = {0, 0},
more = stream_chunked(<<"4\r">>, S0),
{more, <<>>, 6, S1} = stream_chunked(<<"4\r\n">>, S0),
{more, <<"Wiki">>, 0, S2} = stream_chunked(<<"Wiki\r\n">>, S1),
{more, <<"pedia">>, <<"e\r">>, S3} = stream_chunked(<<"5\r\npedia\r\ne\r">>, S2),
{more, <<" in\r\n\r\nchunks.">>, 2, S4} = stream_chunked(<<"e\r\n in\r\n\r\nchunks.">>, S3),
{done, no_trailers, <<>>} = stream_chunked(<<"\r\n0\r\n\r\n">>, S4),
%% A few extra for coverage purposes.
more = stream_chunked(<<"\n3">>, {1, 0}),
{more, <<"abc">>, 2, {2, 3}} = stream_chunked(<<"\n3\r\nabc">>, {1, 0}),
{more, <<"abc">>, {1, 3}} = stream_chunked(<<"3\r\nabc\r">>, {0, 0}),
{more, <<"abc">>, <<"123">>, {0, 3}} = stream_chunked(<<"3\r\nabc\r\n123">>, {0, 0}),
ok.
stream_chunked_dripfeed_test() ->
dripfeed(<<
"4\r\n"
"Wiki\r\n"
"5\r\n"
"pedia\r\n"
"e\r\n"
" in\r\n\r\nchunks.\r\n"
"0\r\n"
"\r\n">>, <<>>, {0, 0}, fun stream_chunked/2).
do_body_to_chunks(_, <<>>, Acc) ->
lists:reverse([<<"0\r\n\r\n">> | Acc]);
do_body_to_chunks(ChunkSize, Body, Acc) ->
BodySize = byte_size(Body),
ChunkSize2 = case BodySize < ChunkSize of
true -> BodySize;
false -> ChunkSize
end,
<<Chunk:ChunkSize2/binary, Rest/binary>> = Body,
ChunkSizeBin = list_to_binary(integer_to_list(ChunkSize2, 16)),
do_body_to_chunks(ChunkSize, Rest,
[<<ChunkSizeBin/binary, "\r\n", Chunk/binary, "\r\n">> | Acc]).
stream_chunked_dripfeed2_test() ->
Body = list_to_binary(io_lib:format("~p", [lists:seq(1, 100)])),
Body2 = iolist_to_binary(do_body_to_chunks(50, Body, [])),
dripfeed(Body2, <<>>, {0, 0}, fun stream_chunked/2).
stream_chunked_error_test_() ->
Tests = [
{<<>>, undefined},
{<<"\n\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa">>, {2, 0}}
],
[{lists:flatten(io_lib:format("value ~p state ~p", [V, S])),
fun() -> {'EXIT', _} = (catch stream_chunked(V, S)) end}
|| {V, S} <- Tests].
horse_stream_chunked() ->
horse:repeat(10000,
stream_chunked(<<
"4\r\n"
"Wiki\r\n"
"5\r\n"
"pedia\r\n"
"e\r\n"
" in\r\n\r\nchunks.\r\n"
"0\r\n"
"\r\n">>, {0, 0})
).
horse_stream_chunked_dripfeed() ->
horse:repeat(10000,
dripfeed(<<
"4\r\n"
"Wiki\r\n"
"5\r\n"
"pedia\r\n"
"e\r\n"
" in\r\n\r\nchunks.\r\n"
"0\r\n"
"\r\n">>, <<>>, {0, 43}, fun stream_chunked/2)
).
-endif.

+ 0
- 95
src/wsLib/cow_iolists.erl Vedi File

@ -1,95 +0,0 @@
%% Copyright (c) 2017-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_iolists).
-export([split/2]).
-ifdef(TEST).
-include_lib("proper/include/proper.hrl").
-endif.
-spec split(non_neg_integer(), iodata()) -> {iodata(), iodata()}.
split(N, Iolist) ->
case split(N, Iolist, []) of
{ok, Before, After} ->
{Before, After};
{more, _, Before} ->
{lists:reverse(Before), <<>>}
end.
split(0, Rest, Acc) ->
{ok, lists:reverse(Acc), Rest};
split(N, [], Acc) ->
{more, N, Acc};
split(N, Binary, Acc) when byte_size(Binary) =< N ->
{more, N - byte_size(Binary), [Binary | Acc]};
split(N, Binary, Acc) when is_binary(Binary) ->
<<Before:N/binary, After/bits>> = Binary,
{ok, lists:reverse([Before | Acc]), After};
split(N, [Binary | Tail], Acc) when byte_size(Binary) =< N ->
split(N - byte_size(Binary), Tail, [Binary | Acc]);
split(N, [Binary | Tail], Acc) when is_binary(Binary) ->
<<Before:N/binary, After/bits>> = Binary,
{ok, lists:reverse([Before | Acc]), [After | Tail]};
split(N, [Char | Tail], Acc) when is_integer(Char) ->
split(N - 1, Tail, [Char | Acc]);
split(N, [List | Tail], Acc0) ->
case split(N, List, Acc0) of
{ok, Before, After} ->
{ok, Before, [After | Tail]};
{more, More, Acc} ->
split(More, Tail, Acc)
end.
-ifdef(TEST).
split_test_() ->
Tests = [
{10, "Hello world!", "Hello worl", "d!"},
{10, <<"Hello world!">>, "Hello worl", "d!"},
{10, ["He", [<<"llo">>], $\s, [["world"], <<"!">>]], "Hello worl", "d!"},
{10, ["Hello " | <<"world!">>], "Hello worl", "d!"},
{10, "Hello!", "Hello!", ""},
{10, <<"Hello!">>, "Hello!", ""},
{10, ["He", [<<"ll">>], $o, [["!"]]], "Hello!", ""},
{10, ["Hel" | <<"lo!">>], "Hello!", ""},
{10, [[<<>> | <<>>], [], <<"Hello world!">>], "Hello worl", "d!"},
{10, [[<<"He">> | <<"llo">>], [$\s], <<"world!">>], "Hello worl", "d!"},
{10, [[[] | <<"He">>], [[] | <<"llo wor">>] | <<"ld!">>], "Hello worl", "d!"}
],
[{iolist_to_binary(V), fun() ->
{B, A} = split(N, V),
true = iolist_to_binary(RB) =:= iolist_to_binary(B),
true = iolist_to_binary(RA) =:= iolist_to_binary(A)
end} || {N, V, RB, RA} <- Tests].
prop_split_test() ->
?FORALL({N, Input},
{non_neg_integer(), iolist()},
begin
Size = iolist_size(Input),
{Before, After} = split(N, Input),
if
N >= Size ->
((iolist_size(After) =:= 0)
andalso iolist_to_binary(Before) =:= iolist_to_binary(Input));
true ->
<<ExpectBefore:N/binary, ExpectAfter/bits>> = iolist_to_binary(Input),
(ExpectBefore =:= iolist_to_binary(Before))
andalso (ExpectAfter =:= iolist_to_binary(After))
end
end).
-endif.

+ 0
- 445
src/wsLib/cow_link.erl Vedi File

@ -1,445 +0,0 @@
%% Copyright (c) 2019, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_link).
-compile({no_auto_import, [link/1]}).
-export([parse_link/1]).
-export([resolve_link/2]).
-export([resolve_link/3]).
-export([link/1]).
-include("cow_inline.hrl").
-include("cow_parse.hrl").
-type link() :: #{
target := binary(),
rel := binary(),
attributes := [{binary(), binary()}]
}.
-export_type([link/0]).
-type resolve_opts() :: #{
allow_anchor => boolean()
}.
-type uri() :: uri_string:uri_map() | uri_string:uri_string() | undefined.
%% Parse a link header.
%% This function returns the URI target from the header directly.
%% Relative URIs must then be resolved as per RFC3986 5. In some
%% cases it might not be possible to resolve URIs, for example when
%% the link header is returned with a 404 status code.
-spec parse_link(binary()) -> [link()].
parse_link(Link) ->
before_target(Link, []).
before_target(<<>>, Acc) -> lists:reverse(Acc);
before_target(<<$<, R/bits>>, Acc) -> target(R, Acc, <<>>);
before_target(<<C, R/bits>>, Acc) when ?IS_WS(C) -> before_target(R, Acc).
target(<<$>, R/bits>>, Acc, T) -> param_sep(R, Acc, T, []);
target(<<C, R/bits>>, Acc, T) -> target(R, Acc, <<T/binary, C>>).
param_sep(<<>>, Acc, T, P) -> lists:reverse(acc_link(Acc, T, P));
param_sep(<<$,, R/bits>>, Acc, T, P) -> before_target(R, acc_link(Acc, T, P));
param_sep(<<$;, R/bits>>, Acc, T, P) -> before_param(R, Acc, T, P);
param_sep(<<C, R/bits>>, Acc, T, P) when ?IS_WS(C) -> param_sep(R, Acc, T, P).
before_param(<<C, R/bits>>, Acc, T, P) when ?IS_WS(C) -> before_param(R, Acc, T, P);
before_param(<<C, R/bits>>, Acc, T, P) when ?IS_TOKEN(C) -> ?LOWER(param, R, Acc, T, P, <<>>).
param(<<$=, $", R/bits>>, Acc, T, P, K) -> quoted(R, Acc, T, P, K, <<>>);
param(<<$=, C, R/bits>>, Acc, T, P, K) when ?IS_TOKEN(C) -> value(R, Acc, T, P, K, <<C>>);
param(<<C, R/bits>>, Acc, T, P, K) when ?IS_TOKEN(C) -> ?LOWER(param, R, Acc, T, P, K).
quoted(<<$", R/bits>>, Acc, T, P, K, V) -> param_sep(R, Acc, T, [{K, V} | P]);
quoted(<<$\\, C, R/bits>>, Acc, T, P, K, V) when ?IS_VCHAR_OBS(C) -> quoted(R, Acc, T, P, K, <<V/binary, C>>);
quoted(<<C, R/bits>>, Acc, T, P, K, V) when ?IS_VCHAR_OBS(C) -> quoted(R, Acc, T, P, K, <<V/binary, C>>).
value(<<C, R/bits>>, Acc, T, P, K, V) when ?IS_TOKEN(C) -> value(R, Acc, T, P, K, <<V/binary, C>>);
value(R, Acc, T, P, K, V) -> param_sep(R, Acc, T, [{K, V} | P]).
acc_link(Acc, Target, Params0) ->
Params1 = lists:reverse(Params0),
%% The rel parameter MUST be present. (RFC8288 3.3)
{value, {_, Rel}, Params2} = lists:keytake(<<"rel">>, 1, Params1),
%% Occurrences after the first MUST be ignored by parsers.
Params = filter_out_duplicates(Params2, #{}),
[#{
target => Target,
rel => ?LOWER(Rel),
attributes => Params
} | Acc].
%% This function removes duplicates for attributes that don't allow them.
filter_out_duplicates([], _) ->
[];
%% The "rel" is mandatory and was already removed from params.
filter_out_duplicates([{<<"rel">>, _} | Tail], State) ->
filter_out_duplicates(Tail, State);
filter_out_duplicates([{<<"anchor">>, _} | Tail], State = #{anchor := true}) ->
filter_out_duplicates(Tail, State);
filter_out_duplicates([{<<"media">>, _} | Tail], State = #{media := true}) ->
filter_out_duplicates(Tail, State);
filter_out_duplicates([{<<"title">>, _} | Tail], State = #{title := true}) ->
filter_out_duplicates(Tail, State);
filter_out_duplicates([{<<"title*">>, _} | Tail], State = #{title_star := true}) ->
filter_out_duplicates(Tail, State);
filter_out_duplicates([{<<"type">>, _} | Tail], State = #{type := true}) ->
filter_out_duplicates(Tail, State);
filter_out_duplicates([Tuple = {<<"anchor">>, _} | Tail], State) ->
[Tuple | filter_out_duplicates(Tail, State#{anchor => true})];
filter_out_duplicates([Tuple = {<<"media">>, _} | Tail], State) ->
[Tuple | filter_out_duplicates(Tail, State#{media => true})];
filter_out_duplicates([Tuple = {<<"title">>, _} | Tail], State) ->
[Tuple | filter_out_duplicates(Tail, State#{title => true})];
filter_out_duplicates([Tuple = {<<"title*">>, _} | Tail], State) ->
[Tuple | filter_out_duplicates(Tail, State#{title_star => true})];
filter_out_duplicates([Tuple = {<<"type">>, _} | Tail], State) ->
[Tuple | filter_out_duplicates(Tail, State#{type => true})];
filter_out_duplicates([Tuple | Tail], State) ->
[Tuple | filter_out_duplicates(Tail, State)].
-ifdef(TEST).
parse_link_test_() ->
Tests = [
{<<>>, []},
{<<" ">>, []},
%% Examples from the RFC.
{<<"<http://example.com/TheBook/chapter2>; rel=\"previous\"; title=\"previous chapter\"">>, [
#{
target => <<"http://example.com/TheBook/chapter2">>,
rel => <<"previous">>,
attributes => [
{<<"title">>, <<"previous chapter">>}
]
}
]},
{<<"</>; rel=\"http://example.net/foo\"">>, [
#{
target => <<"/">>,
rel => <<"http://example.net/foo">>,
attributes => []
}
]},
{<<"</terms>; rel=\"copyright\"; anchor=\"#foo\"">>, [
#{
target => <<"/terms">>,
rel => <<"copyright">>,
attributes => [
{<<"anchor">>, <<"#foo">>}
]
}
]},
% {<<"</TheBook/chapter2>; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, "
% "</TheBook/chapter4>; rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel">>, [
% %% @todo
% ]}
{<<"<http://example.org/>; rel=\"start http://example.net/relation/other\"">>, [
#{
target => <<"http://example.org/">>,
rel => <<"start http://example.net/relation/other">>,
attributes => []
}
]},
{<<"<https://example.org/>; rel=\"start\", "
"<https://example.org/index>; rel=\"index\"">>, [
#{
target => <<"https://example.org/">>,
rel => <<"start">>,
attributes => []
},
#{
target => <<"https://example.org/index">>,
rel => <<"index">>,
attributes => []
}
]},
%% Relation types are case insensitive.
{<<"</>; rel=\"SELF\"">>, [
#{
target => <<"/">>,
rel => <<"self">>,
attributes => []
}
]},
{<<"</>; rel=\"HTTP://EXAMPLE.NET/FOO\"">>, [
#{
target => <<"/">>,
rel => <<"http://example.net/foo">>,
attributes => []
}
]},
%% Attribute names are case insensitive.
{<<"</terms>; REL=\"copyright\"; ANCHOR=\"#foo\"">>, [
#{
target => <<"/terms">>,
rel => <<"copyright">>,
attributes => [
{<<"anchor">>, <<"#foo">>}
]
}
]}
],
[{V, fun() -> R = parse_link(V) end} || {V, R} <- Tests].
-endif.
%% Resolve a link based on the context URI and options.
-spec resolve_link(Link, uri()) -> Link | false when Link :: link().
resolve_link(Link, ContextURI) ->
resolve_link(Link, ContextURI, #{}).
-spec resolve_link(Link, uri(), resolve_opts()) -> Link | false when Link :: link().
%% When we do not have a context URI we only succeed when the target URI is absolute.
%% The target URI will only be normalized in that case.
resolve_link(Link = #{target := TargetURI}, undefined, _) ->
case uri_string:parse(TargetURI) of
URIMap = #{scheme := _} ->
Link#{target => uri_string:normalize(URIMap)};
_ ->
false
end;
resolve_link(Link = #{attributes := Params}, ContextURI, Opts) ->
AllowAnchor = maps:get(allow_anchor, Opts, true),
case lists:keyfind(<<"anchor">>, 1, Params) of
false ->
do_resolve_link(Link, ContextURI);
{_, Anchor} when AllowAnchor ->
do_resolve_link(Link, resolve(Anchor, ContextURI));
_ ->
false
end.
do_resolve_link(Link = #{target := TargetURI}, ContextURI) ->
Link#{target => uri_string:recompose(resolve(TargetURI, ContextURI))}.
-ifdef(TEST).
resolve_link_test_() ->
Tests = [
%% No context URI available.
{#{target => <<"http://a/b/./c">>}, undefined, #{},
#{target => <<"http://a/b/c">>}},
{#{target => <<"a/b/./c">>}, undefined, #{},
false},
%% Context URI available, allow_anchor => true.
{#{target => <<"http://a/b">>, attributes => []}, <<"http://a/c">>, #{},
#{target => <<"http://a/b">>, attributes => []}},
{#{target => <<"b">>, attributes => []}, <<"http://a/c">>, #{},
#{target => <<"http://a/b">>, attributes => []}},
{#{target => <<"b">>, attributes => [{<<"anchor">>, <<"#frag">>}]}, <<"http://a/c">>, #{},
#{target => <<"http://a/b">>, attributes => [{<<"anchor">>, <<"#frag">>}]}},
{#{target => <<"b">>, attributes => [{<<"anchor">>, <<"d/e">>}]}, <<"http://a/c">>, #{},
#{target => <<"http://a/d/b">>, attributes => [{<<"anchor">>, <<"d/e">>}]}},
%% Context URI available, allow_anchor => false.
{#{target => <<"http://a/b">>, attributes => []}, <<"http://a/c">>, #{allow_anchor => false},
#{target => <<"http://a/b">>, attributes => []}},
{#{target => <<"b">>, attributes => []}, <<"http://a/c">>, #{allow_anchor => false},
#{target => <<"http://a/b">>, attributes => []}},
{#{target => <<"b">>, attributes => [{<<"anchor">>, <<"#frag">>}]},
<<"http://a/c">>, #{allow_anchor => false}, false},
{#{target => <<"b">>, attributes => [{<<"anchor">>, <<"d/e">>}]},
<<"http://a/c">>, #{allow_anchor => false}, false}
],
[{iolist_to_binary(io_lib:format("~0p", [L])),
fun() -> R = resolve_link(L, C, O) end} || {L, C, O, R} <- Tests].
-endif.
%% @todo This function has been added to Erlang/OTP 22.3 as uri_string:resolve/2,3.
resolve(URI, BaseURI) ->
case resolve1(ensure_map_uri(URI), BaseURI) of
TargetURI = #{path := Path0} ->
%% We remove dot segments. Normalizing the entire URI
%% will sometimes add an extra slash we don't want.
#{path := Path} = uri_string:normalize(#{path => Path0}, [return_map]),
TargetURI#{path => Path};
TargetURI ->
TargetURI
end.
resolve1(URI = #{scheme := _}, _) ->
URI;
resolve1(URI = #{host := _}, BaseURI) ->
#{scheme := Scheme} = ensure_map_uri(BaseURI),
URI#{scheme => Scheme};
resolve1(URI = #{path := <<>>}, BaseURI0) ->
BaseURI = ensure_map_uri(BaseURI0),
Keys = case maps:is_key(query, URI) of
true -> [scheme, host, port, path];
false -> [scheme, host, port, path, query]
end,
maps:merge(URI, maps:with(Keys, BaseURI));
resolve1(URI = #{path := <<"/", _/bits>>}, BaseURI0) ->
BaseURI = ensure_map_uri(BaseURI0),
maps:merge(URI, maps:with([scheme, host, port], BaseURI));
resolve1(URI = #{path := Path}, BaseURI0) ->
BaseURI = ensure_map_uri(BaseURI0),
maps:merge(
URI#{path := merge_paths(Path, BaseURI)},
maps:with([scheme, host, port], BaseURI)).
merge_paths(Path, #{host := _, path := <<>>}) ->
<<$/, Path/binary>>;
merge_paths(Path, #{path := BasePath0}) ->
case string:split(BasePath0, <<$/>>, trailing) of
[BasePath, _] -> <<BasePath/binary, $/, Path/binary>>;
[_] -> <<$/, Path/binary>>
end.
ensure_map_uri(URI) when is_map(URI) -> URI;
ensure_map_uri(URI) -> uri_string:parse(iolist_to_binary(URI)).
-ifdef(TEST).
resolve_test_() ->
Tests = [
%% 5.4.1. Normal Examples
{<<"g:h">>, <<"g:h">>},
{<<"g">>, <<"http://a/b/c/g">>},
{<<"./g">>, <<"http://a/b/c/g">>},
{<<"g/">>, <<"http://a/b/c/g/">>},
{<<"/g">>, <<"http://a/g">>},
{<<"//g">>, <<"http://g">>},
{<<"?y">>, <<"http://a/b/c/d;p?y">>},
{<<"g?y">>, <<"http://a/b/c/g?y">>},
{<<"#s">>, <<"http://a/b/c/d;p?q#s">>},
{<<"g#s">>, <<"http://a/b/c/g#s">>},
{<<"g?y#s">>, <<"http://a/b/c/g?y#s">>},
{<<";x">>, <<"http://a/b/c/;x">>},
{<<"g;x">>, <<"http://a/b/c/g;x">>},
{<<"g;x?y#s">>, <<"http://a/b/c/g;x?y#s">>},
{<<"">>, <<"http://a/b/c/d;p?q">>},
{<<".">>, <<"http://a/b/c/">>},
{<<"./">>, <<"http://a/b/c/">>},
{<<"..">>, <<"http://a/b/">>},
{<<"../">>, <<"http://a/b/">>},
{<<"../g">>, <<"http://a/b/g">>},
{<<"../..">>, <<"http://a/">>},
{<<"../../">>, <<"http://a/">>},
{<<"../../g">>, <<"http://a/g">>},
%% 5.4.2. Abnormal Examples
{<<"../../../g">>, <<"http://a/g">>},
{<<"../../../../g">>, <<"http://a/g">>},
{<<"/./g">>, <<"http://a/g">>},
{<<"/../g">>, <<"http://a/g">>},
{<<"g.">>, <<"http://a/b/c/g.">>},
{<<".g">>, <<"http://a/b/c/.g">>},
{<<"g..">>, <<"http://a/b/c/g..">>},
{<<"..g">>, <<"http://a/b/c/..g">>},
{<<"./../g">>, <<"http://a/b/g">>},
{<<"./g/.">>, <<"http://a/b/c/g/">>},
{<<"g/./h">>, <<"http://a/b/c/g/h">>},
{<<"g/../h">>, <<"http://a/b/c/h">>},
{<<"g;x=1/./y">>, <<"http://a/b/c/g;x=1/y">>},
{<<"g;x=1/../y">>, <<"http://a/b/c/y">>},
{<<"g?y/./x">>, <<"http://a/b/c/g?y/./x">>},
{<<"g?y/../x">>, <<"http://a/b/c/g?y/../x">>},
{<<"g#s/./x">>, <<"http://a/b/c/g#s/./x">>},
{<<"g#s/../x">>, <<"http://a/b/c/g#s/../x">>},
{<<"http:g">>, <<"http:g">>} %% for strict parsers
],
[{V, fun() -> R = uri_string:recompose(resolve(V, <<"http://a/b/c/d;p?q">>)) end} || {V, R} <- Tests].
-endif.
%% Build a link header.
-spec link([#{
target := binary(),
rel := binary(),
attributes := [{binary(), binary()}]
}]) -> iodata().
link(Links) ->
lists:join(<<", ">>, [do_link(Link) || Link <- Links]).
do_link(#{target := TargetURI, rel := Rel, attributes := Params}) ->
[
$<, TargetURI, <<">"
"; rel=\"">>, Rel, $",
[[<<"; ">>, Key, <<"=\"">>, escape(iolist_to_binary(Value), <<>>), $"]
|| {Key, Value} <- Params]
].
escape(<<>>, Acc) -> Acc;
escape(<<$\\, R/bits>>, Acc) -> escape(R, <<Acc/binary, $\\, $\\>>);
escape(<<$\", R/bits>>, Acc) -> escape(R, <<Acc/binary, $\\, $\">>);
escape(<<C, R/bits>>, Acc) -> escape(R, <<Acc/binary, C>>).
-ifdef(TEST).
link_test_() ->
Tests = [
{<<>>, []},
%% Examples from the RFC.
{<<"<http://example.com/TheBook/chapter2>; rel=\"previous\"; title=\"previous chapter\"">>, [
#{
target => <<"http://example.com/TheBook/chapter2">>,
rel => <<"previous">>,
attributes => [
{<<"title">>, <<"previous chapter">>}
]
}
]},
{<<"</>; rel=\"http://example.net/foo\"">>, [
#{
target => <<"/">>,
rel => <<"http://example.net/foo">>,
attributes => []
}
]},
{<<"</terms>; rel=\"copyright\"; anchor=\"#foo\"">>, [
#{
target => <<"/terms">>,
rel => <<"copyright">>,
attributes => [
{<<"anchor">>, <<"#foo">>}
]
}
]},
% {<<"</TheBook/chapter2>; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, "
% "</TheBook/chapter4>; rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel">>, [
% %% @todo
% ]}
{<<"<http://example.org/>; rel=\"start http://example.net/relation/other\"">>, [
#{
target => <<"http://example.org/">>,
rel => <<"start http://example.net/relation/other">>,
attributes => []
}
]},
{<<"<https://example.org/>; rel=\"start\", "
"<https://example.org/index>; rel=\"index\"">>, [
#{
target => <<"https://example.org/">>,
rel => <<"start">>,
attributes => []
},
#{
target => <<"https://example.org/index">>,
rel => <<"index">>,
attributes => []
}
]},
{<<"</>; rel=\"previous\"; quoted=\"name=\\\"value\\\"\"">>, [
#{
target => <<"/">>,
rel => <<"previous">>,
attributes => [
{<<"quoted">>, <<"name=\"value\"">>}
]
}
]}
],
[{iolist_to_binary(io_lib:format("~0p", [V])),
fun() -> R = iolist_to_binary(link(V)) end} || {R, V} <- Tests].
-endif.

+ 0
- 1045
src/wsLib/cow_mimetypes.erl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 61
src/wsLib/cow_mimetypes.erl.src Vedi File

@ -1,61 +0,0 @@
%% Copyright (c) 2013-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_mimetypes).
-export([all/1]).
-export([web/1]).
%% @doc Return the mimetype for any file by looking at its extension.
-spec all(binary()) -> {binary(), binary(), []}.
all(Path) ->
case filename:extension(Path) of
<<>> -> {<<"application">>, <<"octet-stream">>, []};
%% @todo Convert to string:lowercase on OTP-20+.
<< $., Ext/binary >> -> all_ext(list_to_binary(string:to_lower(binary_to_list(Ext))))
end.
%% @doc Return the mimetype for a Web related file by looking at its extension.
-spec web(binary()) -> {binary(), binary(), []}.
web(Path) ->
case filename:extension(Path) of
<<>> -> {<<"application">>, <<"octet-stream">>, []};
%% @todo Convert to string:lowercase on OTP-20+.
<< $., Ext/binary >> -> web_ext(list_to_binary(string:to_lower(binary_to_list(Ext))))
end.
%% Internal.
%% GENERATED
all_ext(_) -> {<<"application">>, <<"octet-stream">>, []}.
web_ext(<<"css">>) -> {<<"text">>, <<"css">>, []};
web_ext(<<"gif">>) -> {<<"image">>, <<"gif">>, []};
web_ext(<<"html">>) -> {<<"text">>, <<"html">>, []};
web_ext(<<"htm">>) -> {<<"text">>, <<"html">>, []};
web_ext(<<"ico">>) -> {<<"image">>, <<"x-icon">>, []};
web_ext(<<"jpeg">>) -> {<<"image">>, <<"jpeg">>, []};
web_ext(<<"jpg">>) -> {<<"image">>, <<"jpeg">>, []};
web_ext(<<"js">>) -> {<<"application">>, <<"javascript">>, []};
web_ext(<<"mp3">>) -> {<<"audio">>, <<"mpeg">>, []};
web_ext(<<"mp4">>) -> {<<"video">>, <<"mp4">>, []};
web_ext(<<"ogg">>) -> {<<"audio">>, <<"ogg">>, []};
web_ext(<<"ogv">>) -> {<<"video">>, <<"ogg">>, []};
web_ext(<<"png">>) -> {<<"image">>, <<"png">>, []};
web_ext(<<"svg">>) -> {<<"image">>, <<"svg+xml">>, []};
web_ext(<<"wav">>) -> {<<"audio">>, <<"x-wav">>, []};
web_ext(<<"webm">>) -> {<<"video">>, <<"webm">>, []};
web_ext(_) -> {<<"application">>, <<"octet-stream">>, []}.

+ 0
- 775
src/wsLib/cow_multipart.erl Vedi File

@ -1,775 +0,0 @@
%% Copyright (c) 2014-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_multipart).
%% Parsing.
-export([parse_headers/2]).
-export([parse_body/2]).
%% Building.
-export([boundary/0]).
-export([first_part/2]).
-export([part/2]).
-export([close/1]).
%% Headers.
-export([form_data/1]).
-export([parse_content_disposition/1]).
-export([parse_content_transfer_encoding/1]).
-export([parse_content_type/1]).
-type headers() :: [{iodata(), iodata()}].
-export_type([headers/0]).
-include("cow_inline.hrl").
-define(TEST1_MIME, <<
"This is a message with multiple parts in MIME format.\r\n"
"--frontier\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"This is the body of the message.\r\n"
"--frontier\r\n"
"Content-Type: application/octet-stream\r\n"
"Content-Transfer-Encoding: base64\r\n"
"\r\n"
"PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg\r\n"
"Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==\r\n"
"--frontier--"
>>).
-define(TEST1_BOUNDARY, <<"frontier">>).
-define(TEST2_MIME, <<
"--AaB03x\r\n"
"Content-Disposition: form-data; name=\"submit-name\"\r\n"
"\r\n"
"Larry\r\n"
"--AaB03x\r\n"
"Content-Disposition: form-data; name=\"files\"\r\n"
"Content-Type: multipart/mixed; boundary=BbC04y\r\n"
"\r\n"
"--BbC04y\r\n"
"Content-Disposition: file; filename=\"file1.txt\"\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"... contents of file1.txt ...\r\n"
"--BbC04y\r\n"
"Content-Disposition: file; filename=\"file2.gif\"\r\n"
"Content-Type: image/gif\r\n"
"Content-Transfer-Encoding: binary\r\n"
"\r\n"
"...contents of file2.gif...\r\n"
"--BbC04y--\r\n"
"--AaB03x--"
>>).
-define(TEST2_BOUNDARY, <<"AaB03x">>).
-define(TEST3_MIME, <<
"This is the preamble.\r\n"
"--boundary\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"This is the body of the message.\r\n"
"--boundary--"
"\r\nThis is the epilogue. Here it includes leading CRLF"
>>).
-define(TEST3_BOUNDARY, <<"boundary">>).
-define(TEST4_MIME, <<
"This is the preamble.\r\n"
"--boundary\r\n"
"Content-Type: text/plain\r\n"
"\r\n"
"This is the body of the message.\r\n"
"--boundary--"
"\r\n"
>>).
-define(TEST4_BOUNDARY, <<"boundary">>).
%% RFC 2046, Section 5.1.1
-define(TEST5_MIME, <<
"This is the preamble. It is to be ignored, though it\r\n"
"is a handy place for composition agents to include an\r\n"
"explanatory note to non-MIME conformant readers.\r\n"
"\r\n"
"--simple boundary\r\n",
"\r\n"
"This is implicitly typed plain US-ASCII text.\r\n"
"It does NOT end with a linebreak."
"\r\n"
"--simple boundary\r\n",
"Content-type: text/plain; charset=us-ascii\r\n"
"\r\n"
"This is explicitly typed plain US-ASCII text.\r\n"
"It DOES end with a linebreak.\r\n"
"\r\n"
"--simple boundary--\r\n"
"\r\n"
"This is the epilogue. It is also to be ignored."
>>).
-define(TEST5_BOUNDARY, <<"simple boundary">>).
%% Parsing.
%%
%% The multipart format is defined in RFC 2045.
%% @doc Parse the headers for the next multipart part.
%%
%% This function skips any preamble before the boundary.
%% The preamble may be retrieved using parse_body/2.
%%
%% This function will accept input of any size, it is
%% up to the caller to limit it if needed.
-spec parse_headers(binary(), binary())
-> more | {more, binary()}
| {ok, headers(), binary()}
| {done, binary()}.
%% If the stream starts with the boundary we can make a few assumptions
%% and quickly figure out if we got the complete list of headers.
parse_headers(<<"--", Stream/bits>>, Boundary) ->
BoundarySize = byte_size(Boundary),
case Stream of
%% Last boundary. Return the epilogue.
<<Boundary:BoundarySize/binary, "--", Stream2/bits>> ->
{done, Stream2};
<<Boundary:BoundarySize/binary, Stream2/bits>> ->
%% We have all the headers only if there is a \r\n\r\n
%% somewhere in the data after the boundary.
case binary:match(Stream2, <<"\r\n\r\n">>) of
nomatch ->
more;
_ ->
before_parse_headers(Stream2)
end;
%% If there isn't enough to represent Boundary \r\n\r\n
%% then we definitely don't have all the headers.
_ when byte_size(Stream) < byte_size(Boundary) + 4 ->
more;
%% Otherwise we have preamble data to skip.
%% We still got rid of the first two misleading bytes.
_ ->
skip_preamble(Stream, Boundary)
end;
%% Otherwise we have preamble data to skip.
parse_headers(Stream, Boundary) ->
skip_preamble(Stream, Boundary).
%% We need to find the boundary and a \r\n\r\n after that.
%% Since the boundary isn't at the start, it must be right
%% after a \r\n too.
skip_preamble(Stream, Boundary) ->
case binary:match(Stream, <<"\r\n--", Boundary/bits>>) of
%% No boundary, need more data.
nomatch ->
%% We can safely skip the size of the stream
%% minus the last 3 bytes which may be a partial boundary.
SkipSize = byte_size(Stream) - 3,
case SkipSize > 0 of
false ->
more;
true ->
<<_:SkipSize/binary, Stream2/bits>> = Stream,
{more, Stream2}
end;
{Start, Length} ->
Start2 = Start + Length,
<<_:Start2/binary, Stream2/bits>> = Stream,
case Stream2 of
%% Last boundary. Return the epilogue.
<<"--", Stream3/bits>> ->
{done, Stream3};
_ ->
case binary:match(Stream, <<"\r\n\r\n">>) of
%% We don't have the full headers.
nomatch ->
{more, Stream2};
_ ->
before_parse_headers(Stream2)
end
end
end.
before_parse_headers(<<"\r\n\r\n", Stream/bits>>) ->
%% This indicates that there are no headers, so we can abort immediately.
{ok, [], Stream};
before_parse_headers(<<"\r\n", Stream/bits>>) ->
%% There is a line break right after the boundary, skip it.
parse_hd_name(Stream, [], <<>>).
parse_hd_name(<<C, Rest/bits>>, H, SoFar) ->
case C of
$: -> parse_hd_before_value(Rest, H, SoFar);
$\s -> parse_hd_name_ws(Rest, H, SoFar);
$\t -> parse_hd_name_ws(Rest, H, SoFar);
_ -> ?LOWER(parse_hd_name, Rest, H, SoFar)
end.
parse_hd_name_ws(<<C, Rest/bits>>, H, Name) ->
case C of
$\s -> parse_hd_name_ws(Rest, H, Name);
$\t -> parse_hd_name_ws(Rest, H, Name);
$: -> parse_hd_before_value(Rest, H, Name)
end.
parse_hd_before_value(<<$\s, Rest/bits>>, H, N) ->
parse_hd_before_value(Rest, H, N);
parse_hd_before_value(<<$\t, Rest/bits>>, H, N) ->
parse_hd_before_value(Rest, H, N);
parse_hd_before_value(Buffer, H, N) ->
parse_hd_value(Buffer, H, N, <<>>).
parse_hd_value(<<$\r, Rest/bits>>, Headers, Name, SoFar) ->
case Rest of
<<"\n\r\n", Rest2/bits>> ->
{ok, [{Name, SoFar} | Headers], Rest2};
<<$\n, C, Rest2/bits>> when C =:= $\s; C =:= $\t ->
parse_hd_value(Rest2, Headers, Name, SoFar);
<<$\n, Rest2/bits>> ->
parse_hd_name(Rest2, [{Name, SoFar} | Headers], <<>>)
end;
parse_hd_value(<<C, Rest/bits>>, H, N, SoFar) ->
parse_hd_value(Rest, H, N, <<SoFar/binary, C>>).
%% @doc Parse the body of the current multipart part.
%%
%% The body is everything until the next boundary.
-spec parse_body(binary(), binary())
-> {ok, binary()} | {ok, binary(), binary()}
| done | {done, binary()} | {done, binary(), binary()}.
parse_body(Stream, Boundary) ->
BoundarySize = byte_size(Boundary),
case Stream of
<<"--", Boundary:BoundarySize/binary, _/bits>> ->
done;
_ ->
case binary:match(Stream, <<"\r\n--", Boundary/bits>>) of
%% No boundary, check for a possible partial at the end.
%% Return more or less of the body depending on the result.
nomatch ->
StreamSize = byte_size(Stream),
From = StreamSize - BoundarySize - 3,
MatchOpts = if
%% Binary too small to contain boundary, check it fully.
From < 0 -> [];
%% Optimize, only check the end of the binary.
true -> [{scope, {From, StreamSize - From}}]
end,
case binary:match(Stream, <<"\r">>, MatchOpts) of
nomatch ->
{ok, Stream};
{Pos, _} ->
case Stream of
<<Body:Pos/binary>> ->
{ok, Body};
<<Body:Pos/binary, Rest/bits>> ->
{ok, Body, Rest}
end
end;
%% Boundary found, this is the last chunk of the body.
{Pos, _} ->
case Stream of
<<Body:Pos/binary, "\r\n">> ->
{done, Body};
<<Body:Pos/binary, "\r\n", Rest/bits>> ->
{done, Body, Rest};
<<Body:Pos/binary, Rest/bits>> ->
{done, Body, Rest}
end
end
end.
-ifdef(TEST).
parse_test() ->
H1 = [{<<"content-type">>, <<"text/plain">>}],
Body1 = <<"This is the body of the message.">>,
H2 = lists:sort([{<<"content-type">>, <<"application/octet-stream">>},
{<<"content-transfer-encoding">>, <<"base64">>}]),
Body2 = <<"PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg\r\n"
"Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==">>,
{ok, H1, Rest} = parse_headers(?TEST1_MIME, ?TEST1_BOUNDARY),
{done, Body1, Rest2} = parse_body(Rest, ?TEST1_BOUNDARY),
done = parse_body(Rest2, ?TEST1_BOUNDARY),
{ok, H2Unsorted, Rest3} = parse_headers(Rest2, ?TEST1_BOUNDARY),
H2 = lists:sort(H2Unsorted),
{done, Body2, Rest4} = parse_body(Rest3, ?TEST1_BOUNDARY),
done = parse_body(Rest4, ?TEST1_BOUNDARY),
{done, <<>>} = parse_headers(Rest4, ?TEST1_BOUNDARY),
ok.
parse_interleaved_test() ->
H1 = [{<<"content-disposition">>, <<"form-data; name=\"submit-name\"">>}],
Body1 = <<"Larry">>,
H2 = lists:sort([{<<"content-disposition">>, <<"form-data; name=\"files\"">>},
{<<"content-type">>, <<"multipart/mixed; boundary=BbC04y">>}]),
InH1 = lists:sort([{<<"content-disposition">>, <<"file; filename=\"file1.txt\"">>},
{<<"content-type">>, <<"text/plain">>}]),
InBody1 = <<"... contents of file1.txt ...">>,
InH2 = lists:sort([{<<"content-disposition">>, <<"file; filename=\"file2.gif\"">>},
{<<"content-type">>, <<"image/gif">>},
{<<"content-transfer-encoding">>, <<"binary">>}]),
InBody2 = <<"...contents of file2.gif...">>,
{ok, H1, Rest} = parse_headers(?TEST2_MIME, ?TEST2_BOUNDARY),
{done, Body1, Rest2} = parse_body(Rest, ?TEST2_BOUNDARY),
done = parse_body(Rest2, ?TEST2_BOUNDARY),
{ok, H2Unsorted, Rest3} = parse_headers(Rest2, ?TEST2_BOUNDARY),
H2 = lists:sort(H2Unsorted),
{_, ContentType} = lists:keyfind(<<"content-type">>, 1, H2),
{<<"multipart">>, <<"mixed">>, [{<<"boundary">>, InBoundary}]}
= parse_content_type(ContentType),
{ok, InH1Unsorted, InRest} = parse_headers(Rest3, InBoundary),
InH1 = lists:sort(InH1Unsorted),
{done, InBody1, InRest2} = parse_body(InRest, InBoundary),
done = parse_body(InRest2, InBoundary),
{ok, InH2Unsorted, InRest3} = parse_headers(InRest2, InBoundary),
InH2 = lists:sort(InH2Unsorted),
{done, InBody2, InRest4} = parse_body(InRest3, InBoundary),
done = parse_body(InRest4, InBoundary),
{done, Rest4} = parse_headers(InRest4, InBoundary),
{done, <<>>} = parse_headers(Rest4, ?TEST2_BOUNDARY),
ok.
parse_epilogue_test() ->
H1 = [{<<"content-type">>, <<"text/plain">>}],
Body1 = <<"This is the body of the message.">>,
Epilogue = <<"\r\nThis is the epilogue. Here it includes leading CRLF">>,
{ok, H1, Rest} = parse_headers(?TEST3_MIME, ?TEST3_BOUNDARY),
{done, Body1, Rest2} = parse_body(Rest, ?TEST3_BOUNDARY),
done = parse_body(Rest2, ?TEST3_BOUNDARY),
{done, Epilogue} = parse_headers(Rest2, ?TEST3_BOUNDARY),
ok.
parse_epilogue_crlf_test() ->
H1 = [{<<"content-type">>, <<"text/plain">>}],
Body1 = <<"This is the body of the message.">>,
Epilogue = <<"\r\n">>,
{ok, H1, Rest} = parse_headers(?TEST4_MIME, ?TEST4_BOUNDARY),
{done, Body1, Rest2} = parse_body(Rest, ?TEST4_BOUNDARY),
done = parse_body(Rest2, ?TEST4_BOUNDARY),
{done, Epilogue} = parse_headers(Rest2, ?TEST4_BOUNDARY),
ok.
parse_rfc2046_test() ->
%% The following is an example included in RFC 2046, Section 5.1.1.
Body1 = <<"This is implicitly typed plain US-ASCII text.\r\n"
"It does NOT end with a linebreak.">>,
Body2 = <<"This is explicitly typed plain US-ASCII text.\r\n"
"It DOES end with a linebreak.\r\n">>,
H2 = [{<<"content-type">>, <<"text/plain; charset=us-ascii">>}],
Epilogue = <<"\r\n\r\nThis is the epilogue. It is also to be ignored.">>,
{ok, [], Rest} = parse_headers(?TEST5_MIME, ?TEST5_BOUNDARY),
{done, Body1, Rest2} = parse_body(Rest, ?TEST5_BOUNDARY),
{ok, H2, Rest3} = parse_headers(Rest2, ?TEST5_BOUNDARY),
{done, Body2, Rest4} = parse_body(Rest3, ?TEST5_BOUNDARY),
{done, Epilogue} = parse_headers(Rest4, ?TEST5_BOUNDARY),
ok.
parse_partial_test() ->
{ok, <<0:8000, "abcdef">>, <<"\rghij">>}
= parse_body(<<0:8000, "abcdef\rghij">>, <<"boundary">>),
{ok, <<"abcdef">>, <<"\rghij">>}
= parse_body(<<"abcdef\rghij">>, <<"boundary">>),
{ok, <<"abc">>, <<"\rdef">>}
= parse_body(<<"abc\rdef">>, <<"boundaryboundary">>),
{ok, <<0:8000, "abcdef">>, <<"\r\nghij">>}
= parse_body(<<0:8000, "abcdef\r\nghij">>, <<"boundary">>),
{ok, <<"abcdef">>, <<"\r\nghij">>}
= parse_body(<<"abcdef\r\nghij">>, <<"boundary">>),
{ok, <<"abc">>, <<"\r\ndef">>}
= parse_body(<<"abc\r\ndef">>, <<"boundaryboundary">>),
{ok, <<"boundary">>, <<"\r">>}
= parse_body(<<"boundary\r">>, <<"boundary">>),
{ok, <<"boundary">>, <<"\r\n">>}
= parse_body(<<"boundary\r\n">>, <<"boundary">>),
{ok, <<"boundary">>, <<"\r\n-">>}
= parse_body(<<"boundary\r\n-">>, <<"boundary">>),
{ok, <<"boundary">>, <<"\r\n--">>}
= parse_body(<<"boundary\r\n--">>, <<"boundary">>),
ok.
perf_parse_multipart(Stream, Boundary) ->
case parse_headers(Stream, Boundary) of
{ok, _, Rest} ->
{_, _, Rest2} = parse_body(Rest, Boundary),
perf_parse_multipart(Rest2, Boundary);
{done, _} ->
ok
end.
horse_parse() ->
horse:repeat(50000,
perf_parse_multipart(?TEST1_MIME, ?TEST1_BOUNDARY)
).
-endif.
%% Building.
%% @doc Generate a new random boundary.
%%
%% The boundary generated has a low probability of ever appearing
%% in the data.
-spec boundary() -> binary().
boundary() ->
cow_base64url:encode(crypto:strong_rand_bytes(48), #{padding => false}).
%% @doc Return the first part's head.
%%
%% This works exactly like the part/2 function except there is
%% no leading \r\n. It's not required to use this function,
%% just makes the output a little smaller and prettier.
-spec first_part(binary(), headers()) -> iodata().
first_part(Boundary, Headers) ->
[<<"--">>, Boundary, <<"\r\n">>, headers_to_iolist(Headers, [])].
%% @doc Return a part's head.
-spec part(binary(), headers()) -> iodata().
part(Boundary, Headers) ->
[<<"\r\n--">>, Boundary, <<"\r\n">>, headers_to_iolist(Headers, [])].
headers_to_iolist([], Acc) ->
lists:reverse([<<"\r\n">> | Acc]);
headers_to_iolist([{N, V} | Tail], Acc) ->
%% We don't want to create a sublist so we list the
%% values in reverse order so that it gets reversed properly.
headers_to_iolist(Tail, [<<"\r\n">>, V, <<": ">>, N | Acc]).
%% @doc Return the closing delimiter of the multipart message.
-spec close(binary()) -> iodata().
close(Boundary) ->
[<<"\r\n--">>, Boundary, <<"--">>].
-ifdef(TEST).
build_test() ->
Result = string:to_lower(binary_to_list(?TEST1_MIME)),
Result = string:to_lower(binary_to_list(iolist_to_binary([
<<"This is a message with multiple parts in MIME format.\r\n">>,
first_part(?TEST1_BOUNDARY, [{<<"content-type">>, <<"text/plain">>}]),
<<"This is the body of the message.">>,
part(?TEST1_BOUNDARY, [
{<<"content-type">>, <<"application/octet-stream">>},
{<<"content-transfer-encoding">>, <<"base64">>}]),
<<"PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg\r\n"
"Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==">>,
close(?TEST1_BOUNDARY)
]))),
ok.
identity_test() ->
B = boundary(),
Preamble = <<"This is a message with multiple parts in MIME format.">>,
H1 = [{<<"content-type">>, <<"text/plain">>}],
Body1 = <<"This is the body of the message.">>,
H2 = lists:sort([{<<"content-type">>, <<"application/octet-stream">>},
{<<"content-transfer-encoding">>, <<"base64">>}]),
Body2 = <<"PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg\r\n"
"Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==">>,
Epilogue = <<"Gotta go fast!">>,
M = iolist_to_binary([
Preamble,
part(B, H1), Body1,
part(B, H2), Body2,
close(B),
Epilogue
]),
{done, Preamble, M2} = parse_body(M, B),
{ok, H1, M3} = parse_headers(M2, B),
{done, Body1, M4} = parse_body(M3, B),
{ok, H2Unsorted, M5} = parse_headers(M4, B),
H2 = lists:sort(H2Unsorted),
{done, Body2, M6} = parse_body(M5, B),
{done, Epilogue} = parse_headers(M6, B),
ok.
perf_build_multipart() ->
B = boundary(),
[
<<"preamble\r\n">>,
first_part(B, [{<<"content-type">>, <<"text/plain">>}]),
<<"This is the body of the message.">>,
part(B, [
{<<"content-type">>, <<"application/octet-stream">>},
{<<"content-transfer-encoding">>, <<"base64">>}]),
<<"PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg\r\n"
"Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==">>,
close(B),
<<"epilogue">>
].
horse_build() ->
horse:repeat(50000,
perf_build_multipart()
).
-endif.
%% Headers.
%% @doc Convenience function for extracting information from headers
%% when parsing a multipart/form-data stream.
-spec form_data(headers() | #{binary() => binary()})
-> {data, binary()}
| {file, binary(), binary(), binary()}.
form_data(Headers) when is_map(Headers) ->
form_data(maps:to_list(Headers));
form_data(Headers) ->
{_, DispositionBin} = lists:keyfind(<<"content-disposition">>, 1, Headers),
{<<"form-data">>, Params} = parse_content_disposition(DispositionBin),
{_, FieldName} = lists:keyfind(<<"name">>, 1, Params),
case lists:keyfind(<<"filename">>, 1, Params) of
false ->
{data, FieldName};
{_, Filename} ->
Type = case lists:keyfind(<<"content-type">>, 1, Headers) of
false -> <<"text/plain">>;
{_, T} -> T
end,
{file, FieldName, Filename, Type}
end.
-ifdef(TEST).
form_data_test_() ->
Tests = [
{[{<<"content-disposition">>, <<"form-data; name=\"submit-name\"">>}],
{data, <<"submit-name">>}},
{[{<<"content-disposition">>,
<<"form-data; name=\"files\"; filename=\"file1.txt\"">>},
{<<"content-type">>, <<"text/x-plain">>}],
{file, <<"files">>, <<"file1.txt">>, <<"text/x-plain">>}}
],
[{lists:flatten(io_lib:format("~p", [V])),
fun() -> R = form_data(V) end} || {V, R} <- Tests].
-endif.
%% @todo parse_content_description
%% @todo parse_content_id
%% @doc Parse an RFC 2183 content-disposition value.
%% @todo Support RFC 2231.
-spec parse_content_disposition(binary())
-> {binary(), [{binary(), binary()}]}.
parse_content_disposition(Bin) ->
parse_cd_type(Bin, <<>>).
parse_cd_type(<<>>, Acc) ->
{Acc, []};
parse_cd_type(<<C, Rest/bits>>, Acc) ->
case C of
$; -> {Acc, parse_before_param(Rest, [])};
$\s -> {Acc, parse_before_param(Rest, [])};
$\t -> {Acc, parse_before_param(Rest, [])};
_ -> ?LOWER(parse_cd_type, Rest, Acc)
end.
-ifdef(TEST).
parse_content_disposition_test_() ->
Tests = [
{<<"inline">>, {<<"inline">>, []}},
{<<"attachment">>, {<<"attachment">>, []}},
{<<"attachment; filename=genome.jpeg;"
" modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";">>,
{<<"attachment">>, [
{<<"filename">>, <<"genome.jpeg">>},
{<<"modification-date">>, <<"Wed, 12 Feb 1997 16:29:51 -0500">>}
]}},
{<<"form-data; name=\"user\"">>,
{<<"form-data">>, [{<<"name">>, <<"user">>}]}},
{<<"form-data; NAME=\"submit-name\"">>,
{<<"form-data">>, [{<<"name">>, <<"submit-name">>}]}},
{<<"form-data; name=\"files\"; filename=\"file1.txt\"">>,
{<<"form-data">>, [
{<<"name">>, <<"files">>},
{<<"filename">>, <<"file1.txt">>}
]}},
{<<"file; filename=\"file1.txt\"">>,
{<<"file">>, [{<<"filename">>, <<"file1.txt">>}]}},
{<<"file; filename=\"file2.gif\"">>,
{<<"file">>, [{<<"filename">>, <<"file2.gif">>}]}}
],
[{V, fun() -> R = parse_content_disposition(V) end} || {V, R} <- Tests].
horse_parse_content_disposition_attachment() ->
horse:repeat(100000,
parse_content_disposition(<<"attachment; filename=genome.jpeg;"
" modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";">>)
).
horse_parse_content_disposition_form_data() ->
horse:repeat(100000,
parse_content_disposition(
<<"form-data; name=\"files\"; filename=\"file1.txt\"">>)
).
horse_parse_content_disposition_inline() ->
horse:repeat(100000,
parse_content_disposition(<<"inline">>)
).
-endif.
%% @doc Parse an RFC 2045 content-transfer-encoding header.
-spec parse_content_transfer_encoding(binary()) -> binary().
parse_content_transfer_encoding(Bin) ->
?LOWER(Bin).
-ifdef(TEST).
parse_content_transfer_encoding_test_() ->
Tests = [
{<<"7bit">>, <<"7bit">>},
{<<"7BIT">>, <<"7bit">>},
{<<"8bit">>, <<"8bit">>},
{<<"binary">>, <<"binary">>},
{<<"quoted-printable">>, <<"quoted-printable">>},
{<<"base64">>, <<"base64">>},
{<<"Base64">>, <<"base64">>},
{<<"BASE64">>, <<"base64">>},
{<<"bAsE64">>, <<"base64">>}
],
[{V, fun() -> R = parse_content_transfer_encoding(V) end}
|| {V, R} <- Tests].
horse_parse_content_transfer_encoding() ->
horse:repeat(100000,
parse_content_transfer_encoding(<<"QUOTED-PRINTABLE">>)
).
-endif.
%% @doc Parse an RFC 2045 content-type header.
-spec parse_content_type(binary())
-> {binary(), binary(), [{binary(), binary()}]}.
parse_content_type(Bin) ->
parse_ct_type(Bin, <<>>).
parse_ct_type(<<C, Rest/bits>>, Acc) ->
case C of
$/ -> parse_ct_subtype(Rest, Acc, <<>>);
_ -> ?LOWER(parse_ct_type, Rest, Acc)
end.
parse_ct_subtype(<<>>, Type, Subtype) when Subtype =/= <<>> ->
{Type, Subtype, []};
parse_ct_subtype(<<C, Rest/bits>>, Type, Acc) ->
case C of
$; -> {Type, Acc, parse_before_param(Rest, [])};
$\s -> {Type, Acc, parse_before_param(Rest, [])};
$\t -> {Type, Acc, parse_before_param(Rest, [])};
_ -> ?LOWER(parse_ct_subtype, Rest, Type, Acc)
end.
-ifdef(TEST).
parse_content_type_test_() ->
Tests = [
{<<"image/gif">>,
{<<"image">>, <<"gif">>, []}},
{<<"text/plain">>,
{<<"text">>, <<"plain">>, []}},
{<<"text/plain; charset=us-ascii">>,
{<<"text">>, <<"plain">>, [{<<"charset">>, <<"us-ascii">>}]}},
{<<"text/plain; charset=\"us-ascii\"">>,
{<<"text">>, <<"plain">>, [{<<"charset">>, <<"us-ascii">>}]}},
{<<"multipart/form-data; boundary=AaB03x">>,
{<<"multipart">>, <<"form-data">>,
[{<<"boundary">>, <<"AaB03x">>}]}},
{<<"multipart/mixed; boundary=BbC04y">>,
{<<"multipart">>, <<"mixed">>, [{<<"boundary">>, <<"BbC04y">>}]}},
{<<"multipart/mixed; boundary=--------">>,
{<<"multipart">>, <<"mixed">>, [{<<"boundary">>, <<"--------">>}]}},
{<<"application/x-horse; filename=genome.jpeg;"
" some-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";"
" charset=us-ascii; empty=; number=12345">>,
{<<"application">>, <<"x-horse">>, [
{<<"filename">>, <<"genome.jpeg">>},
{<<"some-date">>, <<"Wed, 12 Feb 1997 16:29:51 -0500">>},
{<<"charset">>, <<"us-ascii">>},
{<<"empty">>, <<>>},
{<<"number">>, <<"12345">>}
]}}
],
[{V, fun() -> R = parse_content_type(V) end}
|| {V, R} <- Tests].
horse_parse_content_type_zero() ->
horse:repeat(100000,
parse_content_type(<<"text/plain">>)
).
horse_parse_content_type_one() ->
horse:repeat(100000,
parse_content_type(<<"text/plain; charset=\"us-ascii\"">>)
).
horse_parse_content_type_five() ->
horse:repeat(100000,
parse_content_type(<<"application/x-horse; filename=genome.jpeg;"
" some-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";"
" charset=us-ascii; empty=; number=12345">>)
).
-endif.
%% @doc Parse RFC 2045 parameters.
parse_before_param(<<>>, Params) ->
lists:reverse(Params);
parse_before_param(<<C, Rest/bits>>, Params) ->
case C of
$; -> parse_before_param(Rest, Params);
$\s -> parse_before_param(Rest, Params);
$\t -> parse_before_param(Rest, Params);
_ -> ?LOWER(parse_param_name, Rest, Params, <<>>)
end.
parse_param_name(<<>>, Params, Acc) ->
lists:reverse([{Acc, <<>>} | Params]);
parse_param_name(<<C, Rest/bits>>, Params, Acc) ->
case C of
$= -> parse_param_value(Rest, Params, Acc);
_ -> ?LOWER(parse_param_name, Rest, Params, Acc)
end.
parse_param_value(<<>>, Params, Name) ->
lists:reverse([{Name, <<>>} | Params]);
parse_param_value(<<C, Rest/bits>>, Params, Name) ->
case C of
$" -> parse_param_quoted_value(Rest, Params, Name, <<>>);
$; -> parse_before_param(Rest, [{Name, <<>>} | Params]);
$\s -> parse_before_param(Rest, [{Name, <<>>} | Params]);
$\t -> parse_before_param(Rest, [{Name, <<>>} | Params]);
C -> parse_param_value(Rest, Params, Name, <<C>>)
end.
parse_param_value(<<>>, Params, Name, Acc) ->
lists:reverse([{Name, Acc} | Params]);
parse_param_value(<<C, Rest/bits>>, Params, Name, Acc) ->
case C of
$; -> parse_before_param(Rest, [{Name, Acc} | Params]);
$\s -> parse_before_param(Rest, [{Name, Acc} | Params]);
$\t -> parse_before_param(Rest, [{Name, Acc} | Params]);
C -> parse_param_value(Rest, Params, Name, <<Acc/binary, C>>)
end.
%% We expect a final $" so no need to test for <<>>.
parse_param_quoted_value(<<$\\, C, Rest/bits>>, Params, Name, Acc) ->
parse_param_quoted_value(Rest, Params, Name, <<Acc/binary, C>>);
parse_param_quoted_value(<<$", Rest/bits>>, Params, Name, Acc) ->
parse_before_param(Rest, [{Name, Acc} | Params]);
parse_param_quoted_value(<<C, Rest/bits>>, Params, Name, Acc)
when C =/= $\r ->
parse_param_quoted_value(Rest, Params, Name, <<Acc/binary, C>>).

+ 0
- 563
src/wsLib/cow_qs.erl Vedi File

@ -1,563 +0,0 @@
%% Copyright (c) 2013-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_qs).
-export([parse_qs/1]).
-export([qs/1]).
-export([urldecode/1]).
-export([urlencode/1]).
-type qs_vals() :: [{binary(), binary() | true}].
%% @doc Parse an application/x-www-form-urlencoded string.
%%
%% The percent decoding is inlined to greatly improve the performance
%% by avoiding copying binaries twice (once for extracting, once for
%% decoding) instead of just extracting the proper representation.
-spec parse_qs(binary()) -> qs_vals().
parse_qs(B) ->
parse_qs_name(B, [], <<>>).
parse_qs_name(<<$%, H, L, Rest/bits>>, Acc, Name) ->
C = (unhex(H) bsl 4 bor unhex(L)),
parse_qs_name(Rest, Acc, <<Name/bits, C>>);
parse_qs_name(<<$+, Rest/bits>>, Acc, Name) ->
parse_qs_name(Rest, Acc, <<Name/bits, " ">>);
parse_qs_name(<<$=, Rest/bits>>, Acc, Name) when Name =/= <<>> ->
parse_qs_value(Rest, Acc, Name, <<>>);
parse_qs_name(<<$&, Rest/bits>>, Acc, Name) ->
case Name of
<<>> -> parse_qs_name(Rest, Acc, <<>>);
_ -> parse_qs_name(Rest, [{Name, true} | Acc], <<>>)
end;
parse_qs_name(<<C, Rest/bits>>, Acc, Name) when C =/= $%, C =/= $= ->
parse_qs_name(Rest, Acc, <<Name/bits, C>>);
parse_qs_name(<<>>, Acc, Name) ->
case Name of
<<>> -> lists:reverse(Acc);
_ -> lists:reverse([{Name, true} | Acc])
end.
parse_qs_value(<<$%, H, L, Rest/bits>>, Acc, Name, Value) ->
C = (unhex(H) bsl 4 bor unhex(L)),
parse_qs_value(Rest, Acc, Name, <<Value/bits, C>>);
parse_qs_value(<<$+, Rest/bits>>, Acc, Name, Value) ->
parse_qs_value(Rest, Acc, Name, <<Value/bits, " ">>);
parse_qs_value(<<$&, Rest/bits>>, Acc, Name, Value) ->
parse_qs_name(Rest, [{Name, Value} | Acc], <<>>);
parse_qs_value(<<C, Rest/bits>>, Acc, Name, Value) when C =/= $% ->
parse_qs_value(Rest, Acc, Name, <<Value/bits, C>>);
parse_qs_value(<<>>, Acc, Name, Value) ->
lists:reverse([{Name, Value} | Acc]).
-ifdef(TEST).
parse_qs_test_() ->
Tests = [
{<<>>, []},
{<<"&">>, []},
{<<"a">>, [{<<"a">>, true}]},
{<<"a&">>, [{<<"a">>, true}]},
{<<"&a">>, [{<<"a">>, true}]},
{<<"a&b">>, [{<<"a">>, true}, {<<"b">>, true}]},
{<<"a&&b">>, [{<<"a">>, true}, {<<"b">>, true}]},
{<<"a&b&">>, [{<<"a">>, true}, {<<"b">>, true}]},
{<<"=">>, error},
{<<"=b">>, error},
{<<"a=">>, [{<<"a">>, <<>>}]},
{<<"a=b">>, [{<<"a">>, <<"b">>}]},
{<<"a=&b=">>, [{<<"a">>, <<>>}, {<<"b">>, <<>>}]},
{<<"a=b&c&d=e">>, [{<<"a">>, <<"b">>},
{<<"c">>, true}, {<<"d">>, <<"e">>}]},
{<<"a=b=c&d=e=f&g=h=i">>, [{<<"a">>, <<"b=c">>},
{<<"d">>, <<"e=f">>}, {<<"g">>, <<"h=i">>}]},
{<<"+">>, [{<<" ">>, true}]},
{<<"+=+">>, [{<<" ">>, <<" ">>}]},
{<<"a+b=c+d">>, [{<<"a b">>, <<"c d">>}]},
{<<"+a+=+b+&+c+=+d+">>, [{<<" a ">>, <<" b ">>},
{<<" c ">>, <<" d ">>}]},
{<<"a%20b=c%20d">>, [{<<"a b">>, <<"c d">>}]},
{<<"%25%26%3D=%25%26%3D&_-.=.-_">>, [{<<"%&=">>, <<"%&=">>},
{<<"_-.">>, <<".-_">>}]},
{<<"for=extend%2Franch">>, [{<<"for">>, <<"extend/ranch">>}]}
],
[{Qs, fun() ->
E = try parse_qs(Qs) of
R -> R
catch _:_ ->
error
end
end} || {Qs, E} <- Tests].
parse_qs_identity_test_() ->
Tests = [
<<"+">>,
<<"hl=en&q=erlang+cowboy">>,
<<"direction=desc&for=extend%2Franch&sort=updated&state=open">>,
<<"i=EWiIXmPj5gl6&v=QowBp0oDLQXdd4x_GwiywA&ip=98.20.31.81&"
"la=en&pg=New8.undertonebrandsafe.com%2F698a2525065ee2"
"60c0b2f2aaad89ab82&re=&sz=1&fc=1&fr=140&br=3&bv=11.0."
"696.16&os=3&ov=&rs=vpl&k=cookies%7Csale%7Cbrowser%7Cm"
"ore%7Cprivacy%7Cstatistics%7Cactivities%7Cauction%7Ce"
"mail%7Cfree%7Cin...&t=112373&xt=5%7C61%7C0&tz=-1&ev=x"
"&tk=&za=1&ortb-za=1&zu=&zl=&ax=U&ay=U&ortb-pid=536454"
".55&ortb-sid=112373.8&seats=999&ortb-xt=IAB24&ortb-ugc=">>,
<<"i=9pQNskA&v=0ySQQd1F&ev=12345678&t=12345&sz=3&ip=67.58."
"236.89&la=en&pg=http%3A%2F%2Fwww.yahoo.com%2Fpage1.ht"
"m&re=http%3A%2F%2Fsearch.google.com&fc=1&fr=1&br=2&bv"
"=3.0.14&os=1&ov=XP&k=cars%2Cford&rs=js&xt=5%7C22%7C23"
"4&tz=%2B180&tk=key1%3Dvalue1%7Ckey2%3Dvalue2&zl=4%2C5"
"%2C6&za=4&zu=competitor.com&ua=Mozilla%2F5.0+%28Windo"
"ws%3B+U%3B+Windows+NT+6.1%3B+en-US%29+AppleWebKit%2F5"
"34.13+%28KHTML%2C+like+Gecko%29+Chrome%2F9.0.597.98+S"
"afari%2F534.13&ortb-za=1%2C6%2C13&ortb-pid=521732&ort"
"b-sid=521732&ortb-xt=IAB3&ortb-ugc=">>
],
[{V, fun() -> V = qs(parse_qs(V)) end} || V <- Tests].
horse_parse_qs_shorter() ->
horse:repeat(20000,
parse_qs(<<"hl=en&q=erlang%20cowboy">>)
).
horse_parse_qs_short() ->
horse:repeat(20000,
parse_qs(
<<"direction=desc&for=extend%2Franch&sort=updated&state=open">>)
).
horse_parse_qs_long() ->
horse:repeat(20000,
parse_qs(<<"i=EWiIXmPj5gl6&v=QowBp0oDLQXdd4x_GwiywA&ip=98.20.31.81&"
"la=en&pg=New8.undertonebrandsafe.com%2F698a2525065ee260c0b2f2a"
"aad89ab82&re=&sz=1&fc=1&fr=140&br=3&bv=11.0.696.16&os=3&ov=&rs"
"=vpl&k=cookies%7Csale%7Cbrowser%7Cmore%7Cprivacy%7Cstatistics%"
"7Cactivities%7Cauction%7Cemail%7Cfree%7Cin...&t=112373&xt=5%7C"
"61%7C0&tz=-1&ev=x&tk=&za=1&ortb-za=1&zu=&zl=&ax=U&ay=U&ortb-pi"
"d=536454.55&ortb-sid=112373.8&seats=999&ortb-xt=IAB24&ortb-ugc"
"=">>)
).
horse_parse_qs_longer() ->
horse:repeat(20000,
parse_qs(<<"i=9pQNskA&v=0ySQQd1F&ev=12345678&t=12345&sz=3&ip=67.58."
"236.89&la=en&pg=http%3A%2F%2Fwww.yahoo.com%2Fpage1.htm&re=http"
"%3A%2F%2Fsearch.google.com&fc=1&fr=1&br=2&bv=3.0.14&os=1&ov=XP"
"&k=cars%2cford&rs=js&xt=5%7c22%7c234&tz=%2b180&tk=key1%3Dvalue"
"1%7Ckey2%3Dvalue2&zl=4,5,6&za=4&zu=competitor.com&ua=Mozilla%2"
"F5.0%20(Windows%3B%20U%3B%20Windows%20NT%206.1%3B%20en-US)%20A"
"ppleWebKit%2F534.13%20(KHTML%2C%20like%20Gecko)%20Chrome%2F9.0"
".597.98%20Safari%2F534.13&ortb-za=1%2C6%2C13&ortb-pid=521732&o"
"rtb-sid=521732&ortb-xt=IAB3&ortb-ugc=">>)
).
-endif.
%% @doc Build an application/x-www-form-urlencoded string.
-spec qs(qs_vals()) -> binary().
qs([]) ->
<<>>;
qs(L) ->
qs(L, <<>>).
qs([], Acc) ->
<<$&, Qs/bits>> = Acc,
Qs;
qs([{Name, true} | Tail], Acc) ->
Acc2 = urlencode(Name, <<Acc/bits, $&>>),
qs(Tail, Acc2);
qs([{Name, Value} | Tail], Acc) ->
Acc2 = urlencode(Name, <<Acc/bits, $&>>),
Acc3 = urlencode(Value, <<Acc2/bits, $=>>),
qs(Tail, Acc3).
-define(QS_SHORTER, [
{<<"hl">>, <<"en">>},
{<<"q">>, <<"erlang cowboy">>}
]).
-define(QS_SHORT, [
{<<"direction">>, <<"desc">>},
{<<"for">>, <<"extend/ranch">>},
{<<"sort">>, <<"updated">>},
{<<"state">>, <<"open">>}
]).
-define(QS_LONG, [
{<<"i">>, <<"EWiIXmPj5gl6">>},
{<<"v">>, <<"QowBp0oDLQXdd4x_GwiywA">>},
{<<"ip">>, <<"98.20.31.81">>},
{<<"la">>, <<"en">>},
{<<"pg">>, <<"New8.undertonebrandsafe.com/"
"698a2525065ee260c0b2f2aaad89ab82">>},
{<<"re">>, <<>>},
{<<"sz">>, <<"1">>},
{<<"fc">>, <<"1">>},
{<<"fr">>, <<"140">>},
{<<"br">>, <<"3">>},
{<<"bv">>, <<"11.0.696.16">>},
{<<"os">>, <<"3">>},
{<<"ov">>, <<>>},
{<<"rs">>, <<"vpl">>},
{<<"k">>, <<"cookies|sale|browser|more|privacy|statistics|"
"activities|auction|email|free|in...">>},
{<<"t">>, <<"112373">>},
{<<"xt">>, <<"5|61|0">>},
{<<"tz">>, <<"-1">>},
{<<"ev">>, <<"x">>},
{<<"tk">>, <<>>},
{<<"za">>, <<"1">>},
{<<"ortb-za">>, <<"1">>},
{<<"zu">>, <<>>},
{<<"zl">>, <<>>},
{<<"ax">>, <<"U">>},
{<<"ay">>, <<"U">>},
{<<"ortb-pid">>, <<"536454.55">>},
{<<"ortb-sid">>, <<"112373.8">>},
{<<"seats">>, <<"999">>},
{<<"ortb-xt">>, <<"IAB24">>},
{<<"ortb-ugc">>, <<>>}
]).
-define(QS_LONGER, [
{<<"i">>, <<"9pQNskA">>},
{<<"v">>, <<"0ySQQd1F">>},
{<<"ev">>, <<"12345678">>},
{<<"t">>, <<"12345">>},
{<<"sz">>, <<"3">>},
{<<"ip">>, <<"67.58.236.89">>},
{<<"la">>, <<"en">>},
{<<"pg">>, <<"http://www.yahoo.com/page1.htm">>},
{<<"re">>, <<"http://search.google.com">>},
{<<"fc">>, <<"1">>},
{<<"fr">>, <<"1">>},
{<<"br">>, <<"2">>},
{<<"bv">>, <<"3.0.14">>},
{<<"os">>, <<"1">>},
{<<"ov">>, <<"XP">>},
{<<"k">>, <<"cars,ford">>},
{<<"rs">>, <<"js">>},
{<<"xt">>, <<"5|22|234">>},
{<<"tz">>, <<"+180">>},
{<<"tk">>, <<"key1=value1|key2=value2">>},
{<<"zl">>, <<"4,5,6">>},
{<<"za">>, <<"4">>},
{<<"zu">>, <<"competitor.com">>},
{<<"ua">>, <<"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) "
"AppleWebKit/534.13 (KHTML, like Gecko) Chrome/9.0.597.98 "
"Safari/534.13">>},
{<<"ortb-za">>, <<"1,6,13">>},
{<<"ortb-pid">>, <<"521732">>},
{<<"ortb-sid">>, <<"521732">>},
{<<"ortb-xt">>, <<"IAB3">>},
{<<"ortb-ugc">>, <<>>}
]).
-ifdef(TEST).
qs_test_() ->
Tests = [
{[<<"a">>], error},
{[{<<"a">>, <<"b">>, <<"c">>}], error},
{[], <<>>},
{[{<<"a">>, true}], <<"a">>},
{[{<<"a">>, true}, {<<"b">>, true}], <<"a&b">>},
{[{<<"a">>, <<>>}], <<"a=">>},
{[{<<"a">>, <<"b">>}], <<"a=b">>},
{[{<<"a">>, <<>>}, {<<"b">>, <<>>}], <<"a=&b=">>},
{[{<<"a">>, <<"b">>}, {<<"c">>, true}, {<<"d">>, <<"e">>}],
<<"a=b&c&d=e">>},
{[{<<"a">>, <<"b=c">>}, {<<"d">>, <<"e=f">>}, {<<"g">>, <<"h=i">>}],
<<"a=b%3Dc&d=e%3Df&g=h%3Di">>},
{[{<<" ">>, true}], <<"+">>},
{[{<<" ">>, <<" ">>}], <<"+=+">>},
{[{<<"a b">>, <<"c d">>}], <<"a+b=c+d">>},
{[{<<" a ">>, <<" b ">>}, {<<" c ">>, <<" d ">>}],
<<"+a+=+b+&+c+=+d+">>},
{[{<<"%&=">>, <<"%&=">>}, {<<"_-.">>, <<".-_">>}],
<<"%25%26%3D=%25%26%3D&_-.=.-_">>},
{[{<<"for">>, <<"extend/ranch">>}], <<"for=extend%2Franch">>}
],
[{lists:flatten(io_lib:format("~p", [Vals])), fun() ->
E = try qs(Vals) of
R -> R
catch _:_ ->
error
end
end} || {Vals, E} <- Tests].
qs_identity_test_() ->
Tests = [
[{<<"+">>, true}],
?QS_SHORTER,
?QS_SHORT,
?QS_LONG,
?QS_LONGER
],
[{lists:flatten(io_lib:format("~p", [V])), fun() ->
V = parse_qs(qs(V))
end} || V <- Tests].
horse_qs_shorter() ->
horse:repeat(20000, qs(?QS_SHORTER)).
horse_qs_short() ->
horse:repeat(20000, qs(?QS_SHORT)).
horse_qs_long() ->
horse:repeat(20000, qs(?QS_LONG)).
horse_qs_longer() ->
horse:repeat(20000, qs(?QS_LONGER)).
-endif.
%% @doc Decode a percent encoded string (x-www-form-urlencoded rules).
-spec urldecode(B) -> B when B :: binary().
urldecode(B) ->
urldecode(B, <<>>).
urldecode(<<$%, H, L, Rest/bits>>, Acc) ->
C = (unhex(H) bsl 4 bor unhex(L)),
urldecode(Rest, <<Acc/bits, C>>);
urldecode(<<$+, Rest/bits>>, Acc) ->
urldecode(Rest, <<Acc/bits, " ">>);
urldecode(<<C, Rest/bits>>, Acc) when C =/= $% ->
urldecode(Rest, <<Acc/bits, C>>);
urldecode(<<>>, Acc) ->
Acc.
unhex($0) -> 0;
unhex($1) -> 1;
unhex($2) -> 2;
unhex($3) -> 3;
unhex($4) -> 4;
unhex($5) -> 5;
unhex($6) -> 6;
unhex($7) -> 7;
unhex($8) -> 8;
unhex($9) -> 9;
unhex($A) -> 10;
unhex($B) -> 11;
unhex($C) -> 12;
unhex($D) -> 13;
unhex($E) -> 14;
unhex($F) -> 15;
unhex($a) -> 10;
unhex($b) -> 11;
unhex($c) -> 12;
unhex($d) -> 13;
unhex($e) -> 14;
unhex($f) -> 15.
-ifdef(TEST).
urldecode_test_() ->
Tests = [
{<<"%20">>, <<" ">>},
{<<"+">>, <<" ">>},
{<<"%00">>, <<0>>},
{<<"%fF">>, <<255>>},
{<<"123">>, <<"123">>},
{<<"%i5">>, error},
{<<"%5">>, error}
],
[{Qs, fun() ->
E = try urldecode(Qs) of
R -> R
catch _:_ ->
error
end
end} || {Qs, E} <- Tests].
urldecode_identity_test_() ->
Tests = [
<<"+">>,
<<"nothingnothingnothingnothing">>,
<<"Small+fast+modular+HTTP+server">>,
<<"Small%2C+fast%2C+modular+HTTP+server.">>,
<<"%E3%83%84%E3%82%A4%E3%83%B3%E3%82%BD%E3%82%A6%E3%83"
"%AB%E3%80%9C%E8%BC%AA%E5%BB%BB%E3%81%99%E3%82%8B%E6%97%8B%E5"
"%BE%8B%E3%80%9C">>
],
[{V, fun() -> V = urlencode(urldecode(V)) end} || V <- Tests].
horse_urldecode() ->
horse:repeat(100000,
urldecode(<<"nothingnothingnothingnothing">>)
).
horse_urldecode_plus() ->
horse:repeat(100000,
urldecode(<<"Small+fast+modular+HTTP+server">>)
).
horse_urldecode_hex() ->
horse:repeat(100000,
urldecode(<<"Small%2C%20fast%2C%20modular%20HTTP%20server.">>)
).
horse_urldecode_jp_hex() ->
horse:repeat(100000,
urldecode(<<"%E3%83%84%E3%82%A4%E3%83%B3%E3%82%BD%E3%82%A6%E3%83"
"%AB%E3%80%9C%E8%BC%AA%E5%BB%BB%E3%81%99%E3%82%8B%E6%97%8B%E5"
"%BE%8B%E3%80%9C">>)
).
horse_urldecode_mix() ->
horse:repeat(100000,
urldecode(<<"Small%2C+fast%2C+modular+HTTP+server.">>)
).
-endif.
%% @doc Percent encode a string (x-www-form-urlencoded rules).
-spec urlencode(B) -> B when B :: binary().
urlencode(B) ->
urlencode(B, <<>>).
urlencode(<<$\s, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $+>>);
urlencode(<<$-, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $->>);
urlencode(<<$., Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $.>>);
urlencode(<<$0, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $0>>);
urlencode(<<$1, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $1>>);
urlencode(<<$2, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $2>>);
urlencode(<<$3, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $3>>);
urlencode(<<$4, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $4>>);
urlencode(<<$5, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $5>>);
urlencode(<<$6, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $6>>);
urlencode(<<$7, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $7>>);
urlencode(<<$8, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $8>>);
urlencode(<<$9, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $9>>);
urlencode(<<$A, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $A>>);
urlencode(<<$B, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $B>>);
urlencode(<<$C, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $C>>);
urlencode(<<$D, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $D>>);
urlencode(<<$E, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $E>>);
urlencode(<<$F, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $F>>);
urlencode(<<$G, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $G>>);
urlencode(<<$H, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $H>>);
urlencode(<<$I, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $I>>);
urlencode(<<$J, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $J>>);
urlencode(<<$K, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $K>>);
urlencode(<<$L, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $L>>);
urlencode(<<$M, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $M>>);
urlencode(<<$N, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $N>>);
urlencode(<<$O, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $O>>);
urlencode(<<$P, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $P>>);
urlencode(<<$Q, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $Q>>);
urlencode(<<$R, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $R>>);
urlencode(<<$S, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $S>>);
urlencode(<<$T, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $T>>);
urlencode(<<$U, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $U>>);
urlencode(<<$V, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $V>>);
urlencode(<<$W, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $W>>);
urlencode(<<$X, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $X>>);
urlencode(<<$Y, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $Y>>);
urlencode(<<$Z, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $Z>>);
urlencode(<<$_, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $_>>);
urlencode(<<$a, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $a>>);
urlencode(<<$b, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $b>>);
urlencode(<<$c, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $c>>);
urlencode(<<$d, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $d>>);
urlencode(<<$e, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $e>>);
urlencode(<<$f, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $f>>);
urlencode(<<$g, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $g>>);
urlencode(<<$h, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $h>>);
urlencode(<<$i, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $i>>);
urlencode(<<$j, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $j>>);
urlencode(<<$k, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $k>>);
urlencode(<<$l, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $l>>);
urlencode(<<$m, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $m>>);
urlencode(<<$n, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $n>>);
urlencode(<<$o, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $o>>);
urlencode(<<$p, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $p>>);
urlencode(<<$q, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $q>>);
urlencode(<<$r, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $r>>);
urlencode(<<$s, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $s>>);
urlencode(<<$t, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $t>>);
urlencode(<<$u, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $u>>);
urlencode(<<$v, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $v>>);
urlencode(<<$w, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $w>>);
urlencode(<<$x, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $x>>);
urlencode(<<$y, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $y>>);
urlencode(<<$z, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $z>>);
urlencode(<<C, Rest/bits>>, Acc) ->
H = hex(C bsr 4),
L = hex(C band 16#0f),
urlencode(Rest, <<Acc/bits, $%, H, L>>);
urlencode(<<>>, Acc) ->
Acc.
hex(0) -> $0;
hex(1) -> $1;
hex(2) -> $2;
hex(3) -> $3;
hex(4) -> $4;
hex(5) -> $5;
hex(6) -> $6;
hex(7) -> $7;
hex(8) -> $8;
hex(9) -> $9;
hex(10) -> $A;
hex(11) -> $B;
hex(12) -> $C;
hex(13) -> $D;
hex(14) -> $E;
hex(15) -> $F.
-ifdef(TEST).
urlencode_test_() ->
Tests = [
{<<255, 0>>, <<"%FF%00">>},
{<<255, " ">>, <<"%FF+">>},
{<<" ">>, <<"+">>},
{<<"aBc123">>, <<"aBc123">>},
{<<".-_">>, <<".-_">>}
],
[{V, fun() -> E = urlencode(V) end} || {V, E} <- Tests].
urlencode_identity_test_() ->
Tests = [
<<"+">>,
<<"nothingnothingnothingnothing">>,
<<"Small fast modular HTTP server">>,
<<"Small, fast, modular HTTP server.">>,
<<227, 131, 132, 227, 130, 164, 227, 131, 179, 227, 130, 189, 227,
130, 166, 227, 131, 171, 227, 128, 156, 232, 188, 170, 229, 187, 187, 227,
129, 153, 227, 130, 139, 230, 151, 139, 229, 190, 139, 227, 128, 156>>
],
[{V, fun() -> V = urldecode(urlencode(V)) end} || V <- Tests].
horse_urlencode() ->
horse:repeat(100000,
urlencode(<<"nothingnothingnothingnothing">>)
).
horse_urlencode_plus() ->
horse:repeat(100000,
urlencode(<<"Small fast modular HTTP server">>)
).
horse_urlencode_jp() ->
horse:repeat(100000,
urlencode(<<227, 131, 132, 227, 130, 164, 227, 131, 179, 227, 130, 189, 227,
130, 166, 227, 131, 171, 227, 128, 156, 232, 188, 170, 229, 187, 187, 227,
129, 153, 227, 130, 139, 230, 151, 139, 229, 190, 139, 227, 128, 156>>)
).
horse_urlencode_mix() ->
horse:repeat(100000,
urlencode(<<"Small, fast, modular HTTP server.">>)
).
-endif.

+ 0
- 313
src/wsLib/cow_spdy.erl Vedi File

@ -1,313 +0,0 @@
%% Copyright (c) 2013-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_spdy).
%% Zstream.
-export([deflate_init/0]).
-export([inflate_init/0]).
%% Parse.
-export([split/1]).
-export([parse/2]).
%% Build.
-export([data/3]).
-export([syn_stream/12]).
-export([syn_reply/6]).
-export([rst_stream/2]).
-export([settings/2]).
-export([ping/1]).
-export([goaway/2]).
%% @todo headers
%% @todo window_update
-include("cow_spdy.hrl").
%% Zstream.
deflate_init() ->
Zdef = zlib:open(),
ok = zlib:deflateInit(Zdef),
_ = zlib:deflateSetDictionary(Zdef, ?ZDICT),
Zdef.
inflate_init() ->
Zinf = zlib:open(),
ok = zlib:inflateInit(Zinf),
Zinf.
%% Parse.
split(Data = <<_:40, Length:24, _/bits>>)
when byte_size(Data) >= Length + 8 ->
Length2 = Length + 8,
<<Frame:Length2/binary, Rest/bits>> = Data,
{true, Frame, Rest};
split(_) ->
false.
parse(<<0:1, StreamID:31, 0:7, IsFinFlag:1, _:24, Data/bits>>, _) ->
{data, StreamID, from_flag(IsFinFlag), Data};
parse(<<1:1, 3:15, 1:16, 0:6, IsUnidirectionalFlag:1, IsFinFlag:1,
_:25, StreamID:31, _:1, AssocToStreamID:31, Priority:3, _:5,
0:8, Rest/bits>>, Zinf) ->
case parse_headers(Rest, Zinf) of
{ok, Headers, [{<<":host">>, Host}, {<<":method">>, Method},
{<<":path">>, Path}, {<<":scheme">>, Scheme},
{<<":version">>, Version}]} ->
{syn_stream, StreamID, AssocToStreamID, from_flag(IsFinFlag),
from_flag(IsUnidirectionalFlag), Priority, Method,
Scheme, Host, Path, Version, Headers};
_ ->
{error, badprotocol}
end;
parse(<<1:1, 3:15, 2:16, 0:7, IsFinFlag:1, _:25,
StreamID:31, Rest/bits>>, Zinf) ->
case parse_headers(Rest, Zinf) of
{ok, Headers, [{<<":status">>, Status}, {<<":version">>, Version}]} ->
{syn_reply, StreamID, from_flag(IsFinFlag),
Status, Version, Headers};
_ ->
{error, badprotocol}
end;
parse(<<1:1, 3:15, 3:16, 0:8, _:56, StatusCode:32>>, _)
when StatusCode =:= 0; StatusCode > 11 ->
{error, badprotocol};
parse(<<1:1, 3:15, 3:16, 0:8, _:25, StreamID:31, StatusCode:32>>, _) ->
Status = case StatusCode of
1 -> protocol_error;
2 -> invalid_stream;
3 -> refused_stream;
4 -> unsupported_version;
5 -> cancel;
6 -> internal_error;
7 -> flow_control_error;
8 -> stream_in_use;
9 -> stream_already_closed;
10 -> invalid_credentials;
11 -> frame_too_large
end,
{rst_stream, StreamID, Status};
parse(<<1:1, 3:15, 4:16, 0:7, ClearSettingsFlag:1, _:24,
NbEntries:32, Rest/bits>>, _) ->
try
Settings = [begin
Is0 = 0,
Key = case ID of
1 -> upload_bandwidth;
2 -> download_bandwidth;
3 -> round_trip_time;
4 -> max_concurrent_streams;
5 -> current_cwnd;
6 -> download_retrans_rate;
7 -> initial_window_size;
8 -> client_certificate_vector_size
end,
{Key, Value, from_flag(PersistFlag), from_flag(WasPersistedFlag)}
end || <<Is0:6, WasPersistedFlag:1, PersistFlag:1,
ID:24, Value:32>> <= Rest],
NbEntries = length(Settings),
{settings, from_flag(ClearSettingsFlag), Settings}
catch _:_ ->
{error, badprotocol}
end;
parse(<<1:1, 3:15, 6:16, 0:8, _:24, PingID:32>>, _) ->
{ping, PingID};
parse(<<1:1, 3:15, 7:16, 0:8, _:56, StatusCode:32>>, _)
when StatusCode > 2 ->
{error, badprotocol};
parse(<<1:1, 3:15, 7:16, 0:8, _:25, LastGoodStreamID:31,
StatusCode:32>>, _) ->
Status = case StatusCode of
0 -> ok;
1 -> protocol_error;
2 -> internal_error
end,
{goaway, LastGoodStreamID, Status};
parse(<<1:1, 3:15, 8:16, 0:7, IsFinFlag:1, _:25, StreamID:31,
Rest/bits>>, Zinf) ->
case parse_headers(Rest, Zinf) of
{ok, Headers, []} ->
{headers, StreamID, from_flag(IsFinFlag), Headers};
_ ->
{error, badprotocol}
end;
parse(<<1:1, 3:15, 9:16, 0:8, _:57, 0:31>>, _) ->
{error, badprotocol};
parse(<<1:1, 3:15, 9:16, 0:8, _:25, StreamID:31,
_:1, DeltaWindowSize:31>>, _) ->
{window_update, StreamID, DeltaWindowSize};
parse(_, _) ->
{error, badprotocol}.
parse_headers(Data, Zinf) ->
[<<NbHeaders:32, Rest/bits>>] = inflate(Zinf, Data),
parse_headers(Rest, NbHeaders, [], []).
parse_headers(<<>>, 0, Headers, SpHeaders) ->
{ok, lists:reverse(Headers), lists:sort(SpHeaders)};
parse_headers(<<>>, _, _, _) ->
error;
parse_headers(_, 0, _, _) ->
error;
parse_headers(<<0:32, _/bits>>, _, _, _) ->
error;
parse_headers(<<L1:32, Key:L1/binary, L2:32, Value:L2/binary, Rest/bits>>,
NbHeaders, Acc, SpAcc) ->
case Key of
<<$:, _/bits>> ->
parse_headers(Rest, NbHeaders - 1, Acc,
lists:keystore(Key, 1, SpAcc, {Key, Value}));
_ ->
parse_headers(Rest, NbHeaders - 1, [{Key, Value} | Acc], SpAcc)
end.
inflate(Zinf, Data) ->
try
zlib:inflate(Zinf, Data)
catch _:_ ->
ok = zlib:inflateSetDictionary(Zinf, ?ZDICT),
zlib:inflate(Zinf, <<>>)
end.
from_flag(0) -> false;
from_flag(1) -> true.
%% Build.
data(StreamID, IsFin, Data) ->
IsFinFlag = to_flag(IsFin),
Length = iolist_size(Data),
[<<0:1, StreamID:31, 0:7, IsFinFlag:1, Length:24>>, Data].
syn_stream(Zdef, StreamID, AssocToStreamID, IsFin, IsUnidirectional,
Priority, Method, Scheme, Host, Path, Version, Headers) ->
IsFinFlag = to_flag(IsFin),
IsUnidirectionalFlag = to_flag(IsUnidirectional),
HeaderBlock = build_headers(Zdef, [
{<<":method">>, Method},
{<<":scheme">>, Scheme},
{<<":host">>, Host},
{<<":path">>, Path},
{<<":version">>, Version}
| Headers]),
Length = 10 + iolist_size(HeaderBlock),
[<<1:1, 3:15, 1:16, 0:6, IsUnidirectionalFlag:1, IsFinFlag:1,
Length:24, 0:1, StreamID:31, 0:1, AssocToStreamID:31,
Priority:3, 0:5, 0:8>>, HeaderBlock].
syn_reply(Zdef, StreamID, IsFin, Status, Version, Headers) ->
IsFinFlag = to_flag(IsFin),
HeaderBlock = build_headers(Zdef, [
{<<":status">>, Status},
{<<":version">>, Version}
| Headers]),
Length = 4 + iolist_size(HeaderBlock),
[<<1:1, 3:15, 2:16, 0:7, IsFinFlag:1, Length:24,
0:1, StreamID:31>>, HeaderBlock].
rst_stream(StreamID, Status) ->
StatusCode = case Status of
protocol_error -> 1;
invalid_stream -> 2;
refused_stream -> 3;
unsupported_version -> 4;
cancel -> 5;
internal_error -> 6;
flow_control_error -> 7;
stream_in_use -> 8;
stream_already_closed -> 9;
invalid_credentials -> 10;
frame_too_large -> 11
end,
<<1:1, 3:15, 3:16, 0:8, 8:24,
0:1, StreamID:31, StatusCode:32>>.
settings(ClearSettingsFlag, Settings) ->
IsClearSettingsFlag = to_flag(ClearSettingsFlag),
NbEntries = length(Settings),
Entries = [begin
IsWasPersistedFlag = to_flag(WasPersistedFlag),
IsPersistFlag = to_flag(PersistFlag),
ID = case Key of
upload_bandwidth -> 1;
download_bandwidth -> 2;
round_trip_time -> 3;
max_concurrent_streams -> 4;
current_cwnd -> 5;
download_retrans_rate -> 6;
initial_window_size -> 7;
client_certificate_vector_size -> 8
end,
<<0:6, IsWasPersistedFlag:1, IsPersistFlag:1, ID:24, Value:32>>
end || {Key, Value, WasPersistedFlag, PersistFlag} <- Settings],
Length = 4 + iolist_size(Entries),
[<<1:1, 3:15, 4:16, 0:7, IsClearSettingsFlag:1, Length:24,
NbEntries:32>>, Entries].
-ifdef(TEST).
settings_frame_test() ->
ClearSettingsFlag = false,
Settings = [{max_concurrent_streams, 1000, false, false},
{initial_window_size, 10485760, false, false}],
Bin = list_to_binary(cow_spdy:settings(ClearSettingsFlag, Settings)),
P = cow_spdy:parse(Bin, undefined),
P = {settings, ClearSettingsFlag, Settings},
ok.
-endif.
ping(PingID) ->
<<1:1, 3:15, 6:16, 0:8, 4:24, PingID:32>>.
goaway(LastGoodStreamID, Status) ->
StatusCode = case Status of
ok -> 0;
protocol_error -> 1;
internal_error -> 2
end,
<<1:1, 3:15, 7:16, 0:8, 8:24,
0:1, LastGoodStreamID:31, StatusCode:32>>.
%% @todo headers
%% @todo window_update
build_headers(Zdef, Headers) ->
Headers1 = merge_headers(lists:sort(Headers), []),
NbHeaders = length(Headers1),
Headers2 = [begin
L1 = iolist_size(Key),
L2 = iolist_size(Value),
[<<L1:32>>, Key, <<L2:32>>, Value]
end || {Key, Value} <- Headers1],
zlib:deflate(Zdef, [<<NbHeaders:32>>, Headers2], full).
merge_headers([], Acc) ->
lists:reverse(Acc);
merge_headers([{Name, Value1}, {Name, Value2} | Tail], Acc) ->
merge_headers([{Name, [Value1, 0, Value2]} | Tail], Acc);
merge_headers([Head | Tail], Acc) ->
merge_headers(Tail, [Head | Acc]).
-ifdef(TEST).
merge_headers_test_() ->
Tests = [
{[{<<"set-cookie">>, <<"session=123">>}, {<<"set-cookie">>, <<"other=456">>}, {<<"content-type">>, <<"text/html">>}],
[{<<"set-cookie">>, [<<"session=123">>, 0, <<"other=456">>]}, {<<"content-type">>, <<"text/html">>}]}
],
[fun() -> D = merge_headers(R, []) end || {R, D} <- Tests].
-endif.
to_flag(false) -> 0;
to_flag(true) -> 1.

+ 0
- 181
src/wsLib/cow_spdy.hrl Vedi File

@ -1,181 +0,0 @@
%% Zlib dictionary.
-define(ZDICT, <<
16#00, 16#00, 16#00, 16#07, 16#6f, 16#70, 16#74, 16#69,
16#6f, 16#6e, 16#73, 16#00, 16#00, 16#00, 16#04, 16#68,
16#65, 16#61, 16#64, 16#00, 16#00, 16#00, 16#04, 16#70,
16#6f, 16#73, 16#74, 16#00, 16#00, 16#00, 16#03, 16#70,
16#75, 16#74, 16#00, 16#00, 16#00, 16#06, 16#64, 16#65,
16#6c, 16#65, 16#74, 16#65, 16#00, 16#00, 16#00, 16#05,
16#74, 16#72, 16#61, 16#63, 16#65, 16#00, 16#00, 16#00,
16#06, 16#61, 16#63, 16#63, 16#65, 16#70, 16#74, 16#00,
16#00, 16#00, 16#0e, 16#61, 16#63, 16#63, 16#65, 16#70,
16#74, 16#2d, 16#63, 16#68, 16#61, 16#72, 16#73, 16#65,
16#74, 16#00, 16#00, 16#00, 16#0f, 16#61, 16#63, 16#63,
16#65, 16#70, 16#74, 16#2d, 16#65, 16#6e, 16#63, 16#6f,
16#64, 16#69, 16#6e, 16#67, 16#00, 16#00, 16#00, 16#0f,
16#61, 16#63, 16#63, 16#65, 16#70, 16#74, 16#2d, 16#6c,
16#61, 16#6e, 16#67, 16#75, 16#61, 16#67, 16#65, 16#00,
16#00, 16#00, 16#0d, 16#61, 16#63, 16#63, 16#65, 16#70,
16#74, 16#2d, 16#72, 16#61, 16#6e, 16#67, 16#65, 16#73,
16#00, 16#00, 16#00, 16#03, 16#61, 16#67, 16#65, 16#00,
16#00, 16#00, 16#05, 16#61, 16#6c, 16#6c, 16#6f, 16#77,
16#00, 16#00, 16#00, 16#0d, 16#61, 16#75, 16#74, 16#68,
16#6f, 16#72, 16#69, 16#7a, 16#61, 16#74, 16#69, 16#6f,
16#6e, 16#00, 16#00, 16#00, 16#0d, 16#63, 16#61, 16#63,
16#68, 16#65, 16#2d, 16#63, 16#6f, 16#6e, 16#74, 16#72,
16#6f, 16#6c, 16#00, 16#00, 16#00, 16#0a, 16#63, 16#6f,
16#6e, 16#6e, 16#65, 16#63, 16#74, 16#69, 16#6f, 16#6e,
16#00, 16#00, 16#00, 16#0c, 16#63, 16#6f, 16#6e, 16#74,
16#65, 16#6e, 16#74, 16#2d, 16#62, 16#61, 16#73, 16#65,
16#00, 16#00, 16#00, 16#10, 16#63, 16#6f, 16#6e, 16#74,
16#65, 16#6e, 16#74, 16#2d, 16#65, 16#6e, 16#63, 16#6f,
16#64, 16#69, 16#6e, 16#67, 16#00, 16#00, 16#00, 16#10,
16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 16#74, 16#2d,
16#6c, 16#61, 16#6e, 16#67, 16#75, 16#61, 16#67, 16#65,
16#00, 16#00, 16#00, 16#0e, 16#63, 16#6f, 16#6e, 16#74,
16#65, 16#6e, 16#74, 16#2d, 16#6c, 16#65, 16#6e, 16#67,
16#74, 16#68, 16#00, 16#00, 16#00, 16#10, 16#63, 16#6f,
16#6e, 16#74, 16#65, 16#6e, 16#74, 16#2d, 16#6c, 16#6f,
16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00,
16#00, 16#0b, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e,
16#74, 16#2d, 16#6d, 16#64, 16#35, 16#00, 16#00, 16#00,
16#0d, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 16#74,
16#2d, 16#72, 16#61, 16#6e, 16#67, 16#65, 16#00, 16#00,
16#00, 16#0c, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e,
16#74, 16#2d, 16#74, 16#79, 16#70, 16#65, 16#00, 16#00,
16#00, 16#04, 16#64, 16#61, 16#74, 16#65, 16#00, 16#00,
16#00, 16#04, 16#65, 16#74, 16#61, 16#67, 16#00, 16#00,
16#00, 16#06, 16#65, 16#78, 16#70, 16#65, 16#63, 16#74,
16#00, 16#00, 16#00, 16#07, 16#65, 16#78, 16#70, 16#69,
16#72, 16#65, 16#73, 16#00, 16#00, 16#00, 16#04, 16#66,
16#72, 16#6f, 16#6d, 16#00, 16#00, 16#00, 16#04, 16#68,
16#6f, 16#73, 16#74, 16#00, 16#00, 16#00, 16#08, 16#69,
16#66, 16#2d, 16#6d, 16#61, 16#74, 16#63, 16#68, 16#00,
16#00, 16#00, 16#11, 16#69, 16#66, 16#2d, 16#6d, 16#6f,
16#64, 16#69, 16#66, 16#69, 16#65, 16#64, 16#2d, 16#73,
16#69, 16#6e, 16#63, 16#65, 16#00, 16#00, 16#00, 16#0d,
16#69, 16#66, 16#2d, 16#6e, 16#6f, 16#6e, 16#65, 16#2d,
16#6d, 16#61, 16#74, 16#63, 16#68, 16#00, 16#00, 16#00,
16#08, 16#69, 16#66, 16#2d, 16#72, 16#61, 16#6e, 16#67,
16#65, 16#00, 16#00, 16#00, 16#13, 16#69, 16#66, 16#2d,
16#75, 16#6e, 16#6d, 16#6f, 16#64, 16#69, 16#66, 16#69,
16#65, 16#64, 16#2d, 16#73, 16#69, 16#6e, 16#63, 16#65,
16#00, 16#00, 16#00, 16#0d, 16#6c, 16#61, 16#73, 16#74,
16#2d, 16#6d, 16#6f, 16#64, 16#69, 16#66, 16#69, 16#65,
16#64, 16#00, 16#00, 16#00, 16#08, 16#6c, 16#6f, 16#63,
16#61, 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, 16#00,
16#0c, 16#6d, 16#61, 16#78, 16#2d, 16#66, 16#6f, 16#72,
16#77, 16#61, 16#72, 16#64, 16#73, 16#00, 16#00, 16#00,
16#06, 16#70, 16#72, 16#61, 16#67, 16#6d, 16#61, 16#00,
16#00, 16#00, 16#12, 16#70, 16#72, 16#6f, 16#78, 16#79,
16#2d, 16#61, 16#75, 16#74, 16#68, 16#65, 16#6e, 16#74,
16#69, 16#63, 16#61, 16#74, 16#65, 16#00, 16#00, 16#00,
16#13, 16#70, 16#72, 16#6f, 16#78, 16#79, 16#2d, 16#61,
16#75, 16#74, 16#68, 16#6f, 16#72, 16#69, 16#7a, 16#61,
16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, 16#00, 16#05,
16#72, 16#61, 16#6e, 16#67, 16#65, 16#00, 16#00, 16#00,
16#07, 16#72, 16#65, 16#66, 16#65, 16#72, 16#65, 16#72,
16#00, 16#00, 16#00, 16#0b, 16#72, 16#65, 16#74, 16#72,
16#79, 16#2d, 16#61, 16#66, 16#74, 16#65, 16#72, 16#00,
16#00, 16#00, 16#06, 16#73, 16#65, 16#72, 16#76, 16#65,
16#72, 16#00, 16#00, 16#00, 16#02, 16#74, 16#65, 16#00,
16#00, 16#00, 16#07, 16#74, 16#72, 16#61, 16#69, 16#6c,
16#65, 16#72, 16#00, 16#00, 16#00, 16#11, 16#74, 16#72,
16#61, 16#6e, 16#73, 16#66, 16#65, 16#72, 16#2d, 16#65,
16#6e, 16#63, 16#6f, 16#64, 16#69, 16#6e, 16#67, 16#00,
16#00, 16#00, 16#07, 16#75, 16#70, 16#67, 16#72, 16#61,
16#64, 16#65, 16#00, 16#00, 16#00, 16#0a, 16#75, 16#73,
16#65, 16#72, 16#2d, 16#61, 16#67, 16#65, 16#6e, 16#74,
16#00, 16#00, 16#00, 16#04, 16#76, 16#61, 16#72, 16#79,
16#00, 16#00, 16#00, 16#03, 16#76, 16#69, 16#61, 16#00,
16#00, 16#00, 16#07, 16#77, 16#61, 16#72, 16#6e, 16#69,
16#6e, 16#67, 16#00, 16#00, 16#00, 16#10, 16#77, 16#77,
16#77, 16#2d, 16#61, 16#75, 16#74, 16#68, 16#65, 16#6e,
16#74, 16#69, 16#63, 16#61, 16#74, 16#65, 16#00, 16#00,
16#00, 16#06, 16#6d, 16#65, 16#74, 16#68, 16#6f, 16#64,
16#00, 16#00, 16#00, 16#03, 16#67, 16#65, 16#74, 16#00,
16#00, 16#00, 16#06, 16#73, 16#74, 16#61, 16#74, 16#75,
16#73, 16#00, 16#00, 16#00, 16#06, 16#32, 16#30, 16#30,
16#20, 16#4f, 16#4b, 16#00, 16#00, 16#00, 16#07, 16#76,
16#65, 16#72, 16#73, 16#69, 16#6f, 16#6e, 16#00, 16#00,
16#00, 16#08, 16#48, 16#54, 16#54, 16#50, 16#2f, 16#31,
16#2e, 16#31, 16#00, 16#00, 16#00, 16#03, 16#75, 16#72,
16#6c, 16#00, 16#00, 16#00, 16#06, 16#70, 16#75, 16#62,
16#6c, 16#69, 16#63, 16#00, 16#00, 16#00, 16#0a, 16#73,
16#65, 16#74, 16#2d, 16#63, 16#6f, 16#6f, 16#6b, 16#69,
16#65, 16#00, 16#00, 16#00, 16#0a, 16#6b, 16#65, 16#65,
16#70, 16#2d, 16#61, 16#6c, 16#69, 16#76, 16#65, 16#00,
16#00, 16#00, 16#06, 16#6f, 16#72, 16#69, 16#67, 16#69,
16#6e, 16#31, 16#30, 16#30, 16#31, 16#30, 16#31, 16#32,
16#30, 16#31, 16#32, 16#30, 16#32, 16#32, 16#30, 16#35,
16#32, 16#30, 16#36, 16#33, 16#30, 16#30, 16#33, 16#30,
16#32, 16#33, 16#30, 16#33, 16#33, 16#30, 16#34, 16#33,
16#30, 16#35, 16#33, 16#30, 16#36, 16#33, 16#30, 16#37,
16#34, 16#30, 16#32, 16#34, 16#30, 16#35, 16#34, 16#30,
16#36, 16#34, 16#30, 16#37, 16#34, 16#30, 16#38, 16#34,
16#30, 16#39, 16#34, 16#31, 16#30, 16#34, 16#31, 16#31,
16#34, 16#31, 16#32, 16#34, 16#31, 16#33, 16#34, 16#31,
16#34, 16#34, 16#31, 16#35, 16#34, 16#31, 16#36, 16#34,
16#31, 16#37, 16#35, 16#30, 16#32, 16#35, 16#30, 16#34,
16#35, 16#30, 16#35, 16#32, 16#30, 16#33, 16#20, 16#4e,
16#6f, 16#6e, 16#2d, 16#41, 16#75, 16#74, 16#68, 16#6f,
16#72, 16#69, 16#74, 16#61, 16#74, 16#69, 16#76, 16#65,
16#20, 16#49, 16#6e, 16#66, 16#6f, 16#72, 16#6d, 16#61,
16#74, 16#69, 16#6f, 16#6e, 16#32, 16#30, 16#34, 16#20,
16#4e, 16#6f, 16#20, 16#43, 16#6f, 16#6e, 16#74, 16#65,
16#6e, 16#74, 16#33, 16#30, 16#31, 16#20, 16#4d, 16#6f,
16#76, 16#65, 16#64, 16#20, 16#50, 16#65, 16#72, 16#6d,
16#61, 16#6e, 16#65, 16#6e, 16#74, 16#6c, 16#79, 16#34,
16#30, 16#30, 16#20, 16#42, 16#61, 16#64, 16#20, 16#52,
16#65, 16#71, 16#75, 16#65, 16#73, 16#74, 16#34, 16#30,
16#31, 16#20, 16#55, 16#6e, 16#61, 16#75, 16#74, 16#68,
16#6f, 16#72, 16#69, 16#7a, 16#65, 16#64, 16#34, 16#30,
16#33, 16#20, 16#46, 16#6f, 16#72, 16#62, 16#69, 16#64,
16#64, 16#65, 16#6e, 16#34, 16#30, 16#34, 16#20, 16#4e,
16#6f, 16#74, 16#20, 16#46, 16#6f, 16#75, 16#6e, 16#64,
16#35, 16#30, 16#30, 16#20, 16#49, 16#6e, 16#74, 16#65,
16#72, 16#6e, 16#61, 16#6c, 16#20, 16#53, 16#65, 16#72,
16#76, 16#65, 16#72, 16#20, 16#45, 16#72, 16#72, 16#6f,
16#72, 16#35, 16#30, 16#31, 16#20, 16#4e, 16#6f, 16#74,
16#20, 16#49, 16#6d, 16#70, 16#6c, 16#65, 16#6d, 16#65,
16#6e, 16#74, 16#65, 16#64, 16#35, 16#30, 16#33, 16#20,
16#53, 16#65, 16#72, 16#76, 16#69, 16#63, 16#65, 16#20,
16#55, 16#6e, 16#61, 16#76, 16#61, 16#69, 16#6c, 16#61,
16#62, 16#6c, 16#65, 16#4a, 16#61, 16#6e, 16#20, 16#46,
16#65, 16#62, 16#20, 16#4d, 16#61, 16#72, 16#20, 16#41,
16#70, 16#72, 16#20, 16#4d, 16#61, 16#79, 16#20, 16#4a,
16#75, 16#6e, 16#20, 16#4a, 16#75, 16#6c, 16#20, 16#41,
16#75, 16#67, 16#20, 16#53, 16#65, 16#70, 16#74, 16#20,
16#4f, 16#63, 16#74, 16#20, 16#4e, 16#6f, 16#76, 16#20,
16#44, 16#65, 16#63, 16#20, 16#30, 16#30, 16#3a, 16#30,
16#30, 16#3a, 16#30, 16#30, 16#20, 16#4d, 16#6f, 16#6e,
16#2c, 16#20, 16#54, 16#75, 16#65, 16#2c, 16#20, 16#57,
16#65, 16#64, 16#2c, 16#20, 16#54, 16#68, 16#75, 16#2c,
16#20, 16#46, 16#72, 16#69, 16#2c, 16#20, 16#53, 16#61,
16#74, 16#2c, 16#20, 16#53, 16#75, 16#6e, 16#2c, 16#20,
16#47, 16#4d, 16#54, 16#63, 16#68, 16#75, 16#6e, 16#6b,
16#65, 16#64, 16#2c, 16#74, 16#65, 16#78, 16#74, 16#2f,
16#68, 16#74, 16#6d, 16#6c, 16#2c, 16#69, 16#6d, 16#61,
16#67, 16#65, 16#2f, 16#70, 16#6e, 16#67, 16#2c, 16#69,
16#6d, 16#61, 16#67, 16#65, 16#2f, 16#6a, 16#70, 16#67,
16#2c, 16#69, 16#6d, 16#61, 16#67, 16#65, 16#2f, 16#67,
16#69, 16#66, 16#2c, 16#61, 16#70, 16#70, 16#6c, 16#69,
16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#2f, 16#78,
16#6d, 16#6c, 16#2c, 16#61, 16#70, 16#70, 16#6c, 16#69,
16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#2f, 16#78,
16#68, 16#74, 16#6d, 16#6c, 16#2b, 16#78, 16#6d, 16#6c,
16#2c, 16#74, 16#65, 16#78, 16#74, 16#2f, 16#70, 16#6c,
16#61, 16#69, 16#6e, 16#2c, 16#74, 16#65, 16#78, 16#74,
16#2f, 16#6a, 16#61, 16#76, 16#61, 16#73, 16#63, 16#72,
16#69, 16#70, 16#74, 16#2c, 16#70, 16#75, 16#62, 16#6c,
16#69, 16#63, 16#70, 16#72, 16#69, 16#76, 16#61, 16#74,
16#65, 16#6d, 16#61, 16#78, 16#2d, 16#61, 16#67, 16#65,
16#3d, 16#67, 16#7a, 16#69, 16#70, 16#2c, 16#64, 16#65,
16#66, 16#6c, 16#61, 16#74, 16#65, 16#2c, 16#73, 16#64,
16#63, 16#68, 16#63, 16#68, 16#61, 16#72, 16#73, 16#65,
16#74, 16#3d, 16#75, 16#74, 16#66, 16#2d, 16#38, 16#63,
16#68, 16#61, 16#72, 16#73, 16#65, 16#74, 16#3d, 16#69,
16#73, 16#6f, 16#2d, 16#38, 16#38, 16#35, 16#39, 16#2d,
16#31, 16#2c, 16#75, 16#74, 16#66, 16#2d, 16#2c, 16#2a,
16#2c, 16#65, 16#6e, 16#71, 16#3d, 16#30, 16#2e>>).

+ 0
- 348
src/wsLib/cow_sse.erl Vedi File

@ -1,348 +0,0 @@
%% Copyright (c) 2017-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_sse).
-export([init/0]).
-export([parse/2]).
-export([events/1]).
-export([event/1]).
-record(state, {
state_name = bom :: bom | events,
buffer = <<>> :: binary(),
last_event_id = <<>> :: binary(),
last_event_id_set = false :: boolean(),
event_type = <<>> :: binary(),
data = [] :: iolist(),
retry = undefined :: undefined | non_neg_integer()
}).
-type state() :: #state{}.
-export_type([state/0]).
-type parsed_event() :: #{
last_event_id := binary(),
event_type := binary(),
data := iolist()
}.
-type event() :: #{
comment => iodata(),
data => iodata(),
event => iodata() | atom(),
id => iodata(),
retry => non_neg_integer()
}.
-export_type([event/0]).
-spec init() -> state().
init() ->
#state{}.
%% @todo Add a function to retrieve the retry value from the state.
-spec parse(binary(), state())
-> {event, parsed_event(), State} | {more, State}.
parse(Data0, State = #state{state_name = bom, buffer = Buffer}) ->
Data1 = case Buffer of
<<>> -> Data0;
_ -> <<Buffer/binary, Data0/binary>>
end,
case Data1 of
%% Skip the BOM.
<<16#fe, 16#ff, Data/bits>> ->
parse_event(Data, State#state{state_name = events, buffer = <<>>});
%% Not enough data to know wether we have a BOM.
<<16#fe>> ->
{more, State#state{buffer = Data1}};
<<>> ->
{more, State};
%% No BOM.
_ ->
parse_event(Data1, State#state{state_name = events, buffer = <<>>})
end;
%% Try to process data from the buffer if there is no new input.
parse(<<>>, State = #state{buffer = Buffer}) ->
parse_event(Buffer, State#state{buffer = <<>>});
%% Otherwise process the input data as-is.
parse(Data0, State = #state{buffer = Buffer}) ->
Data = case Buffer of
<<>> -> Data0;
_ -> <<Buffer/binary, Data0/binary>>
end,
parse_event(Data, State).
parse_event(Data, State0) ->
case binary:split(Data, [<<"\r\n">>, <<"\r">>, <<"\n">>]) of
[Line, Rest] ->
case parse_line(Line, State0) of
{ok, State} ->
parse_event(Rest, State);
{event, Event, State} ->
{event, Event, State#state{buffer = Rest}}
end;
[_] ->
{more, State0#state{buffer = Data}}
end.
%% Dispatch events on empty line.
parse_line(<<>>, State) ->
dispatch_event(State);
%% Ignore comments.
parse_line(<<$:, _/bits>>, State) ->
{ok, State};
%% Normal line.
parse_line(Line, State) ->
case binary:split(Line, [<<":\s">>, <<":">>]) of
[Field, Value] ->
process_field(Field, Value, State);
[Field] ->
process_field(Field, <<>>, State)
end.
process_field(<<"event">>, Value, State) ->
{ok, State#state{event_type = Value}};
process_field(<<"data">>, Value, State = #state{data = Data}) ->
{ok, State#state{data = [<<$\n>>, Value | Data]}};
process_field(<<"id">>, Value, State) ->
{ok, State#state{last_event_id = Value, last_event_id_set = true}};
process_field(<<"retry">>, Value, State) ->
try
{ok, State#state{retry = binary_to_integer(Value)}}
catch _:_ ->
{ok, State}
end;
process_field(_, _, State) ->
{ok, State}.
%% Data is an empty string; abort.
dispatch_event(State = #state{last_event_id_set = false, data = []}) ->
{ok, State#state{event_type = <<>>}};
%% Data is an empty string but we have a last_event_id:
%% propagate it on its own so that the caller knows the
%% most recent ID.
dispatch_event(State = #state{last_event_id = LastEventID, data = []}) ->
{event, #{
last_event_id => LastEventID
}, State#state{last_event_id_set = false, event_type = <<>>}};
%% Dispatch the event.
%%
%% Always remove the last linebreak from the data.
dispatch_event(State = #state{last_event_id = LastEventID,
event_type = EventType, data = [_ | Data]}) ->
{event, #{
last_event_id => LastEventID,
event_type => case EventType of
<<>> -> <<"message">>;
_ -> EventType
end,
data => lists:reverse(Data)
}, State#state{last_event_id_set = false, event_type = <<>>, data = []}}.
-ifdef(TEST).
parse_example1_test() ->
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data
}, State} = parse(<<
"data: YHOO\n"
"data: +2\n"
"data: 10\n"
"\n">>, init()),
<<"YHOO\n+2\n10">> = iolist_to_binary(Data),
{more, _} = parse(<<>>, State),
ok.
parse_example2_test() ->
{event, #{
event_type := <<"message">>,
last_event_id := <<"1">>,
data := Data1
}, State0} = parse(<<
": test stream\n"
"\n"
"data: first event\n"
"id: 1\n"
"\n"
"data:second event\n"
"id\n"
"\n"
"data: third event\n"
"\n">>, init()),
<<"first event">> = iolist_to_binary(Data1),
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data2
}, State1} = parse(<<>>, State0),
<<"second event">> = iolist_to_binary(Data2),
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data3
}, State} = parse(<<>>, State1),
<<" third event">> = iolist_to_binary(Data3),
{more, _} = parse(<<>>, State),
ok.
parse_example3_test() ->
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data1
}, State0} = parse(<<
"data\n"
"\n"
"data\n"
"data\n"
"\n"
"data:\n">>, init()),
<<>> = iolist_to_binary(Data1),
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data2
}, State} = parse(<<>>, State0),
<<"\n">> = iolist_to_binary(Data2),
{more, _} = parse(<<>>, State),
ok.
parse_example4_test() ->
{event, Event, State0} = parse(<<
"data:test\n"
"\n"
"data: test\n"
"\n">>, init()),
{event, Event, State} = parse(<<>>, State0),
{more, _} = parse(<<>>, State),
ok.
parse_id_without_data_test() ->
{event, Event1, State0} = parse(<<
"id: 1\n"
"\n"
"data: data\n"
"\n"
"id: 2\n"
"\n">>, init()),
1 = maps:size(Event1),
#{last_event_id := <<"1">>} = Event1,
{event, #{
event_type := <<"message">>,
last_event_id := <<"1">>,
data := Data
}, State1} = parse(<<>>, State0),
<<"data">> = iolist_to_binary(Data),
{event, Event2, State} = parse(<<>>, State1),
1 = maps:size(Event2),
#{last_event_id := <<"2">>} = Event2,
{more, _} = parse(<<>>, State),
ok.
parse_repeated_id_without_data_test() ->
{event, Event1, State0} = parse(<<
"id: 1\n"
"\n"
"event: message\n" %% This will be ignored since there's no data.
"\n"
"id: 1\n"
"\n"
"id: 2\n"
"\n">>, init()),
{event, Event1, State1} = parse(<<>>, State0),
1 = maps:size(Event1),
#{last_event_id := <<"1">>} = Event1,
{event, Event2, State} = parse(<<>>, State1),
1 = maps:size(Event2),
#{last_event_id := <<"2">>} = Event2,
{more, _} = parse(<<>>, State),
ok.
parse_split_event_test() ->
{more, State} = parse(<<
"data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA">>, init()),
{event, _, _} = parse(<<"==\n\n">>, State),
ok.
-endif.
-spec events([event()]) -> iolist().
events(Events) ->
[event(Event) || Event <- Events].
-spec event(event()) -> iolist().
event(Event) ->
[
event_comment(Event),
event_id(Event),
event_name(Event),
event_data(Event),
event_retry(Event),
$\n
].
event_comment(#{comment := Comment}) ->
prefix_lines(Comment, <<>>);
event_comment(_) ->
[].
event_id(#{id := ID}) ->
nomatch = binary:match(iolist_to_binary(ID), <<"\n">>),
[<<"id: ">>, ID, $\n];
event_id(_) ->
[].
event_name(#{event := Name0}) ->
Name = if
is_atom(Name0) -> atom_to_binary(Name0, utf8);
true -> iolist_to_binary(Name0)
end,
nomatch = binary:match(Name, <<"\n">>),
[<<"event: ">>, Name, $\n];
event_name(_) ->
[].
event_data(#{data := Data}) ->
prefix_lines(Data, <<"data">>);
event_data(_) ->
[].
event_retry(#{retry := Retry}) ->
[<<"retry: ">>, integer_to_binary(Retry), $\n];
event_retry(_) ->
[].
prefix_lines(IoData, Prefix) ->
Lines = binary:split(iolist_to_binary(IoData), <<"\n">>, [global]),
[[Prefix, <<": ">>, Line, $\n] || Line <- Lines].
-ifdef(TEST).
event_test() ->
_ = event(#{}),
_ = event(#{comment => "test"}),
_ = event(#{data => "test"}),
_ = event(#{data => "test\ntest\ntest"}),
_ = event(#{data => "test\ntest\ntest\n"}),
_ = event(#{data => <<"test\ntest\ntest">>}),
_ = event(#{data => [<<"test">>, $\n, <<"test">>, [$\n, "test"]]}),
_ = event(#{event => test}),
_ = event(#{event => "test"}),
_ = event(#{id => "test"}),
_ = event(#{retry => 5000}),
_ = event(#{event => "test", data => "test"}),
_ = event(#{id => "test", event => "test", data => "test"}),
ok.
-endif.

+ 0
- 339
src/wsLib/cow_uri.erl Vedi File

@ -1,339 +0,0 @@
%% Copyright (c) 2016-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_uri).
-export([urldecode/1]).
-export([urlencode/1]).
%% @doc Decode a percent encoded string. (RFC3986 2.1)
-spec urldecode(B) -> B when B :: binary().
urldecode(B) ->
urldecode(B, <<>>).
urldecode(<<$%, H, L, Rest/bits>>, Acc) ->
C = (unhex(H) bsl 4 bor unhex(L)),
urldecode(Rest, <<Acc/bits, C>>);
urldecode(<<$!, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $!>>);
urldecode(<<$$, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $$>>);
urldecode(<<$&, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $&>>);
urldecode(<<$', Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $'>>);
urldecode(<<$(, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $(>>);
urldecode(<<$), Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $)>>);
urldecode(<<$*, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $*>>);
urldecode(<<$+, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $+>>);
urldecode(<<$,, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $,>>);
urldecode(<<$-, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $->>);
urldecode(<<$., Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $.>>);
urldecode(<<$0, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $0>>);
urldecode(<<$1, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $1>>);
urldecode(<<$2, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $2>>);
urldecode(<<$3, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $3>>);
urldecode(<<$4, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $4>>);
urldecode(<<$5, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $5>>);
urldecode(<<$6, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $6>>);
urldecode(<<$7, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $7>>);
urldecode(<<$8, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $8>>);
urldecode(<<$9, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $9>>);
urldecode(<<$:, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $:>>);
urldecode(<<$;, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $;>>);
urldecode(<<$=, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $=>>);
urldecode(<<$@, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $@>>);
urldecode(<<$A, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $A>>);
urldecode(<<$B, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $B>>);
urldecode(<<$C, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $C>>);
urldecode(<<$D, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $D>>);
urldecode(<<$E, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $E>>);
urldecode(<<$F, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $F>>);
urldecode(<<$G, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $G>>);
urldecode(<<$H, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $H>>);
urldecode(<<$I, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $I>>);
urldecode(<<$J, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $J>>);
urldecode(<<$K, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $K>>);
urldecode(<<$L, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $L>>);
urldecode(<<$M, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $M>>);
urldecode(<<$N, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $N>>);
urldecode(<<$O, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $O>>);
urldecode(<<$P, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $P>>);
urldecode(<<$Q, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $Q>>);
urldecode(<<$R, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $R>>);
urldecode(<<$S, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $S>>);
urldecode(<<$T, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $T>>);
urldecode(<<$U, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $U>>);
urldecode(<<$V, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $V>>);
urldecode(<<$W, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $W>>);
urldecode(<<$X, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $X>>);
urldecode(<<$Y, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $Y>>);
urldecode(<<$Z, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $Z>>);
urldecode(<<$_, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $_>>);
urldecode(<<$a, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $a>>);
urldecode(<<$b, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $b>>);
urldecode(<<$c, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $c>>);
urldecode(<<$d, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $d>>);
urldecode(<<$e, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $e>>);
urldecode(<<$f, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $f>>);
urldecode(<<$g, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $g>>);
urldecode(<<$h, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $h>>);
urldecode(<<$i, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $i>>);
urldecode(<<$j, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $j>>);
urldecode(<<$k, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $k>>);
urldecode(<<$l, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $l>>);
urldecode(<<$m, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $m>>);
urldecode(<<$n, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $n>>);
urldecode(<<$o, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $o>>);
urldecode(<<$p, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $p>>);
urldecode(<<$q, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $q>>);
urldecode(<<$r, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $r>>);
urldecode(<<$s, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $s>>);
urldecode(<<$t, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $t>>);
urldecode(<<$u, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $u>>);
urldecode(<<$v, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $v>>);
urldecode(<<$w, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $w>>);
urldecode(<<$x, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $x>>);
urldecode(<<$y, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $y>>);
urldecode(<<$z, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $z>>);
urldecode(<<$~, Rest/bits>>, Acc) -> urldecode(Rest, <<Acc/bits, $~>>);
urldecode(<<>>, Acc) -> Acc.
unhex($0) -> 0;
unhex($1) -> 1;
unhex($2) -> 2;
unhex($3) -> 3;
unhex($4) -> 4;
unhex($5) -> 5;
unhex($6) -> 6;
unhex($7) -> 7;
unhex($8) -> 8;
unhex($9) -> 9;
unhex($A) -> 10;
unhex($B) -> 11;
unhex($C) -> 12;
unhex($D) -> 13;
unhex($E) -> 14;
unhex($F) -> 15;
unhex($a) -> 10;
unhex($b) -> 11;
unhex($c) -> 12;
unhex($d) -> 13;
unhex($e) -> 14;
unhex($f) -> 15.
-ifdef(TEST).
urldecode_test_() ->
Tests = [
{<<"%20">>, <<" ">>},
{<<"+">>, <<"+">>},
{<<"%00">>, <<0>>},
{<<"%fF">>, <<255>>},
{<<"123">>, <<"123">>},
{<<"%i5">>, error},
{<<"%5">>, error}
],
[{Qs, fun() ->
E = try urldecode(Qs) of
R -> R
catch _:_ ->
error
end
end} || {Qs, E} <- Tests].
urldecode_identity_test_() ->
Tests = [
<<"%20">>,
<<"+">>,
<<"nothingnothingnothingnothing">>,
<<"Small+fast+modular+HTTP+server">>,
<<"Small%20fast%20modular%20HTTP%20server">>,
<<"Small%2F+fast%2F+modular+HTTP+server.">>,
<<"%E3%83%84%E3%82%A4%E3%83%B3%E3%82%BD%E3%82%A6%E3%83"
"%AB%E3%80%9C%E8%BC%AA%E5%BB%BB%E3%81%99%E3%82%8B%E6%97%8B%E5"
"%BE%8B%E3%80%9C">>
],
[{V, fun() -> V = urlencode(urldecode(V)) end} || V <- Tests].
horse_urldecode() ->
horse:repeat(100000,
urldecode(<<"nothingnothingnothingnothing">>)
).
horse_urldecode_hex() ->
horse:repeat(100000,
urldecode(<<"Small%2C%20fast%2C%20modular%20HTTP%20server.">>)
).
horse_urldecode_jp_hex() ->
horse:repeat(100000,
urldecode(<<"%E3%83%84%E3%82%A4%E3%83%B3%E3%82%BD%E3%82%A6%E3%83"
"%AB%E3%80%9C%E8%BC%AA%E5%BB%BB%E3%81%99%E3%82%8B%E6%97%8B%E5"
"%BE%8B%E3%80%9C">>)
).
-endif.
%% @doc Percent encode a string. (RFC3986 2.1)
%%
%% This function is meant to be used for path components.
-spec urlencode(B) -> B when B :: binary().
urlencode(B) ->
urlencode(B, <<>>).
urlencode(<<$!, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $!>>);
urlencode(<<$$, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $$>>);
urlencode(<<$&, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $&>>);
urlencode(<<$', Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $'>>);
urlencode(<<$(, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $(>>);
urlencode(<<$), Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $)>>);
urlencode(<<$*, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $*>>);
urlencode(<<$+, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $+>>);
urlencode(<<$,, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $,>>);
urlencode(<<$-, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $->>);
urlencode(<<$., Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $.>>);
urlencode(<<$0, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $0>>);
urlencode(<<$1, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $1>>);
urlencode(<<$2, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $2>>);
urlencode(<<$3, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $3>>);
urlencode(<<$4, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $4>>);
urlencode(<<$5, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $5>>);
urlencode(<<$6, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $6>>);
urlencode(<<$7, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $7>>);
urlencode(<<$8, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $8>>);
urlencode(<<$9, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $9>>);
urlencode(<<$:, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $:>>);
urlencode(<<$;, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $;>>);
urlencode(<<$=, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $=>>);
urlencode(<<$@, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $@>>);
urlencode(<<$A, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $A>>);
urlencode(<<$B, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $B>>);
urlencode(<<$C, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $C>>);
urlencode(<<$D, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $D>>);
urlencode(<<$E, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $E>>);
urlencode(<<$F, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $F>>);
urlencode(<<$G, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $G>>);
urlencode(<<$H, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $H>>);
urlencode(<<$I, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $I>>);
urlencode(<<$J, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $J>>);
urlencode(<<$K, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $K>>);
urlencode(<<$L, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $L>>);
urlencode(<<$M, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $M>>);
urlencode(<<$N, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $N>>);
urlencode(<<$O, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $O>>);
urlencode(<<$P, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $P>>);
urlencode(<<$Q, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $Q>>);
urlencode(<<$R, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $R>>);
urlencode(<<$S, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $S>>);
urlencode(<<$T, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $T>>);
urlencode(<<$U, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $U>>);
urlencode(<<$V, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $V>>);
urlencode(<<$W, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $W>>);
urlencode(<<$X, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $X>>);
urlencode(<<$Y, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $Y>>);
urlencode(<<$Z, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $Z>>);
urlencode(<<$_, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $_>>);
urlencode(<<$a, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $a>>);
urlencode(<<$b, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $b>>);
urlencode(<<$c, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $c>>);
urlencode(<<$d, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $d>>);
urlencode(<<$e, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $e>>);
urlencode(<<$f, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $f>>);
urlencode(<<$g, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $g>>);
urlencode(<<$h, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $h>>);
urlencode(<<$i, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $i>>);
urlencode(<<$j, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $j>>);
urlencode(<<$k, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $k>>);
urlencode(<<$l, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $l>>);
urlencode(<<$m, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $m>>);
urlencode(<<$n, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $n>>);
urlencode(<<$o, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $o>>);
urlencode(<<$p, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $p>>);
urlencode(<<$q, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $q>>);
urlencode(<<$r, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $r>>);
urlencode(<<$s, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $s>>);
urlencode(<<$t, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $t>>);
urlencode(<<$u, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $u>>);
urlencode(<<$v, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $v>>);
urlencode(<<$w, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $w>>);
urlencode(<<$x, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $x>>);
urlencode(<<$y, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $y>>);
urlencode(<<$z, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $z>>);
urlencode(<<$~, Rest/bits>>, Acc) -> urlencode(Rest, <<Acc/bits, $~>>);
urlencode(<<C, Rest/bits>>, Acc) ->
H = hex(C bsr 4),
L = hex(C band 16#0f),
urlencode(Rest, <<Acc/bits, $%, H, L>>);
urlencode(<<>>, Acc) ->
Acc.
hex(0) -> $0;
hex(1) -> $1;
hex(2) -> $2;
hex(3) -> $3;
hex(4) -> $4;
hex(5) -> $5;
hex(6) -> $6;
hex(7) -> $7;
hex(8) -> $8;
hex(9) -> $9;
hex(10) -> $A;
hex(11) -> $B;
hex(12) -> $C;
hex(13) -> $D;
hex(14) -> $E;
hex(15) -> $F.
-ifdef(TEST).
urlencode_test_() ->
Tests = [
{<<255, 0>>, <<"%FF%00">>},
{<<255, " ">>, <<"%FF%20">>},
{<<"+">>, <<"+">>},
{<<"aBc123">>, <<"aBc123">>},
{<<"!$&'()*+,:;=@-._~">>, <<"!$&'()*+,:;=@-._~">>}
],
[{V, fun() -> E = urlencode(V) end} || {V, E} <- Tests].
urlencode_identity_test_() ->
Tests = [
<<"+">>,
<<"nothingnothingnothingnothing">>,
<<"Small fast modular HTTP server">>,
<<"Small, fast, modular HTTP server.">>,
<<227, 131, 132, 227, 130, 164, 227, 131, 179, 227, 130, 189, 227,
130, 166, 227, 131, 171, 227, 128, 156, 232, 188, 170, 229, 187, 187, 227,
129, 153, 227, 130, 139, 230, 151, 139, 229, 190, 139, 227, 128, 156>>
],
[{V, fun() -> V = urldecode(urlencode(V)) end} || V <- Tests].
horse_urlencode() ->
horse:repeat(100000,
urlencode(<<"nothingnothingnothingnothing">>)
).
horse_urlencode_spaces() ->
horse:repeat(100000,
urlencode(<<"Small fast modular HTTP server">>)
).
horse_urlencode_jp() ->
horse:repeat(100000,
urlencode(<<227, 131, 132, 227, 130, 164, 227, 131, 179, 227, 130, 189, 227,
130, 166, 227, 131, 171, 227, 128, 156, 232, 188, 170, 229, 187, 187, 227,
129, 153, 227, 130, 139, 230, 151, 139, 229, 190, 139, 227, 128, 156>>)
).
horse_urlencode_mix() ->
horse:repeat(100000,
urlencode(<<"Small, fast, modular HTTP server.">>)
).
-endif.

+ 0
- 356
src/wsLib/cow_uri_template.erl Vedi File

@ -1,356 +0,0 @@
%% Copyright (c) 2019, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% This is a full level 4 implementation of URI Templates
%% as defined by RFC6570.
-module(cow_uri_template).
-export([parse/1]).
-export([expand/2]).
-type op() :: simple_string_expansion
| reserved_expansion
| fragment_expansion
| label_expansion_with_dot_prefix
| path_segment_expansion
| path_style_parameter_expansion
| form_style_query_expansion
| form_style_query_continuation.
-type var_list() :: [
{no_modifier, binary()}
| {{prefix_modifier, pos_integer()}, binary()}
| {explode_modifier, binary()}
].
-type uri_template() :: [
binary() | {expr, op(), var_list()}
].
-export_type([uri_template/0]).
-type variables() :: #{
binary() => binary()
| integer()
| float()
| [binary()]
| #{binary() => binary()}
}.
-include("cow_inline.hrl").
-include("cow_parse.hrl").
%% Parse a URI template.
-spec parse(binary()) -> uri_template().
parse(URITemplate) ->
parse(URITemplate, <<>>).
parse(<<>>, <<>>) ->
[];
parse(<<>>, Acc) ->
[Acc];
parse(<<${, R/bits>>, <<>>) ->
parse_expr(R);
parse(<<${, R/bits>>, Acc) ->
[Acc | parse_expr(R)];
%% @todo Probably should reject unallowed characters so that
%% we don't produce invalid URIs.
parse(<<C, R/bits>>, Acc) when C =/= $} ->
parse(R, <<Acc/binary, C>>).
parse_expr(<<$+, R/bits>>) ->
parse_var_list(R, reserved_expansion, []);
parse_expr(<<$#, R/bits>>) ->
parse_var_list(R, fragment_expansion, []);
parse_expr(<<$., R/bits>>) ->
parse_var_list(R, label_expansion_with_dot_prefix, []);
parse_expr(<<$/, R/bits>>) ->
parse_var_list(R, path_segment_expansion, []);
parse_expr(<<$;, R/bits>>) ->
parse_var_list(R, path_style_parameter_expansion, []);
parse_expr(<<$?, R/bits>>) ->
parse_var_list(R, form_style_query_expansion, []);
parse_expr(<<$&, R/bits>>) ->
parse_var_list(R, form_style_query_continuation, []);
parse_expr(R) ->
parse_var_list(R, simple_string_expansion, []).
parse_var_list(<<C, R/bits>>, Op, List)
when ?IS_ALPHANUM(C) or (C =:= $_) ->
parse_varname(R, Op, List, <<C>>).
parse_varname(<<C, R/bits>>, Op, List, Name)
when ?IS_ALPHANUM(C) or (C =:= $_) or (C =:= $.) or (C =:= $%) ->
parse_varname(R, Op, List, <<Name/binary, C>>);
parse_varname(<<$:, C, R/bits>>, Op, List, Name)
when (C =:= $1) or (C =:= $2) or (C =:= $3) or (C =:= $4) or (C =:= $5)
or (C =:= $6) or (C =:= $7) or (C =:= $8) or (C =:= $9) ->
parse_prefix_modifier(R, Op, List, Name, <<C>>);
parse_varname(<<$*, $,, R/bits>>, Op, List, Name) ->
parse_var_list(R, Op, [{explode_modifier, Name} | List]);
parse_varname(<<$*, $}, R/bits>>, Op, List, Name) ->
[{expr, Op, lists:reverse([{explode_modifier, Name} | List])} | parse(R, <<>>)];
parse_varname(<<$,, R/bits>>, Op, List, Name) ->
parse_var_list(R, Op, [{no_modifier, Name} | List]);
parse_varname(<<$}, R/bits>>, Op, List, Name) ->
[{expr, Op, lists:reverse([{no_modifier, Name} | List])} | parse(R, <<>>)].
parse_prefix_modifier(<<C, R/bits>>, Op, List, Name, Acc)
when ?IS_DIGIT(C), byte_size(Acc) < 4 ->
parse_prefix_modifier(R, Op, List, Name, <<Acc/binary, C>>);
parse_prefix_modifier(<<$,, R/bits>>, Op, List, Name, Acc) ->
parse_var_list(R, Op, [{{prefix_modifier, binary_to_integer(Acc)}, Name} | List]);
parse_prefix_modifier(<<$}, R/bits>>, Op, List, Name, Acc) ->
[{expr, Op, lists:reverse([{{prefix_modifier, binary_to_integer(Acc)}, Name} | List])} | parse(R, <<>>)].
%% Expand a URI template (after parsing it if necessary).
-spec expand(binary() | uri_template(), variables()) -> iodata().
expand(URITemplate, Vars) when is_binary(URITemplate) ->
expand(parse(URITemplate), Vars);
expand(URITemplate, Vars) ->
expand1(URITemplate, Vars).
expand1([], _) ->
[];
expand1([Literal | Tail], Vars) when is_binary(Literal) ->
[Literal | expand1(Tail, Vars)];
expand1([{expr, simple_string_expansion, VarList} | Tail], Vars) ->
[simple_string_expansion(VarList, Vars) | expand1(Tail, Vars)];
expand1([{expr, reserved_expansion, VarList} | Tail], Vars) ->
[reserved_expansion(VarList, Vars) | expand1(Tail, Vars)];
expand1([{expr, fragment_expansion, VarList} | Tail], Vars) ->
[fragment_expansion(VarList, Vars) | expand1(Tail, Vars)];
expand1([{expr, label_expansion_with_dot_prefix, VarList} | Tail], Vars) ->
[label_expansion_with_dot_prefix(VarList, Vars) | expand1(Tail, Vars)];
expand1([{expr, path_segment_expansion, VarList} | Tail], Vars) ->
[path_segment_expansion(VarList, Vars) | expand1(Tail, Vars)];
expand1([{expr, path_style_parameter_expansion, VarList} | Tail], Vars) ->
[path_style_parameter_expansion(VarList, Vars) | expand1(Tail, Vars)];
expand1([{expr, form_style_query_expansion, VarList} | Tail], Vars) ->
[form_style_query_expansion(VarList, Vars) | expand1(Tail, Vars)];
expand1([{expr, form_style_query_continuation, VarList} | Tail], Vars) ->
[form_style_query_continuation(VarList, Vars) | expand1(Tail, Vars)].
simple_string_expansion(VarList, Vars) ->
lists:join($,, [
apply_modifier(Modifier, unreserved, $,, Value)
|| {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]).
reserved_expansion(VarList, Vars) ->
lists:join($,, [
apply_modifier(Modifier, reserved, $,, Value)
|| {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]).
fragment_expansion(VarList, Vars) ->
case reserved_expansion(VarList, Vars) of
[] -> [];
Expanded -> [$#, Expanded]
end.
label_expansion_with_dot_prefix(VarList, Vars) ->
segment_expansion(VarList, Vars, $.).
path_segment_expansion(VarList, Vars) ->
segment_expansion(VarList, Vars, $/).
segment_expansion(VarList, Vars, Sep) ->
Expanded = lists:join(Sep, [
apply_modifier(Modifier, unreserved, Sep, Value)
|| {Modifier, _Name, Value} <- lookup_variables(VarList, Vars)]),
case Expanded of
[] -> [];
[[]] -> [];
_ -> [Sep, Expanded]
end.
path_style_parameter_expansion(VarList, Vars) ->
parameter_expansion(VarList, Vars, $;, $;, trim).
form_style_query_expansion(VarList, Vars) ->
parameter_expansion(VarList, Vars, $?, $&, no_trim).
form_style_query_continuation(VarList, Vars) ->
parameter_expansion(VarList, Vars, $&, $&, no_trim).
parameter_expansion(VarList, Vars, LeadingSep, Sep, Trim) ->
Expanded = lists:join(Sep, [
apply_parameter_modifier(Modifier, unreserved, Sep, Trim, Name, Value)
|| {Modifier, Name, Value} <- lookup_variables(VarList, Vars)]),
case Expanded of
[] -> [];
[[]] -> [];
_ -> [LeadingSep, Expanded]
end.
lookup_variables([], _) ->
[];
lookup_variables([{Modifier, Name} | Tail], Vars) ->
case Vars of
#{Name := Value} -> [{Modifier, Name, Value} | lookup_variables(Tail, Vars)];
_ -> lookup_variables(Tail, Vars)
end.
apply_modifier(no_modifier, AllowedChars, _, List) when is_list(List) ->
lists:join($,, [urlencode(Value, AllowedChars) || Value <- List]);
apply_modifier(explode_modifier, AllowedChars, ExplodeSep, List) when is_list(List) ->
lists:join(ExplodeSep, [urlencode(Value, AllowedChars) || Value <- List]);
apply_modifier(Modifier, AllowedChars, ExplodeSep, Map) when is_map(Map) ->
{JoinSep, KVSep} = case Modifier of
no_modifier -> {$,, $,};
explode_modifier -> {ExplodeSep, $=}
end,
lists:reverse(lists:join(JoinSep,
maps:fold(fun(Key, Value, Acc) ->
[[
urlencode(Key, AllowedChars),
KVSep,
urlencode(Value, AllowedChars)
] | Acc]
end, [], Map)
));
apply_modifier({prefix_modifier, MaxLen}, AllowedChars, _, Value) ->
urlencode(string:slice(binarize(Value), 0, MaxLen), AllowedChars);
apply_modifier(_, AllowedChars, _, Value) ->
urlencode(binarize(Value), AllowedChars).
apply_parameter_modifier(_, _, _, _, _, []) ->
[];
apply_parameter_modifier(_, _, _, _, _, Map) when Map =:= #{} ->
[];
apply_parameter_modifier(no_modifier, AllowedChars, _, _, Name, List) when is_list(List) ->
[
Name,
$=,
lists:join($,, [urlencode(Value, AllowedChars) || Value <- List])
];
apply_parameter_modifier(explode_modifier, AllowedChars, ExplodeSep, _, Name, List) when is_list(List) ->
lists:join(ExplodeSep, [[
Name,
$=,
urlencode(Value, AllowedChars)
] || Value <- List]);
apply_parameter_modifier(Modifier, AllowedChars, ExplodeSep, _, Name, Map) when is_map(Map) ->
{JoinSep, KVSep} = case Modifier of
no_modifier -> {$,, $,};
explode_modifier -> {ExplodeSep, $=}
end,
[
case Modifier of
no_modifier ->
[
Name,
$=
];
explode_modifier ->
[]
end,
lists:reverse(lists:join(JoinSep,
maps:fold(fun(Key, Value, Acc) ->
[[
urlencode(Key, AllowedChars),
KVSep,
urlencode(Value, AllowedChars)
] | Acc]
end, [], Map)
))
];
apply_parameter_modifier(Modifier, AllowedChars, _, Trim, Name, Value0) ->
Value1 = binarize(Value0),
Value = case Modifier of
{prefix_modifier, MaxLen} ->
string:slice(Value1, 0, MaxLen);
no_modifier ->
Value1
end,
[
Name,
case Value of
<<>> when Trim =:= trim ->
[];
<<>> when Trim =:= no_trim ->
$=;
_ ->
[
$=,
urlencode(Value, AllowedChars)
]
end
].
binarize(Value) when is_integer(Value) ->
integer_to_binary(Value);
binarize(Value) when is_float(Value) ->
float_to_binary(Value, [{decimals, 10}, compact]);
binarize(Value) ->
Value.
urlencode(Value, unreserved) ->
urlencode_unreserved(Value, <<>>);
urlencode(Value, reserved) ->
urlencode_reserved(Value, <<>>).
urlencode_unreserved(<<C, R/bits>>, Acc)
when ?IS_URI_UNRESERVED(C) ->
urlencode_unreserved(R, <<Acc/binary, C>>);
urlencode_unreserved(<<C, R/bits>>, Acc) ->
urlencode_unreserved(R, <<Acc/binary, $%, ?HEX(C)>>);
urlencode_unreserved(<<>>, Acc) ->
Acc.
urlencode_reserved(<<C, R/bits>>, Acc)
when ?IS_URI_UNRESERVED(C) or ?IS_URI_GEN_DELIMS(C) or ?IS_URI_SUB_DELIMS(C) ->
urlencode_reserved(R, <<Acc/binary, C>>);
urlencode_reserved(<<C, R/bits>>, Acc) ->
urlencode_reserved(R, <<Acc/binary, $%, ?HEX(C)>>);
urlencode_reserved(<<>>, Acc) ->
Acc.
-ifdef(TEST).
expand_uritemplate_test_() ->
Files = filelib:wildcard("deps/uritemplate-tests/*.json"),
lists:flatten([begin
{ok, JSON} = file:read_file(File),
Tests = jsx:decode(JSON, [return_maps]),
[begin
%% Erlang doesn't have a NULL value.
Vars = maps:remove(<<"undef">>, Vars0),
[
{iolist_to_binary(io_lib:format("~s - ~s: ~s => ~s",
[filename:basename(File), Section, URITemplate,
if
is_list(Expected) -> lists:join(<<" OR ">>, Expected);
true -> Expected
end
])),
fun() ->
case Expected of
false ->
{'EXIT', _} = (catch expand(URITemplate, Vars));
[_ | _] ->
Result = iolist_to_binary(expand(URITemplate, Vars)),
io:format("~p", [Result]),
true = lists:member(Result, Expected);
_ ->
Expected = iolist_to_binary(expand(URITemplate, Vars))
end
end}
|| [URITemplate, Expected] <- Cases]
end || {Section, #{
<<"variables">> := Vars0,
<<"testcases">> := Cases
}} <- maps:to_list(Tests)]
end || File <- Files]).
-endif.

+ 0
- 741
src/wsLib/cow_ws.erl Vedi File

@ -1,741 +0,0 @@
%% Copyright (c) 2015-2018, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cow_ws).
-export([key/0]).
-export([encode_key/1]).
-export([negotiate_permessage_deflate/3]).
-export([negotiate_x_webkit_deflate_frame/3]).
-export([validate_permessage_deflate/3]).
-export([parse_header/3]).
-export([parse_payload/9]).
-export([make_frame/4]).
-export([frame/2]).
-export([masked_frame/2]).
-type close_code() :: 1000..1003 | 1006..1011 | 3000..4999.
-export_type([close_code/0]).
-type extensions() :: map().
-export_type([extensions/0]).
-type deflate_opts() :: #{
%% Compression parameters.
level => zlib:zlevel(),
mem_level => zlib:zmemlevel(),
strategy => zlib:zstrategy(),
%% Whether the compression context will carry over between frames.
server_context_takeover => takeover | no_takeover,
client_context_takeover => takeover | no_takeover,
%% LZ77 sliding window size limits.
server_max_window_bits => 8..15,
client_max_window_bits => 8..15
}.
-export_type([deflate_opts/0]).
-type frag_state() :: undefined | {fin | nofin, text | binary, rsv()}.
-export_type([frag_state/0]).
-type frame() :: close | ping | pong
| {text | binary | close | ping | pong, iodata()}
| {close, close_code(), iodata()}
| {fragment, fin | nofin, text | binary | continuation, iodata()}.
-export_type([frame/0]).
-type frame_type() :: fragment | text | binary | close | ping | pong.
-export_type([frame_type/0]).
-type mask_key() :: undefined | 0..16#ffffffff.
-export_type([mask_key/0]).
-type rsv() :: <<_:3>>.
-export_type([rsv/0]).
-type utf8_state() :: 0..8 | undefined.
-export_type([utf8_state/0]).
%% @doc Generate a key for the Websocket handshake request.
-spec key() -> binary().
key() ->
base64:encode(crypto:strong_rand_bytes(16)).
%% @doc Encode the key into the accept value for the Websocket handshake response.
-spec encode_key(binary()) -> binary().
encode_key(Key) ->
base64:encode(crypto:hash(sha, [Key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"])).
%% @doc Negotiate the permessage-deflate extension.
-spec negotiate_permessage_deflate(
[binary() | {binary(), binary()}], Exts, deflate_opts())
-> ignore | {ok, iolist(), Exts} when Exts :: extensions().
%% Ignore if deflate already negotiated.
negotiate_permessage_deflate(_, #{deflate := _}, _) ->
ignore;
negotiate_permessage_deflate(Params, Extensions, Opts) ->
case lists:usort(Params) of
%% Ignore if multiple parameters with the same name.
Params2 when length(Params) =/= length(Params2) ->
ignore;
Params2 ->
negotiate_permessage_deflate1(Params2, Extensions, Opts)
end.
negotiate_permessage_deflate1(Params, Extensions, Opts) ->
%% We are allowed to send back no_takeover even if the client
%% accepts takeover. Therefore we use no_takeover if any of
%% the inputs have it.
ServerTakeover = maps:get(server_context_takeover, Opts, takeover),
ClientTakeover = maps:get(client_context_takeover, Opts, takeover),
%% We can send back window bits smaller than or equal to what
%% the client sends us.
ServerMaxWindowBits = maps:get(server_max_window_bits, Opts, 15),
ClientMaxWindowBits = maps:get(client_max_window_bits, Opts, 15),
%% We may need to send back no_context_takeover depending on configuration.
RespParams0 = case ServerTakeover of
takeover -> [];
no_takeover -> [<<"; server_no_context_takeover">>]
end,
RespParams1 = case ClientTakeover of
takeover -> RespParams0;
no_takeover -> [<<"; client_no_context_takeover">> | RespParams0]
end,
Negotiated0 = #{
server_context_takeover => ServerTakeover,
client_context_takeover => ClientTakeover,
server_max_window_bits => ServerMaxWindowBits,
client_max_window_bits => ClientMaxWindowBits
},
case negotiate_params(Params, Negotiated0, RespParams1) of
ignore ->
ignore;
{#{server_max_window_bits := SB}, _} when SB > ServerMaxWindowBits ->
ignore;
{#{client_max_window_bits := CB}, _} when CB > ClientMaxWindowBits ->
ignore;
{Negotiated, RespParams2} ->
%% We add the configured max window bits if necessary.
RespParams = case Negotiated of
#{server_max_window_bits_set := true} -> RespParams2;
_ when ServerMaxWindowBits =:= 15 -> RespParams2;
_ -> [<<"; server_max_window_bits=">>,
integer_to_binary(ServerMaxWindowBits) | RespParams2]
end,
{Inflate, Deflate} = init_permessage_deflate(
maps:get(client_max_window_bits, Negotiated),
maps:get(server_max_window_bits, Negotiated), Opts),
{ok, [<<"permessage-deflate">>, RespParams], Extensions#{
deflate => Deflate,
deflate_takeover => maps:get(server_context_takeover, Negotiated),
inflate => Inflate,
inflate_takeover => maps:get(client_context_takeover, Negotiated)}}
end.
negotiate_params([], Negotiated, RespParams) ->
{Negotiated, RespParams};
%% We must only send the client_max_window_bits parameter if the
%% request explicitly indicated the client supports it.
negotiate_params([<<"client_max_window_bits">> | Tail], Negotiated, RespParams) ->
CB = maps:get(client_max_window_bits, Negotiated),
negotiate_params(Tail, Negotiated#{client_max_window_bits_set => true},
[<<"; client_max_window_bits=">>, integer_to_binary(CB) | RespParams]);
negotiate_params([{<<"client_max_window_bits">>, Max} | Tail], Negotiated, RespParams) ->
CB0 = maps:get(client_max_window_bits, Negotiated, undefined),
case parse_max_window_bits(Max) of
error ->
ignore;
CB when CB =< CB0 ->
negotiate_params(Tail, Negotiated#{client_max_window_bits => CB},
[<<"; client_max_window_bits=">>, Max | RespParams]);
%% When the client sends window bits larger than the server wants
%% to use, we use what the server defined.
_ ->
negotiate_params(Tail, Negotiated,
[<<"; client_max_window_bits=">>, integer_to_binary(CB0) | RespParams])
end;
negotiate_params([{<<"server_max_window_bits">>, Max} | Tail], Negotiated, RespParams) ->
SB0 = maps:get(server_max_window_bits, Negotiated, undefined),
case parse_max_window_bits(Max) of
error ->
ignore;
SB when SB =< SB0 ->
negotiate_params(Tail, Negotiated#{
server_max_window_bits => SB,
server_max_window_bits_set => true},
[<<"; server_max_window_bits=">>, Max | RespParams]);
%% When the client sends window bits larger than the server wants
%% to use, we use what the server defined. The parameter will be
%% set only when this function returns.
_ ->
negotiate_params(Tail, Negotiated, RespParams)
end;
%% We only need to send the no_context_takeover parameter back
%% here if we didn't already define it via configuration.
negotiate_params([<<"client_no_context_takeover">> | Tail], Negotiated, RespParams) ->
case maps:get(client_context_takeover, Negotiated) of
no_takeover ->
negotiate_params(Tail, Negotiated, RespParams);
takeover ->
negotiate_params(Tail, Negotiated#{client_context_takeover => no_takeover},
[<<"; client_no_context_takeover">> | RespParams])
end;
negotiate_params([<<"server_no_context_takeover">> | Tail], Negotiated, RespParams) ->
case maps:get(server_context_takeover, Negotiated) of
no_takeover ->
negotiate_params(Tail, Negotiated, RespParams);
takeover ->
negotiate_params(Tail, Negotiated#{server_context_takeover => no_takeover},
[<<"; server_no_context_takeover">> | RespParams])
end;
%% Ignore if unknown parameter; ignore if parameter with invalid or missing value.
negotiate_params(_, _, _) ->
ignore.
parse_max_window_bits(<<"8">>) -> 8;
parse_max_window_bits(<<"9">>) -> 9;
parse_max_window_bits(<<"10">>) -> 10;
parse_max_window_bits(<<"11">>) -> 11;
parse_max_window_bits(<<"12">>) -> 12;
parse_max_window_bits(<<"13">>) -> 13;
parse_max_window_bits(<<"14">>) -> 14;
parse_max_window_bits(<<"15">>) -> 15;
parse_max_window_bits(_) -> error.
%% A negative WindowBits value indicates that zlib headers are not used.
init_permessage_deflate(InflateWindowBits, DeflateWindowBits, Opts) ->
Inflate = zlib:open(),
ok = zlib:inflateInit(Inflate, -InflateWindowBits),
Deflate = zlib:open(),
%% zlib 1.2.11+ now rejects -8. It used to transform it to -9.
%% We need to use 9 when 8 is requested for interoperability.
DeflateWindowBits2 = case DeflateWindowBits of
8 -> 9;
_ -> DeflateWindowBits
end,
ok = zlib:deflateInit(Deflate,
maps:get(level, Opts, best_compression),
deflated,
-DeflateWindowBits2,
maps:get(mem_level, Opts, 8),
maps:get(strategy, Opts, default)),
%% Set the owner pid of the zlib contexts if requested.
case Opts of
#{owner := Pid} -> set_owner(Pid, Inflate, Deflate);
_ -> ok
end,
{Inflate, Deflate}.
-ifdef(OTP_RELEASE).
%% Using is_port/1 on a zlib context results in a Dialyzer warning in OTP 21.
%% This function helps silence that warning while staying compatible
%% with all supported versions.
set_owner(Pid, Inflate, Deflate) ->
zlib:set_controlling_process(Inflate, Pid),
zlib:set_controlling_process(Deflate, Pid).
-else.
%% The zlib port became a reference in OTP 20.1+. There
%% was however no way to change the controlling process
%% until the OTP 20.1.3 patch version. Since we can't
%% enable compression for 20.1, 20.1.1 and 20.1.2 we
%% explicitly crash. The caller should ignore this extension.
set_owner(Pid, Inflate, Deflate) when is_port(Inflate) ->
true = erlang:port_connect(Inflate, Pid),
true = unlink(Inflate),
true = erlang:port_connect(Deflate, Pid),
true = unlink(Deflate),
ok;
set_owner(Pid, Inflate, Deflate) ->
case erlang:function_exported(zlib, set_controlling_process, 2) of
true ->
zlib:set_controlling_process(Inflate, Pid),
zlib:set_controlling_process(Deflate, Pid);
false ->
exit({error, incompatible_zlib_version,
'OTP 20.1, 20.1.1 and 20.1.2 are missing required functionality.'})
end.
-endif.
%% @doc Negotiate the x-webkit-deflate-frame extension.
%%
%% The implementation is very basic and none of the parameters
%% are currently supported.
-spec negotiate_x_webkit_deflate_frame(
[binary() | {binary(), binary()}], Exts, deflate_opts())
-> ignore | {ok, binary(), Exts} when Exts :: extensions().
negotiate_x_webkit_deflate_frame(_, #{deflate := _}, _) ->
ignore;
negotiate_x_webkit_deflate_frame(_Params, Extensions, Opts) ->
% Since we are negotiating an unconstrained deflate-frame
% then we must be willing to accept frames using the
% maximum window size which is 2^15.
{Inflate, Deflate} = init_permessage_deflate(15, 15, Opts),
{ok, <<"x-webkit-deflate-frame">>,
Extensions#{
deflate => Deflate,
deflate_takeover => takeover,
inflate => Inflate,
inflate_takeover => takeover}}.
%% @doc Validate the negotiated permessage-deflate extension.
%% Error when more than one deflate extension was negotiated.
validate_permessage_deflate(_, #{deflate := _}, _) ->
error;
validate_permessage_deflate(Params, Extensions, Opts) ->
case lists:usort(Params) of
%% Error if multiple parameters with the same name.
Params2 when length(Params) =/= length(Params2) ->
error;
Params2 ->
case parse_response_permessage_deflate_params(Params2, 15, takeover, 15, takeover) of
error ->
error;
{ClientWindowBits, ClientTakeOver, ServerWindowBits, ServerTakeOver} ->
{Inflate, Deflate} = init_permessage_deflate(ServerWindowBits, ClientWindowBits, Opts),
{ok, Extensions#{
deflate => Deflate,
deflate_takeover => ClientTakeOver,
inflate => Inflate,
inflate_takeover => ServerTakeOver}}
end
end.
parse_response_permessage_deflate_params([], CB, CTO, SB, STO) ->
{CB, CTO, SB, STO};
parse_response_permessage_deflate_params([{<<"client_max_window_bits">>, Max} | Tail], _, CTO, SB, STO) ->
case parse_max_window_bits(Max) of
error -> error;
CB -> parse_response_permessage_deflate_params(Tail, CB, CTO, SB, STO)
end;
parse_response_permessage_deflate_params([<<"client_no_context_takeover">> | Tail], CB, _, SB, STO) ->
parse_response_permessage_deflate_params(Tail, CB, no_takeover, SB, STO);
parse_response_permessage_deflate_params([{<<"server_max_window_bits">>, Max} | Tail], CB, CTO, _, STO) ->
case parse_max_window_bits(Max) of
error -> error;
SB -> parse_response_permessage_deflate_params(Tail, CB, CTO, SB, STO)
end;
parse_response_permessage_deflate_params([<<"server_no_context_takeover">> | Tail], CB, CTO, SB, _) ->
parse_response_permessage_deflate_params(Tail, CB, CTO, SB, no_takeover);
%% Error if unknown parameter; error if parameter with invalid or missing value.
parse_response_permessage_deflate_params(_, _, _, _, _) ->
error.
%% @doc Parse and validate the Websocket frame header.
%%
%% This function also updates the fragmentation state according to
%% information found in the frame's header.
-spec parse_header(binary(), extensions(), frag_state())
-> error | more | {frame_type(), frag_state(), rsv(), non_neg_integer(), mask_key(), binary()}.
%% RSV bits MUST be 0 unless an extension is negotiated
%% that defines meanings for non-zero values.
parse_header(<<_:1, Rsv:3, _/bits>>, Extensions, _) when Extensions =:= #{}, Rsv =/= 0 -> error;
%% Last 2 RSV bits MUST be 0 if deflate-frame extension is used.
parse_header(<<_:2, 1:1, _/bits>>, #{deflate := _}, _) -> error;
parse_header(<<_:3, 1:1, _/bits>>, #{deflate := _}, _) -> error;
%% Invalid opcode. Note that these opcodes may be used by extensions.
parse_header(<<_:4, 3:4, _/bits>>, _, _) -> error;
parse_header(<<_:4, 4:4, _/bits>>, _, _) -> error;
parse_header(<<_:4, 5:4, _/bits>>, _, _) -> error;
parse_header(<<_:4, 6:4, _/bits>>, _, _) -> error;
parse_header(<<_:4, 7:4, _/bits>>, _, _) -> error;
parse_header(<<_:4, 11:4, _/bits>>, _, _) -> error;
parse_header(<<_:4, 12:4, _/bits>>, _, _) -> error;
parse_header(<<_:4, 13:4, _/bits>>, _, _) -> error;
parse_header(<<_:4, 14:4, _/bits>>, _, _) -> error;
parse_header(<<_:4, 15:4, _/bits>>, _, _) -> error;
%% Control frames MUST NOT be fragmented.
parse_header(<<0:1, _:3, Opcode:4, _/bits>>, _, _) when Opcode >= 8 -> error;
%% A frame MUST NOT use the zero opcode unless fragmentation was initiated.
parse_header(<<_:4, 0:4, _/bits>>, _, undefined) -> error;
%% Non-control opcode when expecting control message or next fragment.
parse_header(<<_:4, 1:4, _/bits>>, _, {_, _, _}) -> error;
parse_header(<<_:4, 2:4, _/bits>>, _, {_, _, _}) -> error;
parse_header(<<_:4, 3:4, _/bits>>, _, {_, _, _}) -> error;
parse_header(<<_:4, 4:4, _/bits>>, _, {_, _, _}) -> error;
parse_header(<<_:4, 5:4, _/bits>>, _, {_, _, _}) -> error;
parse_header(<<_:4, 6:4, _/bits>>, _, {_, _, _}) -> error;
parse_header(<<_:4, 7:4, _/bits>>, _, {_, _, _}) -> error;
%% Close control frame length MUST be 0 or >= 2.
parse_header(<<_:4, 8:4, _:1, 1:7, _/bits>>, _, _) -> error;
%% Close control frame with incomplete close code. Need more data.
parse_header(Data = <<_:4, 8:4, 0:1, Len:7, _/bits>>, _, _) when Len > 1, byte_size(Data) < 4 -> more;
parse_header(Data = <<_:4, 8:4, 1:1, Len:7, _/bits>>, _, _) when Len > 1, byte_size(Data) < 8 -> more;
%% 7 bits payload length.
parse_header(<<Fin:1, Rsv:3/bits, Opcode:4, 0:1, Len:7, Rest/bits>>, _, FragState) when Len < 126 ->
parse_header(Opcode, Fin, FragState, Rsv, Len, undefined, Rest);
parse_header(<<Fin:1, Rsv:3/bits, Opcode:4, 1:1, Len:7, MaskKey:32, Rest/bits>>, _, FragState) when Len < 126 ->
parse_header(Opcode, Fin, FragState, Rsv, Len, MaskKey, Rest);
%% 16 bits payload length.
parse_header(<<Fin:1, Rsv:3/bits, Opcode:4, 0:1, 126:7, Len:16, Rest/bits>>, _, FragState) when Len > 125, Opcode < 8 ->
parse_header(Opcode, Fin, FragState, Rsv, Len, undefined, Rest);
parse_header(<<Fin:1, Rsv:3/bits, Opcode:4, 1:1, 126:7, Len:16, MaskKey:32, Rest/bits>>, _, FragState) when Len > 125, Opcode < 8 ->
parse_header(Opcode, Fin, FragState, Rsv, Len, MaskKey, Rest);
%% 63 bits payload length.
parse_header(<<Fin:1, Rsv:3/bits, Opcode:4, 0:1, 127:7, 0:1, Len:63, Rest/bits>>, _, FragState) when Len > 16#ffff, Opcode < 8 ->
parse_header(Opcode, Fin, FragState, Rsv, Len, undefined, Rest);
parse_header(<<Fin:1, Rsv:3/bits, Opcode:4, 1:1, 127:7, 0:1, Len:63, MaskKey:32, Rest/bits>>, _, FragState) when Len > 16#ffff, Opcode < 8 ->
parse_header(Opcode, Fin, FragState, Rsv, Len, MaskKey, Rest);
%% When payload length is over 63 bits, the most significant bit MUST be 0.
parse_header(<<_:9, 127:7, 1:1, _/bits>>, _, _) -> error;
%% For the next two clauses, it can be one of the following:
%%
%% * The minimal number of bytes MUST be used to encode the length
%% * All control frames MUST have a payload length of 125 bytes or less
parse_header(<<_:8, 0:1, 126:7, _:16, _/bits>>, _, _) -> error;
parse_header(<<_:8, 1:1, 126:7, _:48, _/bits>>, _, _) -> error;
parse_header(<<_:8, 0:1, 127:7, _:64, _/bits>>, _, _) -> error;
parse_header(<<_:8, 1:1, 127:7, _:96, _/bits>>, _, _) -> error;
%% Need more data.
parse_header(_, _, _) -> more.
parse_header(Opcode, Fin, FragState, Rsv, Len, MaskKey, Rest) ->
Type = opcode_to_frame_type(Opcode),
Type2 = case Fin of
0 -> fragment;
1 -> Type
end,
{Type2, frag_state(Type, Fin, Rsv, FragState), Rsv, Len, MaskKey, Rest}.
opcode_to_frame_type(0) -> fragment;
opcode_to_frame_type(1) -> text;
opcode_to_frame_type(2) -> binary;
opcode_to_frame_type(8) -> close;
opcode_to_frame_type(9) -> ping;
opcode_to_frame_type(10) -> pong.
frag_state(Type, 0, Rsv, undefined) -> {nofin, Type, Rsv};
frag_state(fragment, 0, _, FragState = {nofin, _, _}) -> FragState;
frag_state(fragment, 1, _, {nofin, Type, Rsv}) -> {fin, Type, Rsv};
frag_state(_, 1, _, FragState) -> FragState.
%% @doc Parse and validate the frame's payload.
%%
%% Validation is only required for text and close frames which feature
%% a UTF-8 payload.
-spec parse_payload(binary(), mask_key(), utf8_state(), non_neg_integer(),
frame_type(), non_neg_integer(), frag_state(), extensions(), rsv())
-> {ok, binary(), utf8_state(), binary()}
| {ok, close_code(), binary(), utf8_state(), binary()}
| {more, binary(), utf8_state()}
| {more, close_code(), binary(), utf8_state()}
| {error, badframe | badencoding}.
%% Empty last frame of compressed message.
parse_payload(Data, _, Utf8State, _, _, 0, {fin, _, <<1:1, 0:2>>},
#{inflate := Inflate, inflate_takeover := TakeOver}, _) ->
_ = zlib:inflate(Inflate, <<0, 0, 255, 255>>),
case TakeOver of
no_takeover -> zlib:inflateReset(Inflate);
takeover -> ok
end,
{ok, <<>>, Utf8State, Data};
%% Compressed fragmented frame.
parse_payload(Data, MaskKey, Utf8State, ParsedLen, Type, Len, FragState = {_, _, <<1:1, 0:2>>},
#{inflate := Inflate, inflate_takeover := TakeOver}, _) ->
{Data2, Rest, Eof} = split_payload(Data, Len),
Payload = inflate_frame(unmask(Data2, MaskKey, ParsedLen), Inflate, TakeOver, FragState, Eof),
validate_payload(Payload, Rest, Utf8State, ParsedLen, Type, FragState, Eof);
%% Compressed frame.
parse_payload(Data, MaskKey, Utf8State, ParsedLen, Type, Len, FragState,
#{inflate := Inflate, inflate_takeover := TakeOver}, <<1:1, 0:2>>) when Type =:= text; Type =:= binary ->
{Data2, Rest, Eof} = split_payload(Data, Len),
Payload = inflate_frame(unmask(Data2, MaskKey, ParsedLen), Inflate, TakeOver, FragState, Eof),
validate_payload(Payload, Rest, Utf8State, ParsedLen, Type, FragState, Eof);
%% Empty frame.
parse_payload(Data, _, Utf8State, 0, _, 0, _, _, _)
when Utf8State =:= 0; Utf8State =:= undefined ->
{ok, <<>>, Utf8State, Data};
%% Start of close frame.
parse_payload(Data, MaskKey, Utf8State, 0, Type = close, Len, FragState, _, <<0:3>>) ->
{<<MaskedCode:2/binary, Data2/bits>>, Rest, Eof} = split_payload(Data, Len),
<<CloseCode:16>> = unmask(MaskedCode, MaskKey, 0),
case validate_close_code(CloseCode) of
ok ->
Payload = unmask(Data2, MaskKey, 2),
case validate_payload(Payload, Rest, Utf8State, 2, Type, FragState, Eof) of
{ok, _, Utf8State2, _} -> {ok, CloseCode, Payload, Utf8State2, Rest};
{more, _, Utf8State2} -> {more, CloseCode, Payload, Utf8State2};
Error -> Error
end;
error ->
{error, badframe}
end;
%% Normal frame.
parse_payload(Data, MaskKey, Utf8State, ParsedLen, Type, Len, FragState, _, <<0:3>>) ->
{Data2, Rest, Eof} = split_payload(Data, Len),
Payload = unmask(Data2, MaskKey, ParsedLen),
validate_payload(Payload, Rest, Utf8State, ParsedLen, Type, FragState, Eof).
split_payload(Data, Len) ->
case byte_size(Data) of
Len ->
{Data, <<>>, true};
DataLen when DataLen < Len ->
{Data, <<>>, false};
_ ->
<<Data2:Len/binary, Rest/bits>> = Data,
{Data2, Rest, true}
end.
validate_close_code(Code) ->
if
Code < 1000 -> error;
Code =:= 1004 -> error;
Code =:= 1005 -> error;
Code =:= 1006 -> error;
Code > 1011, Code < 3000 -> error;
Code > 4999 -> error;
true -> ok
end.
unmask(Data, undefined, _) ->
Data;
unmask(Data, MaskKey, 0) ->
mask(Data, MaskKey, <<>>);
%% We unmask on the fly so we need to continue from the right mask byte.
unmask(Data, MaskKey, UnmaskedLen) ->
Left = UnmaskedLen rem 4,
Right = 4 - Left,
MaskKey2 = (MaskKey bsl (Left * 8)) + (MaskKey bsr (Right * 8)),
mask(Data, MaskKey2, <<>>).
mask(<<>>, _, Unmasked) ->
Unmasked;
mask(<<O:32, Rest/bits>>, MaskKey, Acc) ->
T = O bxor MaskKey,
mask(Rest, MaskKey, <<Acc/binary, T:32>>);
mask(<<O:24>>, MaskKey, Acc) ->
<<MaskKey2:24, _:8>> = <<MaskKey:32>>,
T = O bxor MaskKey2,
<<Acc/binary, T:24>>;
mask(<<O:16>>, MaskKey, Acc) ->
<<MaskKey2:16, _:16>> = <<MaskKey:32>>,
T = O bxor MaskKey2,
<<Acc/binary, T:16>>;
mask(<<O:8>>, MaskKey, Acc) ->
<<MaskKey2:8, _:24>> = <<MaskKey:32>>,
T = O bxor MaskKey2,
<<Acc/binary, T:8>>.
inflate_frame(Data, Inflate, TakeOver, FragState, true)
when FragState =:= undefined; element(1, FragState) =:= fin ->
Data2 = zlib:inflate(Inflate, <<Data/binary, 0, 0, 255, 255>>),
case TakeOver of
no_takeover -> zlib:inflateReset(Inflate);
takeover -> ok
end,
iolist_to_binary(Data2);
inflate_frame(Data, Inflate, _T, _F, _E) ->
iolist_to_binary(zlib:inflate(Inflate, Data)).
%% The Utf8State variable can be set to 'undefined' to disable the validation.
validate_payload(Payload, _, undefined, _, _, _, false) ->
{more, Payload, undefined};
validate_payload(Payload, Rest, undefined, _, _, _, true) ->
{ok, Payload, undefined, Rest};
%% Text frames and close control frames MUST have a payload that is valid UTF-8.
validate_payload(Payload, Rest, Utf8State, _, Type, _, Eof) when Type =:= text; Type =:= close ->
case validate_utf8(Payload, Utf8State) of
1 -> {error, badencoding};
Utf8State2 when not Eof -> {more, Payload, Utf8State2};
0 when Eof -> {ok, Payload, 0, Rest};
_ -> {error, badencoding}
end;
validate_payload(Payload, Rest, Utf8State, _, fragment, {Fin, text, _}, Eof) ->
case validate_utf8(Payload, Utf8State) of
1 -> {error, badencoding};
0 when Eof -> {ok, Payload, 0, Rest};
Utf8State2 when Eof, Fin =:= nofin -> {ok, Payload, Utf8State2, Rest};
Utf8State2 when not Eof -> {more, Payload, Utf8State2};
_ -> {error, badencoding}
end;
validate_payload(Payload, _, Utf8State, _, _, _, false) ->
{more, Payload, Utf8State};
validate_payload(Payload, Rest, Utf8State, _, _, _, true) ->
{ok, Payload, Utf8State, Rest}.
%% Based on the Flexible and Economical UTF-8 Decoder algorithm by
%% Bjoern Hoehrmann <bjoern@hoehrmann.de> (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
%%
%% The original algorithm has been unrolled into all combinations of values for C and State
%% each with a clause. The common clauses were then grouped together.
%%
%% This function returns 0 on success, 1 on error, and 2..8 on incomplete data.
validate_utf8(<<>>, State) -> State;
validate_utf8(<<C, Rest/bits>>, 0) when C < 128 -> validate_utf8(Rest, 0);
validate_utf8(<<C, Rest/bits>>, 2) when C >= 128, C < 144 -> validate_utf8(Rest, 0);
validate_utf8(<<C, Rest/bits>>, 3) when C >= 128, C < 144 -> validate_utf8(Rest, 2);
validate_utf8(<<C, Rest/bits>>, 5) when C >= 128, C < 144 -> validate_utf8(Rest, 2);
validate_utf8(<<C, Rest/bits>>, 7) when C >= 128, C < 144 -> validate_utf8(Rest, 3);
validate_utf8(<<C, Rest/bits>>, 8) when C >= 128, C < 144 -> validate_utf8(Rest, 3);
validate_utf8(<<C, Rest/bits>>, 2) when C >= 144, C < 160 -> validate_utf8(Rest, 0);
validate_utf8(<<C, Rest/bits>>, 3) when C >= 144, C < 160 -> validate_utf8(Rest, 2);
validate_utf8(<<C, Rest/bits>>, 5) when C >= 144, C < 160 -> validate_utf8(Rest, 2);
validate_utf8(<<C, Rest/bits>>, 6) when C >= 144, C < 160 -> validate_utf8(Rest, 3);
validate_utf8(<<C, Rest/bits>>, 7) when C >= 144, C < 160 -> validate_utf8(Rest, 3);
validate_utf8(<<C, Rest/bits>>, 2) when C >= 160, C < 192 -> validate_utf8(Rest, 0);
validate_utf8(<<C, Rest/bits>>, 3) when C >= 160, C < 192 -> validate_utf8(Rest, 2);
validate_utf8(<<C, Rest/bits>>, 4) when C >= 160, C < 192 -> validate_utf8(Rest, 2);
validate_utf8(<<C, Rest/bits>>, 6) when C >= 160, C < 192 -> validate_utf8(Rest, 3);
validate_utf8(<<C, Rest/bits>>, 7) when C >= 160, C < 192 -> validate_utf8(Rest, 3);
validate_utf8(<<C, Rest/bits>>, 0) when C >= 194, C < 224 -> validate_utf8(Rest, 2);
validate_utf8(<<224, Rest/bits>>, 0) -> validate_utf8(Rest, 4);
validate_utf8(<<C, Rest/bits>>, 0) when C >= 225, C < 237 -> validate_utf8(Rest, 3);
validate_utf8(<<237, Rest/bits>>, 0) -> validate_utf8(Rest, 5);
validate_utf8(<<C, Rest/bits>>, 0) when C =:= 238; C =:= 239 -> validate_utf8(Rest, 3);
validate_utf8(<<240, Rest/bits>>, 0) -> validate_utf8(Rest, 6);
validate_utf8(<<C, Rest/bits>>, 0) when C =:= 241; C =:= 242; C =:= 243 -> validate_utf8(Rest, 7);
validate_utf8(<<244, Rest/bits>>, 0) -> validate_utf8(Rest, 8);
validate_utf8(_, _) -> 1.
%% @doc Return a frame tuple from parsed state and data.
-spec make_frame(frame_type(), binary(), close_code(), frag_state()) -> frame().
%% Fragmented frame.
make_frame(fragment, Payload, _, {Fin, Type, _}) -> {fragment, Fin, Type, Payload};
make_frame(text, Payload, _, _) -> {text, Payload};
make_frame(binary, Payload, _, _) -> {binary, Payload};
make_frame(close, <<>>, undefined, _) -> close;
make_frame(close, Payload, CloseCode, _) -> {close, CloseCode, Payload};
make_frame(ping, <<>>, _, _) -> ping;
make_frame(ping, Payload, _, _) -> {ping, Payload};
make_frame(pong, <<>>, _, _) -> pong;
make_frame(pong, Payload, _, _) -> {pong, Payload}.
%% @doc Construct an unmasked Websocket frame.
-spec frame(frame(), extensions()) -> iodata().
%% Control frames. Control packets must not be > 125 in length.
frame(close, _) ->
<<1:1, 0:3, 8:4, 0:8>>;
frame(ping, _) ->
<<1:1, 0:3, 9:4, 0:8>>;
frame(pong, _) ->
<<1:1, 0:3, 10:4, 0:8>>;
frame({close, Payload}, Extensions) ->
frame({close, 1000, Payload}, Extensions);
frame({close, StatusCode, Payload}, _) ->
Len = 2 + iolist_size(Payload),
true = Len =< 125,
[<<1:1, 0:3, 8:4, 0:1, Len:7, StatusCode:16>>, Payload];
frame({ping, Payload}, _) ->
Len = iolist_size(Payload),
true = Len =< 125,
[<<1:1, 0:3, 9:4, 0:1, Len:7>>, Payload];
frame({pong, Payload}, _) ->
Len = iolist_size(Payload),
true = Len =< 125,
[<<1:1, 0:3, 10:4, 0:1, Len:7>>, Payload];
%% Data frames, deflate-frame extension.
frame({text, Payload}, #{deflate := Deflate, deflate_takeover := TakeOver})
when Deflate =/= false ->
Payload2 = deflate_frame(Payload, Deflate, TakeOver),
Len = payload_length(Payload2),
[<<1:1, 1:1, 0:2, 1:4, 0:1, Len/bits>>, Payload2];
frame({binary, Payload}, #{deflate := Deflate, deflate_takeover := TakeOver})
when Deflate =/= false ->
Payload2 = deflate_frame(Payload, Deflate, TakeOver),
Len = payload_length(Payload2),
[<<1:1, 1:1, 0:2, 2:4, 0:1, Len/bits>>, Payload2];
%% Data frames.
frame({text, Payload}, _) ->
Len = payload_length(Payload),
[<<1:1, 0:3, 1:4, 0:1, Len/bits>>, Payload];
frame({binary, Payload}, _) ->
Len = payload_length(Payload),
[<<1:1, 0:3, 2:4, 0:1, Len/bits>>, Payload].
%% @doc Construct a masked Websocket frame.
%%
%% We use a mask key of 0 if there is no payload for close, ping and pong frames.
-spec masked_frame(frame(), extensions()) -> iodata().
%% Control frames. Control packets must not be > 125 in length.
masked_frame(close, _) ->
<<1:1, 0:3, 8:4, 1:1, 0:39>>;
masked_frame(ping, _) ->
<<1:1, 0:3, 9:4, 1:1, 0:39>>;
masked_frame(pong, _) ->
<<1:1, 0:3, 10:4, 1:1, 0:39>>;
masked_frame({close, Payload}, Extensions) ->
frame({close, 1000, Payload}, Extensions);
masked_frame({close, StatusCode, Payload}, _) ->
Len = 2 + iolist_size(Payload),
true = Len =< 125,
MaskKeyBin = <<MaskKey:32>> = crypto:strong_rand_bytes(4),
[<<1:1, 0:3, 8:4, 1:1, Len:7>>, MaskKeyBin, mask(iolist_to_binary([<<StatusCode:16>>, Payload]), MaskKey, <<>>)];
masked_frame({ping, Payload}, _) ->
Len = iolist_size(Payload),
true = Len =< 125,
MaskKeyBin = <<MaskKey:32>> = crypto:strong_rand_bytes(4),
[<<1:1, 0:3, 9:4, 1:1, Len:7>>, MaskKeyBin, mask(iolist_to_binary(Payload), MaskKey, <<>>)];
masked_frame({pong, Payload}, _) ->
Len = iolist_size(Payload),
true = Len =< 125,
MaskKeyBin = <<MaskKey:32>> = crypto:strong_rand_bytes(4),
[<<1:1, 0:3, 10:4, 1:1, Len:7>>, MaskKeyBin, mask(iolist_to_binary(Payload), MaskKey, <<>>)];
%% Data frames, deflate-frame extension.
masked_frame({text, Payload}, #{deflate := Deflate, deflate_takeover := TakeOver})
when Deflate =/= false ->
MaskKeyBin = <<MaskKey:32>> = crypto:strong_rand_bytes(4),
Payload2 = mask(deflate_frame(Payload, Deflate, TakeOver), MaskKey, <<>>),
Len = payload_length(Payload2),
[<<1:1, 1:1, 0:2, 1:4, 1:1, Len/bits>>, MaskKeyBin, Payload2];
masked_frame({binary, Payload}, #{deflate := Deflate, deflate_takeover := TakeOver})
when Deflate =/= false ->
MaskKeyBin = <<MaskKey:32>> = crypto:strong_rand_bytes(4),
Payload2 = mask(deflate_frame(Payload, Deflate, TakeOver), MaskKey, <<>>),
Len = payload_length(Payload2),
[<<1:1, 1:1, 0:2, 2:4, 1:1, Len/bits>>, MaskKeyBin, Payload2];
%% Data frames.
masked_frame({text, Payload}, _) ->
MaskKeyBin = <<MaskKey:32>> = crypto:strong_rand_bytes(4),
Len = payload_length(Payload),
[<<1:1, 0:3, 1:4, 1:1, Len/bits>>, MaskKeyBin, mask(iolist_to_binary(Payload), MaskKey, <<>>)];
masked_frame({binary, Payload}, _) ->
MaskKeyBin = <<MaskKey:32>> = crypto:strong_rand_bytes(4),
Len = payload_length(Payload),
[<<1:1, 0:3, 2:4, 1:1, Len/bits>>, MaskKeyBin, mask(iolist_to_binary(Payload), MaskKey, <<>>)].
payload_length(Payload) ->
case iolist_size(Payload) of
N when N =< 125 -> <<N:7>>;
N when N =< 16#ffff -> <<126:7, N:16>>;
N when N =< 16#7fffffffffffffff -> <<127:7, N:64>>
end.
deflate_frame(Payload, Deflate, TakeOver) ->
Deflated = iolist_to_binary(zlib:deflate(Deflate, Payload, sync)),
case TakeOver of
no_takeover -> zlib:deflateReset(Deflate);
takeover -> ok
end,
Len = byte_size(Deflated) - 4,
case Deflated of
<<Body:Len/binary, 0:8, 0:8, 255:8, 255:8>> -> Body;
_ -> Deflated
end.

+ 0
- 625
src/wsNet/ranch.erl Vedi File

@ -1,625 +0,0 @@
%% Copyright (c) 2011-2021, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2020-2021, Jan Uhlig <juhlig@hnc-agency.org>
%% Copyright (c) 2021, Maria Scott <maria-12648430@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch).
-export([start_listener/5]).
-export([normalize_opts/1]).
-export([stop_listener/1]).
-export([suspend_listener/1]).
-export([resume_listener/1]).
-export([stop_all_acceptors/0]).
-export([restart_all_acceptors/0]).
-export([child_spec/5]).
-export([handshake/1]).
-export([handshake/2]).
-export([handshake_continue/1]).
-export([handshake_continue/2]).
-export([handshake_cancel/1]).
-export([recv_proxy_header/2]).
-export([remove_connection/1]).
-export([get_status/1]).
-export([get_addr/1]).
-export([get_port/1]).
-export([get_max_connections/1]).
-export([set_max_connections/2]).
-export([get_transport_options/1]).
-export([set_transport_options/2]).
-export([get_protocol_options/1]).
-export([set_protocol_options/2]).
-export([info/0]).
-export([info/1]).
-export([procs/2]).
-export([wait_for_connections/3]).
-export([wait_for_connections/4]).
-export([filter_options/4]).
-export([set_option_default/3]).
-export([require/1]).
-export([log/4]).
-type max_conns() :: non_neg_integer() | infinity.
-export_type([max_conns/0]).
-type opts() :: any() | transport_opts(any()).
-export_type([opts/0]).
-type alarm(Type, Callback) :: #{
type := Type,
callback := Callback,
treshold := non_neg_integer(),
cooldown => non_neg_integer()
}.
-type alarm_num_connections() :: alarm(num_connections, fun((ref(), term(), pid(), [pid()]) -> any())).
-type transport_opts(SocketOpts) :: #{
alarms => #{term() => alarm_num_connections()},
connection_type => worker | supervisor,
handshake_timeout => timeout(),
logger => module(),
max_connections => max_conns(),
num_acceptors => pos_integer(),
num_conns_sups => pos_integer(),
num_listen_sockets => pos_integer(),
post_listen_callback => fun((term()) -> ok | {error, term()}),
shutdown => timeout() | brutal_kill,
socket_opts => SocketOpts
}.
-export_type([transport_opts/1]).
-type ref() :: any().
-export_type([ref/0]).
-spec start_listener(ref(), module(), opts(), module(), any())
-> supervisor:startchild_ret().
start_listener(Ref, Transport, TransOpts0, Protocol, ProtoOpts)
when is_atom(Transport), is_atom(Protocol) ->
TransOpts = normalize_opts(TransOpts0),
_ = code:ensure_loaded(Transport),
case {erlang:function_exported(Transport, name, 0), validate_transport_opts(TransOpts)} of
{true, ok} ->
ChildSpec = #{id => {ranch_listener_sup, Ref}, start => {ranch_listener_sup, start_link, [
Ref, Transport, TransOpts, Protocol, ProtoOpts
]}, type => supervisor},
maybe_started(supervisor:start_child(ranch_sup, ChildSpec));
{false, _} ->
{error, {bad_transport, Transport}};
{_, TransOptsError} ->
TransOptsError
end.
-spec normalize_opts(opts()) -> transport_opts(any()).
normalize_opts(Map) when is_map(Map) ->
Map;
normalize_opts(Any) ->
#{socket_opts => Any}.
-spec validate_transport_opts(transport_opts(any())) -> ok | {error, any()}.
validate_transport_opts(Opts) ->
maps:fold(fun
(Key, Value, ok) ->
case validate_transport_opt(Key, Value, Opts) of
true ->
ok;
false ->
{error, {bad_option, Key}}
end;
(_, _, Acc) ->
Acc
end, ok, Opts).
-spec validate_transport_opt(any(), any(), transport_opts(any())) -> boolean().
validate_transport_opt(connection_type, worker, _) ->
true;
validate_transport_opt(connection_type, supervisor, _) ->
true;
validate_transport_opt(handshake_timeout, infinity, _) ->
true;
validate_transport_opt(handshake_timeout, Value, _) ->
is_integer(Value) andalso Value >= 0;
validate_transport_opt(max_connections, infinity, _) ->
true;
validate_transport_opt(max_connections, Value, _) ->
is_integer(Value) andalso Value >= 0;
validate_transport_opt(alarms, Alarms, _) ->
maps:fold(
fun
(_, Opts, true) ->
validate_alarm(Opts);
(_, _, false) ->
false
end,
true,
Alarms);
validate_transport_opt(logger, Value, _) ->
is_atom(Value);
validate_transport_opt(num_acceptors, Value, _) ->
is_integer(Value) andalso Value > 0;
validate_transport_opt(num_conns_sups, Value, _) ->
is_integer(Value) andalso Value > 0;
validate_transport_opt(num_listen_sockets, Value, Opts) ->
is_integer(Value) andalso Value > 0
andalso Value =< maps:get(num_acceptors, Opts, 10);
validate_transport_opt(post_listen_callback, Value, _) ->
is_function(Value, 1);
validate_transport_opt(shutdown, brutal_kill, _) ->
true;
validate_transport_opt(shutdown, infinity, _) ->
true;
validate_transport_opt(shutdown, Value, _) ->
is_integer(Value) andalso Value >= 0;
validate_transport_opt(socket_opts, _, _) ->
true;
validate_transport_opt(_, _, _) ->
false.
validate_alarm(Alarm = #{type := num_connections, treshold := Treshold,
callback := Callback}) ->
is_integer(Treshold) andalso Treshold >= 0
andalso is_function(Callback, 4)
andalso case Alarm of
#{cooldown := Cooldown} ->
is_integer(Cooldown) andalso Cooldown >= 0;
_ ->
true
end;
validate_alarm(_) ->
false.
maybe_started({error, {{shutdown,
{failed_to_start_child, ranch_acceptors_sup,
{listen_error, _, Reason}}}, _}} = Error) ->
start_error(Reason, Error);
maybe_started(Res) ->
Res.
start_error(E = eaddrinuse, _) -> {error, E};
start_error(E = eacces, _) -> {error, E};
start_error(E = no_cert, _) -> {error, E};
start_error(_, Error) -> Error.
-spec stop_listener(ref()) -> ok | {error, not_found}.
stop_listener(Ref) ->
[_, Transport, _, _, _] = ranch_server:get_listener_start_args(Ref),
TransOpts = get_transport_options(Ref),
case supervisor:terminate_child(ranch_sup, {ranch_listener_sup, Ref}) of
ok ->
_ = supervisor:delete_child(ranch_sup, {ranch_listener_sup, Ref}),
ranch_server:cleanup_listener_opts(Ref),
Transport:cleanup(TransOpts);
{error, Reason} ->
{error, Reason}
end.
-spec suspend_listener(ref()) -> ok | {error, any()}.
suspend_listener(Ref) ->
case get_status(Ref) of
running ->
ListenerSup = ranch_server:get_listener_sup(Ref),
ok = ranch_server:set_addr(Ref, {undefined, undefined}),
supervisor:terminate_child(ListenerSup, ranch_acceptors_sup);
suspended ->
ok
end.
-spec resume_listener(ref()) -> ok | {error, any()}.
resume_listener(Ref) ->
case get_status(Ref) of
running ->
ok;
suspended ->
ListenerSup = ranch_server:get_listener_sup(Ref),
Res = supervisor:restart_child(ListenerSup, ranch_acceptors_sup),
maybe_resumed(Res)
end.
maybe_resumed(Error = {error, {listen_error, _, Reason}}) ->
start_error(Reason, Error);
maybe_resumed({ok, _}) ->
ok;
maybe_resumed({ok, _, _}) ->
ok;
maybe_resumed(Res) ->
Res.
-spec stop_all_acceptors() -> ok.
stop_all_acceptors() ->
_ = [ok = do_acceptors(Pid, terminate_child)
|| {_, Pid} <- ranch_server:get_listener_sups()],
ok.
-spec restart_all_acceptors() -> ok.
restart_all_acceptors() ->
_ = [ok = do_acceptors(Pid, restart_child)
|| {_, Pid} <- ranch_server:get_listener_sups()],
ok.
do_acceptors(ListenerSup, F) ->
ListenerChildren = supervisor:which_children(ListenerSup),
case lists:keyfind(ranch_acceptors_sup, 1, ListenerChildren) of
{_, AcceptorsSup, _, _} when is_pid(AcceptorsSup) ->
AcceptorChildren = supervisor:which_children(AcceptorsSup),
%% @todo What about errors?
_ = [supervisor:F(AcceptorsSup, AcceptorId)
|| {AcceptorId, _, _, _} <- AcceptorChildren],
ok;
{_, Atom, _, _} ->
{error, Atom}
end.
-spec child_spec(ref(), module(), opts(), module(), any())
-> supervisor:child_spec().
child_spec(Ref, Transport, TransOpts0, Protocol, ProtoOpts) ->
TransOpts = normalize_opts(TransOpts0),
#{id => {ranch_embedded_sup, Ref}, start => {ranch_embedded_sup, start_link, [
Ref, Transport, TransOpts, Protocol, ProtoOpts
]}, type => supervisor}.
-spec handshake(ref()) -> {ok, ranch_transport:socket()} | {continue, any()}.
handshake(Ref) ->
handshake1(Ref, undefined).
-spec handshake(ref(), any()) -> {ok, ranch_transport:socket()} | {continue, any()}.
handshake(Ref, Opts) ->
handshake1(Ref, {opts, Opts}).
handshake1(Ref, Opts) ->
receive {handshake, Ref, Transport, CSocket, Timeout} ->
Handshake = handshake_transport(Transport, handshake, CSocket, Opts, Timeout),
handshake_result(Handshake, Ref, Transport, CSocket, Timeout)
end.
-spec handshake_continue(ref()) -> {ok, ranch_transport:socket()}.
handshake_continue(Ref) ->
handshake_continue1(Ref, undefined).
-spec handshake_continue(ref(), any()) -> {ok, ranch_transport:socket()}.
handshake_continue(Ref, Opts) ->
handshake_continue1(Ref, {opts, Opts}).
handshake_continue1(Ref, Opts) ->
receive {handshake_continue, Ref, Transport, CSocket, Timeout} ->
Handshake = handshake_transport(Transport, handshake_continue, CSocket, Opts, Timeout),
handshake_result(Handshake, Ref, Transport, CSocket, Timeout)
end.
handshake_transport(Transport, Fun, CSocket, undefined, Timeout) ->
Transport:Fun(CSocket, Timeout);
handshake_transport(Transport, Fun, CSocket, {opts, Opts}, Timeout) ->
Transport:Fun(CSocket, Opts, Timeout).
handshake_result(Result, Ref, Transport, CSocket, Timeout) ->
case Result of
OK = {ok, _} ->
OK;
{ok, CSocket2, Info} ->
self() ! {handshake_continue, Ref, Transport, CSocket2, Timeout},
{continue, Info};
{error, {tls_alert, _}} ->
ok = Transport:close(CSocket),
exit(normal);
{error, Reason} when Reason =:= timeout; Reason =:= closed ->
ok = Transport:close(CSocket),
exit(normal);
{error, Reason} ->
ok = Transport:close(CSocket),
error(Reason)
end.
-spec handshake_cancel(ref()) -> ok.
handshake_cancel(Ref) ->
receive {handshake_continue, Ref, Transport, CSocket, _} ->
Transport:handshake_cancel(CSocket)
end.
%% Unlike handshake/2 this function always return errors because
%% the communication between the proxy and the server are expected
%% to be reliable. If there is a problem while receiving the proxy
%% header, we probably want to know about it.
-spec recv_proxy_header(ref(), timeout())
-> {ok, ranch_proxy_header:proxy_info()}
| {error, closed | atom()}
| {error, protocol_error, atom()}.
recv_proxy_header(Ref, Timeout) ->
receive HandshakeState = {handshake, Ref, Transport, CSocket, _} ->
self() ! HandshakeState,
Transport:recv_proxy_header(CSocket, Timeout)
end.
-spec remove_connection(ref()) -> ok.
remove_connection(Ref) ->
ListenerSup = ranch_server:get_listener_sup(Ref),
{_, ConnsSupSup, _, _} = lists:keyfind(ranch_conns_sup_sup, 1,
supervisor:which_children(ListenerSup)),
_ = [ConnsSup ! {remove_connection, Ref, self()} ||
{_, ConnsSup, _, _} <- supervisor:which_children(ConnsSupSup)],
ok.
-spec get_status(ref()) -> running | suspended.
get_status(Ref) ->
ListenerSup = ranch_server:get_listener_sup(Ref),
Children = supervisor:which_children(ListenerSup),
case lists:keyfind(ranch_acceptors_sup, 1, Children) of
{_, undefined, _, _} ->
suspended;
_ ->
running
end.
-spec get_addr(ref()) -> {inet:ip_address(), inet:port_number()} |
{local, binary()} | {undefined, undefined}.
get_addr(Ref) ->
ranch_server:get_addr(Ref).
-spec get_port(ref()) -> inet:port_number() | undefined.
get_port(Ref) ->
case get_addr(Ref) of
{local, _} ->
undefined;
{_, Port} ->
Port
end.
-spec get_connections(ref(), active|all) -> non_neg_integer().
get_connections(Ref, active) ->
SupCounts = [ranch_conns_sup:active_connections(ConnsSup) ||
{_, ConnsSup} <- ranch_server:get_connections_sups(Ref)],
lists:sum(SupCounts);
get_connections(Ref, all) ->
SupCounts = [proplists:get_value(active, supervisor:count_children(ConnsSup)) ||
{_, ConnsSup} <- ranch_server:get_connections_sups(Ref)],
lists:sum(SupCounts).
-spec get_max_connections(ref()) -> max_conns().
get_max_connections(Ref) ->
ranch_server:get_max_connections(Ref).
-spec set_max_connections(ref(), max_conns()) -> ok.
set_max_connections(Ref, MaxConnections) ->
ranch_server:set_max_connections(Ref, MaxConnections).
-spec get_transport_options(ref()) -> transport_opts(any()).
get_transport_options(Ref) ->
ranch_server:get_transport_options(Ref).
-spec set_transport_options(ref(), opts()) -> ok | {error, term()}.
set_transport_options(Ref, TransOpts0) ->
TransOpts = normalize_opts(TransOpts0),
case validate_transport_opts(TransOpts) of
ok ->
ok = ranch_server:set_transport_options(Ref, TransOpts),
ok = apply_transport_options(Ref, TransOpts);
TransOptsError ->
TransOptsError
end.
apply_transport_options(Ref, TransOpts) ->
_ = [ConnsSup ! {set_transport_options, TransOpts}
|| {_, ConnsSup} <- ranch_server:get_connections_sups(Ref)],
ok.
-spec get_protocol_options(ref()) -> any().
get_protocol_options(Ref) ->
ranch_server:get_protocol_options(Ref).
-spec set_protocol_options(ref(), any()) -> ok.
set_protocol_options(Ref, Opts) ->
ranch_server:set_protocol_options(Ref, Opts).
-spec info() -> #{ref() := #{atom() := term()}}.
info() ->
lists:foldl(
fun({Ref, Pid}, Acc) ->
Acc#{Ref => listener_info(Ref, Pid)}
end,
#{},
ranch_server:get_listener_sups()
).
-spec info(ref()) -> #{atom() := term()}.
info(Ref) ->
Pid = ranch_server:get_listener_sup(Ref),
listener_info(Ref, Pid).
listener_info(Ref, Pid) ->
[_, Transport, _, Protocol, _] = ranch_server:get_listener_start_args(Ref),
Status = get_status(Ref),
{IP, Port} = case get_addr(Ref) of
Addr = {local, _} ->
{Addr, undefined};
Addr ->
Addr
end,
MaxConns = get_max_connections(Ref),
TransOpts = ranch_server:get_transport_options(Ref),
ProtoOpts = get_protocol_options(Ref),
#{
pid => Pid,
status => Status,
ip => IP,
port => Port,
max_connections => MaxConns,
active_connections => get_connections(Ref, active),
all_connections => get_connections(Ref, all),
transport => Transport,
transport_options => TransOpts,
protocol => Protocol,
protocol_options => ProtoOpts,
metrics => metrics(Ref)
}.
-spec procs(ref(), acceptors | connections) -> [pid()].
procs(Ref, Type) ->
ListenerSup = ranch_server:get_listener_sup(Ref),
procs1(ListenerSup, Type).
procs1(ListenerSup, acceptors) ->
{_, SupPid, _, _} = lists:keyfind(ranch_acceptors_sup, 1,
supervisor:which_children(ListenerSup)),
try
[Pid || {_, Pid, _, _} <- supervisor:which_children(SupPid)]
catch exit:{noproc, _} ->
[]
end;
procs1(ListenerSup, connections) ->
{_, SupSupPid, _, _} = lists:keyfind(ranch_conns_sup_sup, 1,
supervisor:which_children(ListenerSup)),
Conns =
lists:map(fun({_, SupPid, _, _}) ->
[Pid || {_, Pid, _, _} <- supervisor:which_children(SupPid)]
end,
supervisor:which_children(SupSupPid)
),
lists:flatten(Conns).
-spec metrics(ref()) -> #{}.
metrics(Ref) ->
Counters = ranch_server:get_stats_counters(Ref),
CounterInfo = counters:info(Counters),
NumCounters = maps:get(size, CounterInfo),
NumConnsSups = NumCounters div 2,
lists:foldl(
fun(Id, Acc) ->
Acc#{
{conns_sup, Id, accept} => counters:get(Counters, 2 * Id - 1),
{conns_sup, Id, terminate} => counters:get(Counters, 2 * Id)
}
end,
#{},
lists:seq(1, NumConnsSups)
).
-spec wait_for_connections
(ref(), '>' | '>=' | '==' | '=<', non_neg_integer()) -> ok;
(ref(), '<', pos_integer()) -> ok.
wait_for_connections(Ref, Op, NumConns) ->
wait_for_connections(Ref, Op, NumConns, 1000).
-spec wait_for_connections
(ref(), '>' | '>=' | '==' | '=<', non_neg_integer(), non_neg_integer()) -> ok;
(ref(), '<', pos_integer(), non_neg_integer()) -> ok.
wait_for_connections(Ref, Op, NumConns, Interval) ->
validate_op(Op, NumConns),
validate_num_conns(NumConns),
validate_interval(Interval),
wait_for_connections_loop(Ref, Op, NumConns, Interval).
validate_op('>', _) -> ok;
validate_op('>=', _) -> ok;
validate_op('==', _) -> ok;
validate_op('=<', _) -> ok;
validate_op('<', NumConns) when NumConns > 0 -> ok;
validate_op(_, _) -> error(badarg).
validate_num_conns(NumConns) when is_integer(NumConns), NumConns >= 0 -> ok;
validate_num_conns(_) -> error(badarg).
validate_interval(Interval) when is_integer(Interval), Interval >= 0 -> ok;
validate_interval(_) -> error(badarg).
wait_for_connections_loop(Ref, Op, NumConns, Interval) ->
CurConns = try
get_connections(Ref, all)
catch _:_ ->
0
end,
case erlang:Op(CurConns, NumConns) of
true ->
ok;
false when Interval =:= 0 ->
wait_for_connections_loop(Ref, Op, NumConns, Interval);
false ->
timer:sleep(Interval),
wait_for_connections_loop(Ref, Op, NumConns, Interval)
end.
-spec filter_options([inet | inet6 | {atom(), any()} | {raw, any(), any(), any()}],
[atom()], Acc, module()) -> Acc when Acc :: [any()].
filter_options(UserOptions, DisallowedKeys, DefaultOptions, Logger) ->
AllowedOptions = filter_user_options(UserOptions, DisallowedKeys, Logger),
lists:foldl(fun merge_options/2, DefaultOptions, AllowedOptions).
%% 2-tuple options.
filter_user_options([Opt = {Key, _} | Tail], DisallowedKeys, Logger) ->
case lists:member(Key, DisallowedKeys) of
false ->
[Opt | filter_user_options(Tail, DisallowedKeys, Logger)];
true ->
filter_options_warning(Opt, Logger),
filter_user_options(Tail, DisallowedKeys, Logger)
end;
%% Special option forms.
filter_user_options([inet | Tail], DisallowedKeys, Logger) ->
[inet | filter_user_options(Tail, DisallowedKeys, Logger)];
filter_user_options([inet6 | Tail], DisallowedKeys, Logger) ->
[inet6 | filter_user_options(Tail, DisallowedKeys, Logger)];
filter_user_options([Opt = {raw, _, _, _} | Tail], DisallowedKeys, Logger) ->
[Opt | filter_user_options(Tail, DisallowedKeys, Logger)];
filter_user_options([Opt | Tail], DisallowedKeys, Logger) ->
filter_options_warning(Opt, Logger),
filter_user_options(Tail, DisallowedKeys, Logger);
filter_user_options([], _, _) ->
[].
filter_options_warning(Opt, Logger) ->
log(warning,
"Transport option ~p unknown or invalid.~n",
[Opt], Logger).
merge_options({Key, _} = Option, OptionList) ->
lists:keystore(Key, 1, OptionList, Option);
merge_options(Option, OptionList) ->
[Option | OptionList].
-spec set_option_default(Opts, atom(), any())
-> Opts when Opts :: [{atom(), any()}].
set_option_default(Opts, Key, Value) ->
case lists:keymember(Key, 1, Opts) of
true -> Opts;
false -> [{Key, Value} | Opts]
end.
-spec require([atom()]) -> ok.
require([]) ->
ok;
require([App | Tail]) ->
case application:start(App) of
ok -> ok;
{error, {already_started, App}} -> ok
end,
require(Tail).
-spec log(logger:level(), io:format(), list(), module() | #{logger => module()}) -> ok.
log(Level, Format, Args, Logger) when is_atom(Logger) ->
log(Level, Format, Args, #{logger => Logger});
log(Level, Format, Args, #{logger := Logger})
when Logger =/= error_logger ->
_ = Logger:Level(Format, Args),
ok;
%% Because error_logger does not have all the levels
%% we accept we have to do some mapping to error_logger functions.
log(Level, Format, Args, _) ->
Function = case Level of
emergency -> error_msg;
alert -> error_msg;
critical -> error_msg;
error -> error_msg;
warning -> warning_msg;
notice -> warning_msg;
info -> info_msg;
debug -> info_msg
end,
error_logger:Function(Format, Args).

+ 0
- 72
src/wsNet/ranch_acceptor.erl Vedi File

@ -1,72 +0,0 @@
%% Copyright (c) 2011-2021, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_acceptor).
-export([start_link/5]).
-export([init/4]).
-export([loop/5]).
-spec start_link(ranch:ref(), pos_integer(), inet:socket(), module(), module())
-> {ok, pid()}.
start_link(Ref, AcceptorId, LSocket, Transport, Logger) ->
ConnsSup = ranch_server:get_connections_sup(Ref, AcceptorId),
Pid = spawn_link(?MODULE, init, [LSocket, Transport, Logger, ConnsSup]),
{ok, Pid}.
-spec init(inet:socket(), module(), module(), pid()) -> no_return().
init(LSocket, Transport, Logger, ConnsSup) ->
MonitorRef = monitor(process, ConnsSup),
loop(LSocket, Transport, Logger, ConnsSup, MonitorRef).
-spec loop(inet:socket(), module(), module(), pid(), reference()) -> no_return().
loop(LSocket, Transport, Logger, ConnsSup, MonitorRef) ->
_ = case Transport:accept(LSocket, infinity) of
{ok, CSocket} ->
case Transport:controlling_process(CSocket, ConnsSup) of
ok ->
%% This call will not return until process has been started
%% AND we are below the maximum number of connections.
ranch_conns_sup:start_protocol(ConnsSup, MonitorRef,
CSocket);
{error, _} ->
Transport:close(CSocket)
end;
%% Reduce the accept rate if we run out of file descriptors.
%% We can't accept anymore anyway, so we might as well wait
%% a little for the situation to resolve itself.
{error, emfile} ->
ranch:log(warning,
"Ranch acceptor reducing accept rate: out of file descriptors~n",
[], Logger),
receive after 100 -> ok end;
%% Exit if the listening socket got closed.
{error, closed} ->
exit(closed);
%% Continue otherwise.
{error, _} ->
ok
end,
flush(Logger),
?MODULE:loop(LSocket, Transport, Logger, ConnsSup, MonitorRef).
flush(Logger) ->
receive Msg ->
ranch:log(warning,
"Ranch acceptor received unexpected message: ~p~n",
[Msg], Logger),
flush(Logger)
after 0 ->
ok
end.

+ 0
- 103
src/wsNet/ranch_acceptors_sup.erl Vedi File

@ -1,103 +0,0 @@
%% Copyright (c) 2011-2021, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2020-2021, Jan Uhlig <juhlig@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_acceptors_sup).
-behaviour(supervisor).
-export([start_link/3]).
-export([init/1]).
-spec start_link(ranch:ref(), module(), module())
-> {ok, pid()}.
start_link(Ref, Transport, Logger) ->
supervisor:start_link(?MODULE, [Ref, Transport, Logger]).
-spec init([term()]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init([Ref, Transport, Logger]) ->
TransOpts = ranch_server:get_transport_options(Ref),
NumAcceptors = maps:get(num_acceptors, TransOpts, 10),
NumListenSockets = maps:get(num_listen_sockets, TransOpts, 1),
LSockets = case get(lsockets) of
undefined ->
LSockets1 = start_listen_sockets(Ref, NumListenSockets, Transport, TransOpts, Logger),
put(lsockets, LSockets1),
LSockets1;
LSockets1 ->
LSockets1
end,
Procs = [begin
LSocketId = (AcceptorId rem NumListenSockets) + 1,
{_, LSocket} = lists:keyfind(LSocketId, 1, LSockets),
#{
id => {acceptor, self(), AcceptorId},
start => {ranch_acceptor, start_link, [Ref, AcceptorId, LSocket, Transport, Logger]},
shutdown => brutal_kill
}
end || AcceptorId <- lists:seq(1, NumAcceptors)],
{ok, {#{intensity => 1 + ceil(math:log2(NumAcceptors))}, Procs}}.
-spec start_listen_sockets(any(), pos_integer(), module(), map(), module())
-> [{pos_integer(), inet:socket()}].
start_listen_sockets(Ref, NumListenSockets, Transport, TransOpts0, Logger) when NumListenSockets > 0 ->
BaseSocket = start_listen_socket(Ref, Transport, TransOpts0, Logger),
{ok, Addr} = Transport:sockname(BaseSocket),
ExtraSockets = case Addr of
{local, _} when NumListenSockets > 1 ->
listen_error(Ref, Transport, TransOpts0, reuseport_local, Logger);
{local, _} ->
[];
{_, Port} ->
SocketOpts = maps:get(socket_opts, TransOpts0, []),
SocketOpts1 = lists:keystore(port, 1, SocketOpts, {port, Port}),
TransOpts1 = TransOpts0#{socket_opts => SocketOpts1},
[{N, start_listen_socket(Ref, Transport, TransOpts1, Logger)}
|| N <- lists:seq(2, NumListenSockets)]
end,
ranch_server:set_addr(Ref, Addr),
[{1, BaseSocket} | ExtraSockets].
-spec start_listen_socket(any(), module(), map(), module()) -> inet:socket().
start_listen_socket(Ref, Transport, TransOpts, Logger) ->
case Transport:listen(TransOpts) of
{ok, Socket} ->
PostListenCb = maps:get(post_listen_callback, TransOpts, fun(_) -> ok end),
case PostListenCb(Socket) of
ok ->
Socket;
{error, Reason} ->
listen_error(Ref, Transport, TransOpts, Reason, Logger)
end;
{error, Reason} ->
listen_error(Ref, Transport, TransOpts, Reason, Logger)
end.
-spec listen_error(any(), module(), any(), atom(), module()) -> no_return().
listen_error(Ref, Transport, TransOpts0, Reason, Logger) ->
SocketOpts0 = maps:get(socket_opts, TransOpts0, []),
SocketOpts1 = [{cert, '...'} | proplists:delete(cert, SocketOpts0)],
SocketOpts2 = [{key, '...'} | proplists:delete(key, SocketOpts1)],
SocketOpts = [{cacerts, '...'} | proplists:delete(cacerts, SocketOpts2)],
TransOpts = TransOpts0#{socket_opts => SocketOpts},
ranch:log(error,
"Failed to start Ranch listener ~p in ~p:listen(~999999p) for reason ~p (~s)~n",
[Ref, Transport, TransOpts, Reason, format_error(Reason)], Logger),
exit({listen_error, Ref, Reason}).
format_error(no_cert) ->
"no certificate provided; see cert, certfile, sni_fun or sni_hosts options";
format_error(reuseport_local) ->
"num_listen_sockets must be set to 1 for local sockets";
format_error(Reason) ->
inet:format_error(Reason).

+ 0
- 48
src/wsNet/ranch_app.erl Vedi File

@ -1,48 +0,0 @@
%% Copyright (c) 2011-2021, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_app).
-behaviour(application).
-export([start/2]).
-export([stop/1]).
-export([profile_output/0]).
-spec start(application:start_type(), term()) -> {ok, pid()} | {error, term()}.
start(_, _) ->
_ = consider_profiling(),
ranch_server = ets:new(ranch_server, [
ordered_set, public, named_table]),
ranch_sup:start_link().
-spec stop(term()) -> ok.
stop(_) ->
ok.
-spec profile_output() -> ok.
profile_output() ->
eprof:stop_profiling(),
eprof:log("procs.profile"),
eprof:analyze(procs),
eprof:log("total.profile"),
eprof:analyze(total).
consider_profiling() ->
case application:get_env(profile) of
{ok, true} ->
{ok, _Pid} = eprof:start(),
eprof:start_profiling([self()]);
_ ->
not_profiling
end.

+ 0
- 508
src/wsNet/ranch_conns_sup.erl Vedi File

@ -1,508 +0,0 @@
%% Copyright (c) 2011-2021, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2021, Maria Scott <maria-12648430@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% Make sure to never reload this module outside a release upgrade,
%% as calling l(ranch_conns_sup) twice will kill the process and all
%% the currently open connections.
-module(ranch_conns_sup).
%% API.
-export([start_link/6]).
-export([start_protocol/3]).
-export([active_connections/1]).
%% Supervisor internals.
-export([init/7]).
-export([system_continue/3]).
-export([system_terminate/4]).
-export([system_code_change/4]).
-type conn_type() :: worker | supervisor.
-type shutdown() :: brutal_kill | timeout().
-record(state, {
parent = undefined :: pid(),
ref :: ranch:ref(),
id :: pos_integer(),
conn_type :: conn_type(),
shutdown :: shutdown(),
transport = undefined :: module(),
protocol = undefined :: module(),
opts :: any(),
handshake_timeout :: timeout(),
max_conns = undefined :: ranch:max_conns(),
stats_counters_ref :: counters:counters_ref(),
alarms = #{} :: #{term() => {map(), undefined | reference()}},
logger = undefined :: module()
}).
%% API.
-spec start_link(ranch:ref(), pos_integer(), module(), any(), module(), module()) -> {ok, pid()}.
start_link(Ref, Id, Transport, TransOpts, Protocol, Logger) ->
proc_lib:start_link(?MODULE, init,
[self(), Ref, Id, Transport, TransOpts, Protocol, Logger]).
%% We can safely assume we are on the same node as the supervisor.
%%
%% We can also safely avoid having a monitor and a timeout here
%% because only three things can happen:
%% * The supervisor died; rest_for_one strategy killed all acceptors
%% so this very calling process is going to di--
%% * There's too many connections, the supervisor will resume the
%% acceptor only when we get below the limit again.
%% * The supervisor is overloaded, there's either too many acceptors
%% or the max_connections limit is too large. It's better if we
%% don't keep accepting connections because this leaves
%% more room for the situation to be resolved.
%%
%% We do not need the reply, we only need the ok from the supervisor
%% to continue. The supervisor sends its own pid when the acceptor can
%% continue.
-spec start_protocol(pid(), reference(), inet:socket()) -> ok.
start_protocol(SupPid, MonitorRef, Socket) ->
SupPid ! {?MODULE, start_protocol, self(), Socket},
receive
SupPid ->
ok;
{'DOWN', MonitorRef, process, SupPid, Reason} ->
error(Reason)
end.
%% We can't make the above assumptions here. This function might be
%% called from anywhere.
-spec active_connections(pid()) -> non_neg_integer().
active_connections(SupPid) ->
Tag = erlang:monitor(process, SupPid),
catch erlang:send(SupPid, {?MODULE, active_connections, self(), Tag},
[noconnect]),
receive
{Tag, Ret} ->
erlang:demonitor(Tag, [flush]),
Ret;
{'DOWN', Tag, _, _, noconnection} ->
exit({nodedown, node(SupPid)});
{'DOWN', Tag, _, _, Reason} ->
exit(Reason)
after 5000 ->
erlang:demonitor(Tag, [flush]),
exit(timeout)
end.
%% Supervisor internals.
-spec init(pid(), ranch:ref(), pos_integer(), module(), any(), module(), module()) -> no_return().
init(Parent, Ref, Id, Transport, TransOpts, Protocol, Logger) ->
process_flag(trap_exit, true),
ok = ranch_server:set_connections_sup(Ref, Id, self()),
MaxConns = ranch_server:get_max_connections(Ref),
Alarms = get_alarms(TransOpts),
ConnType = maps:get(connection_type, TransOpts, worker),
Shutdown = maps:get(shutdown, TransOpts, 5000),
HandshakeTimeout = maps:get(handshake_timeout, TransOpts, 5000),
ProtoOpts = ranch_server:get_protocol_options(Ref),
StatsCounters = ranch_server:get_stats_counters(Ref),
ok = proc_lib:init_ack(Parent, {ok, self()}),
loop(#state{parent = Parent, ref = Ref, id = Id, conn_type = ConnType,
shutdown = Shutdown, transport = Transport, protocol = Protocol,
opts = ProtoOpts, stats_counters_ref = StatsCounters,
handshake_timeout = HandshakeTimeout,
max_conns = MaxConns, alarms = Alarms,
logger = Logger}, 0, 0, []).
loop(State = #state{parent = Parent, ref = Ref, id = Id, conn_type = ConnType,
transport = Transport, protocol = Protocol, opts = Opts, stats_counters_ref = StatsCounters,
alarms = Alarms, max_conns = MaxConns, logger = Logger}, CurConns, NbChildren, Sleepers) ->
receive
{?MODULE, start_protocol, To, Socket} ->
try Protocol:start_link(Ref, Transport, Opts) of
{ok, Pid} ->
inc_accept(StatsCounters, Id, 1),
handshake(State, CurConns, NbChildren, Sleepers, To, Socket, Pid, Pid);
{ok, SupPid, ProtocolPid} when ConnType =:= supervisor ->
inc_accept(StatsCounters, Id, 1),
handshake(State, CurConns, NbChildren, Sleepers, To, Socket, SupPid, ProtocolPid);
Ret ->
To ! self(),
ranch:log(error,
"Ranch listener ~p connection process start failure; "
"~p:start_link/3 returned: ~999999p~n",
[Ref, Protocol, Ret], Logger),
Transport:close(Socket),
loop(State, CurConns, NbChildren, Sleepers)
catch Class:Reason ->
To ! self(),
ranch:log(error,
"Ranch listener ~p connection process start failure; "
"~p:start_link/3 crashed with reason: ~p:~999999p~n",
[Ref, Protocol, Class, Reason], Logger),
Transport:close(Socket),
loop(State, CurConns, NbChildren, Sleepers)
end;
{?MODULE, active_connections, To, Tag} ->
To ! {Tag, CurConns},
loop(State, CurConns, NbChildren, Sleepers);
%% Remove a connection from the count of connections.
{remove_connection, Ref, Pid} ->
case put(Pid, removed) of
active when Sleepers =:= [] ->
loop(State, CurConns - 1, NbChildren, Sleepers);
active ->
[To | Sleepers2] = Sleepers,
To ! self(),
loop(State, CurConns - 1, NbChildren, Sleepers2);
removed ->
loop(State, CurConns, NbChildren, Sleepers);
undefined ->
_ = erase(Pid),
loop(State, CurConns, NbChildren, Sleepers)
end;
%% Upgrade the max number of connections allowed concurrently.
%% We resume all sleeping acceptors if this number increases.
{set_max_conns, MaxConns2} when MaxConns2 > MaxConns ->
_ = [To ! self() || To <- Sleepers],
loop(State#state{max_conns = MaxConns2},
CurConns, NbChildren, []);
{set_max_conns, MaxConns2} ->
loop(State#state{max_conns = MaxConns2},
CurConns, NbChildren, Sleepers);
%% Upgrade the transport options.
{set_transport_options, TransOpts} ->
set_transport_options(State, CurConns, NbChildren, Sleepers, TransOpts);
%% Upgrade the protocol options.
{set_protocol_options, Opts2} ->
loop(State#state{opts = Opts2},
CurConns, NbChildren, Sleepers);
{timeout, _, {activate_alarm, AlarmName}} when is_map_key(AlarmName, Alarms) ->
{AlarmOpts, _} = maps:get(AlarmName, Alarms),
NewAlarm = trigger_alarm(Ref, AlarmName, {AlarmOpts, undefined}, CurConns),
loop(State#state{alarms = Alarms#{AlarmName => NewAlarm}}, CurConns, NbChildren, Sleepers);
{timeout, _, {activate_alarm, _}} ->
loop(State, CurConns, NbChildren, Sleepers);
{'EXIT', Parent, Reason} ->
terminate(State, Reason, NbChildren);
{'EXIT', Pid, Reason} when Sleepers =:= [] ->
case erase(Pid) of
active ->
inc_terminate(StatsCounters, Id, 1),
report_error(Logger, Ref, Protocol, Pid, Reason),
loop(State, CurConns - 1, NbChildren - 1, Sleepers);
removed ->
inc_terminate(StatsCounters, Id, 1),
report_error(Logger, Ref, Protocol, Pid, Reason),
loop(State, CurConns, NbChildren - 1, Sleepers);
undefined ->
loop(State, CurConns, NbChildren, Sleepers)
end;
%% Resume a sleeping acceptor if needed.
{'EXIT', Pid, Reason} ->
case erase(Pid) of
active when CurConns > MaxConns ->
inc_terminate(StatsCounters, Id, 1),
report_error(Logger, Ref, Protocol, Pid, Reason),
loop(State, CurConns - 1, NbChildren - 1, Sleepers);
active ->
inc_terminate(StatsCounters, Id, 1),
report_error(Logger, Ref, Protocol, Pid, Reason),
[To | Sleepers2] = Sleepers,
To ! self(),
loop(State, CurConns - 1, NbChildren - 1, Sleepers2);
removed ->
inc_terminate(StatsCounters, Id, 1),
report_error(Logger, Ref, Protocol, Pid, Reason),
loop(State, CurConns, NbChildren - 1, Sleepers);
undefined ->
loop(State, CurConns, NbChildren, Sleepers)
end;
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
{State, CurConns, NbChildren, Sleepers});
%% Calls from the supervisor module.
{'$gen_call', {To, Tag}, which_children} ->
Children = [{Protocol, Pid, ConnType, [Protocol]}
|| {Pid, Type} <- get(),
Type =:= active orelse Type =:= removed],
To ! {Tag, Children},
loop(State, CurConns, NbChildren, Sleepers);
{'$gen_call', {To, Tag}, count_children} ->
Counts = case ConnType of
worker -> [{supervisors, 0}, {workers, NbChildren}];
supervisor -> [{supervisors, NbChildren}, {workers, 0}]
end,
Counts2 = [{specs, 1}, {active, NbChildren} | Counts],
To ! {Tag, Counts2},
loop(State, CurConns, NbChildren, Sleepers);
{'$gen_call', {To, Tag}, _} ->
To ! {Tag, {error, ?MODULE}},
loop(State, CurConns, NbChildren, Sleepers);
Msg ->
ranch:log(error,
"Ranch listener ~p received unexpected message ~p~n",
[Ref, Msg], Logger),
loop(State, CurConns, NbChildren, Sleepers)
end.
handshake(State = #state{ref = Ref, transport = Transport, handshake_timeout = HandshakeTimeout,
max_conns = MaxConns, alarms = Alarms0}, CurConns, NbChildren, Sleepers, To, Socket, SupPid, ProtocolPid) ->
case Transport:controlling_process(Socket, ProtocolPid) of
ok ->
ProtocolPid ! {handshake, Ref, Transport, Socket, HandshakeTimeout},
put(SupPid, active),
CurConns2 = CurConns + 1,
Sleepers2 = if CurConns2 < MaxConns ->
To ! self(),
Sleepers;
true ->
[To | Sleepers]
end,
Alarms1 = trigger_alarms(Ref, Alarms0, CurConns2),
loop(State#state{alarms = Alarms1}, CurConns2, NbChildren + 1, Sleepers2);
{error, _} ->
Transport:close(Socket),
%% Only kill the supervised pid, because the connection's pid,
%% when different, is supposed to be sitting under it and linked.
exit(SupPid, kill),
To ! self(),
loop(State, CurConns, NbChildren, Sleepers)
end.
trigger_alarms(Ref, Alarms, CurConns) ->
maps:map(
fun
(AlarmName, Alarm) ->
trigger_alarm(Ref, AlarmName, Alarm, CurConns)
end,
Alarms
).
trigger_alarm(Ref, AlarmName, {Opts = #{treshold := Treshold, callback := Callback}, undefined}, CurConns) when CurConns >= Treshold ->
ActiveConns = [Pid || {Pid, active} <- get()],
case Callback of
{Mod, Fun} ->
spawn(Mod, Fun, [Ref, AlarmName, self(), ActiveConns]);
_ ->
Self = self(),
spawn(fun() -> Callback(Ref, AlarmName, Self, ActiveConns) end)
end,
{Opts, schedule_activate_alarm(AlarmName, Opts)};
trigger_alarm(_, _, Alarm, _) ->
Alarm.
schedule_activate_alarm(AlarmName, #{cooldown := Cooldown}) when Cooldown > 0 ->
erlang:start_timer(Cooldown, self(), {activate_alarm, AlarmName});
schedule_activate_alarm(_, _) ->
undefined.
get_alarms(#{alarms := Alarms}) when is_map(Alarms) ->
maps:fold(
fun
(Name, Opts = #{type := num_connections, cooldown := _}, Acc) ->
Acc#{Name => {Opts, undefined}};
(Name, Opts = #{type := num_connections}, Acc) ->
Acc#{Name => {Opts#{cooldown => 5000}, undefined}};
(_, _, Acc) -> Acc
end,
#{},
Alarms
);
get_alarms(_) ->
#{}.
set_transport_options(State = #state{max_conns = MaxConns0}, CurConns, NbChildren, Sleepers0, TransOpts) ->
MaxConns1 = maps:get(max_connections, TransOpts, 1024),
HandshakeTimeout = maps:get(handshake_timeout, TransOpts, 5000),
Shutdown = maps:get(shutdown, TransOpts, 5000),
Sleepers1 = case MaxConns1 > MaxConns0 of
true ->
_ = [To ! self() || To <- Sleepers0],
[];
false ->
Sleepers0
end,
State1 = set_alarm_option(State, TransOpts, CurConns),
loop(State1#state{max_conns = MaxConns1, handshake_timeout = HandshakeTimeout, shutdown = Shutdown},
CurConns, NbChildren, Sleepers1).
set_alarm_option(State = #state{ref = Ref, alarms = OldAlarms}, TransOpts, CurConns) ->
NewAlarms0 = get_alarms(TransOpts),
NewAlarms1 = merge_alarms(OldAlarms, NewAlarms0),
NewAlarms2 = trigger_alarms(Ref, NewAlarms1, CurConns),
State#state{alarms = NewAlarms2}.
merge_alarms(Old, New) ->
OldList = lists:sort(maps:to_list(Old)),
NewList = lists:sort(maps:to_list(New)),
Merged = merge_alarms(OldList, NewList, []),
maps:from_list(Merged).
merge_alarms([], News, Acc) ->
News ++ Acc;
merge_alarms([{_, {_, undefined}} | Olds], [], Acc) ->
merge_alarms(Olds, [], Acc);
merge_alarms([{_, {_, Timer}} | Olds], [], Acc) ->
_ = cancel_alarm_reactivation_timer(Timer),
merge_alarms(Olds, [], Acc);
merge_alarms([{Name, {OldOpts, Timer}} | Olds], [{Name, {NewOpts, _}} | News], Acc) ->
merge_alarms(Olds, News, [{Name, {NewOpts, adapt_alarm_timer(Name, Timer, OldOpts, NewOpts)}} | Acc]);
merge_alarms([{OldName, {_, Timer}} | Olds], News = [{NewName, _} | _], Acc) when OldName < NewName ->
_ = cancel_alarm_reactivation_timer(Timer),
merge_alarms(Olds, News, Acc);
merge_alarms(Olds, [New | News], Acc) ->
merge_alarms(Olds, News, [New | Acc]).
%% Not in cooldown.
adapt_alarm_timer(_, undefined, _, _) ->
undefined;
%% Cooldown unchanged.
adapt_alarm_timer(_, Timer, #{cooldown := Cooldown}, #{cooldown := Cooldown}) ->
Timer;
%% Cooldown changed to no cooldown, cancel cooldown timer.
adapt_alarm_timer(_, Timer, _, #{cooldown := 0}) ->
_ = cancel_alarm_reactivation_timer(Timer),
undefined;
%% Cooldown changed, cancel current and start new timer taking the already elapsed time into account.
adapt_alarm_timer(Name, Timer, #{cooldown := OldCooldown}, #{cooldown := NewCooldown}) ->
OldTimeLeft = cancel_alarm_reactivation_timer(Timer),
case NewCooldown - OldCooldown + OldTimeLeft of
NewTimeLeft when NewTimeLeft > 0 ->
erlang:start_timer(NewTimeLeft, self(), {activate_alarm, Name});
_ ->
undefined
end.
cancel_alarm_reactivation_timer(Timer) ->
case erlang:cancel_timer(Timer) of
%% Timer had already expired when we tried to cancel it, so we flush the
%% reactivation message it sent and return 0 as remaining time.
false ->
ok = receive {timeout, Timer, {activate_alarm, _}} -> ok after 0 -> ok end,
0;
%% Timer has not yet expired, we return the amount of time that was remaining.
TimeLeft ->
TimeLeft
end.
-spec terminate(#state{}, any(), non_neg_integer()) -> no_return().
terminate(#state{shutdown = brutal_kill, id = Id,
stats_counters_ref = StatsCounters}, Reason, NbChildren) ->
kill_children(get_keys(active)),
kill_children(get_keys(removed)),
inc_terminate(StatsCounters, Id, NbChildren),
exit(Reason);
%% Attempt to gracefully shutdown all children.
terminate(#state{shutdown = Shutdown, id = Id,
stats_counters_ref = StatsCounters}, Reason, NbChildren) ->
shutdown_children(get_keys(active)),
shutdown_children(get_keys(removed)),
_ = if
Shutdown =:= infinity ->
ok;
true ->
erlang:send_after(Shutdown, self(), kill)
end,
wait_children(NbChildren),
inc_terminate(StatsCounters, Id, NbChildren),
exit(Reason).
inc_accept(StatsCounters, Id, N) ->
%% Accepts are counted in the odd indexes.
counters:add(StatsCounters, 2 * Id - 1, N).
inc_terminate(StatsCounters, Id, N) ->
%% Terminates are counted in the even indexes.
counters:add(StatsCounters, 2 * Id, N).
%% Kill all children and then exit. We unlink first to avoid
%% getting a message for each child getting killed.
kill_children(Pids) ->
_ = [begin
unlink(P),
exit(P, kill)
end || P <- Pids],
ok.
%% Monitor processes so we can know which ones have shutdown
%% before the timeout. Unlink so we avoid receiving an extra
%% message. Then send a shutdown exit signal.
shutdown_children(Pids) ->
_ = [begin
monitor(process, P),
unlink(P),
exit(P, shutdown)
end || P <- Pids],
ok.
wait_children(0) ->
ok;
wait_children(NbChildren) ->
receive
{'DOWN', _, process, Pid, _} ->
case erase(Pid) of
active -> wait_children(NbChildren - 1);
removed -> wait_children(NbChildren - 1);
_ -> wait_children(NbChildren)
end;
kill ->
Active = get_keys(active),
_ = [exit(P, kill) || P <- Active],
Removed = get_keys(removed),
_ = [exit(P, kill) || P <- Removed],
ok
end.
-spec system_continue(_, _, any()) -> no_return().
system_continue(_, _, {State, CurConns, NbChildren, Sleepers}) ->
loop(State, CurConns, NbChildren, Sleepers).
-spec system_terminate(any(), _, _, _) -> no_return().
system_terminate(Reason, _, _, {State, _, NbChildren, _}) ->
terminate(State, Reason, NbChildren).
-spec system_code_change(any(), _, _, _) -> {ok, any()}.
system_code_change({#state{parent = Parent, ref = Ref, conn_type = ConnType,
shutdown = Shutdown, transport = Transport, protocol = Protocol,
opts = Opts, handshake_timeout = HandshakeTimeout,
max_conns = MaxConns, logger = Logger}, CurConns, NbChildren,
Sleepers}, _, {down, _}, _) ->
{ok, {{state, Parent, Ref, ConnType, Shutdown, Transport, Protocol,
Opts, HandshakeTimeout, MaxConns, Logger}, CurConns, NbChildren,
Sleepers}};
system_code_change({{state, Parent, Ref, ConnType, Shutdown, Transport, Protocol,
Opts, HandshakeTimeout, MaxConns, Logger}, CurConns, NbChildren,
Sleepers}, _, _, _) ->
Self = self(),
[Id] = [Id || {Id, Pid} <- ranch_server:get_connections_sups(Ref), Pid =:= Self],
StatsCounters = ranch_server:get_stats_counters(Ref),
{ok, {#state{parent = Parent, ref = Ref, id = Id, conn_type = ConnType, shutdown = Shutdown,
transport = Transport, protocol = Protocol, opts = Opts,
handshake_timeout = HandshakeTimeout, max_conns = MaxConns,
stats_counters_ref = StatsCounters,
logger = Logger}, CurConns, NbChildren, Sleepers}};
system_code_change(Misc, _, _, _) ->
{ok, Misc}.
%% We use ~999999p here instead of ~w because the latter doesn't
%% support printable strings.
report_error(_, _, _, _, normal) ->
ok;
report_error(_, _, _, _, shutdown) ->
ok;
report_error(_, _, _, _, {shutdown, _}) ->
ok;
report_error(Logger, Ref, Protocol, Pid, Reason) ->
ranch:log(error,
"Ranch listener ~p had connection process started with "
"~p:start_link/3 at ~p exit with reason: ~999999p~n",
[Ref, Protocol, Pid, Reason], Logger).

+ 0
- 42
src/wsNet/ranch_conns_sup_sup.erl Vedi File

@ -1,42 +0,0 @@
%% Copyright (c) 2019-2021, Jan Uhlig <juhlig@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_conns_sup_sup).
-behaviour(supervisor).
-export([start_link/4]).
-export([init/1]).
-spec start_link(ranch:ref(), module(), module(), module()) -> {ok, pid()}.
start_link(Ref, Transport, Protocol, Logger) ->
ok = ranch_server:cleanup_connections_sups(Ref),
supervisor:start_link(?MODULE, {
Ref, Transport, Protocol, Logger
}).
-spec init({ranch:ref(), module(), module(), module()})
-> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init({Ref, Transport, Protocol, Logger}) ->
TransOpts = ranch_server:get_transport_options(Ref),
NumAcceptors = maps:get(num_acceptors, TransOpts, 10),
NumConnsSups = maps:get(num_conns_sups, TransOpts, NumAcceptors),
StatsCounters = counters:new(2 * NumConnsSups, []),
ok = ranch_server:set_stats_counters(Ref, StatsCounters),
ChildSpecs = [#{
id => {ranch_conns_sup, N},
start => {ranch_conns_sup, start_link, [Ref, N, Transport, TransOpts, Protocol, Logger]},
type => supervisor
} || N <- lists:seq(1, NumConnsSups)],
{ok, {#{intensity => 1 + ceil(math:log2(NumConnsSups))}, ChildSpecs}}.

+ 0
- 115
src/wsNet/ranch_crc32c.erl Vedi File

@ -1,115 +0,0 @@
%% Copyright (c) 2018-2021, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_crc32c).
-export([crc32c/1]).
-export([crc32c/2]).
-define(CRC32C_TABLE, {
16#00000000, 16#F26B8303, 16#E13B70F7, 16#1350F3F4,
16#C79A971F, 16#35F1141C, 16#26A1E7E8, 16#D4CA64EB,
16#8AD958CF, 16#78B2DBCC, 16#6BE22838, 16#9989AB3B,
16#4D43CFD0, 16#BF284CD3, 16#AC78BF27, 16#5E133C24,
16#105EC76F, 16#E235446C, 16#F165B798, 16#030E349B,
16#D7C45070, 16#25AFD373, 16#36FF2087, 16#C494A384,
16#9A879FA0, 16#68EC1CA3, 16#7BBCEF57, 16#89D76C54,
16#5D1D08BF, 16#AF768BBC, 16#BC267848, 16#4E4DFB4B,
16#20BD8EDE, 16#D2D60DDD, 16#C186FE29, 16#33ED7D2A,
16#E72719C1, 16#154C9AC2, 16#061C6936, 16#F477EA35,
16#AA64D611, 16#580F5512, 16#4B5FA6E6, 16#B93425E5,
16#6DFE410E, 16#9F95C20D, 16#8CC531F9, 16#7EAEB2FA,
16#30E349B1, 16#C288CAB2, 16#D1D83946, 16#23B3BA45,
16#F779DEAE, 16#05125DAD, 16#1642AE59, 16#E4292D5A,
16#BA3A117E, 16#4851927D, 16#5B016189, 16#A96AE28A,
16#7DA08661, 16#8FCB0562, 16#9C9BF696, 16#6EF07595,
16#417B1DBC, 16#B3109EBF, 16#A0406D4B, 16#522BEE48,
16#86E18AA3, 16#748A09A0, 16#67DAFA54, 16#95B17957,
16#CBA24573, 16#39C9C670, 16#2A993584, 16#D8F2B687,
16#0C38D26C, 16#FE53516F, 16#ED03A29B, 16#1F682198,
16#5125DAD3, 16#A34E59D0, 16#B01EAA24, 16#42752927,
16#96BF4DCC, 16#64D4CECF, 16#77843D3B, 16#85EFBE38,
16#DBFC821C, 16#2997011F, 16#3AC7F2EB, 16#C8AC71E8,
16#1C661503, 16#EE0D9600, 16#FD5D65F4, 16#0F36E6F7,
16#61C69362, 16#93AD1061, 16#80FDE395, 16#72966096,
16#A65C047D, 16#5437877E, 16#4767748A, 16#B50CF789,
16#EB1FCBAD, 16#197448AE, 16#0A24BB5A, 16#F84F3859,
16#2C855CB2, 16#DEEEDFB1, 16#CDBE2C45, 16#3FD5AF46,
16#7198540D, 16#83F3D70E, 16#90A324FA, 16#62C8A7F9,
16#B602C312, 16#44694011, 16#5739B3E5, 16#A55230E6,
16#FB410CC2, 16#092A8FC1, 16#1A7A7C35, 16#E811FF36,
16#3CDB9BDD, 16#CEB018DE, 16#DDE0EB2A, 16#2F8B6829,
16#82F63B78, 16#709DB87B, 16#63CD4B8F, 16#91A6C88C,
16#456CAC67, 16#B7072F64, 16#A457DC90, 16#563C5F93,
16#082F63B7, 16#FA44E0B4, 16#E9141340, 16#1B7F9043,
16#CFB5F4A8, 16#3DDE77AB, 16#2E8E845F, 16#DCE5075C,
16#92A8FC17, 16#60C37F14, 16#73938CE0, 16#81F80FE3,
16#55326B08, 16#A759E80B, 16#B4091BFF, 16#466298FC,
16#1871A4D8, 16#EA1A27DB, 16#F94AD42F, 16#0B21572C,
16#DFEB33C7, 16#2D80B0C4, 16#3ED04330, 16#CCBBC033,
16#A24BB5A6, 16#502036A5, 16#4370C551, 16#B11B4652,
16#65D122B9, 16#97BAA1BA, 16#84EA524E, 16#7681D14D,
16#2892ED69, 16#DAF96E6A, 16#C9A99D9E, 16#3BC21E9D,
16#EF087A76, 16#1D63F975, 16#0E330A81, 16#FC588982,
16#B21572C9, 16#407EF1CA, 16#532E023E, 16#A145813D,
16#758FE5D6, 16#87E466D5, 16#94B49521, 16#66DF1622,
16#38CC2A06, 16#CAA7A905, 16#D9F75AF1, 16#2B9CD9F2,
16#FF56BD19, 16#0D3D3E1A, 16#1E6DCDEE, 16#EC064EED,
16#C38D26C4, 16#31E6A5C7, 16#22B65633, 16#D0DDD530,
16#0417B1DB, 16#F67C32D8, 16#E52CC12C, 16#1747422F,
16#49547E0B, 16#BB3FFD08, 16#A86F0EFC, 16#5A048DFF,
16#8ECEE914, 16#7CA56A17, 16#6FF599E3, 16#9D9E1AE0,
16#D3D3E1AB, 16#21B862A8, 16#32E8915C, 16#C083125F,
16#144976B4, 16#E622F5B7, 16#F5720643, 16#07198540,
16#590AB964, 16#AB613A67, 16#B831C993, 16#4A5A4A90,
16#9E902E7B, 16#6CFBAD78, 16#7FAB5E8C, 16#8DC0DD8F,
16#E330A81A, 16#115B2B19, 16#020BD8ED, 16#F0605BEE,
16#24AA3F05, 16#D6C1BC06, 16#C5914FF2, 16#37FACCF1,
16#69E9F0D5, 16#9B8273D6, 16#88D28022, 16#7AB90321,
16#AE7367CA, 16#5C18E4C9, 16#4F48173D, 16#BD23943E,
16#F36E6F75, 16#0105EC76, 16#12551F82, 16#E03E9C81,
16#34F4F86A, 16#C69F7B69, 16#D5CF889D, 16#27A40B9E,
16#79B737BA, 16#8BDCB4B9, 16#988C474D, 16#6AE7C44E,
16#BE2DA0A5, 16#4C4623A6, 16#5F16D052, 16#AD7D5351
}).
%% The interface mirrors erlang:crc32/1,2.
-spec crc32c(iodata()) -> non_neg_integer().
crc32c(Data) ->
do_crc32c(16#ffffffff, iolist_to_binary(Data)).
-spec crc32c(CRC, iodata()) -> CRC when CRC :: non_neg_integer().
crc32c(OldCrc, Data) ->
do_crc32c(OldCrc bxor 16#ffffffff, iolist_to_binary(Data)).
do_crc32c(OldCrc, <<C, Rest/bits>>) ->
do_crc32c((OldCrc bsr 8) bxor element(1 + ((OldCrc bxor C) band 16#ff), ?CRC32C_TABLE),
Rest);
do_crc32c(OldCrc, <<>>) ->
OldCrc bxor 16#ffffffff.
-ifdef(TEST).
crc32c_test_() ->
Tests = [
%% Tests from RFC3720 B.4.
{<<0:32/unit:8>>, 16#8a9136aa},
{iolist_to_binary([16#ff || _ <- lists:seq(1, 32)]), 16#62a8ab43},
{iolist_to_binary([N || N <- lists:seq(0, 16#1f)]), 16#46dd794e},
{iolist_to_binary([N || N <- lists:seq(16#1f, 0, -1)]), 16#113fdb5c},
{<<16#01c00000:32, 0:32, 0:32, 0:32, 16#14000000:32, 16#00000400:32, 16#00000014:32,
16#00000018:32, 16#28000000:32, 0:32, 16#02000000:32, 0:32>>, 16#d9963a56}
],
[{iolist_to_binary(io_lib:format("16#~8.16.0b", [R])),
fun() -> R = crc32c(V) end} || {V, R} <- Tests].
-endif.

+ 0
- 36
src/wsNet/ranch_embedded_sup.erl Vedi File

@ -1,36 +0,0 @@
%% Copyright (c) 2019-2021, Jan Uhlig <juhlig@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_embedded_sup).
-behavior(supervisor).
-export([start_link/5]).
-export([init/1]).
-spec start_link(ranch:ref(), module(), any(), module(), any())
-> {ok, pid()}.
start_link(Ref, Transport, TransOpts, Protocol, ProtoOpts) ->
supervisor:start_link(?MODULE, {Ref, Transport, TransOpts, Protocol, ProtoOpts}).
-spec init({ranch:ref(), module(), any(), module(), any()})
-> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init({Ref, Transport, TransOpts, Protocol, ProtoOpts}) ->
Proxy = #{id => ranch_server_proxy,
start => {ranch_server_proxy, start_link, []},
shutdown => brutal_kill},
Listener = #{id => {ranch_listener_sup, Ref},
start => {ranch_listener_sup, start_link, [Ref, Transport, TransOpts, Protocol, ProtoOpts]},
type => supervisor},
{ok, {#{strategy => rest_for_one}, [Proxy, Listener]}}.

+ 0
- 48
src/wsNet/ranch_listener_sup.erl Vedi File

@ -1,48 +0,0 @@
%% Copyright (c) 2011-2021, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_listener_sup).
-behaviour(supervisor).
-export([start_link/5]).
-export([init/1]).
-spec start_link(ranch:ref(), module(), any(), module(), any())
-> {ok, pid()}.
start_link(Ref, Transport, TransOpts, Protocol, ProtoOpts) ->
MaxConns = maps:get(max_connections, TransOpts, 1024),
Logger = maps:get(logger, TransOpts, logger),
ranch_server:set_new_listener_opts(Ref, MaxConns, TransOpts, ProtoOpts,
[Ref, Transport, TransOpts, Protocol, ProtoOpts]),
supervisor:start_link(?MODULE, {
Ref, Transport, Protocol, Logger
}).
-spec init({ranch:ref(), module(), module(), module()})
-> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init({Ref, Transport, Protocol, Logger}) ->
ok = ranch_server:set_listener_sup(Ref, self()),
ChildSpecs = [
#{
id => ranch_conns_sup_sup,
start => {ranch_conns_sup_sup, start_link, [Ref, Transport, Protocol, Logger]},
type => supervisor
},
#{
id => ranch_acceptors_sup,
start => {ranch_acceptors_sup, start_link, [Ref, Transport, Logger]},
type => supervisor
}
],
{ok, {#{strategy => rest_for_one}, ChildSpecs}}.

+ 0
- 23
src/wsNet/ranch_protocol.erl Vedi File

@ -1,23 +0,0 @@
%% Copyright (c) 2012-2021, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_protocol).
%% Start a new connection process for the given socket.
-callback start_link(
Ref :: ranch:ref(),
Transport :: module(),
ProtocolOptions :: any())
-> {ok, ConnectionPid :: pid()}
| {ok, SupPid :: pid(), ConnectionPid :: pid()}.

+ 0
- 1007
src/wsNet/ranch_proxy_header.erl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 279
src/wsNet/ranch_server.erl Vedi File

@ -1,279 +0,0 @@
%% Copyright (c) 2012-2021, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2020-2021, Jan Uhlig <juhlig@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_server).
-behaviour(gen_server).
%% API.
-export([start_link/0]).
-export([set_new_listener_opts/5]).
-export([cleanup_listener_opts/1]).
-export([cleanup_connections_sups/1]).
-export([set_connections_sup/3]).
-export([get_connections_sup/2]).
-export([get_connections_sups/1]).
-export([get_connections_sups/0]).
-export([set_listener_sup/2]).
-export([get_listener_sup/1]).
-export([get_listener_sups/0]).
-export([set_addr/2]).
-export([get_addr/1]).
-export([set_max_connections/2]).
-export([get_max_connections/1]).
-export([set_stats_counters/2]).
-export([get_stats_counters/1]).
-export([set_transport_options/2]).
-export([get_transport_options/1]).
-export([set_protocol_options/2]).
-export([get_protocol_options/1]).
-export([get_listener_start_args/1]).
-export([count_connections/1]).
%% gen_server.
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([terminate/2]).
-export([code_change/3]).
-define(TAB, ?MODULE).
-type monitors() :: [{{reference(), pid()}, any()}].
-record(state, {
monitors = [] :: monitors()
}).
%% API.
-spec start_link() -> {ok, pid()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec set_new_listener_opts(ranch:ref(), ranch:max_conns(), any(), any(), [any()]) -> ok.
set_new_listener_opts(Ref, MaxConns, TransOpts, ProtoOpts, StartArgs) ->
gen_server:call(?MODULE, {set_new_listener_opts, Ref, MaxConns, TransOpts, ProtoOpts, StartArgs}).
-spec cleanup_listener_opts(ranch:ref()) -> ok.
cleanup_listener_opts(Ref) ->
_ = ets:delete(?TAB, {addr, Ref}),
_ = ets:delete(?TAB, {max_conns, Ref}),
_ = ets:delete(?TAB, {trans_opts, Ref}),
_ = ets:delete(?TAB, {proto_opts, Ref}),
_ = ets:delete(?TAB, {listener_start_args, Ref}),
%% We also remove the pid of the connection supervisors.
%% Depending on the timing, they might already have been deleted
%% when we handled the monitor DOWN message. However, in some
%% cases when calling stop_listener followed by get_connections_sup,
%% we could end up with the pid still being returned, when we
%% expected a crash (because the listener was stopped).
%% Deleting it explicitly here removes any possible confusion.
_ = ets:match_delete(?TAB, {{conns_sup, Ref, '_'}, '_'}),
_ = ets:delete(?TAB, {stats_counters, Ref}),
%% Ditto for the listener supervisor.
_ = ets:delete(?TAB, {listener_sup, Ref}),
ok.
-spec cleanup_connections_sups(ranch:ref()) -> ok.
cleanup_connections_sups(Ref) ->
_ = ets:match_delete(?TAB, {{conns_sup, Ref, '_'}, '_'}),
_ = ets:delete(?TAB, {stats_counters, Ref}),
ok.
-spec set_connections_sup(ranch:ref(), non_neg_integer(), pid()) -> ok.
set_connections_sup(Ref, Id, Pid) ->
gen_server:call(?MODULE, {set_connections_sup, Ref, Id, Pid}).
-spec get_connections_sup(ranch:ref(), pos_integer()) -> pid().
get_connections_sup(Ref, Id) ->
ConnsSups = get_connections_sups(Ref),
NConnsSups = length(ConnsSups),
{_, Pid} = lists:keyfind((Id rem NConnsSups) + 1, 1, ConnsSups),
Pid.
-spec get_connections_sups(ranch:ref()) -> [{pos_integer(), pid()}].
get_connections_sups(Ref) ->
[{Id, Pid} ||
[Id, Pid] <- ets:match(?TAB, {{conns_sup, Ref, '$1'}, '$2'})].
-spec get_connections_sups() -> [{ranch:ref(), pos_integer(), pid()}].
get_connections_sups() ->
[{Ref, Id, Pid} ||
[Ref, Id, Pid] <- ets:match(?TAB, {{conns_sup, '$1', '$2'}, '$3'})].
-spec set_listener_sup(ranch:ref(), pid()) -> ok.
set_listener_sup(Ref, Pid) ->
gen_server:call(?MODULE, {set_listener_sup, Ref, Pid}).
-spec get_listener_sup(ranch:ref()) -> pid().
get_listener_sup(Ref) ->
ets:lookup_element(?TAB, {listener_sup, Ref}, 2).
-spec get_listener_sups() -> [{ranch:ref(), pid()}].
get_listener_sups() ->
[{Ref, Pid} || [Ref, Pid] <- ets:match(?TAB, {{listener_sup, '$1'}, '$2'})].
-spec set_addr(ranch:ref(), {inet:ip_address(), inet:port_number()} |
{local, binary()} | {undefined, undefined}) -> ok.
set_addr(Ref, Addr) ->
gen_server:call(?MODULE, {set_addr, Ref, Addr}).
-spec get_addr(ranch:ref()) -> {inet:ip_address(), inet:port_number()} |
{local, binary()} | {undefined, undefined}.
get_addr(Ref) ->
ets:lookup_element(?TAB, {addr, Ref}, 2).
-spec set_max_connections(ranch:ref(), ranch:max_conns()) -> ok.
set_max_connections(Ref, MaxConnections) ->
gen_server:call(?MODULE, {set_max_conns, Ref, MaxConnections}).
-spec get_max_connections(ranch:ref()) -> ranch:max_conns().
get_max_connections(Ref) ->
ets:lookup_element(?TAB, {max_conns, Ref}, 2).
-spec set_stats_counters(ranch:ref(), counters:counters_ref()) -> ok.
set_stats_counters(Ref, Counters) ->
gen_server:call(?MODULE, {set_stats_counters, Ref, Counters}).
-spec get_stats_counters(ranch:ref()) -> counters:counters_ref().
get_stats_counters(Ref) ->
ets:lookup_element(?TAB, {stats_counters, Ref}, 2).
-spec set_transport_options(ranch:ref(), any()) -> ok.
set_transport_options(Ref, TransOpts) ->
gen_server:call(?MODULE, {set_trans_opts, Ref, TransOpts}).
-spec get_transport_options(ranch:ref()) -> any().
get_transport_options(Ref) ->
ets:lookup_element(?TAB, {trans_opts, Ref}, 2).
-spec set_protocol_options(ranch:ref(), any()) -> ok.
set_protocol_options(Ref, ProtoOpts) ->
gen_server:call(?MODULE, {set_proto_opts, Ref, ProtoOpts}).
-spec get_protocol_options(ranch:ref()) -> any().
get_protocol_options(Ref) ->
ets:lookup_element(?TAB, {proto_opts, Ref}, 2).
-spec get_listener_start_args(ranch:ref()) -> [any()].
get_listener_start_args(Ref) ->
ets:lookup_element(?TAB, {listener_start_args, Ref}, 2).
-spec count_connections(ranch:ref()) -> non_neg_integer().
count_connections(Ref) ->
lists:foldl(
fun({_, ConnsSup}, Acc) ->
Acc + ranch_conns_sup:active_connections(ConnsSup)
end,
0,
get_connections_sups(Ref)).
%% gen_server.
-spec init([]) -> {ok, #state{}}.
init([]) ->
ConnMonitors = [{{erlang:monitor(process, Pid), Pid}, {conns_sup, Ref, Id}} ||
[Ref, Id, Pid] <- ets:match(?TAB, {{conns_sup, '$1', '$2'}, '$3'})],
ListenerMonitors = [{{erlang:monitor(process, Pid), Pid}, {listener_sup, Ref}} ||
[Ref, Pid] <- ets:match(?TAB, {{listener_sup, '$1'}, '$2'})],
{ok, #state{monitors = ConnMonitors ++ ListenerMonitors}}.
-spec handle_call(term(), {pid(), reference()}, #state{}) -> {reply, ok | ignore, #state{}}.
handle_call({set_new_listener_opts, Ref, MaxConns, TransOpts, ProtoOpts, StartArgs}, _, State) ->
ets:insert_new(?TAB, {{max_conns, Ref}, MaxConns}),
ets:insert_new(?TAB, {{trans_opts, Ref}, TransOpts}),
ets:insert_new(?TAB, {{proto_opts, Ref}, ProtoOpts}),
ets:insert_new(?TAB, {{listener_start_args, Ref}, StartArgs}),
{reply, ok, State};
handle_call({set_connections_sup, Ref, Id, Pid}, _, State0) ->
State = set_monitored_process({conns_sup, Ref, Id}, Pid, State0),
{reply, ok, State};
handle_call({set_listener_sup, Ref, Pid}, _, State0) ->
State = set_monitored_process({listener_sup, Ref}, Pid, State0),
{reply, ok, State};
handle_call({set_addr, Ref, Addr}, _, State) ->
true = ets:insert(?TAB, {{addr, Ref}, Addr}),
{reply, ok, State};
handle_call({set_max_conns, Ref, MaxConns}, _, State) ->
ets:insert(?TAB, {{max_conns, Ref}, MaxConns}),
_ = [ConnsSup ! {set_max_conns, MaxConns} || {_, ConnsSup} <- get_connections_sups(Ref)],
{reply, ok, State};
handle_call({set_stats_counters, Ref, Counters}, _, State) ->
ets:insert(?TAB, {{stats_counters, Ref}, Counters}),
{reply, ok, State};
handle_call({set_trans_opts, Ref, Opts}, _, State) ->
ets:insert(?TAB, {{trans_opts, Ref}, Opts}),
{reply, ok, State};
handle_call({set_proto_opts, Ref, Opts}, _, State) ->
ets:insert(?TAB, {{proto_opts, Ref}, Opts}),
_ = [ConnsSup ! {set_protocol_options, Opts} || {_, ConnsSup} <- get_connections_sups(Ref)],
{reply, ok, State};
handle_call(_Request, _From, State) ->
{reply, ignore, State}.
-spec handle_cast(_, #state{}) -> {noreply, #state{}}.
handle_cast(_Request, State) ->
{noreply, State}.
-spec handle_info(term(), #state{}) -> {noreply, #state{}}.
handle_info({'DOWN', MonitorRef, process, Pid, Reason},
State = #state{monitors = Monitors}) ->
{_, TypeRef} = lists:keyfind({MonitorRef, Pid}, 1, Monitors),
ok = case {TypeRef, Reason} of
{{listener_sup, Ref}, normal} ->
cleanup_listener_opts(Ref);
{{listener_sup, Ref}, shutdown} ->
cleanup_listener_opts(Ref);
{{listener_sup, Ref}, {shutdown, _}} ->
cleanup_listener_opts(Ref);
_ ->
_ = ets:delete(?TAB, TypeRef),
ok
end,
Monitors2 = lists:keydelete({MonitorRef, Pid}, 1, Monitors),
{noreply, State#state{monitors = Monitors2}};
handle_info(_Info, State) ->
{noreply, State}.
-spec terminate(_, #state{}) -> ok.
terminate(_Reason, _State) ->
ok.
-spec code_change(term() | {down, term()}, #state{}, term()) -> {ok, term()}.
code_change({down, _}, State, _Extra) ->
true = ets:match_delete(?TAB, {{stats_counters, '_'}, '_'}),
{ok, State};
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal.
set_monitored_process(Key, Pid, State = #state{monitors = Monitors0}) ->
%% First we cleanup the monitor if a residual one exists.
%% This can happen during crashes when the restart is faster
%% than the cleanup.
Monitors = case lists:keytake(Key, 2, Monitors0) of
false ->
Monitors0;
{value, {{OldMonitorRef, _}, _}, Monitors1} ->
true = erlang:demonitor(OldMonitorRef, [flush]),
Monitors1
end,
%% Then we unconditionally insert in the ets table.
%% If residual data is there, it will be overwritten.
true = ets:insert(?TAB, {Key, Pid}),
%% Finally we start monitoring this new process.
MonitorRef = erlang:monitor(process, Pid),
State#state{monitors = [{{MonitorRef, Pid}, Key} | Monitors]}.

+ 0
- 67
src/wsNet/ranch_server_proxy.erl Vedi File

@ -1,67 +0,0 @@
%% Copyright (c) 2019-2021, Jan Uhlig <juhlig@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_server_proxy).
-behavior(gen_server).
-export([start_link/0]).
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([code_change/3]).
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
gen_server:start_link(?MODULE, [], []).
-spec init([]) -> {ok, pid()} | {stop, term()}.
init([]) ->
case wait_ranch_server(50) of
{ok, Monitor} ->
{ok, Monitor, hibernate};
{error, Reason} ->
{stop, Reason}
end.
-spec handle_call(_, _, reference()) -> {noreply, reference(), hibernate}.
handle_call(_, _, Monitor) ->
{noreply, Monitor, hibernate}.
-spec handle_cast(_, reference()) -> {noreply, reference(), hibernate}.
handle_cast(_, Monitor) ->
{noreply, Monitor, hibernate}.
-spec handle_info(term(), reference()) -> {noreply, reference(), hibernate} | {stop, term(), reference()}.
handle_info({'DOWN', Monitor, process, _, Reason}, Monitor) ->
{stop, Reason, Monitor};
handle_info(_, Monitor) ->
{noreply, Monitor, hibernate}.
-spec code_change(term() | {down, term()}, reference(), term()) -> {ok, reference()}.
code_change(_, Monitor, _) ->
{ok, Monitor}.
wait_ranch_server(N) ->
case whereis(ranch_server) of
undefined when N > 0 ->
receive after 100 -> ok end,
wait_ranch_server(N - 1);
undefined ->
{error, noproc};
Pid ->
Monitor = monitor(process, Pid),
{ok, Monitor}
end.

+ 0
- 341
src/wsNet/ranch_ssl.erl Vedi File

@ -1,341 +0,0 @@
%% Copyright (c) 2011-2021, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2020-2021, Jan Uhlig <juhlig@hnc-agency.org>
%% Copyright (c) 2021, Maria Scott <maria-12648430@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_ssl).
-behaviour(ranch_transport).
-export([name/0]).
-export([secure/0]).
-export([messages/0]).
-export([listen/1]).
-export([disallowed_listen_options/0]).
-export([accept/2]).
-export([handshake/2]).
-export([handshake/3]).
-export([handshake_continue/2]).
-export([handshake_continue/3]).
-export([handshake_cancel/1]).
-export([connect/3]).
-export([connect/4]).
-export([recv/3]).
-export([recv_proxy_header/2]).
-export([send/2]).
-export([sendfile/2]).
-export([sendfile/4]).
-export([sendfile/5]).
-export([setopts/2]).
-export([getopts/2]).
-export([getstat/1]).
-export([getstat/2]).
-export([controlling_process/2]).
-export([peername/1]).
-export([sockname/1]).
-export([shutdown/2]).
-export([close/1]).
-export([cleanup/1]).
-type ssl_opt() :: {alpn_preferred_protocols, [binary()]}
| {anti_replay, '10k' | '100k' | {integer(), integer(), integer()}}
| {beast_mitigation, one_n_minus_one | zero_n | disabled}
| {cacertfile, file:filename()}
| {cacerts, [public_key:der_encoded()]}
| {cert, public_key:der_encoded()}
| {certfile, file:filename()}
| {ciphers, ssl:ciphers()}
| {client_renegotiation, boolean()}
| {crl_cache, [any()]}
| {crl_check, boolean() | peer | best_effort}
| {depth, integer()}
| {dh, binary()}
| {dhfile, file:filename()}
| {eccs, [ssl:named_curve()]}
| {fail_if_no_peer_cert, boolean()}
| {handshake, hello | full}
| {hibernate_after, timeout()}
| {honor_cipher_order, boolean()}
| {honor_ecc_order, boolean()}
| {key, ssl:key()}
| {key_update_at, pos_integer()}
| {keyfile, file:filename()}
| {log_alert, boolean()}
| {log_level, logger:level()}
| {max_handshake_size, integer()}
| {middlebox_comp_mode, boolean()}
| {next_protocols_advertised, [binary()]}
| {padding_check, boolean()}
| {partial_chain, fun()}
| {password, string()}
| {protocol, tls | dtls}
| {psk_identity, string()}
| {reuse_session, fun()}
| {reuse_sessions, boolean()}
| {secure_renegotiate, boolean()}
| {session_tickets, disabled | stateful | stateless}
| {signature_algs, [{ssl:hash(), ssl:sign_algo()}]}
| {signature_algs_cert, [ssl:sign_scheme()]}
| {sni_fun, fun()}
| {sni_hosts, [{string(), ssl_opt()}]}
| {supported_groups, [ssl:group()]}
| {user_lookup_fun, {fun(), any()}}
| {verify, verify_none | verify_peer}
| {verify_fun, {fun(), any()}}
| {versions, [ssl:protocol_version()]}.
-export_type([ssl_opt/0]).
-type opt() :: ranch_tcp:opt() | ssl_opt().
-export_type([opt/0]).
-type opts() :: [opt()].
-export_type([opts/0]).
-spec name() -> ssl.
name() -> ssl.
-spec secure() -> boolean().
secure() ->
true.
-spec messages() -> {ssl, ssl_closed, ssl_error, ssl_passive}.
messages() -> {ssl, ssl_closed, ssl_error, ssl_passive}.
-spec listen(ranch:transport_opts(opts())) -> {ok, ssl:sslsocket()} | {error, atom()}.
listen(TransOpts) ->
ok = cleanup(TransOpts),
SocketOpts = maps:get(socket_opts, TransOpts, []),
case lists:keymember(cert, 1, SocketOpts)
orelse lists:keymember(certfile, 1, SocketOpts)
orelse lists:keymember(sni_fun, 1, SocketOpts)
orelse lists:keymember(sni_hosts, 1, SocketOpts)
orelse lists:keymember(user_lookup_fun, 1, SocketOpts) of
true ->
Logger = maps:get(logger, TransOpts, logger),
do_listen(SocketOpts, Logger);
false ->
{error, no_cert}
end.
do_listen(SocketOpts0, Logger) ->
SocketOpts1 = ranch:set_option_default(SocketOpts0, backlog, 1024),
SocketOpts2 = ranch:set_option_default(SocketOpts1, nodelay, true),
SocketOpts3 = ranch:set_option_default(SocketOpts2, send_timeout, 30000),
SocketOpts = ranch:set_option_default(SocketOpts3, send_timeout_close, true),
DisallowedOpts0 = disallowed_listen_options(),
DisallowedOpts = unsupported_tls_options(SocketOpts) ++ DisallowedOpts0,
%% We set the port to 0 because it is given in the Opts directly.
%% The port in the options takes precedence over the one in the
%% first argument.
ssl:listen(0, ranch:filter_options(SocketOpts, DisallowedOpts,
[binary, {active, false}, {packet, raw}, {reuseaddr, true}], Logger)).
%% 'binary' and 'list' are disallowed but they are handled
%% specifically as they do not have 2-tuple equivalents.
-spec disallowed_listen_options() -> [atom()].
disallowed_listen_options() ->
[alpn_advertised_protocols, client_preferred_next_protocols,
fallback, server_name_indication, srp_identity
| ranch_tcp:disallowed_listen_options()].
unsupported_tls_options(SocketOpts) ->
unsupported_tls_version_options(lists:usort(get_tls_versions(SocketOpts))).
unsupported_tls_version_options([tlsv1 | _]) ->
[];
unsupported_tls_version_options(['tlsv1.1' | _]) ->
[beast_mitigation, padding_check];
unsupported_tls_version_options(['tlsv1.2' | _]) ->
[beast_mitigation, padding_check];
unsupported_tls_version_options(['tlsv1.3' | _]) ->
[beast_mitigation, client_renegotiation, next_protocols_advertised,
padding_check, psk_identity, reuse_session, reuse_sessions,
secure_renegotiate, user_lookup_fun];
unsupported_tls_version_options(_) ->
[].
-spec accept(ssl:sslsocket(), timeout())
-> {ok, ssl:sslsocket()} | {error, closed | timeout | atom()}.
accept(LSocket, Timeout) ->
ssl:transport_accept(LSocket, Timeout).
-spec handshake(inet:socket() | ssl:sslsocket(), timeout())
-> {ok, ssl:sslsocket()} | {ok, ssl:sslsocket(), ssl:protocol_extensions()} | {error, any()}.
handshake(CSocket, Timeout) ->
handshake(CSocket, [], Timeout).
-spec handshake(inet:socket() | ssl:sslsocket(), opts(), timeout())
-> {ok, ssl:sslsocket()} | {ok, ssl:sslsocket(), ssl:protocol_extensions()} | {error, any()}.
handshake(CSocket, Opts, Timeout) ->
case ssl:handshake(CSocket, Opts, Timeout) of
OK = {ok, _} ->
OK;
OK = {ok, _, _} ->
OK;
Error = {error, _} ->
Error
end.
-spec handshake_continue(ssl:sslsocket(), timeout())
-> {ok, ssl:sslsocket()} | {error, any()}.
handshake_continue(CSocket, Timeout) ->
handshake_continue(CSocket, [], Timeout).
-spec handshake_continue(ssl:sslsocket(), [ssl:tls_server_option()], timeout())
-> {ok, ssl:sslsocket()} | {error, any()}.
handshake_continue(CSocket, Opts, Timeout) ->
case ssl:handshake_continue(CSocket, Opts, Timeout) of
OK = {ok, _} ->
OK;
Error = {error, _} ->
Error
end.
-spec handshake_cancel(ssl:sslsocket()) -> ok.
handshake_cancel(CSocket) ->
ok = ssl:handshake_cancel(CSocket).
%% @todo Probably filter Opts?
-spec connect(inet:ip_address() | inet:hostname(),
inet:port_number(), any())
-> {ok, inet:socket()} | {error, atom()}.
connect(Host, Port, Opts) when is_integer(Port) ->
ssl:connect(Host, Port,
Opts ++ [binary, {active, false}, {packet, raw}]).
%% @todo Probably filter Opts?
-spec connect(inet:ip_address() | inet:hostname(),
inet:port_number(), any(), timeout())
-> {ok, inet:socket()} | {error, atom()}.
connect(Host, Port, Opts, Timeout) when is_integer(Port) ->
ssl:connect(Host, Port,
Opts ++ [binary, {active, false}, {packet, raw}],
Timeout).
-spec recv(ssl:sslsocket(), non_neg_integer(), timeout())
-> {ok, any()} | {error, closed | atom()}.
recv(Socket, Length, Timeout) ->
ssl:recv(Socket, Length, Timeout).
-spec recv_proxy_header(ssl:sslsocket(), timeout())
-> {ok, ranch_proxy_header:proxy_info()}
| {error, closed | atom()}
| {error, protocol_error, atom()}.
recv_proxy_header(SSLSocket, Timeout) ->
%% There's currently no documented way to perform a TCP recv
%% on an sslsocket(), even before the TLS handshake. However
%% nothing prevents us from retrieving the TCP socket and using
%% it. Since it's an undocumented interface this may however
%% make forward-compatibility more difficult.
{sslsocket, {gen_tcp, TCPSocket, _, _}, _} = SSLSocket,
ranch_tcp:recv_proxy_header(TCPSocket, Timeout).
-spec send(ssl:sslsocket(), iodata()) -> ok | {error, atom()}.
send(Socket, Packet) ->
ssl:send(Socket, Packet).
-spec sendfile(ssl:sslsocket(), file:name_all() | file:fd())
-> {ok, non_neg_integer()} | {error, atom()}.
sendfile(Socket, Filename) ->
sendfile(Socket, Filename, 0, 0, []).
-spec sendfile(ssl:sslsocket(), file:name_all() | file:fd(),
non_neg_integer(), non_neg_integer())
-> {ok, non_neg_integer()} | {error, atom()}.
sendfile(Socket, File, Offset, Bytes) ->
sendfile(Socket, File, Offset, Bytes, []).
%% Unlike with TCP, no syscall can be used here, so sending files
%% through SSL will be much slower in comparison. Note that unlike
%% file:sendfile/5 this function accepts either a file or a file name.
-spec sendfile(ssl:sslsocket(), file:name_all() | file:fd(),
non_neg_integer(), non_neg_integer(), ranch_transport:sendfile_opts())
-> {ok, non_neg_integer()} | {error, atom()}.
sendfile(Socket, File, Offset, Bytes, Opts) ->
ranch_transport:sendfile(?MODULE, Socket, File, Offset, Bytes, Opts).
%% @todo Probably filter Opts?
-spec setopts(ssl:sslsocket(), list()) -> ok | {error, atom()}.
setopts(Socket, Opts) ->
ssl:setopts(Socket, Opts).
-spec getopts(ssl:sslsocket(), [atom()]) -> {ok, list()} | {error, atom()}.
getopts(Socket, Opts) ->
ssl:getopts(Socket, Opts).
-spec getstat(ssl:sslsocket()) -> {ok, list()} | {error, atom()}.
getstat(Socket) ->
ssl:getstat(Socket).
-spec getstat(ssl:sslsocket(), [atom()]) -> {ok, list()} | {error, atom()}.
getstat(Socket, OptionNames) ->
ssl:getstat(Socket, OptionNames).
-spec controlling_process(ssl:sslsocket(), pid())
-> ok | {error, closed | not_owner | atom()}.
controlling_process(Socket, Pid) ->
ssl:controlling_process(Socket, Pid).
-spec peername(ssl:sslsocket())
-> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}.
peername(Socket) ->
ssl:peername(Socket).
-spec sockname(ssl:sslsocket())
-> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}.
sockname(Socket) ->
ssl:sockname(Socket).
-spec shutdown(ssl:sslsocket(), read | write | read_write)
-> ok | {error, atom()}.
shutdown(Socket, How) ->
ssl:shutdown(Socket, How).
-spec close(ssl:sslsocket()) -> ok.
close(Socket) ->
ssl:close(Socket).
-spec cleanup(ranch:transport_opts(opts())) -> ok.
cleanup(#{socket_opts := SocketOpts}) ->
case lists:keyfind(ip, 1, lists:reverse(SocketOpts)) of
{ip, {local, SockFile}} ->
_ = file:delete(SockFile),
ok;
_ ->
ok
end;
cleanup(_) ->
ok.
get_tls_versions(SocketOpts) ->
%% Socket options need to be reversed for keyfind because later options
%% take precedence when contained multiple times, but keyfind will return
%% the earliest occurence.
case lists:keyfind(versions, 1, lists:reverse(SocketOpts)) of
{versions, Versions} ->
Versions;
false ->
get_tls_versions_env()
end.
get_tls_versions_env() ->
case application:get_env(ssl, protocol_version) of
{ok, Versions} ->
Versions;
undefined ->
get_tls_versions_app()
end.
get_tls_versions_app() ->
{supported, Versions} = lists:keyfind(supported, 1, ssl:versions()),
Versions.

+ 0
- 39
src/wsNet/ranch_sup.erl Vedi File

@ -1,39 +0,0 @@
%% Copyright (c) 2011-2021, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2020-2021, Jan Uhlig <juhlig@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
init([]) ->
Intensity = case application:get_env(ranch_sup_intensity) of
{ok, Value1} -> Value1;
undefined -> 1
end,
Period = case application:get_env(ranch_sup_period) of
{ok, Value2} -> Value2;
undefined -> 5
end,
Procs = [
#{id => ranch_server, start => {ranch_server, start_link, []}}
],
{ok, {#{intensity => Intensity, period => Period}, Procs}}.

+ 0
- 287
src/wsNet/ranch_tcp.erl Vedi File

@ -1,287 +0,0 @@
%% Copyright (c) 2011-2021, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2020-2021, Jan Uhlig <juhlig@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_tcp).
-behaviour(ranch_transport).
-export([name/0]).
-export([secure/0]).
-export([messages/0]).
-export([listen/1]).
-export([disallowed_listen_options/0]).
-export([accept/2]).
-export([handshake/2]).
-export([handshake/3]).
-export([handshake_continue/2]).
-export([handshake_continue/3]).
-export([handshake_cancel/1]).
-export([connect/3]).
-export([connect/4]).
-export([recv/3]).
-export([recv_proxy_header/2]).
-export([send/2]).
-export([sendfile/2]).
-export([sendfile/4]).
-export([sendfile/5]).
-export([setopts/2]).
-export([getopts/2]).
-export([getstat/1]).
-export([getstat/2]).
-export([controlling_process/2]).
-export([peername/1]).
-export([sockname/1]).
-export([shutdown/2]).
-export([close/1]).
-export([cleanup/1]).
-type opt() :: {backlog, non_neg_integer()}
| {buffer, non_neg_integer()}
| {delay_send, boolean()}
| {dontroute, boolean()}
| {exit_on_close, boolean()}
| {fd, non_neg_integer()}
| {high_msgq_watermark, non_neg_integer()}
| {high_watermark, non_neg_integer()}
| inet
| inet6
| {ip, inet:ip_address() | inet:local_address()}
| {ipv6_v6only, boolean()}
| {keepalive, boolean()}
| {linger, {boolean(), non_neg_integer()}}
| {low_msgq_watermark, non_neg_integer()}
| {low_watermark, non_neg_integer()}
| {nodelay, boolean()}
| {port, inet:port_number()}
| {priority, integer()}
| {raw, non_neg_integer(), non_neg_integer(), binary()}
| {recbuf, non_neg_integer()}
| {send_timeout, timeout()}
| {send_timeout_close, boolean()}
| {sndbuf, non_neg_integer()}
| {tos, integer()}.
-export_type([opt/0]).
-type opts() :: [opt()].
-export_type([opts/0]).
-spec name() -> tcp.
name() -> tcp.
-spec secure() -> boolean().
secure() ->
false.
-spec messages() -> {tcp, tcp_closed, tcp_error, tcp_passive}.
messages() -> {tcp, tcp_closed, tcp_error, tcp_passive}.
-spec listen(ranch:transport_opts(opts())) -> {ok, inet:socket()} | {error, atom()}.
listen(TransOpts) ->
ok = cleanup(TransOpts),
Logger = maps:get(logger, TransOpts, logger),
SocketOpts = maps:get(socket_opts, TransOpts, []),
%% We set the port to 0 because it is given in the Opts directly.
%% The port in the options takes precedence over the one in the
%% first argument.
gen_tcp:listen(0, prepare_socket_opts(SocketOpts, Logger)).
prepare_socket_opts([Backend = {inet_backend, _} | SocketOpts], Logger) ->
%% In OTP/23, the inet_backend option may be used to activate the
%% experimental socket backend for inet/gen_tcp. If present, it must
%% be the first option in the list.
[Backend | prepare_socket_opts(SocketOpts, Logger)];
prepare_socket_opts(SocketOpts0, Logger) ->
SocketOpts1 = ranch:set_option_default(SocketOpts0, backlog, 1024),
SocketOpts2 = ranch:set_option_default(SocketOpts1, nodelay, true),
SocketOpts3 = ranch:set_option_default(SocketOpts2, send_timeout, 30000),
SocketOpts4 = ranch:set_option_default(SocketOpts3, send_timeout_close, true),
ranch:filter_options(SocketOpts4, disallowed_listen_options(),
[binary, {active, false}, {packet, raw}, {reuseaddr, true}], Logger).
%% 'binary' and 'list' are disallowed but they are handled
%% specifically as they do not have 2-tuple equivalents.
-spec disallowed_listen_options() -> [atom()].
disallowed_listen_options() ->
[active, header, mode, packet, packet_size, line_delimiter, reuseaddr].
-spec accept(inet:socket(), timeout())
-> {ok, inet:socket()} | {error, closed | timeout | atom()}.
accept(LSocket, Timeout) ->
gen_tcp:accept(LSocket, Timeout).
-spec handshake(inet:socket(), timeout()) -> {ok, inet:socket()}.
handshake(CSocket, Timeout) ->
handshake(CSocket, [], Timeout).
-spec handshake(inet:socket(), opts(), timeout()) -> {ok, inet:socket()}.
handshake(CSocket, _, _) ->
{ok, CSocket}.
-spec handshake_continue(inet:socket(), timeout()) -> no_return().
handshake_continue(CSocket, Timeout) ->
handshake_continue(CSocket, [], Timeout).
-spec handshake_continue(inet:socket(), opts(), timeout()) -> no_return().
handshake_continue(_, _, _) ->
error(not_supported).
-spec handshake_cancel(inet:socket()) -> no_return().
handshake_cancel(_) ->
error(not_supported).
%% @todo Probably filter Opts?
-spec connect(inet:ip_address() | inet:hostname(),
inet:port_number(), any())
-> {ok, inet:socket()} | {error, atom()}.
connect(Host, Port, Opts) when is_integer(Port) ->
gen_tcp:connect(Host, Port,
Opts ++ [binary, {active, false}, {packet, raw}]).
%% @todo Probably filter Opts?
-spec connect(inet:ip_address() | inet:hostname(),
inet:port_number(), any(), timeout())
-> {ok, inet:socket()} | {error, atom()}.
connect(Host, Port, Opts, Timeout) when is_integer(Port) ->
gen_tcp:connect(Host, Port,
Opts ++ [binary, {active, false}, {packet, raw}],
Timeout).
-spec recv(inet:socket(), non_neg_integer(), timeout())
-> {ok, any()} | {error, closed | atom()}.
recv(Socket, Length, Timeout) ->
gen_tcp:recv(Socket, Length, Timeout).
-spec recv_proxy_header(inet:socket(), timeout())
-> {ok, ranch_proxy_header:proxy_info()}
| {error, closed | atom()}
| {error, protocol_error, atom()}.
recv_proxy_header(Socket, Timeout) ->
case recv(Socket, 0, Timeout) of
{ok, Data} ->
case ranch_proxy_header:parse(Data) of
{ok, ProxyInfo, <<>>} ->
{ok, ProxyInfo};
{ok, ProxyInfo, Rest} ->
case gen_tcp:unrecv(Socket, Rest) of
ok ->
{ok, ProxyInfo};
Error ->
Error
end;
{error, HumanReadable} ->
{error, protocol_error, HumanReadable}
end;
Error ->
Error
end.
-spec send(inet:socket(), iodata()) -> ok | {error, atom()}.
send(Socket, Packet) ->
gen_tcp:send(Socket, Packet).
-spec sendfile(inet:socket(), file:name_all() | file:fd())
-> {ok, non_neg_integer()} | {error, atom()}.
sendfile(Socket, Filename) ->
sendfile(Socket, Filename, 0, 0, []).
-spec sendfile(inet:socket(), file:name_all() | file:fd(), non_neg_integer(),
non_neg_integer())
-> {ok, non_neg_integer()} | {error, atom()}.
sendfile(Socket, File, Offset, Bytes) ->
sendfile(Socket, File, Offset, Bytes, []).
-spec sendfile(inet:socket(), file:name_all() | file:fd(), non_neg_integer(),
non_neg_integer(), [{chunk_size, non_neg_integer()}])
-> {ok, non_neg_integer()} | {error, atom()}.
sendfile(Socket, Filename, Offset, Bytes, Opts)
when is_list(Filename) orelse is_atom(Filename)
orelse is_binary(Filename) ->
case file:open(Filename, [read, raw, binary]) of
{ok, RawFile} ->
try sendfile(Socket, RawFile, Offset, Bytes, Opts) of
Result -> Result
after
ok = file:close(RawFile)
end;
{error, _} = Error ->
Error
end;
sendfile(Socket, RawFile, Offset, Bytes, Opts) ->
Opts2 = case Opts of
[] -> [{chunk_size, 16#1FFF}];
_ -> Opts
end,
try file:sendfile(RawFile, Socket, Offset, Bytes, Opts2) of
Result -> Result
catch
error:{badmatch, {error, enotconn}} ->
%% file:sendfile/5 might fail by throwing a
%% {badmatch, {error, enotconn}}. This is because its
%% implementation fails with a badmatch in
%% prim_file:sendfile/10 if the socket is not connected.
{error, closed}
end.
%% @todo Probably filter Opts?
-spec setopts(inet:socket(), list()) -> ok | {error, atom()}.
setopts(Socket, Opts) ->
inet:setopts(Socket, Opts).
-spec getopts(inet:socket(), [atom()]) -> {ok, list()} | {error, atom()}.
getopts(Socket, Opts) ->
inet:getopts(Socket, Opts).
-spec getstat(inet:socket()) -> {ok, list()} | {error, atom()}.
getstat(Socket) ->
inet:getstat(Socket).
-spec getstat(inet:socket(), [atom()]) -> {ok, list()} | {error, atom()}.
getstat(Socket, OptionNames) ->
inet:getstat(Socket, OptionNames).
-spec controlling_process(inet:socket(), pid())
-> ok | {error, closed | not_owner | atom()}.
controlling_process(Socket, Pid) ->
gen_tcp:controlling_process(Socket, Pid).
-spec peername(inet:socket())
-> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}.
peername(Socket) ->
inet:peername(Socket).
-spec sockname(inet:socket())
-> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}.
sockname(Socket) ->
inet:sockname(Socket).
-spec shutdown(inet:socket(), read | write | read_write)
-> ok | {error, atom()}.
shutdown(Socket, How) ->
gen_tcp:shutdown(Socket, How).
-spec close(inet:socket()) -> ok.
close(Socket) ->
gen_tcp:close(Socket).
-spec cleanup(ranch:transport_opts(opts())) -> ok.
cleanup(#{socket_opts := SocketOpts}) ->
case lists:keyfind(ip, 1, lists:reverse(SocketOpts)) of
{ip, {local, SockFile}} ->
_ = file:delete(SockFile),
ok;
_ ->
ok
end;
cleanup(_) ->
ok.

+ 0
- 157
src/wsNet/ranch_transport.erl Vedi File

@ -1,157 +0,0 @@
%% Copyright (c) 2012-2021, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2020-2021, Jan Uhlig <juhlig@hnc-agency.org>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(ranch_transport).
-export([sendfile/6]).
-type socket() :: any().
-export_type([socket/0]).
-type opts() :: any().
-type stats() :: any().
-type sendfile_opts() :: [{chunk_size, non_neg_integer()}].
-export_type([sendfile_opts/0]).
-callback name() -> atom().
-callback secure() -> boolean().
-callback messages() -> {OK :: atom(), Closed :: atom(), Error :: atom(), Passive :: atom()}.
-callback listen(ranch:transport_opts(any())) -> {ok, socket()} | {error, atom()}.
-callback accept(socket(), timeout())
-> {ok, socket()} | {error, closed | timeout | atom()}.
-callback handshake(socket(), timeout()) -> {ok, socket()} | {ok, socket(), any()} | {error, any()}.
-callback handshake(socket(), opts(), timeout()) -> {ok, socket()} | {ok, socket(), any()} | {error, any()}.
-callback handshake_continue(socket(), timeout()) -> {ok, socket()} | {error, any()}.
-callback handshake_continue(socket(), opts(), timeout()) -> {ok, socket()} | {error, any()}.
-callback handshake_cancel(socket()) -> ok.
-callback connect(string(), inet:port_number(), opts())
-> {ok, socket()} | {error, atom()}.
-callback connect(string(), inet:port_number(), opts(), timeout())
-> {ok, socket()} | {error, atom()}.
-callback recv(socket(), non_neg_integer(), timeout())
-> {ok, any()} | {error, closed | timeout | atom()}.
-callback recv_proxy_header(socket(), timeout())
-> {ok, ranch_proxy_header:proxy_info()}
| {error, closed | atom()}
| {error, protocol_error, atom()}.
-callback send(socket(), iodata()) -> ok | {error, atom()}.
-callback sendfile(socket(), file:name_all() | file:fd())
-> {ok, non_neg_integer()} | {error, atom()}.
-callback sendfile(socket(), file:name_all() | file:fd(), non_neg_integer(),
non_neg_integer()) -> {ok, non_neg_integer()} | {error, atom()}.
-callback sendfile(socket(), file:name_all() | file:fd(), non_neg_integer(),
non_neg_integer(), sendfile_opts())
-> {ok, non_neg_integer()} | {error, atom()}.
-callback setopts(socket(), opts()) -> ok | {error, atom()}.
-callback getopts(socket(), [atom()]) -> {ok, opts()} | {error, atom()}.
-callback getstat(socket()) -> {ok, stats()} | {error, atom()}.
-callback getstat(socket(), [atom()]) -> {ok, stats()} | {error, atom()}.
-callback controlling_process(socket(), pid())
-> ok | {error, closed | not_owner | atom()}.
-callback peername(socket())
-> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}.
-callback sockname(socket())
-> {ok, {inet:ip_address(), inet:port_number()} | {local, binary()}} | {error, atom()}.
-callback shutdown(socket(), read | write | read_write)
-> ok | {error, atom()}.
-callback close(socket()) -> ok.
-callback cleanup(ranch:transport_opts(any())) -> ok.
%% A fallback for transports that don't have a native sendfile implementation.
%% Note that the ordering of arguments is different from file:sendfile/5 and
%% that this function accepts either a raw file or a file name.
-spec sendfile(module(), socket(), file:name_all() | file:fd(),
non_neg_integer(), non_neg_integer(), sendfile_opts())
-> {ok, non_neg_integer()} | {error, atom()}.
sendfile(Transport, Socket, Filename, Offset, Bytes, Opts)
when is_list(Filename) orelse is_atom(Filename)
orelse is_binary(Filename) ->
ChunkSize = chunk_size(Opts),
case file:open(Filename, [read, raw, binary]) of
{ok, RawFile} ->
_ = case Offset of
0 ->
ok;
_ ->
{ok, _} = file:position(RawFile, {bof, Offset})
end,
try
sendfile_loop(Transport, Socket, RawFile, Bytes, 0, ChunkSize)
after
ok = file:close(RawFile)
end;
{error, _Reason} = Error ->
Error
end;
sendfile(Transport, Socket, RawFile, Offset, Bytes, Opts) ->
ChunkSize = chunk_size(Opts),
Initial2 = case file:position(RawFile, {cur, 0}) of
{ok, Offset} ->
Offset;
{ok, Initial} ->
{ok, _} = file:position(RawFile, {bof, Offset}),
Initial
end,
case sendfile_loop(Transport, Socket, RawFile, Bytes, 0, ChunkSize) of
{ok, _Sent} = Result ->
{ok, _} = file:position(RawFile, {bof, Initial2}),
Result;
{error, _Reason} = Error ->
Error
end.
-spec chunk_size(sendfile_opts()) -> pos_integer().
chunk_size(Opts) ->
case lists:keyfind(chunk_size, 1, Opts) of
{chunk_size, ChunkSize}
when is_integer(ChunkSize) andalso ChunkSize > 0 ->
ChunkSize;
{chunk_size, 0} ->
16#1FFF;
false ->
16#1FFF
end.
-spec sendfile_loop(module(), socket(), file:fd(), non_neg_integer(),
non_neg_integer(), pos_integer())
-> {ok, non_neg_integer()} | {error, any()}.
sendfile_loop(_Transport, _Socket, _RawFile, Sent, Sent, _ChunkSize)
when Sent =/= 0 ->
%% All requested data has been read and sent, return number of bytes sent.
{ok, Sent};
sendfile_loop(Transport, Socket, RawFile, Bytes, Sent, ChunkSize) ->
ReadSize = read_size(Bytes, Sent, ChunkSize),
case file:read(RawFile, ReadSize) of
{ok, IoData} ->
case Transport:send(Socket, IoData) of
ok ->
Sent2 = iolist_size(IoData) + Sent,
sendfile_loop(Transport, Socket, RawFile, Bytes, Sent2,
ChunkSize);
{error, _Reason} = Error ->
Error
end;
eof ->
{ok, Sent};
{error, _Reason} = Error ->
Error
end.
-spec read_size(non_neg_integer(), non_neg_integer(), non_neg_integer()) ->
non_neg_integer().
read_size(0, _Sent, ChunkSize) ->
ChunkSize;
read_size(Bytes, Sent, ChunkSize) ->
min(Bytes - Sent, ChunkSize).

+ 0
- 105
src/wsSrv/cowboy.erl Vedi File

@ -1,105 +0,0 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy).
-export([start_clear/3]).
-export([start_tls/3]).
-export([stop_listener/1]).
-export([set_env/3]).
%% Internal.
-export([log/2]).
-export([log/4]).
-type opts() :: cowboy_http:opts() | cowboy_http2:opts().
-export_type([opts/0]).
-type fields() :: [atom()
| {atom(), cowboy_constraints:constraint() | [cowboy_constraints:constraint()]}
| {atom(), cowboy_constraints:constraint() | [cowboy_constraints:constraint()], any()}].
-export_type([fields/0]).
-type http_headers() :: #{binary() => iodata()}.
-export_type([http_headers/0]).
-type http_status() :: non_neg_integer() | binary().
-export_type([http_status/0]).
-type http_version() :: 'HTTP/2' | 'HTTP/1.1' | 'HTTP/1.0'.
-export_type([http_version/0]).
-spec start_clear(ranch:ref(), ranch:opts(), opts())
-> {ok, pid()} | {error, any()}.
start_clear(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0),
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts1),
ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
ranch:start_listener(Ref, ranch_tcp, TransOpts, cowboy_clear, ProtoOpts).
-spec start_tls(ranch:ref(), ranch:opts(), opts())
-> {ok, pid()} | {error, any()}.
start_tls(Ref, TransOpts0, ProtoOpts0) ->
TransOpts1 = ranch:normalize_opts(TransOpts0),
SocketOpts = maps:get(socket_opts, TransOpts1, []),
TransOpts2 = TransOpts1#{socket_opts => [
{next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
{alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
| SocketOpts]},
{TransOpts, ConnectionType} = ensure_connection_type(TransOpts2),
ProtoOpts = ProtoOpts0#{connection_type => ConnectionType},
ranch:start_listener(Ref, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts).
ensure_connection_type(TransOpts = #{connection_type := ConnectionType}) ->
{TransOpts, ConnectionType};
ensure_connection_type(TransOpts) ->
{TransOpts#{connection_type => supervisor}, supervisor}.
-spec stop_listener(ranch:ref()) -> ok | {error, not_found}.
stop_listener(Ref) ->
ranch:stop_listener(Ref).
-spec set_env(ranch:ref(), atom(), any()) -> ok.
set_env(Ref, Name, Value) ->
Opts = ranch:get_protocol_options(Ref),
Env = maps:get(env, Opts, #{}),
Opts2 = maps:put(env, maps:put(Name, Value, Env), Opts),
ok = ranch:set_protocol_options(Ref, Opts2).
%% Internal.
-spec log({log, logger:level(), io:format(), list()}, opts()) -> ok.
log({log, Level, Format, Args}, Opts) ->
log(Level, Format, Args, Opts).
-spec log(logger:level(), io:format(), list(), opts()) -> ok.
log(Level, Format, Args, #{logger := Logger})
when Logger =/= error_logger ->
_ = Logger:Level(Format, Args),
ok;
%% We use error_logger by default. Because error_logger does
%% not have all the levels we accept we have to do some
%% mapping to error_logger functions.
log(Level, Format, Args, _) ->
Function = case Level of
emergency -> error_msg;
alert -> error_msg;
critical -> error_msg;
error -> error_msg;
warning -> warning_msg;
notice -> warning_msg;
info -> info_msg;
debug -> info_msg
end,
error_logger:Function(Format, Args).

+ 0
- 27
src/wsSrv/cowboy_app.erl Vedi File

@ -1,27 +0,0 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_app).
-behaviour(application).
-export([start/2]).
-export([stop/1]).
-spec start(_, _) -> {ok, pid()}.
start(_, _) ->
cowboy_sup:start_link().
-spec stop(_) -> ok.
stop(_) ->
ok.

+ 0
- 123
src/wsSrv/cowboy_bstr.erl Vedi File

@ -1,123 +0,0 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_bstr).
%% Binary strings.
-export([capitalize_token/1]).
-export([to_lower/1]).
-export([to_upper/1]).
%% Characters.
-export([char_to_lower/1]).
-export([char_to_upper/1]).
%% The first letter and all letters after a dash are capitalized.
%% This is the form seen for header names in the HTTP/1.1 RFC and
%% others. Note that using this form isn't required, as header names
%% are case insensitive, and it is only provided for use with eventual
%% badly implemented clients.
-spec capitalize_token(B) -> B when B :: binary().
capitalize_token(B) ->
capitalize_token(B, true, <<>>).
capitalize_token(<<>>, _, Acc) ->
Acc;
capitalize_token(<<$-, Rest/bits>>, _, Acc) ->
capitalize_token(Rest, true, <<Acc/binary, $->>);
capitalize_token(<<C, Rest/bits>>, true, Acc) ->
capitalize_token(Rest, false, <<Acc/binary, (char_to_upper(C))>>);
capitalize_token(<<C, Rest/bits>>, false, Acc) ->
capitalize_token(Rest, false, <<Acc/binary, (char_to_lower(C))>>).
-spec to_lower(B) -> B when B :: binary().
to_lower(B) ->
<<<<(char_to_lower(C))>> || <<C>> <= B>>.
-spec to_upper(B) -> B when B :: binary().
to_upper(B) ->
<<<<(char_to_upper(C))>> || <<C>> <= B>>.
-spec char_to_lower(char()) -> char().
char_to_lower($A) -> $a;
char_to_lower($B) -> $b;
char_to_lower($C) -> $c;
char_to_lower($D) -> $d;
char_to_lower($E) -> $e;
char_to_lower($F) -> $f;
char_to_lower($G) -> $g;
char_to_lower($H) -> $h;
char_to_lower($I) -> $i;
char_to_lower($J) -> $j;
char_to_lower($K) -> $k;
char_to_lower($L) -> $l;
char_to_lower($M) -> $m;
char_to_lower($N) -> $n;
char_to_lower($O) -> $o;
char_to_lower($P) -> $p;
char_to_lower($Q) -> $q;
char_to_lower($R) -> $r;
char_to_lower($S) -> $s;
char_to_lower($T) -> $t;
char_to_lower($U) -> $u;
char_to_lower($V) -> $v;
char_to_lower($W) -> $w;
char_to_lower($X) -> $x;
char_to_lower($Y) -> $y;
char_to_lower($Z) -> $z;
char_to_lower(Ch) -> Ch.
-spec char_to_upper(char()) -> char().
char_to_upper($a) -> $A;
char_to_upper($b) -> $B;
char_to_upper($c) -> $C;
char_to_upper($d) -> $D;
char_to_upper($e) -> $E;
char_to_upper($f) -> $F;
char_to_upper($g) -> $G;
char_to_upper($h) -> $H;
char_to_upper($i) -> $I;
char_to_upper($j) -> $J;
char_to_upper($k) -> $K;
char_to_upper($l) -> $L;
char_to_upper($m) -> $M;
char_to_upper($n) -> $N;
char_to_upper($o) -> $O;
char_to_upper($p) -> $P;
char_to_upper($q) -> $Q;
char_to_upper($r) -> $R;
char_to_upper($s) -> $S;
char_to_upper($t) -> $T;
char_to_upper($u) -> $U;
char_to_upper($v) -> $V;
char_to_upper($w) -> $W;
char_to_upper($x) -> $X;
char_to_upper($y) -> $Y;
char_to_upper($z) -> $Z;
char_to_upper(Ch) -> Ch.
%% Tests.
-ifdef(TEST).
capitalize_token_test_() ->
Tests = [
{<<"heLLo-woRld">>, <<"Hello-World">>},
{<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>},
{<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>},
{<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>},
{<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>},
{<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--Version">>},
{<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>}
],
[{H, fun() -> R = capitalize_token(H) end} || {H, R} <- Tests].
-endif.

+ 0
- 192
src/wsSrv/cowboy_children.erl Vedi File

@ -1,192 +0,0 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_children).
-export([init/0]).
-export([up/4]).
-export([down/2]).
-export([shutdown/2]).
-export([shutdown_timeout/3]).
-export([terminate/1]).
-export([handle_supervisor_call/4]).
-record(child, {
pid :: pid(),
streamid :: cowboy_stream:streamid() | undefined,
shutdown :: timeout(),
timer = undefined :: undefined | reference()
}).
-type children() :: [#child{}].
-export_type([children/0]).
-spec init() -> [].
init() ->
[].
-spec up(Children, pid(), cowboy_stream:streamid(), timeout())
-> Children when Children :: children().
up(Children, Pid, StreamID, Shutdown) ->
[#child{
pid = Pid,
streamid = StreamID,
shutdown = Shutdown
} | Children].
-spec down(Children, pid())
-> {ok, cowboy_stream:streamid() | undefined, Children} | error
when Children :: children().
down(Children0, Pid) ->
case lists:keytake(Pid, #child.pid, Children0) of
{value, #child{streamid = StreamID, timer = Ref}, Children} ->
_ = case Ref of
undefined -> ok;
_ -> erlang:cancel_timer(Ref, [{async, true}, {info, false}])
end,
{ok, StreamID, Children};
false ->
error
end.
%% We ask the processes to shutdown first. This gives
%% a chance to processes that are trapping exits to
%% shut down gracefully. Others will exit immediately.
%%
%% @todo We currently fire one timer per process being
%% shut down. This is probably not the most efficient.
%% A more efficient solution could be to maintain a
%% single timer and decrease the shutdown time of all
%% processes when it fires. This is however much more
%% complex, and there aren't that many processes that
%% will need to be shutdown through this function, so
%% this is left for later.
-spec shutdown(Children, cowboy_stream:streamid())
-> Children when Children :: children().
shutdown(Children0, StreamID) ->
[
case Child of
#child{pid = Pid, streamid = StreamID, shutdown = Shutdown} ->
exit(Pid, shutdown),
Ref = erlang:start_timer(Shutdown, self(), {shutdown, Pid}),
Child#child{streamid = undefined, timer = Ref};
_ ->
Child
end
|| Child <- Children0].
-spec shutdown_timeout(children(), reference(), pid()) -> ok.
shutdown_timeout(Children, Ref, Pid) ->
case lists:keyfind(Pid, #child.pid, Children) of
#child{timer = Ref} ->
exit(Pid, kill),
ok;
_ ->
ok
end.
-spec terminate(children()) -> ok.
terminate(Children) ->
%% For each child, either ask for it to shut down,
%% or cancel its shutdown timer if it already is.
%%
%% We do not need to flush stray timeout messages out because
%% we are either terminating or switching protocols,
%% and in the latter case we flush all messages.
_ = [case TRef of
undefined -> exit(Pid, shutdown);
_ -> erlang:cancel_timer(TRef, [{async, true}, {info, false}])
end || #child{pid = Pid, timer = TRef} <- Children],
before_terminate_loop(Children).
before_terminate_loop([]) ->
ok;
before_terminate_loop(Children) ->
%% Find the longest shutdown time.
Time = longest_shutdown_time(Children, 0),
%% We delay the creation of the timer if one of the
%% processes has an infinity shutdown value.
TRef = case Time of
infinity -> undefined;
_ -> erlang:start_timer(Time, self(), terminate)
end,
%% Loop until that time or until all children are dead.
terminate_loop(Children, TRef).
terminate_loop([], TRef) ->
%% Don't forget to cancel the timer, if any!
case TRef of
undefined ->
ok;
_ ->
_ = erlang:cancel_timer(TRef, [{async, true}, {info, false}]),
ok
end;
terminate_loop(Children, TRef) ->
receive
{'EXIT', Pid, _} when TRef =:= undefined ->
{value, #child{shutdown = Shutdown}, Children1}
= lists:keytake(Pid, #child.pid, Children),
%% We delayed the creation of the timer. If a process with
%% infinity shutdown just ended, we might have to start that timer.
case Shutdown of
infinity -> before_terminate_loop(Children1);
_ -> terminate_loop(Children1, TRef)
end;
{'EXIT', Pid, _} ->
terminate_loop(lists:keydelete(Pid, #child.pid, Children), TRef);
{timeout, TRef, terminate} ->
%% Brutally kill any remaining children.
_ = [exit(Pid, kill) || #child{pid = Pid} <- Children],
ok
end.
longest_shutdown_time([], Time) ->
Time;
longest_shutdown_time([#child{shutdown = ChildTime} | Tail], Time) when ChildTime > Time ->
longest_shutdown_time(Tail, ChildTime);
longest_shutdown_time([_ | Tail], Time) ->
longest_shutdown_time(Tail, Time).
-spec handle_supervisor_call(any(), {pid(), any()}, children(), module()) -> ok.
handle_supervisor_call(which_children, {From, Tag}, Children, Module) ->
From ! {Tag, which_children(Children, Module)},
ok;
handle_supervisor_call(count_children, {From, Tag}, Children, _) ->
From ! {Tag, count_children(Children)},
ok;
%% We disable start_child since only incoming requests
%% end up creating a new process.
handle_supervisor_call({start_child, _}, {From, Tag}, _, _) ->
From ! {Tag, {error, start_child_disabled}},
ok;
%% All other calls refer to children. We act in a similar way
%% to a simple_one_for_one so we never find those.
handle_supervisor_call(_, {From, Tag}, _, _) ->
From ! {Tag, {error, not_found}},
ok.
-spec which_children(children(), module()) -> [{module(), pid(), worker, [module()]}].
which_children(Children, Module) ->
[{Module, Pid, worker, [Module]} || #child{pid = Pid} <- Children].
-spec count_children(children()) -> [{atom(), non_neg_integer()}].
count_children(Children) ->
Count = length(Children),
[
{specs, 1},
{active, Count},
{supervisors, 0},
{workers, Count}
].

+ 0
- 60
src/wsSrv/cowboy_clear.erl Vedi File

@ -1,60 +0,0 @@
%% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_clear).
-behavior(ranch_protocol).
-export([start_link/3]).
-export([start_link/4]).
-export([connection_process/4]).
%% Ranch 1.
-spec start_link(ranch:ref(), inet:socket(), module(), cowboy:opts()) -> {ok, pid()}.
start_link(Ref, _Socket, Transport, Opts) ->
start_link(Ref, Transport, Opts).
%% Ranch 2.
-spec start_link(ranch:ref(), module(), cowboy:opts()) -> {ok, pid()}.
start_link(Ref, Transport, Opts) ->
Pid = proc_lib:spawn_link(?MODULE, connection_process,
[self(), Ref, Transport, Opts]),
{ok, Pid}.
-spec connection_process(pid(), ranch:ref(), module(), cowboy:opts()) -> ok.
connection_process(Parent, Ref, Transport, Opts) ->
ProxyInfo = case maps:get(proxy_header, Opts, false) of
true ->
{ok, ProxyInfo0} = ranch:recv_proxy_header(Ref, 1000),
ProxyInfo0;
false ->
undefined
end,
{ok, Socket} = ranch:handshake(Ref),
%% Use cowboy_http2 directly only when 'http' is missing.
%% Otherwise switch to cowboy_http2 from cowboy_http.
%%
%% @todo Extend this option to cowboy_tls and allow disabling
%% the switch to cowboy_http2 in cowboy_http. Also document it.
Protocol = case maps:get(protocols, Opts, [http2, http]) of
[http2] -> cowboy_http2;
[_ | _] -> cowboy_http
end,
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol).
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) ->
_ = case maps:get(connection_type, Opts, supervisor) of
worker -> ok;
supervisor -> process_flag(trap_exit, true)
end,
Protocol:init(Parent, Ref, Socket, Transport, ProxyInfo, Opts).

+ 0
- 221
src/wsSrv/cowboy_clock.erl Vedi File

@ -1,221 +0,0 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% While a gen_server process runs in the background to update
%% the cache of formatted dates every second, all API calls are
%% local and directly read from the ETS cache table, providing
%% fast time and date computations.
-module(cowboy_clock).
-behaviour(gen_server).
%% API.
-export([start_link/0]).
-export([stop/0]).
-export([rfc1123/0]).
-export([rfc1123/1]).
%% gen_server.
-export([init/1]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([terminate/2]).
-export([code_change/3]).
-record(state, {
universaltime = undefined :: undefined | calendar:datetime(),
rfc1123 = <<>> :: binary(),
tref = undefined :: undefined | reference()
}).
%% API.
-spec start_link() -> {ok, pid()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
-spec stop() -> stopped.
stop() ->
gen_server:call(?MODULE, stop).
%% When the ets table doesn't exist, either because of a bug
%% or because Cowboy is being restarted, we perform in a
%% slightly degraded state and build a new timestamp for
%% every request.
-spec rfc1123() -> binary().
rfc1123() ->
try
ets:lookup_element(?MODULE, rfc1123, 2)
catch error:badarg ->
rfc1123(erlang:universaltime())
end.
-spec rfc1123(calendar:datetime()) -> binary().
rfc1123(DateTime) ->
update_rfc1123(<<>>, undefined, DateTime).
%% gen_server.
-spec init([]) -> {ok, #state{}}.
init([]) ->
?MODULE = ets:new(?MODULE, [set, protected,
named_table, {read_concurrency, true}]),
T = erlang:universaltime(),
B = update_rfc1123(<<>>, undefined, T),
TRef = erlang:send_after(1000, self(), update),
ets:insert(?MODULE, {rfc1123, B}),
{ok, #state{universaltime = T, rfc1123 = B, tref = TRef}}.
-type from() :: {pid(), term()}.
-spec handle_call
(stop, from(), State) -> {stop, normal, stopped, State}
when State :: #state{}.
handle_call(stop, _From, State) ->
{stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
{reply, ignored, State}.
-spec handle_cast(_, State) -> {noreply, State} when State :: #state{}.
handle_cast(_Msg, State) ->
{noreply, State}.
-spec handle_info(any(), State) -> {noreply, State} when State :: #state{}.
handle_info(update, #state{universaltime = Prev, rfc1123 = B1, tref = TRef0}) ->
%% Cancel the timer in case an external process sent an update message.
_ = erlang:cancel_timer(TRef0),
T = erlang:universaltime(),
B2 = update_rfc1123(B1, Prev, T),
ets:insert(?MODULE, {rfc1123, B2}),
TRef = erlang:send_after(1000, self(), update),
{noreply, #state{universaltime = T, rfc1123 = B2, tref = TRef}};
handle_info(_Info, State) ->
{noreply, State}.
-spec terminate(_, _) -> ok.
terminate(_Reason, _State) ->
ok.
-spec code_change(_, State, _) -> {ok, State} when State :: #state{}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% Internal.
-spec update_rfc1123(binary(), undefined | calendar:datetime(),
calendar:datetime()) -> binary().
update_rfc1123(Bin, Now, Now) ->
Bin;
update_rfc1123(<<Keep:23/binary, _/bits>>,
{Date, {H, M, _}}, {Date, {H, M, S}}) ->
<<Keep/binary, (pad_int(S))/binary, " GMT">>;
update_rfc1123(<<Keep:20/binary, _/bits>>,
{Date, {H, _, _}}, {Date, {H, M, S}}) ->
<<Keep/binary, (pad_int(M))/binary, $:, (pad_int(S))/binary, " GMT">>;
update_rfc1123(<<Keep:17/binary, _/bits>>, {Date, _}, {Date, {H, M, S}}) ->
<<Keep/binary, (pad_int(H))/binary, $:, (pad_int(M))/binary,
$:, (pad_int(S))/binary, " GMT">>;
update_rfc1123(<<_:7/binary, Keep:10/binary, _/bits>>,
{{Y, Mo, _}, _}, {Date = {Y, Mo, D}, {H, M, S}}) ->
Wday = calendar:day_of_the_week(Date),
<<(weekday(Wday))/binary, ", ", (pad_int(D))/binary, Keep/binary,
(pad_int(H))/binary, $:, (pad_int(M))/binary,
$:, (pad_int(S))/binary, " GMT">>;
update_rfc1123(<<_:11/binary, Keep:6/binary, _/bits>>,
{{Y, _, _}, _}, {Date = {Y, Mo, D}, {H, M, S}}) ->
Wday = calendar:day_of_the_week(Date),
<<(weekday(Wday))/binary, ", ", (pad_int(D))/binary, " ",
(month(Mo))/binary, Keep/binary,
(pad_int(H))/binary, $:, (pad_int(M))/binary,
$:, (pad_int(S))/binary, " GMT">>;
update_rfc1123(_, _, {Date = {Y, Mo, D}, {H, M, S}}) ->
Wday = calendar:day_of_the_week(Date),
<<(weekday(Wday))/binary, ", ", (pad_int(D))/binary, " ",
(month(Mo))/binary, " ", (integer_to_binary(Y))/binary,
" ", (pad_int(H))/binary, $:, (pad_int(M))/binary,
$:, (pad_int(S))/binary, " GMT">>.
%% Following suggestion by MononcQc on #erlounge.
-spec pad_int(0..59) -> binary().
pad_int(X) when X < 10 ->
<<$0, ($0 + X)>>;
pad_int(X) ->
integer_to_binary(X).
-spec weekday(1..7) -> <<_:24>>.
weekday(1) -> <<"Mon">>;
weekday(2) -> <<"Tue">>;
weekday(3) -> <<"Wed">>;
weekday(4) -> <<"Thu">>;
weekday(5) -> <<"Fri">>;
weekday(6) -> <<"Sat">>;
weekday(7) -> <<"Sun">>.
-spec month(1..12) -> <<_:24>>.
month(1) -> <<"Jan">>;
month(2) -> <<"Feb">>;
month(3) -> <<"Mar">>;
month(4) -> <<"Apr">>;
month(5) -> <<"May">>;
month(6) -> <<"Jun">>;
month(7) -> <<"Jul">>;
month(8) -> <<"Aug">>;
month(9) -> <<"Sep">>;
month(10) -> <<"Oct">>;
month(11) -> <<"Nov">>;
month(12) -> <<"Dec">>.
%% Tests.
-ifdef(TEST).
update_rfc1123_test_() ->
Tests = [
{<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined,
{{2011, 5, 14}, {14, 25, 33}}, <<>>},
{<<"Sat, 14 May 2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}},
{{2011, 5, 14}, {14, 25, 33}}, <<"Sat, 14 May 2011 14:25:33 GMT">>},
{<<"Sat, 14 May 2011 14:25:34 GMT">>, {{2011, 5, 14}, {14, 25, 33}},
{{2011, 5, 14}, {14, 25, 34}}, <<"Sat, 14 May 2011 14:25:33 GMT">>},
{<<"Sat, 14 May 2011 14:26:00 GMT">>, {{2011, 5, 14}, {14, 25, 59}},
{{2011, 5, 14}, {14, 26, 0}}, <<"Sat, 14 May 2011 14:25:59 GMT">>},
{<<"Sat, 14 May 2011 15:00:00 GMT">>, {{2011, 5, 14}, {14, 59, 59}},
{{2011, 5, 14}, {15, 0, 0}}, <<"Sat, 14 May 2011 14:59:59 GMT">>},
{<<"Sun, 15 May 2011 00:00:00 GMT">>, {{2011, 5, 14}, {23, 59, 59}},
{{2011, 5, 15}, {0, 0, 0}}, <<"Sat, 14 May 2011 23:59:59 GMT">>},
{<<"Wed, 01 Jun 2011 00:00:00 GMT">>, {{2011, 5, 31}, {23, 59, 59}},
{{2011, 6, 1}, {0, 0, 0}}, <<"Tue, 31 May 2011 23:59:59 GMT">>},
{<<"Sun, 01 Jan 2012 00:00:00 GMT">>, {{2011, 5, 31}, {23, 59, 59}},
{{2012, 1, 1}, {0, 0, 0}}, <<"Sat, 31 Dec 2011 23:59:59 GMT">>}
],
[{R, fun() -> R = update_rfc1123(B, P, N) end} || {R, P, N, B} <- Tests].
pad_int_test_() ->
Tests = [
{0, <<"00">>}, {1, <<"01">>}, {2, <<"02">>}, {3, <<"03">>},
{4, <<"04">>}, {5, <<"05">>}, {6, <<"06">>}, {7, <<"07">>},
{8, <<"08">>}, {9, <<"09">>}, {10, <<"10">>}, {11, <<"11">>},
{12, <<"12">>}, {13, <<"13">>}, {14, <<"14">>}, {15, <<"15">>},
{16, <<"16">>}, {17, <<"17">>}, {18, <<"18">>}, {19, <<"19">>},
{20, <<"20">>}, {21, <<"21">>}, {22, <<"22">>}, {23, <<"23">>},
{24, <<"24">>}, {25, <<"25">>}, {26, <<"26">>}, {27, <<"27">>},
{28, <<"28">>}, {29, <<"29">>}, {30, <<"30">>}, {31, <<"31">>},
{32, <<"32">>}, {33, <<"33">>}, {34, <<"34">>}, {35, <<"35">>},
{36, <<"36">>}, {37, <<"37">>}, {38, <<"38">>}, {39, <<"39">>},
{40, <<"40">>}, {41, <<"41">>}, {42, <<"42">>}, {43, <<"43">>},
{44, <<"44">>}, {45, <<"45">>}, {46, <<"46">>}, {47, <<"47">>},
{48, <<"48">>}, {49, <<"49">>}, {50, <<"50">>}, {51, <<"51">>},
{52, <<"52">>}, {53, <<"53">>}, {54, <<"54">>}, {55, <<"55">>},
{56, <<"56">>}, {57, <<"57">>}, {58, <<"58">>}, {59, <<"59">>}
],
[{I, fun() -> O = pad_int(I) end} || {I, O} <- Tests].
-endif.

+ 0
- 249
src/wsSrv/cowboy_compress_h.erl Vedi File

@ -1,249 +0,0 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_compress_h).
-behavior(cowboy_stream).
-export([init/3]).
-export([data/4]).
-export([info/3]).
-export([terminate/3]).
-export([early_error/5]).
-record(state, {
next :: any(),
threshold :: non_neg_integer() | undefined,
compress = undefined :: undefined | gzip,
deflate = undefined :: undefined | zlib:zstream(),
deflate_flush = sync :: none | sync
}).
-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
-> {cowboy_stream:commands(), #state{}}.
init(StreamID, Req, Opts) ->
State0 = check_req(Req),
CompressThreshold = maps:get(compress_threshold, Opts, 300),
DeflateFlush = buffering_to_zflush(maps:get(compress_buffering, Opts, false)),
{Commands0, Next} = cowboy_stream:init(StreamID, Req, Opts),
fold(Commands0, State0#state{next = Next,
threshold = CompressThreshold,
deflate_flush = DeflateFlush}).
-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
-> {cowboy_stream:commands(), State} when State :: #state{}.
data(StreamID, IsFin, Data, State0 = #state{next = Next0}) ->
{Commands0, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),
fold(Commands0, State0#state{next = Next}).
-spec info(cowboy_stream:streamid(), any(), State)
-> {cowboy_stream:commands(), State} when State :: #state{}.
info(StreamID, Info, State0 = #state{next = Next0}) ->
{Commands0, Next} = cowboy_stream:info(StreamID, Info, Next0),
fold(Commands0, State0#state{next = Next}).
-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any().
terminate(StreamID, Reason, #state{next = Next, deflate = Z}) ->
%% Clean the zlib:stream() in case something went wrong.
%% In the normal scenario the stream is already closed.
case Z of
undefined -> ok;
_ -> zlib:close(Z)
end,
cowboy_stream:terminate(StreamID, Reason, Next).
-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
when Resp :: cowboy_stream:resp_command().
early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).
%% Internal.
%% Check if the client supports decoding of gzip responses.
%%
%% A malformed accept-encoding header is ignored (no compression).
check_req(Req) ->
try cowboy_req:parse_header(<<"accept-encoding">>, Req) of
%% Client doesn't support any compression algorithm.
undefined ->
#state{compress = undefined};
Encodings ->
%% We only support gzip so look for it specifically.
%% @todo A recipient SHOULD consider "x-gzip" to be
%% equivalent to "gzip". (RFC7230 4.2.3)
case [E || E = {<<"gzip">>, Q} <- Encodings, Q =/= 0] of
[] ->
#state{compress = undefined};
_ ->
#state{compress = gzip}
end
catch
_:_ ->
#state{compress = undefined}
end.
%% Do not compress responses that contain the content-encoding header.
check_resp_headers(#{<<"content-encoding">> := _}, State) ->
State#state{compress = undefined};
check_resp_headers(_, State) ->
State.
fold(Commands, State = #state{compress = undefined}) ->
{Commands, State};
fold(Commands, State) ->
fold(Commands, State, []).
fold([], State, Acc) ->
{lists:reverse(Acc), State};
%% We do not compress full sendfile bodies.
fold([Response = {response, _, _, {sendfile, _, _, _}} | Tail], State, Acc) ->
fold(Tail, State, [Response | Acc]);
%% We compress full responses directly, unless they are lower than
%% the configured threshold or we find we are not able to by looking at the headers.
fold([Response0 = {response, _, Headers, Body} | Tail],
State0 = #state{threshold = CompressThreshold}, Acc) ->
case check_resp_headers(Headers, State0) of
State = #state{compress = undefined} ->
fold(Tail, State, [Response0 | Acc]);
State1 ->
BodyLength = iolist_size(Body),
if
BodyLength =< CompressThreshold ->
fold(Tail, State1, [Response0 | Acc]);
true ->
{Response, State} = gzip_response(Response0, State1),
fold(Tail, State, [Response | Acc])
end
end;
%% Check headers and initiate compression...
fold([Response0 = {headers, _, Headers} | Tail], State0, Acc) ->
case check_resp_headers(Headers, State0) of
State = #state{compress = undefined} ->
fold(Tail, State, [Response0 | Acc]);
State1 ->
{Response, State} = gzip_headers(Response0, State1),
fold(Tail, State, [Response | Acc])
end;
%% then compress each data commands individually.
fold([Data0 = {data, _, _} | Tail], State0 = #state{compress = gzip}, Acc) ->
{Data, State} = gzip_data(Data0, State0),
fold(Tail, State, [Data | Acc]);
%% When trailers are sent we need to end the compression.
%% This results in an extra data command being sent.
fold([Trailers = {trailers, _} | Tail], State0 = #state{compress = gzip}, Acc) ->
{{data, fin, Data}, State} = gzip_data({data, fin, <<>>}, State0),
fold(Tail, State, [Trailers, {data, nofin, Data} | Acc]);
%% All the options from this handler can be updated for the current stream.
%% The set_options command must be propagated as-is regardless.
fold([SetOptions = {set_options, Opts} | Tail], State = #state{
threshold = CompressThreshold0, deflate_flush = DeflateFlush0}, Acc) ->
CompressThreshold = maps:get(compress_threshold, Opts, CompressThreshold0),
DeflateFlush = case Opts of
#{compress_buffering := CompressBuffering} ->
buffering_to_zflush(CompressBuffering);
_ ->
DeflateFlush0
end,
fold(Tail, State#state{threshold = CompressThreshold, deflate_flush = DeflateFlush},
[SetOptions | Acc]);
%% Otherwise, we have an unrelated command or compression is disabled.
fold([Command | Tail], State, Acc) ->
fold(Tail, State, [Command | Acc]).
buffering_to_zflush(true) -> none;
buffering_to_zflush(false) -> sync.
gzip_response({response, Status, Headers, Body}, State) ->
%% We can't call zlib:gzip/1 because it does an
%% iolist_to_binary(GzBody) at the end to return
%% a binary(). Therefore the code here is largely
%% a duplicate of the code of that function.
Z = zlib:open(),
GzBody = try
%% 31 = 16+?MAX_WBITS from zlib.erl
%% @todo It might be good to allow them to be configured?
zlib:deflateInit(Z, default, deflated, 31, 8, default),
Gz = zlib:deflate(Z, Body, finish),
zlib:deflateEnd(Z),
Gz
after
zlib:close(Z)
end,
{{response, Status, vary(Headers#{
<<"content-length">> => integer_to_binary(iolist_size(GzBody)),
<<"content-encoding">> => <<"gzip">>
}), GzBody}, State}.
gzip_headers({headers, Status, Headers0}, State) ->
Z = zlib:open(),
%% We use the same arguments as when compressing the body fully.
%% @todo It might be good to allow them to be configured?
zlib:deflateInit(Z, default, deflated, 31, 8, default),
Headers = maps:remove(<<"content-length">>, Headers0),
{{headers, Status, vary(Headers#{
<<"content-encoding">> => <<"gzip">>
})}, State#state{deflate = Z}}.
%% We must add content-encoding to vary if it's not already there.
vary(Headers = #{<<"vary">> := Vary}) ->
try cow_http_hd:parse_vary(iolist_to_binary(Vary)) of
'*' -> Headers;
List ->
case lists:member(<<"accept-encoding">>, List) of
true -> Headers;
false -> Headers#{<<"vary">> => [Vary, <<", accept-encoding">>]}
end
catch _:_ ->
%% The vary header is invalid. Probably empty. We replace it with ours.
Headers#{<<"vary">> => <<"accept-encoding">>}
end;
vary(Headers) ->
Headers#{<<"vary">> => <<"accept-encoding">>}.
%% It is not possible to combine zlib and the sendfile
%% syscall as far as I can tell, because the zlib format
%% includes a checksum at the end of the stream. We have
%% to read the file in memory, making this not suitable for
%% large files.
gzip_data({data, nofin, Sendfile = {sendfile, _, _, _}},
State = #state{deflate = Z, deflate_flush = Flush}) ->
{ok, Data0} = read_file(Sendfile),
Data = zlib:deflate(Z, Data0, Flush),
{{data, nofin, Data}, State};
gzip_data({data, fin, Sendfile = {sendfile, _, _, _}}, State = #state{deflate = Z}) ->
{ok, Data0} = read_file(Sendfile),
Data = zlib:deflate(Z, Data0, finish),
zlib:deflateEnd(Z),
zlib:close(Z),
{{data, fin, Data}, State#state{deflate = undefined}};
gzip_data({data, nofin, Data0}, State = #state{deflate = Z, deflate_flush = Flush}) ->
Data = zlib:deflate(Z, Data0, Flush),
{{data, nofin, Data}, State};
gzip_data({data, fin, Data0}, State = #state{deflate = Z}) ->
Data = zlib:deflate(Z, Data0, finish),
zlib:deflateEnd(Z),
zlib:close(Z),
{{data, fin, Data}, State#state{deflate = undefined}}.
read_file({sendfile, Offset, Bytes, Path}) ->
{ok, IoDevice} = file:open(Path, [read, raw, binary]),
try
_ = case Offset of
0 -> ok;
_ -> file:position(IoDevice, {bof, Offset})
end,
file:read(IoDevice, Bytes)
after
file:close(IoDevice)
end.

+ 0
- 174
src/wsSrv/cowboy_constraints.erl Vedi File

@ -1,174 +0,0 @@
%% Copyright (c) 2014-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_constraints).
-export([validate/2]).
-export([reverse/2]).
-export([format_error/1]).
-type constraint() :: int | nonempty | fun().
-export_type([constraint/0]).
-type reason() :: {constraint(), any(), any()}.
-export_type([reason/0]).
-spec validate(binary(), constraint() | [constraint()])
-> {ok, any()} | {error, reason()}.
validate(Value, Constraints) when is_list(Constraints) ->
apply_list(forward, Value, Constraints);
validate(Value, Constraint) ->
apply_list(forward, Value, [Constraint]).
-spec reverse(any(), constraint() | [constraint()])
-> {ok, binary()} | {error, reason()}.
reverse(Value, Constraints) when is_list(Constraints) ->
apply_list(reverse, Value, Constraints);
reverse(Value, Constraint) ->
apply_list(reverse, Value, [Constraint]).
-spec format_error(reason()) -> iodata().
format_error({Constraint, Reason, Value}) ->
apply_constraint(format_error, {Reason, Value}, Constraint).
apply_list(_, Value, []) ->
{ok, Value};
apply_list(Type, Value0, [Constraint | Tail]) ->
case apply_constraint(Type, Value0, Constraint) of
{ok, Value} ->
apply_list(Type, Value, Tail);
{error, Reason} ->
{error, {Constraint, Reason, Value0}}
end.
%% @todo {int, From, To}, etc.
apply_constraint(Type, Value, int) ->
int(Type, Value);
apply_constraint(Type, Value, nonempty) ->
nonempty(Type, Value);
apply_constraint(Type, Value, F) when is_function(F) ->
F(Type, Value).
%% Constraint functions.
int(forward, Value) ->
try
{ok, binary_to_integer(Value)}
catch _:_ ->
{error, not_an_integer}
end;
int(reverse, Value) ->
try
{ok, integer_to_binary(Value)}
catch _:_ ->
{error, not_an_integer}
end;
int(format_error, {not_an_integer, Value}) ->
io_lib:format("The value ~p is not an integer.", [Value]).
nonempty(Type, <<>>) when Type =/= format_error ->
{error, empty};
nonempty(Type, Value) when Type =/= format_error, is_binary(Value) ->
{ok, Value};
nonempty(format_error, {empty, Value}) ->
io_lib:format("The value ~p is empty.", [Value]).
-ifdef(TEST).
validate_test() ->
F = fun(_, Value) ->
try
{ok, binary_to_atom(Value, latin1)}
catch _:_ ->
{error, not_a_binary}
end
end,
%% Value, Constraints, Result.
Tests = [
{<<>>, [], <<>>},
{<<"123">>, int, 123},
{<<"123">>, [int], 123},
{<<"123">>, [nonempty, int], 123},
{<<"123">>, [int, nonempty], 123},
{<<>>, nonempty, error},
{<<>>, [nonempty], error},
{<<"hello">>, F, hello},
{<<"hello">>, [F], hello},
{<<"123">>, [F, int], error},
{<<"123">>, [int, F], error},
{<<"hello">>, [nonempty, F], hello},
{<<"hello">>, [F, nonempty], hello}
],
[{lists:flatten(io_lib:format("~p, ~p", [V, C])), fun() ->
case R of
error -> {error, _} = validate(V, C);
_ -> {ok, R} = validate(V, C)
end
end} || {V, C, R} <- Tests].
reverse_test() ->
F = fun(_, Value) ->
try
{ok, atom_to_binary(Value, latin1)}
catch _:_ ->
{error, not_an_atom}
end
end,
%% Value, Constraints, Result.
Tests = [
{<<>>, [], <<>>},
{123, int, <<"123">>},
{123, [int], <<"123">>},
{123, [nonempty, int], <<"123">>},
{123, [int, nonempty], <<"123">>},
{<<>>, nonempty, error},
{<<>>, [nonempty], error},
{hello, F, <<"hello">>},
{hello, [F], <<"hello">>},
{123, [F, int], error},
{123, [int, F], error},
{hello, [nonempty, F], <<"hello">>},
{hello, [F, nonempty], <<"hello">>}
],
[{lists:flatten(io_lib:format("~p, ~p", [V, C])), fun() ->
case R of
error -> {error, _} = reverse(V, C);
_ -> {ok, R} = reverse(V, C)
end
end} || {V, C, R} <- Tests].
int_format_error_test() ->
{error, Reason} = validate(<<"string">>, int),
Bin = iolist_to_binary(format_error(Reason)),
true = is_binary(Bin),
ok.
nonempty_format_error_test() ->
{error, Reason} = validate(<<>>, nonempty),
Bin = iolist_to_binary(format_error(Reason)),
true = is_binary(Bin),
ok.
fun_format_error_test() ->
F = fun
(format_error, {test, <<"value">>}) ->
formatted;
(_, _) ->
{error, test}
end,
{error, Reason} = validate(<<"value">>, F),
formatted = format_error(Reason),
ok.
-endif.

+ 0
- 57
src/wsSrv/cowboy_handler.erl Vedi File

@ -1,57 +0,0 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% Handler middleware.
%%
%% Execute the handler given by the <em>handler</em> and <em>handler_opts</em>
%% environment values. The result of this execution is added to the
%% environment under the <em>result</em> value.
-module(cowboy_handler).
-behaviour(cowboy_middleware).
-export([execute/2]).
-export([terminate/4]).
-callback init(Req, any())
-> {ok | module(), Req, any()}
| {module(), Req, any(), any()}
when Req :: cowboy_req:req().
-callback terminate(any(), map(), any()) -> ok.
-optional_callbacks([terminate/3]).
-spec execute(Req, Env) -> {ok, Req, Env}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
execute(Req, Env = #{handler := Handler, handler_opts := HandlerOpts}) ->
try Handler:init(Req, HandlerOpts) of
{ok, Req2, State} ->
Result = terminate(normal, Req2, State, Handler),
{ok, Req2, Env#{result => Result}};
{Mod, Req2, State} ->
Mod:upgrade(Req2, Env, Handler, State);
{Mod, Req2, State, Opts} ->
Mod:upgrade(Req2, Env, Handler, State, Opts)
catch Class:Reason:Stacktrace ->
terminate({crash, Class, Reason}, Req, HandlerOpts, Handler),
erlang:raise(Class, Reason, Stacktrace)
end.
-spec terminate(any(), Req | undefined, any(), module()) -> ok when Req :: cowboy_req:req().
terminate(Reason, Req, State, Handler) ->
case erlang:function_exported(Handler, terminate, 3) of
true ->
Handler:terminate(Reason, Req, State);
false ->
ok
end.

+ 0
- 1523
src/wsSrv/cowboy_http.erl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 1221
src/wsSrv/cowboy_http2.erl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 108
src/wsSrv/cowboy_loop.erl Vedi File

@ -1,108 +0,0 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_loop).
-behaviour(cowboy_sub_protocol).
-export([upgrade/4]).
-export([upgrade/5]).
-export([loop/4]).
-export([system_continue/3]).
-export([system_terminate/4]).
-export([system_code_change/4]).
-callback init(Req, any())
-> {ok | module(), Req, any()}
| {module(), Req, any(), any()}
when Req :: cowboy_req:req().
-callback info(any(), Req, State)
-> {ok, Req, State}
| {ok, Req, State, hibernate}
| {stop, Req, State}
when Req :: cowboy_req:req(), State :: any().
-callback terminate(any(), cowboy_req:req(), any()) -> ok.
-optional_callbacks([terminate/3]).
-spec upgrade(Req, Env, module(), any())
-> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState) ->
loop(Req, Env, Handler, HandlerState).
-spec upgrade(Req, Env, module(), any(), hibernate)
-> {suspend, ?MODULE, loop, [any()]}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState, hibernate) ->
suspend(Req, Env, Handler, HandlerState).
-spec loop(Req, Env, module(), any())
-> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
%% @todo Handle system messages.
loop(Req = #{pid := Parent}, Env, Handler, HandlerState) ->
receive
%% System messages.
{'EXIT', Parent, Reason} ->
terminate(Req, Env, Handler, HandlerState, Reason);
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
{Req, Env, Handler, HandlerState});
%% Calls from supervisor module.
{'$gen_call', From, Call} ->
cowboy_children:handle_supervisor_call(Call, From, [], ?MODULE),
loop(Req, Env, Handler, HandlerState);
Message ->
call(Req, Env, Handler, HandlerState, Message)
end.
call(Req0, Env, Handler, HandlerState0, Message) ->
try Handler:info(Message, Req0, HandlerState0) of
{ok, Req, HandlerState} ->
loop(Req, Env, Handler, HandlerState);
{ok, Req, HandlerState, hibernate} ->
suspend(Req, Env, Handler, HandlerState);
{stop, Req, HandlerState} ->
terminate(Req, Env, Handler, HandlerState, stop)
catch Class:Reason:Stacktrace ->
cowboy_handler:terminate({crash, Class, Reason}, Req0, HandlerState0, Handler),
erlang:raise(Class, Reason, Stacktrace)
end.
suspend(Req, Env, Handler, HandlerState) ->
{suspend, ?MODULE, loop, [Req, Env, Handler, HandlerState]}.
terminate(Req, Env, Handler, HandlerState, Reason) ->
Result = cowboy_handler:terminate(Reason, Req, HandlerState, Handler),
{ok, Req, Env#{result => Result}}.
%% System callbacks.
-spec system_continue(_, _, {Req, Env, module(), any()})
-> {ok, Req, Env} | {suspend, ?MODULE, loop, [any()]}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
system_continue(_, _, {Req, Env, Handler, HandlerState}) ->
loop(Req, Env, Handler, HandlerState).
-spec system_terminate(any(), _, _, {Req, Env, module(), any()})
-> {ok, Req, Env} when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
system_terminate(Reason, _, _, {Req, Env, Handler, HandlerState}) ->
terminate(Req, Env, Handler, HandlerState, Reason).
-spec system_code_change(Misc, _, _, _) -> {ok, Misc}
when Misc :: {cowboy_req:req(), cowboy_middleware:env(), module(), any()}.
system_code_change(Misc, _, _, _) ->
{ok, Misc}.

+ 0
- 331
src/wsSrv/cowboy_metrics_h.erl Vedi File

@ -1,331 +0,0 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_metrics_h).
-behavior(cowboy_stream).
-export([init/3]).
-export([data/4]).
-export([info/3]).
-export([terminate/3]).
-export([early_error/5]).
-type proc_metrics() :: #{pid() => #{
%% Time at which the process spawned.
spawn := integer(),
%% Time at which the process exited.
exit => integer(),
%% Reason for the process exit.
reason => any()
}}.
-type informational_metrics() :: #{
%% Informational response status.
status := cowboy:http_status(),
%% Headers sent with the informational response.
headers := cowboy:http_headers(),
%% Time when the informational response was sent.
time := integer()
}.
-type metrics() :: #{
%% The identifier for this listener.
ref := ranch:ref(),
%% The pid for this connection.
pid := pid(),
%% The streamid also indicates the total number of requests on
%% this connection (StreamID div 2 + 1).
streamid := cowboy_stream:streamid(),
%% The terminate reason is always useful.
reason := cowboy_stream:reason(),
%% A filtered Req object or a partial Req object
%% depending on how far the request got to.
req => cowboy_req:req(),
partial_req => cowboy_stream:partial_req(),
%% Response status.
resp_status := cowboy:http_status(),
%% Filtered response headers.
resp_headers := cowboy:http_headers(),
%% Start/end of the processing of the request.
%%
%% This represents the time from this stream handler's init
%% to terminate.
req_start => integer(),
req_end => integer(),
%% Start/end of the receiving of the request body.
%% Begins when the first packet has been received.
req_body_start => integer(),
req_body_end => integer(),
%% Start/end of the sending of the response.
%% Begins when we send the headers and ends on the final
%% packet of the response body. If everything is sent at
%% once these values are identical.
resp_start => integer(),
resp_end => integer(),
%% For early errors all we get is the time we received it.
early_error_time => integer(),
%% Start/end of spawned processes. This is where most of
%% the user code lies, excluding stream handlers. On a
%% default Cowboy configuration there should be only one
%% process: the request process.
procs => proc_metrics(),
%% Informational responses sent before the final response.
informational => [informational_metrics()],
%% Length of the request and response bodies. This does
%% not include the framing.
req_body_length => non_neg_integer(),
resp_body_length => non_neg_integer(),
%% Additional metadata set by the user.
user_data => map()
}.
-export_type([metrics/0]).
-type metrics_callback() :: fun((metrics()) -> any()).
-export_type([metrics_callback/0]).
-record(state, {
next :: any(),
callback :: fun((metrics()) -> any()),
resp_headers_filter :: undefined | fun((cowboy:http_headers()) -> cowboy:http_headers()),
req :: map(),
resp_status :: undefined | cowboy:http_status(),
resp_headers :: undefined | cowboy:http_headers(),
ref :: ranch:ref(),
req_start :: integer(),
req_end :: undefined | integer(),
req_body_start :: undefined | integer(),
req_body_end :: undefined | integer(),
resp_start :: undefined | integer(),
resp_end :: undefined | integer(),
procs = #{} :: proc_metrics(),
informational = [] :: [informational_metrics()],
req_body_length = 0 :: non_neg_integer(),
resp_body_length = 0 :: non_neg_integer(),
user_data = #{} :: map()
}).
-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
-> {[{spawn, pid(), timeout()}], #state{}}.
init(StreamID, Req = #{ref := Ref}, Opts = #{metrics_callback := Fun}) ->
ReqStart = erlang:monotonic_time(),
{Commands, Next} = cowboy_stream:init(StreamID, Req, Opts),
FilteredReq = case maps:get(metrics_req_filter, Opts, undefined) of
undefined -> Req;
ReqFilter -> ReqFilter(Req)
end,
RespHeadersFilter = maps:get(metrics_resp_headers_filter, Opts, undefined),
{Commands, fold(Commands, #state{
next = Next,
callback = Fun,
resp_headers_filter = RespHeadersFilter,
req = FilteredReq,
ref = Ref,
req_start = ReqStart
})}.
-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
-> {cowboy_stream:commands(), State} when State :: #state{}.
data(StreamID, IsFin = fin, Data, State = #state{req_body_start = undefined}) ->
ReqBody = erlang:monotonic_time(),
do_data(StreamID, IsFin, Data, State#state{
req_body_start = ReqBody,
req_body_end = ReqBody,
req_body_length = byte_size(Data)
});
data(StreamID, IsFin = fin, Data, State = #state{req_body_length = ReqBodyLen}) ->
ReqBodyEnd = erlang:monotonic_time(),
do_data(StreamID, IsFin, Data, State#state{
req_body_end = ReqBodyEnd,
req_body_length = ReqBodyLen + byte_size(Data)
});
data(StreamID, IsFin, Data, State = #state{req_body_start = undefined}) ->
ReqBodyStart = erlang:monotonic_time(),
do_data(StreamID, IsFin, Data, State#state{
req_body_start = ReqBodyStart,
req_body_length = byte_size(Data)
});
data(StreamID, IsFin, Data, State = #state{req_body_length = ReqBodyLen}) ->
do_data(StreamID, IsFin, Data, State#state{
req_body_length = ReqBodyLen + byte_size(Data)
}).
do_data(StreamID, IsFin, Data, State0 = #state{next = Next0}) ->
{Commands, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),
{Commands, fold(Commands, State0#state{next = Next})}.
-spec info(cowboy_stream:streamid(), any(), State)
-> {cowboy_stream:commands(), State} when State :: #state{}.
info(StreamID, Info = {'EXIT', Pid, Reason}, State0 = #state{procs = Procs}) ->
ProcEnd = erlang:monotonic_time(),
P = maps:get(Pid, Procs),
State = State0#state{procs = Procs#{Pid => P#{
exit => ProcEnd,
reason => Reason
}}},
do_info(StreamID, Info, State);
info(StreamID, Info, State) ->
do_info(StreamID, Info, State).
do_info(StreamID, Info, State0 = #state{next = Next0}) ->
{Commands, Next} = cowboy_stream:info(StreamID, Info, Next0),
{Commands, fold(Commands, State0#state{next = Next})}.
fold([], State) ->
State;
fold([{spawn, Pid, _} | Tail], State0 = #state{procs = Procs}) ->
ProcStart = erlang:monotonic_time(),
State = State0#state{procs = Procs#{Pid => #{spawn => ProcStart}}},
fold(Tail, State);
fold([{inform, Status, Headers} | Tail],
State = #state{informational = Infos}) ->
Time = erlang:monotonic_time(),
fold(Tail, State#state{informational = [#{
status => Status,
headers => Headers,
time => Time
} | Infos]});
fold([{response, Status, Headers, Body} | Tail],
State = #state{resp_headers_filter = RespHeadersFilter}) ->
Resp = erlang:monotonic_time(),
fold(Tail, State#state{
resp_status = Status,
resp_headers = case RespHeadersFilter of
undefined -> Headers;
_ -> RespHeadersFilter(Headers)
end,
resp_start = Resp,
resp_end = Resp,
resp_body_length = resp_body_length(Body)
});
fold([{error_response, Status, Headers, Body} | Tail],
State = #state{resp_status = RespStatus}) ->
%% The error_response command only results in a response
%% if no response was sent before.
case RespStatus of
undefined ->
fold([{response, Status, Headers, Body} | Tail], State);
_ ->
fold(Tail, State)
end;
fold([{headers, Status, Headers} | Tail],
State = #state{resp_headers_filter = RespHeadersFilter}) ->
RespStart = erlang:monotonic_time(),
fold(Tail, State#state{
resp_status = Status,
resp_headers = case RespHeadersFilter of
undefined -> Headers;
_ -> RespHeadersFilter(Headers)
end,
resp_start = RespStart
});
%% @todo It might be worthwhile to keep the sendfile information around,
%% especially if these frames ultimately result in a sendfile syscall.
fold([{data, nofin, Data} | Tail], State = #state{resp_body_length = RespBodyLen}) ->
fold(Tail, State#state{
resp_body_length = RespBodyLen + resp_body_length(Data)
});
fold([{data, fin, Data} | Tail], State = #state{resp_body_length = RespBodyLen}) ->
RespEnd = erlang:monotonic_time(),
fold(Tail, State#state{
resp_end = RespEnd,
resp_body_length = RespBodyLen + resp_body_length(Data)
});
fold([{set_options, SetOpts} | Tail], State0 = #state{user_data = OldUserData}) ->
State = case SetOpts of
#{metrics_user_data := NewUserData} ->
State0#state{user_data = maps:merge(OldUserData, NewUserData)};
_ ->
State0
end,
fold(Tail, State);
fold([_ | Tail], State) ->
fold(Tail, State).
-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> any().
terminate(StreamID, Reason, #state{next = Next, callback = Fun,
req = Req, resp_status = RespStatus, resp_headers = RespHeaders, ref = Ref,
req_start = ReqStart, req_body_start = ReqBodyStart,
req_body_end = ReqBodyEnd, resp_start = RespStart, resp_end = RespEnd,
procs = Procs, informational = Infos, user_data = UserData,
req_body_length = ReqBodyLen, resp_body_length = RespBodyLen}) ->
Res = cowboy_stream:terminate(StreamID, Reason, Next),
ReqEnd = erlang:monotonic_time(),
Metrics = #{
ref => Ref,
pid => self(),
streamid => StreamID,
reason => Reason,
req => Req,
resp_status => RespStatus,
resp_headers => RespHeaders,
req_start => ReqStart,
req_end => ReqEnd,
req_body_start => ReqBodyStart,
req_body_end => ReqBodyEnd,
resp_start => RespStart,
resp_end => RespEnd,
procs => Procs,
informational => lists:reverse(Infos),
req_body_length => ReqBodyLen,
resp_body_length => RespBodyLen,
user_data => UserData
},
Fun(Metrics),
Res.
-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
when Resp :: cowboy_stream:resp_command().
early_error(StreamID, Reason, PartialReq = #{ref := Ref}, Resp0, Opts = #{metrics_callback := Fun}) ->
Time = erlang:monotonic_time(),
Resp = {response, RespStatus, RespHeaders, RespBody}
= cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp0, Opts),
%% As far as metrics go we are limited in what we can provide
%% in this case.
Metrics = #{
ref => Ref,
pid => self(),
streamid => StreamID,
reason => Reason,
partial_req => PartialReq,
resp_status => RespStatus,
resp_headers => RespHeaders,
early_error_time => Time,
resp_body_length => resp_body_length(RespBody)
},
Fun(Metrics),
Resp.
resp_body_length({sendfile, _, Len, _}) ->
Len;
resp_body_length(Data) ->
iolist_size(Data).

+ 0
- 24
src/wsSrv/cowboy_middleware.erl Vedi File

@ -1,24 +0,0 @@
%% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_middleware).
-type env() :: #{atom() => any()}.
-export_type([env/0]).
-callback execute(Req, Env)
-> {ok, Req, Env}
| {suspend, module(), atom(), [any()]}
| {stop, Req}
when Req :: cowboy_req:req(), Env :: env().

+ 0
- 1016
src/wsSrv/cowboy_req.erl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 1637
src/wsSrv/cowboy_rest.erl
File diff soppresso perché troppo grande
Vedi File


+ 0
- 603
src/wsSrv/cowboy_router.erl Vedi File

@ -1,603 +0,0 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% Routing middleware.
%%
%% Resolve the handler to be used for the request based on the
%% routing information found in the <em>dispatch</em> environment value.
%% When found, the handler module and associated data are added to
%% the environment as the <em>handler</em> and <em>handler_opts</em> values
%% respectively.
%%
%% If the route cannot be found, processing stops with either
%% a 400 or a 404 reply.
-module(cowboy_router).
-behaviour(cowboy_middleware).
-export([compile/1]).
-export([execute/2]).
-type bindings() :: #{atom() => any()}.
-type tokens() :: [binary()].
-export_type([bindings/0]).
-export_type([tokens/0]).
-type route_match() :: '_' | iodata().
-type route_path() :: {Path :: route_match(), Handler :: module(), Opts :: any()}
| {Path :: route_match(), cowboy:fields(), Handler :: module(), Opts :: any()}.
-type route_rule() :: {Host :: route_match(), Paths :: [route_path()]}
| {Host :: route_match(), cowboy:fields(), Paths :: [route_path()]}.
-type routes() :: [route_rule()].
-export_type([routes/0]).
-type dispatch_match() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].
-type dispatch_path() :: {dispatch_match(), cowboy:fields(), module(), any()}.
-type dispatch_rule() :: {Host :: dispatch_match(), cowboy:fields(), Paths :: [dispatch_path()]}.
-opaque dispatch_rules() :: [dispatch_rule()].
-export_type([dispatch_rules/0]).
-spec compile(routes()) -> dispatch_rules().
compile(Routes) ->
compile(Routes, []).
compile([], Acc) ->
lists:reverse(Acc);
compile([{Host, Paths} | Tail], Acc) ->
compile([{Host, [], Paths} | Tail], Acc);
compile([{HostMatch, Fields, Paths} | Tail], Acc) ->
HostRules = case HostMatch of
'_' -> '_';
_ -> compile_host(HostMatch)
end,
PathRules = compile_paths(Paths, []),
Hosts = case HostRules of
'_' -> [{'_', Fields, PathRules}];
_ -> [{R, Fields, PathRules} || R <- HostRules]
end,
compile(Tail, Hosts ++ Acc).
compile_host(HostMatch) when is_list(HostMatch) ->
compile_host(list_to_binary(HostMatch));
compile_host(HostMatch) when is_binary(HostMatch) ->
compile_rules(HostMatch, $., [], [], <<>>).
compile_paths([], Acc) ->
lists:reverse(Acc);
compile_paths([{PathMatch, Handler, Opts} | Tail], Acc) ->
compile_paths([{PathMatch, [], Handler, Opts} | Tail], Acc);
compile_paths([{PathMatch, Fields, Handler, Opts} | Tail], Acc)
when is_list(PathMatch) ->
compile_paths([{iolist_to_binary(PathMatch),
Fields, Handler, Opts} | Tail], Acc);
compile_paths([{'_', Fields, Handler, Opts} | Tail], Acc) ->
compile_paths(Tail, [{'_', Fields, Handler, Opts}] ++ Acc);
compile_paths([{<<"*">>, Fields, Handler, Opts} | Tail], Acc) ->
compile_paths(Tail, [{<<"*">>, Fields, Handler, Opts} | Acc]);
compile_paths([{<<$/, PathMatch/bits>>, Fields, Handler, Opts} | Tail],
Acc) ->
PathRules = compile_rules(PathMatch, $/, [], [], <<>>),
Paths = [{lists:reverse(R), Fields, Handler, Opts} || R <- PathRules],
compile_paths(Tail, Paths ++ Acc);
compile_paths([{PathMatch, _, _, _} | _], _) ->
error({badarg, "The following route MUST begin with a slash: "
++ binary_to_list(PathMatch)}).
compile_rules(<<>>, _, Segments, Rules, <<>>) ->
[Segments | Rules];
compile_rules(<<>>, _, Segments, Rules, Acc) ->
[[Acc | Segments] | Rules];
compile_rules(<<S, Rest/bits>>, S, Segments, Rules, <<>>) ->
compile_rules(Rest, S, Segments, Rules, <<>>);
compile_rules(<<S, Rest/bits>>, S, Segments, Rules, Acc) ->
compile_rules(Rest, S, [Acc | Segments], Rules, <<>>);
%% Colon on path segment start is special, otherwise allow.
compile_rules(<<$:, Rest/bits>>, S, Segments, Rules, <<>>) ->
{NameBin, Rest2} = compile_binding(Rest, S, <<>>),
Name = binary_to_atom(NameBin, utf8),
compile_rules(Rest2, S, Segments, Rules, Name);
compile_rules(<<$[, $., $., $., $], Rest/bits>>, S, Segments, Rules, Acc)
when Acc =:= <<>> ->
compile_rules(Rest, S, ['...' | Segments], Rules, Acc);
compile_rules(<<$[, $., $., $., $], Rest/bits>>, S, Segments, Rules, Acc) ->
compile_rules(Rest, S, ['...', Acc | Segments], Rules, Acc);
compile_rules(<<$[, S, Rest/bits>>, S, Segments, Rules, Acc) ->
compile_brackets(Rest, S, [Acc | Segments], Rules);
compile_rules(<<$[, Rest/bits>>, S, Segments, Rules, <<>>) ->
compile_brackets(Rest, S, Segments, Rules);
%% Open bracket in the middle of a segment.
compile_rules(<<$[, _/bits>>, _, _, _, _) ->
error(badarg);
%% Missing an open bracket.
compile_rules(<<$], _/bits>>, _, _, _, _) ->
error(badarg);
compile_rules(<<C, Rest/bits>>, S, Segments, Rules, Acc) ->
compile_rules(Rest, S, Segments, Rules, <<Acc/binary, C>>).
%% Everything past $: until the segment separator ($. for hosts,
%% $/ for paths) or $[ or $] or end of binary is the binding name.
compile_binding(<<>>, _, <<>>) ->
error(badarg);
compile_binding(Rest = <<>>, _, Acc) ->
{Acc, Rest};
compile_binding(Rest = <<C, _/bits>>, S, Acc)
when C =:= S; C =:= $[; C =:= $] ->
{Acc, Rest};
compile_binding(<<C, Rest/bits>>, S, Acc) ->
compile_binding(Rest, S, <<Acc/binary, C>>).
compile_brackets(Rest, S, Segments, Rules) ->
{Bracket, Rest2} = compile_brackets_split(Rest, <<>>, 0),
Rules1 = compile_rules(Rest2, S, Segments, [], <<>>),
Rules2 = compile_rules(<<Bracket/binary, Rest2/binary>>,
S, Segments, [], <<>>),
Rules ++ Rules2 ++ Rules1.
%% Missing a close bracket.
compile_brackets_split(<<>>, _, _) ->
error(badarg);
%% Make sure we don't confuse the closing bracket we're looking for.
compile_brackets_split(<<C, Rest/bits>>, Acc, N) when C =:= $[ ->
compile_brackets_split(Rest, <<Acc/binary, C>>, N + 1);
compile_brackets_split(<<C, Rest/bits>>, Acc, N) when C =:= $], N > 0 ->
compile_brackets_split(Rest, <<Acc/binary, C>>, N - 1);
%% That's the right one.
compile_brackets_split(<<$], Rest/bits>>, Acc, 0) ->
{Acc, Rest};
compile_brackets_split(<<C, Rest/bits>>, Acc, N) ->
compile_brackets_split(Rest, <<Acc/binary, C>>, N).
-spec execute(Req, Env)
-> {ok, Req, Env} | {stop, Req}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
execute(Req = #{host := Host, path := Path}, Env = #{dispatch := Dispatch0}) ->
Dispatch = case Dispatch0 of
{persistent_term, Key} -> persistent_term:get(Key);
_ -> Dispatch0
end,
case match(Dispatch, Host, Path) of
{ok, Handler, HandlerOpts, Bindings, HostInfo, PathInfo} ->
{ok, Req#{
host_info => HostInfo,
path_info => PathInfo,
bindings => Bindings
}, Env#{
handler => Handler,
handler_opts => HandlerOpts
}};
{error, notfound, host} ->
{stop, cowboy_req:reply(400, Req)};
{error, badrequest, path} ->
{stop, cowboy_req:reply(400, Req)};
{error, notfound, path} ->
{stop, cowboy_req:reply(404, Req)}
end.
%% Internal.
%% Match hostname tokens and path tokens against dispatch rules.
%%
%% It is typically used for matching tokens for the hostname and path of
%% the request against a global dispatch rule for your listener.
%%
%% Dispatch rules are a list of <em>{Hostname, PathRules}</em> tuples, with
%% <em>PathRules</em> being a list of <em>{Path, HandlerMod, HandlerOpts}</em>.
%%
%% <em>Hostname</em> and <em>Path</em> are match rules and can be either the
%% atom <em>'_'</em>, which matches everything, `<<"*">>', which match the
%% wildcard path, or a list of tokens.
%%
%% Each token can be either a binary, the atom <em>'_'</em>,
%% the atom '...' or a named atom. A binary token must match exactly,
%% <em>'_'</em> matches everything for a single token, <em>'...'</em> matches
%% everything for the rest of the tokens and a named atom will bind the
%% corresponding token value and return it.
%%
%% The list of hostname tokens is reversed before matching. For example, if
%% we were to match "www.ninenines.eu", we would first match "eu", then
%% "ninenines", then "www". This means that in the context of hostnames,
%% the <em>'...'</em> atom matches properly the lower levels of the domain
%% as would be expected.
%%
%% When a result is found, this function will return the handler module and
%% options found in the dispatch list, a key-value list of bindings and
%% the tokens that were matched by the <em>'...'</em> atom for both the
%% hostname and path.
-spec match(dispatch_rules(), Host :: binary() | tokens(), Path :: binary())
-> {ok, module(), any(), bindings(),
HostInfo :: undefined | tokens(),
PathInfo :: undefined | tokens()}
| {error, notfound, host} | {error, notfound, path}
| {error, badrequest, path}.
match([], _, _) ->
{error, notfound, host};
%% If the host is '_' then there can be no constraints.
match([{'_', [], PathMatchs} | _Tail], _, Path) ->
match_path(PathMatchs, undefined, Path, #{});
match([{HostMatch, Fields, PathMatchs} | Tail], Tokens, Path)
when is_list(Tokens) ->
case list_match(Tokens, HostMatch, #{}) of
false ->
match(Tail, Tokens, Path);
{true, Bindings, HostInfo} ->
HostInfo2 = case HostInfo of
undefined -> undefined;
_ -> lists:reverse(HostInfo)
end,
case check_constraints(Fields, Bindings) of
{ok, Bindings2} ->
match_path(PathMatchs, HostInfo2, Path, Bindings2);
nomatch ->
match(Tail, Tokens, Path)
end
end;
match(Dispatch, Host, Path) ->
match(Dispatch, split_host(Host), Path).
-spec match_path([dispatch_path()],
HostInfo :: undefined | tokens(), binary() | tokens(), bindings())
-> {ok, module(), any(), bindings(),
HostInfo :: undefined | tokens(),
PathInfo :: undefined | tokens()}
| {error, notfound, path} | {error, badrequest, path}.
match_path([], _, _, _) ->
{error, notfound, path};
%% If the path is '_' then there can be no constraints.
match_path([{'_', [], Handler, Opts} | _Tail], HostInfo, _, Bindings) ->
{ok, Handler, Opts, Bindings, HostInfo, undefined};
match_path([{<<"*">>, _, Handler, Opts} | _Tail], HostInfo, <<"*">>, Bindings) ->
{ok, Handler, Opts, Bindings, HostInfo, undefined};
match_path([_ | Tail], HostInfo, <<"*">>, Bindings) ->
match_path(Tail, HostInfo, <<"*">>, Bindings);
match_path([{PathMatch, Fields, Handler, Opts} | Tail], HostInfo, Tokens,
Bindings) when is_list(Tokens) ->
case list_match(Tokens, PathMatch, Bindings) of
false ->
match_path(Tail, HostInfo, Tokens, Bindings);
{true, PathBinds, PathInfo} ->
case check_constraints(Fields, PathBinds) of
{ok, PathBinds2} ->
{ok, Handler, Opts, PathBinds2, HostInfo, PathInfo};
nomatch ->
match_path(Tail, HostInfo, Tokens, Bindings)
end
end;
match_path(_Dispatch, _HostInfo, badrequest, _Bindings) ->
{error, badrequest, path};
match_path(Dispatch, HostInfo, Path, Bindings) ->
match_path(Dispatch, HostInfo, split_path(Path), Bindings).
check_constraints([], Bindings) ->
{ok, Bindings};
check_constraints([Field | Tail], Bindings) when is_atom(Field) ->
check_constraints(Tail, Bindings);
check_constraints([Field | Tail], Bindings) ->
Name = element(1, Field),
case Bindings of
#{Name := Value0} ->
Constraints = element(2, Field),
case cowboy_constraints:validate(Value0, Constraints) of
{ok, Value} ->
check_constraints(Tail, Bindings#{Name => Value});
{error, _} ->
nomatch
end;
_ ->
check_constraints(Tail, Bindings)
end.
-spec split_host(binary()) -> tokens().
split_host(Host) ->
split_host(Host, []).
split_host(Host, Acc) ->
case binary:match(Host, <<".">>) of
nomatch when Host =:= <<>> ->
Acc;
nomatch ->
[Host | Acc];
{Pos, _} ->
<<Segment:Pos/binary, _:8, Rest/bits>> = Host,
false = byte_size(Segment) == 0,
split_host(Rest, [Segment | Acc])
end.
%% Following RFC2396, this function may return path segments containing any
%% character, including <em>/</em> if, and only if, a <em>/</em> was escaped
%% and part of a path segment.
-spec split_path(binary()) -> tokens() | badrequest.
split_path(<<$/, Path/bits>>) ->
split_path(Path, []);
split_path(_) ->
badrequest.
split_path(Path, Acc) ->
try
case binary:match(Path, <<"/">>) of
nomatch when Path =:= <<>> ->
remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- Acc]), []);
nomatch ->
remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- [Path | Acc]]), []);
{Pos, _} ->
<<Segment:Pos/binary, _:8, Rest/bits>> = Path,
split_path(Rest, [Segment | Acc])
end
catch error:_ ->
badrequest
end.
remove_dot_segments([], Acc) ->
lists:reverse(Acc);
remove_dot_segments([<<".">> | Segments], Acc) ->
remove_dot_segments(Segments, Acc);
remove_dot_segments([<<"..">> | Segments], Acc = []) ->
remove_dot_segments(Segments, Acc);
remove_dot_segments([<<"..">> | Segments], [_ | Acc]) ->
remove_dot_segments(Segments, Acc);
remove_dot_segments([S | Segments], Acc) ->
remove_dot_segments(Segments, [S | Acc]).
-ifdef(TEST).
remove_dot_segments_test_() ->
Tests = [
{[<<"a">>, <<"b">>, <<"c">>, <<".">>, <<"..">>, <<"..">>, <<"g">>], [<<"a">>, <<"g">>]},
{[<<"mid">>, <<"content=5">>, <<"..">>, <<"6">>], [<<"mid">>, <<"6">>]},
{[<<"..">>, <<"a">>], [<<"a">>]}
],
[fun() -> R = remove_dot_segments(S, []) end || {S, R} <- Tests].
-endif.
-spec list_match(tokens(), dispatch_match(), bindings())
-> {true, bindings(), undefined | tokens()} | false.
%% Atom '...' matches any trailing path, stop right now.
list_match(List, ['...'], Binds) ->
{true, Binds, List};
%% Atom '_' matches anything, continue.
list_match([_E | Tail], ['_' | TailMatch], Binds) ->
list_match(Tail, TailMatch, Binds);
%% Both values match, continue.
list_match([E | Tail], [E | TailMatch], Binds) ->
list_match(Tail, TailMatch, Binds);
%% Bind E to the variable name V and continue,
%% unless V was already defined and E isn't identical to the previous value.
list_match([E | Tail], [V | TailMatch], Binds) when is_atom(V) ->
case Binds of
%% @todo This isn't right, the constraint must be applied FIRST
%% otherwise we can't check for example ints in both host/path.
#{V := E} ->
list_match(Tail, TailMatch, Binds);
#{V := _} ->
false;
_ ->
list_match(Tail, TailMatch, Binds#{V => E})
end;
%% Match complete.
list_match([], [], Binds) ->
{true, Binds, undefined};
%% Values don't match, stop.
list_match(_List, _Match, _Binds) ->
false.
%% Tests.
-ifdef(TEST).
compile_test_() ->
Tests = [
%% Match any host and path.
{[{'_', [{'_', h, o}]}],
[{'_', [], [{'_', [], h, o}]}]},
{[{"cowboy.example.org",
[{"/", ha, oa}, {"/path/to/resource", hb, ob}]}],
[{[<<"org">>, <<"example">>, <<"cowboy">>], [], [
{[], [], ha, oa},
{[<<"path">>, <<"to">>, <<"resource">>], [], hb, ob}]}]},
{[{'_', [{"/path/to/resource/", h, o}]}],
[{'_', [], [{[<<"path">>, <<"to">>, <<"resource">>], [], h, o}]}]},
% Cyrillic from a latin1 encoded file.
{[{'_', [{[47, 208, 191, 209, 131, 209, 130, 209, 140, 47, 208, 186, 47, 209, 128,
208, 181, 209, 129, 209, 131, 209, 128, 209, 129, 209, 131, 47], h, o}]}],
[{'_', [], [{[<<208, 191, 209, 131, 209, 130, 209, 140>>, <<208, 186>>,
<<209, 128, 208, 181, 209, 129, 209, 131, 209, 128, 209, 129, 209, 131>>],
[], h, o}]}]},
{[{"cowboy.example.org.", [{'_', h, o}]}],
[{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]},
{[{".cowboy.example.org", [{'_', h, o}]}],
[{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]},
% Cyrillic from a latin1 encoded file.
{[{[208, 189, 208, 181, 208, 186, 208, 184, 208, 185, 46, 209, 129, 208, 176,
208, 185, 209, 130, 46, 209, 128, 209, 132, 46], [{'_', h, o}]}],
[{[<<209, 128, 209, 132>>, <<209, 129, 208, 176, 208, 185, 209, 130>>,
<<208, 189, 208, 181, 208, 186, 208, 184, 208, 185>>],
[], [{'_', [], h, o}]}]},
{[{":subdomain.example.org", [{"/hats/:name/prices", h, o}]}],
[{[<<"org">>, <<"example">>, subdomain], [], [
{[<<"hats">>, name, <<"prices">>], [], h, o}]}]},
{[{"ninenines.:_", [{"/hats/:_", h, o}]}],
[{['_', <<"ninenines">>], [], [{[<<"hats">>, '_'], [], h, o}]}]},
{[{"[www.]ninenines.eu",
[{"/horses", h, o}, {"/hats/[page/:number]", h, o}]}], [
{[<<"eu">>, <<"ninenines">>], [], [
{[<<"horses">>], [], h, o},
{[<<"hats">>], [], h, o},
{[<<"hats">>, <<"page">>, number], [], h, o}]},
{[<<"eu">>, <<"ninenines">>, <<"www">>], [], [
{[<<"horses">>], [], h, o},
{[<<"hats">>], [], h, o},
{[<<"hats">>, <<"page">>, number], [], h, o}]}]},
{[{'_', [{"/hats/:page/:number", h, o}]}], [{'_', [], [
{[<<"hats">>, page, number], [], h, o}]}]},
{[{'_', [{"/hats/[page/[:number]]", h, o}]}], [{'_', [], [
{[<<"hats">>], [], h, o},
{[<<"hats">>, <<"page">>], [], h, o},
{[<<"hats">>, <<"page">>, number], [], h, o}]}]},
{[{"[...]ninenines.eu", [{"/hats/[...]", h, o}]}],
[{[<<"eu">>, <<"ninenines">>, '...'], [], [
{[<<"hats">>, '...'], [], h, o}]}]},
%% Path segment containing a colon.
{[{'_', [{"/foo/bar:blah", h, o}]}], [{'_', [], [
{[<<"foo">>, <<"bar:blah">>], [], h, o}]}]}
],
[{lists:flatten(io_lib:format("~p", [Rt])),
fun() -> Rs = compile(Rt) end} || {Rt, Rs} <- Tests].
split_host_test_() ->
Tests = [
{<<"">>, []},
{<<"*">>, [<<"*">>]},
{<<"cowboy.ninenines.eu">>,
[<<"eu">>, <<"ninenines">>, <<"cowboy">>]},
{<<"ninenines.eu">>,
[<<"eu">>, <<"ninenines">>]},
{<<"ninenines.eu.">>,
[<<"eu">>, <<"ninenines">>]},
{<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>,
[<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>,
<<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>,
<<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>,
<<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]}
],
[{H, fun() -> R = split_host(H) end} || {H, R} <- Tests].
split_path_test_() ->
Tests = [
{<<"/">>, []},
{<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]},
{<<"/users">>, [<<"users">>]},
{<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]},
{<<"/users/a%20b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]}
],
[{P, fun() -> R = split_path(P) end} || {P, R} <- Tests].
match_test_() ->
Dispatch = [
{[<<"eu">>, <<"ninenines">>, '_', <<"www">>], [], [
{[<<"users">>, '_', <<"mails">>], [], match_any_subdomain_users, []}
]},
{[<<"eu">>, <<"ninenines">>], [], [
{[<<"users">>, id, <<"friends">>], [], match_extend_users_friends, []},
{'_', [], match_extend, []}
]},
{[var, <<"ninenines">>], [], [
{[<<"threads">>, var], [], match_duplicate_vars,
[we, {expect, two}, var, here]}
]},
{[ext, <<"erlang">>], [], [
{'_', [], match_erlang_ext, []}
]},
{'_', [], [
{[<<"users">>, id, <<"friends">>], [], match_users_friends, []},
{'_', [], match_any, []}
]}
],
Tests = [
{<<"any">>, <<"/">>, {ok, match_any, [], #{}}},
{<<"www.any.ninenines.eu">>, <<"/users/42/mails">>,
{ok, match_any_subdomain_users, [], #{}}},
{<<"www.ninenines.eu">>, <<"/users/42/mails">>,
{ok, match_any, [], #{}}},
{<<"www.ninenines.eu">>, <<"/">>,
{ok, match_any, [], #{}}},
{<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>,
{error, notfound, path}},
{<<"ninenines.eu">>, <<"/">>,
{ok, match_extend, [], #{}}},
{<<"ninenines.eu">>, <<"/users/42/friends">>,
{ok, match_extend_users_friends, [], #{id => <<"42">>}}},
{<<"erlang.fr">>, '_',
{ok, match_erlang_ext, [], #{ext => <<"fr">>}}},
{<<"any">>, <<"/users/444/friends">>,
{ok, match_users_friends, [], #{id => <<"444">>}}},
{<<"any">>, <<"/users//friends">>,
{ok, match_users_friends, [], #{id => <<>>}}}
],
[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
{ok, Handler, Opts, Binds, undefined, undefined}
= match(Dispatch, H, P)
end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests].
match_info_test_() ->
Dispatch = [
{[<<"eu">>, <<"ninenines">>, <<"www">>], [], [
{[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], [], match_path, []}
]},
{[<<"eu">>, <<"ninenines">>, '...'], [], [
{'_', [], match_any, []}
]}
],
Tests = [
{<<"ninenines.eu">>, <<"/">>,
{ok, match_any, [], #{}, [], undefined}},
{<<"bugs.ninenines.eu">>, <<"/">>,
{ok, match_any, [], #{}, [<<"bugs">>], undefined}},
{<<"cowboy.bugs.ninenines.eu">>, <<"/">>,
{ok, match_any, [], #{}, [<<"cowboy">>, <<"bugs">>], undefined}},
{<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>,
{ok, match_path, [], #{}, undefined, []}},
{<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>,
{ok, match_path, [], #{}, undefined, [<<"path_info">>]}},
{<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>,
{ok, match_path, [], #{}, undefined, [<<"foo">>, <<"bar">>]}}
],
[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
R = match(Dispatch, H, P)
end} || {H, P, R} <- Tests].
match_constraints_test() ->
Dispatch0 = [{'_', [],
[{[<<"path">>, value], [{value, int}], match, []}]}],
{ok, _, [], #{value := 123}, _, _} = match(Dispatch0,
<<"ninenines.eu">>, <<"/path/123">>),
{ok, _, [], #{value := 123}, _, _} = match(Dispatch0,
<<"ninenines.eu">>, <<"/path/123/">>),
{error, notfound, path} = match(Dispatch0,
<<"ninenines.eu">>, <<"/path/NaN/">>),
Dispatch1 = [{'_', [],
[{[<<"path">>, value, <<"more">>], [{value, nonempty}], match, []}]}],
{ok, _, [], #{value := <<"something">>}, _, _} = match(Dispatch1,
<<"ninenines.eu">>, <<"/path/something/more">>),
{error, notfound, path} = match(Dispatch1,
<<"ninenines.eu">>, <<"/path//more">>),
Dispatch2 = [{'_', [], [{[<<"path">>, username],
[{username, fun(_, Value) ->
case cowboy_bstr:to_lower(Value) of
Value -> {ok, Value};
_ -> {error, not_lowercase}
end end}],
match, []}]}],
{ok, _, [], #{username := <<"essen">>}, _, _} = match(Dispatch2,
<<"ninenines.eu">>, <<"/path/essen">>),
{error, notfound, path} = match(Dispatch2,
<<"ninenines.eu">>, <<"/path/ESSEN">>),
ok.
match_same_bindings_test() ->
Dispatch = [{[same, same], [], [{'_', [], match, []}]}],
{ok, _, [], #{same := <<"eu">>}, _, _} = match(Dispatch,
<<"eu.eu">>, <<"/">>),
{error, notfound, host} = match(Dispatch,
<<"ninenines.eu">>, <<"/">>),
Dispatch2 = [{[<<"eu">>, <<"ninenines">>, user], [],
[{[<<"path">>, user], [], match, []}]}],
{ok, _, [], #{user := <<"essen">>}, _, _} = match(Dispatch2,
<<"essen.ninenines.eu">>, <<"/path/essen">>),
{ok, _, [], #{user := <<"essen">>}, _, _} = match(Dispatch2,
<<"essen.ninenines.eu">>, <<"/path/essen/">>),
{error, notfound, path} = match(Dispatch2,
<<"essen.ninenines.eu">>, <<"/path/notessen">>),
Dispatch3 = [{'_', [], [{[same, same], [], match, []}]}],
{ok, _, [], #{same := <<"path">>}, _, _} = match(Dispatch3,
<<"ninenines.eu">>, <<"/path/path">>),
{error, notfound, path} = match(Dispatch3,
<<"ninenines.eu">>, <<"/path/to">>),
ok.
-endif.

+ 0
- 418
src/wsSrv/cowboy_static.erl Vedi File

@ -1,418 +0,0 @@
%% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_static).
-export([init/2]).
-export([malformed_request/2]).
-export([forbidden/2]).
-export([content_types_provided/2]).
-export([charsets_provided/2]).
-export([ranges_provided/2]).
-export([resource_exists/2]).
-export([last_modified/2]).
-export([generate_etag/2]).
-export([get_file/2]).
-type extra_charset() :: {charset, module(), function()} | {charset, binary()}.
-type extra_etag() :: {etag, module(), function()} | {etag, false}.
-type extra_mimetypes() :: {mimetypes, module(), function()}
| {mimetypes, binary() | {binary(), binary(), [{binary(), binary()}]}}.
-type extra() :: [extra_charset() | extra_etag() | extra_mimetypes()].
-type opts() :: {file | dir, string() | binary()}
| {file | dir, string() | binary(), extra()}
| {priv_file | priv_dir, atom(), string() | binary()}
| {priv_file | priv_dir, atom(), string() | binary(), extra()}.
-export_type([opts/0]).
-include_lib("kernel/include/file.hrl").
-type state() :: {binary(), {direct | archive, #file_info{}}
| {error, atom()}, extra()}.
%% Resolve the file that will be sent and get its file information.
%% If the handler is configured to manage a directory, check that the
%% requested file is inside the configured directory.
-spec init(Req, opts()) -> {cowboy_rest, Req, error | state()} when Req :: cowboy_req:req().
init(Req, {Name, Path}) ->
init_opts(Req, {Name, Path, []});
init(Req, {Name, App, Path})
when Name =:= priv_file; Name =:= priv_dir ->
init_opts(Req, {Name, App, Path, []});
init(Req, Opts) ->
init_opts(Req, Opts).
init_opts(Req, {priv_file, App, Path, Extra}) ->
{PrivPath, HowToAccess} = priv_path(App, Path),
init_info(Req, absname(PrivPath), HowToAccess, Extra);
init_opts(Req, {file, Path, Extra}) ->
init_info(Req, absname(Path), direct, Extra);
init_opts(Req, {priv_dir, App, Path, Extra}) ->
{PrivPath, HowToAccess} = priv_path(App, Path),
init_dir(Req, PrivPath, HowToAccess, Extra);
init_opts(Req, {dir, Path, Extra}) ->
init_dir(Req, Path, direct, Extra).
priv_path(App, Path) ->
case code:priv_dir(App) of
{error, bad_name} ->
error({badarg, "Can't resolve the priv_dir of application "
++ atom_to_list(App)});
PrivDir when is_list(Path) ->
{
PrivDir ++ "/" ++ Path,
how_to_access_app_priv(PrivDir)
};
PrivDir when is_binary(Path) ->
{
<<(list_to_binary(PrivDir))/binary, $/, Path/binary>>,
how_to_access_app_priv(PrivDir)
}
end.
how_to_access_app_priv(PrivDir) ->
%% If the priv directory is not a directory, it must be
%% inside an Erlang application .ez archive. We call
%% how_to_access_app_priv1() to find the corresponding archive.
case filelib:is_dir(PrivDir) of
true -> direct;
false -> how_to_access_app_priv1(PrivDir)
end.
how_to_access_app_priv1(Dir) ->
%% We go "up" by one path component at a time and look for a
%% regular file.
Archive = filename:dirname(Dir),
case Archive of
Dir ->
%% filename:dirname() returned its argument:
%% we reach the root directory. We found no
%% archive so we return 'direct': the given priv
%% directory doesn't exist.
direct;
_ ->
case filelib:is_regular(Archive) of
true -> {archive, Archive};
false -> how_to_access_app_priv1(Archive)
end
end.
absname(Path) when is_list(Path) ->
filename:absname(list_to_binary(Path));
absname(Path) when is_binary(Path) ->
filename:absname(Path).
init_dir(Req, Path, HowToAccess, Extra) when is_list(Path) ->
init_dir(Req, list_to_binary(Path), HowToAccess, Extra);
init_dir(Req, Path, HowToAccess, Extra) ->
Dir = fullpath(filename:absname(Path)),
case cowboy_req:path_info(Req) of
%% When dir/priv_dir are used and there is no path_info
%% this is a configuration error and we abort immediately.
undefined ->
{ok, cowboy_req:reply(500, Req), error};
PathInfo ->
case validate_reserved(PathInfo) of
error ->
{cowboy_rest, Req, error};
ok ->
Filepath = filename:join([Dir | PathInfo]),
Len = byte_size(Dir),
case fullpath(Filepath) of
<<Dir:Len/binary, $/, _/binary>> ->
init_info(Req, Filepath, HowToAccess, Extra);
<<Dir:Len/binary>> ->
init_info(Req, Filepath, HowToAccess, Extra);
_ ->
{cowboy_rest, Req, error}
end
end
end.
validate_reserved([]) ->
ok;
validate_reserved([P | Tail]) ->
case validate_reserved1(P) of
ok -> validate_reserved(Tail);
error -> error
end.
%% We always reject forward slash, backward slash and NUL as
%% those have special meanings across the supported platforms.
%% We could support the backward slash on some platforms but
%% for the sake of consistency and simplicity we don't.
validate_reserved1(<<>>) ->
ok;
validate_reserved1(<<$/, _/bits>>) ->
error;
validate_reserved1(<<$\\, _/bits>>) ->
error;
validate_reserved1(<<0, _/bits>>) ->
error;
validate_reserved1(<<_, Rest/bits>>) ->
validate_reserved1(Rest).
fullpath(Path) ->
fullpath(filename:split(Path), []).
fullpath([], Acc) ->
filename:join(lists:reverse(Acc));
fullpath([<<".">> | Tail], Acc) ->
fullpath(Tail, Acc);
fullpath([<<"..">> | Tail], Acc = [_]) ->
fullpath(Tail, Acc);
fullpath([<<"..">> | Tail], [_ | Acc]) ->
fullpath(Tail, Acc);
fullpath([Segment | Tail], Acc) ->
fullpath(Tail, [Segment | Acc]).
init_info(Req, Path, HowToAccess, Extra) ->
Info = read_file_info(Path, HowToAccess),
{cowboy_rest, Req, {Path, Info, Extra}}.
read_file_info(Path, direct) ->
case file:read_file_info(Path, [{time, universal}]) of
{ok, Info} -> {direct, Info};
Error -> Error
end;
read_file_info(Path, {archive, Archive}) ->
case file:read_file_info(Archive, [{time, universal}]) of
{ok, ArchiveInfo} ->
%% The Erlang application archive is fine.
%% Now check if the requested file is in that
%% archive. We also need the file_info to merge
%% them with the archive's one.
PathS = binary_to_list(Path),
case erl_prim_loader:read_file_info(PathS) of
{ok, ContainedFileInfo} ->
Info = fix_archived_file_info(
ArchiveInfo,
ContainedFileInfo),
{archive, Info};
error ->
{error, enoent}
end;
Error ->
Error
end.
fix_archived_file_info(ArchiveInfo, ContainedFileInfo) ->
%% We merge the archive and content #file_info because we are
%% interested by the timestamps of the archive, but the type and
%% size of the contained file/directory.
%%
%% We reset the access to 'read', because we won't rewrite the
%% archive.
ArchiveInfo#file_info{
size = ContainedFileInfo#file_info.size,
type = ContainedFileInfo#file_info.type,
access = read
}.
-ifdef(TEST).
fullpath_test_() ->
Tests = [
{<<"/home/cowboy">>, <<"/home/cowboy">>},
{<<"/home/cowboy">>, <<"/home/cowboy/">>},
{<<"/home/cowboy">>, <<"/home/cowboy/./">>},
{<<"/home/cowboy">>, <<"/home/cowboy/./././././.">>},
{<<"/home/cowboy">>, <<"/home/cowboy/abc/..">>},
{<<"/home/cowboy">>, <<"/home/cowboy/abc/../">>},
{<<"/home/cowboy">>, <<"/home/cowboy/abc/./../.">>},
{<<"/">>, <<"/home/cowboy/../../../../../..">>},
{<<"/etc/passwd">>, <<"/home/cowboy/../../etc/passwd">>}
],
[{P, fun() -> R = fullpath(P) end} || {R, P} <- Tests].
good_path_check_test_() ->
Tests = [
<<"/home/cowboy/file">>,
<<"/home/cowboy/file/">>,
<<"/home/cowboy/./file">>,
<<"/home/cowboy/././././././file">>,
<<"/home/cowboy/abc/../file">>,
<<"/home/cowboy/abc/../file">>,
<<"/home/cowboy/abc/./.././file">>
],
[{P, fun() ->
case fullpath(P) of
<<"/home/cowboy/", _/bits>> -> ok
end
end} || P <- Tests].
bad_path_check_test_() ->
Tests = [
<<"/home/cowboy/../../../../../../file">>,
<<"/home/cowboy/../../etc/passwd">>
],
[{P, fun() ->
error = case fullpath(P) of
<<"/home/cowboy/", _/bits>> -> ok;
_ -> error
end
end} || P <- Tests].
good_path_win32_check_test_() ->
Tests = case os:type() of
{unix, _} ->
[];
{win32, _} ->
[
<<"c:/home/cowboy/file">>,
<<"c:/home/cowboy/file/">>,
<<"c:/home/cowboy/./file">>,
<<"c:/home/cowboy/././././././file">>,
<<"c:/home/cowboy/abc/../file">>,
<<"c:/home/cowboy/abc/../file">>,
<<"c:/home/cowboy/abc/./.././file">>
]
end,
[{P, fun() ->
case fullpath(P) of
<<"c:/home/cowboy/", _/bits>> -> ok
end
end} || P <- Tests].
bad_path_win32_check_test_() ->
Tests = case os:type() of
{unix, _} ->
[];
{win32, _} ->
[
<<"c:/home/cowboy/../../secretfile.bat">>,
<<"c:/home/cowboy/c:/secretfile.bat">>,
<<"c:/home/cowboy/..\\..\\secretfile.bat">>,
<<"c:/home/cowboy/c:\\secretfile.bat">>
]
end,
[{P, fun() ->
error = case fullpath(P) of
<<"c:/home/cowboy/", _/bits>> -> ok;
_ -> error
end
end} || P <- Tests].
-endif.
%% Reject requests that tried to access a file outside
%% the target directory, or used reserved characters.
-spec malformed_request(Req, State)
-> {boolean(), Req, State}.
malformed_request(Req, State) ->
{State =:= error, Req, State}.
%% Directories, files that can't be accessed at all and
%% files with no read flag are forbidden.
-spec forbidden(Req, State)
-> {boolean(), Req, State}
when State :: state().
forbidden(Req, State = {_, {_, #file_info{type = directory}}, _}) ->
{true, Req, State};
forbidden(Req, State = {_, {error, eacces}, _}) ->
{true, Req, State};
forbidden(Req, State = {_, {_, #file_info{access = Access}}, _})
when Access =:= write; Access =:= none ->
{true, Req, State};
forbidden(Req, State) ->
{false, Req, State}.
%% Detect the mimetype of the file.
-spec content_types_provided(Req, State)
-> {[{binary(), get_file}], Req, State}
when State :: state().
content_types_provided(Req, State = {Path, _, Extra}) when is_list(Extra) ->
case lists:keyfind(mimetypes, 1, Extra) of
false ->
{[{cow_mimetypes:web(Path), get_file}], Req, State};
{mimetypes, Module, Function} ->
{[{Module:Function(Path), get_file}], Req, State};
{mimetypes, Type} ->
{[{Type, get_file}], Req, State}
end.
%% Detect the charset of the file.
-spec charsets_provided(Req, State)
-> {[binary()], Req, State}
when State :: state().
charsets_provided(Req, State = {Path, _, Extra}) ->
case lists:keyfind(charset, 1, Extra) of
%% We simulate the callback not being exported.
false ->
no_call;
{charset, Module, Function} ->
{[Module:Function(Path)], Req, State};
{charset, Charset} when is_binary(Charset) ->
{[Charset], Req, State}
end.
%% Enable support for range requests.
-spec ranges_provided(Req, State)
-> {[{binary(), auto}], Req, State}
when State :: state().
ranges_provided(Req, State) ->
{[{<<"bytes">>, auto}], Req, State}.
%% Assume the resource doesn't exist if it's not a regular file.
-spec resource_exists(Req, State)
-> {boolean(), Req, State}
when State :: state().
resource_exists(Req, State = {_, {_, #file_info{type = regular}}, _}) ->
{true, Req, State};
resource_exists(Req, State) ->
{false, Req, State}.
%% Generate an etag for the file.
-spec generate_etag(Req, State)
-> {{strong | weak, binary()}, Req, State}
when State :: state().
generate_etag(Req, State = {Path, {_, #file_info{size = Size, mtime = Mtime}},
Extra}) ->
case lists:keyfind(etag, 1, Extra) of
false ->
{generate_default_etag(Size, Mtime), Req, State};
{etag, Module, Function} ->
{Module:Function(Path, Size, Mtime), Req, State};
{etag, false} ->
{undefined, Req, State}
end.
generate_default_etag(Size, Mtime) ->
{strong, integer_to_binary(erlang:phash2({Size, Mtime}, 16#ffffffff))}.
%% Return the time of last modification of the file.
-spec last_modified(Req, State)
-> {calendar:datetime(), Req, State}
when State :: state().
last_modified(Req, State = {_, {_, #file_info{mtime = Modified}}, _}) ->
{Modified, Req, State}.
%% Stream the file.
-spec get_file(Req, State)
-> {{sendfile, 0, non_neg_integer(), binary()}, Req, State}
when State :: state().
get_file(Req, State = {Path, {direct, #file_info{size = Size}}, _}) ->
{{sendfile, 0, Size, Path}, Req, State};
get_file(Req, State = {Path, {archive, _}, _}) ->
PathS = binary_to_list(Path),
{ok, Bin, _} = erl_prim_loader:get_file(PathS),
{Bin, Req, State}.

+ 0
- 193
src/wsSrv/cowboy_stream.erl Vedi File

@ -1,193 +0,0 @@
%% Copyright (c) 2015-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_stream).
-type state() :: any().
-type human_reason() :: atom().
-type streamid() :: any().
-export_type([streamid/0]).
-type fin() :: fin | nofin.
-export_type([fin/0]).
%% @todo Perhaps it makes more sense to have resp_body in this module?
-type resp_command()
:: {response, cowboy:http_status(), cowboy:http_headers(), cowboy_req:resp_body()}.
-export_type([resp_command/0]).
-type commands() :: [{inform, cowboy:http_status(), cowboy:http_headers()}
| resp_command()
| {headers, cowboy:http_status(), cowboy:http_headers()}
| {data, fin(), cowboy_req:resp_body()}
| {trailers, cowboy:http_headers()}
| {push, binary(), binary(), binary(), inet:port_number(),
binary(), binary(), cowboy:http_headers()}
| {flow, pos_integer()}
| {spawn, pid(), timeout()}
| {error_response, cowboy:http_status(), cowboy:http_headers(), iodata()}
| {switch_protocol, cowboy:http_headers(), module(), state()}
| {internal_error, any(), human_reason()}
| {set_options, map()}
| {log, logger:level(), io:format(), list()}
| stop].
-export_type([commands/0]).
-type reason() :: normal | switch_protocol
| {internal_error, timeout | {error | exit | throw, any()}, human_reason()}
| {socket_error, closed | atom(), human_reason()}
| {stream_error, cow_http2:error(), human_reason()}
| {connection_error, cow_http2:error(), human_reason()}
| {stop, cow_http2:frame() | {exit, any()}, human_reason()}.
-export_type([reason/0]).
-type partial_req() :: map(). %% @todo Take what's in cowboy_req with everything? optional.
-export_type([partial_req/0]).
-callback init(streamid(), cowboy_req:req(), cowboy:opts()) -> {commands(), state()}.
-callback data(streamid(), fin(), binary(), State) -> {commands(), State} when State :: state().
-callback info(streamid(), any(), State) -> {commands(), State} when State :: state().
-callback terminate(streamid(), reason(), state()) -> any().
-callback early_error(streamid(), reason(), partial_req(), Resp, cowboy:opts())
-> Resp when Resp :: resp_command().
%% @todo To optimize the number of active timers we could have a command
%% that enables a timeout that is called in the absence of any other call,
%% similar to what gen_server does. However the nice thing about this is
%% that the connection process can keep a single timer around (the same
%% one that would be used to detect half-closed sockets) and use this
%% timer and other events to trigger the timeout in streams at their
%% intended time.
%%
%% This same timer can be used to try and send PING frames to help detect
%% that the connection is indeed unresponsive.
-export([init/3]).
-export([data/4]).
-export([info/3]).
-export([terminate/3]).
-export([early_error/5]).
-export([make_error_log/5]).
%% Note that this and other functions in this module do NOT catch
%% exceptions. We want the exception to go all the way down to the
%% protocol code.
%%
%% OK the failure scenario is not so clear. The problem is
%% that the failure at any point in init/3 will result in the
%% corresponding state being lost. I am unfortunately not
%% confident we can do anything about this. If the crashing
%% handler just created a process, we'll never know about it.
%% Therefore at this time I choose to leave all failure handling
%% to the protocol process.
%%
%% Note that a failure in init/3 will result in terminate/3
%% NOT being called. This is because the state is not available.
-spec init(streamid(), cowboy_req:req(), cowboy:opts())
-> {commands(), {module(), state()} | undefined}.
init(StreamID, Req, Opts) ->
case maps:get(stream_handlers, Opts, [cowboy_stream_h]) of
[] ->
{[], undefined};
[Handler | Tail] ->
%% We call the next handler and remove it from the list of
%% stream handlers. This means that handlers that run after
%% it have no knowledge it exists. Should user require this
%% knowledge they can just define a separate option that will
%% be left untouched.
{Commands, State} = Handler:init(StreamID, Req, Opts#{stream_handlers => Tail}),
{Commands, {Handler, State}}
end.
-spec data(streamid(), fin(), binary(), {Handler, State} | undefined)
-> {commands(), {Handler, State} | undefined}
when Handler :: module(), State :: state().
data(_, _, _, undefined) ->
{[], undefined};
data(StreamID, IsFin, Data, {Handler, State0}) ->
{Commands, State} = Handler:data(StreamID, IsFin, Data, State0),
{Commands, {Handler, State}}.
-spec info(streamid(), any(), {Handler, State} | undefined)
-> {commands(), {Handler, State} | undefined}
when Handler :: module(), State :: state().
info(_, _, undefined) ->
{[], undefined};
info(StreamID, Info, {Handler, State0}) ->
{Commands, State} = Handler:info(StreamID, Info, State0),
{Commands, {Handler, State}}.
-spec terminate(streamid(), reason(), {module(), state()} | undefined) -> ok.
terminate(_, _, undefined) ->
ok;
terminate(StreamID, Reason, {Handler, State}) ->
_ = Handler:terminate(StreamID, Reason, State),
ok.
-spec early_error(streamid(), reason(), partial_req(), Resp, cowboy:opts())
-> Resp when Resp :: resp_command().
early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
case maps:get(stream_handlers, Opts, [cowboy_stream_h]) of
[] ->
Resp;
[Handler | Tail] ->
%% This is the same behavior as in init/3.
Handler:early_error(StreamID, Reason,
PartialReq, Resp, Opts#{stream_handlers => Tail})
end.
-spec make_error_log(init | data | info | terminate | early_error,
list(), error | exit | throw, any(), list())
-> {log, error, string(), list()}.
make_error_log(init, [StreamID, Req, Opts], Class, Exception, Stacktrace) ->
{log, error,
"Unhandled exception ~p:~p in cowboy_stream:init(~p, Req, Opts)~n"
"Stacktrace: ~p~n"
"Req: ~p~n"
"Opts: ~p~n",
[Class, Exception, StreamID, Stacktrace, Req, Opts]};
make_error_log(data, [StreamID, IsFin, Data, State], Class, Exception, Stacktrace) ->
{log, error,
"Unhandled exception ~p:~p in cowboy_stream:data(~p, ~p, Data, State)~n"
"Stacktrace: ~p~n"
"Data: ~p~n"
"State: ~p~n",
[Class, Exception, StreamID, IsFin, Stacktrace, Data, State]};
make_error_log(info, [StreamID, Msg, State], Class, Exception, Stacktrace) ->
{log, error,
"Unhandled exception ~p:~p in cowboy_stream:info(~p, Msg, State)~n"
"Stacktrace: ~p~n"
"Msg: ~p~n"
"State: ~p~n",
[Class, Exception, StreamID, Stacktrace, Msg, State]};
make_error_log(terminate, [StreamID, Reason, State], Class, Exception, Stacktrace) ->
{log, error,
"Unhandled exception ~p:~p in cowboy_stream:terminate(~p, Reason, State)~n"
"Stacktrace: ~p~n"
"Reason: ~p~n"
"State: ~p~n",
[Class, Exception, StreamID, Stacktrace, Reason, State]};
make_error_log(early_error, [StreamID, Reason, PartialReq, Resp, Opts],
Class, Exception, Stacktrace) ->
{log, error,
"Unhandled exception ~p:~p in cowboy_stream:early_error(~p, Reason, PartialReq, Resp, Opts)~n"
"Stacktrace: ~p~n"
"Reason: ~p~n"
"PartialReq: ~p~n"
"Resp: ~p~n"
"Opts: ~p~n",
[Class, Exception, StreamID, Stacktrace, Reason, PartialReq, Resp, Opts]}.

+ 0
- 324
src/wsSrv/cowboy_stream_h.erl Vedi File

@ -1,324 +0,0 @@
%% Copyright (c) 2016-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_stream_h).
-behavior(cowboy_stream).
-export([init/3]).
-export([data/4]).
-export([info/3]).
-export([terminate/3]).
-export([early_error/5]).
-export([request_process/3]).
-export([resume/5]).
-record(state, {
next :: any(),
ref = undefined :: ranch:ref(),
pid = undefined :: pid(),
expect = undefined :: undefined | continue,
read_body_pid = undefined :: pid() | undefined,
read_body_ref = undefined :: reference() | undefined,
read_body_timer_ref = undefined :: reference() | undefined,
read_body_length = 0 :: non_neg_integer() | infinity | auto,
read_body_is_fin = nofin :: nofin | {fin, non_neg_integer()},
read_body_buffer = <<>> :: binary(),
body_length = 0 :: non_neg_integer(),
stream_body_pid = undefined :: pid() | undefined,
stream_body_status = normal :: normal | blocking | blocked
}).
-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
-> {[{spawn, pid(), timeout()}], #state{}}.
init(StreamID, Req = #{ref := Ref}, Opts) ->
Env = maps:get(env, Opts, #{}),
Middlewares = maps:get(middlewares, Opts, [cowboy_router, cowboy_handler]),
Shutdown = maps:get(shutdown_timeout, Opts, 5000),
Pid = proc_lib:spawn_link(?MODULE, request_process, [Req, Env, Middlewares]),
Expect = expect(Req),
{Commands, Next} = cowboy_stream:init(StreamID, Req, Opts),
{[{spawn, Pid, Shutdown} | Commands],
#state{next = Next, ref = Ref, pid = Pid, expect = Expect}}.
%% Ignore the expect header in HTTP/1.0.
expect(#{version := 'HTTP/1.0'}) ->
undefined;
expect(Req) ->
try cowboy_req:parse_header(<<"expect">>, Req) of
Expect ->
Expect
catch _:_ ->
undefined
end.
%% If we receive data and stream is waiting for data:
%% If we accumulated enough data or IsFin=fin, send it.
%% If we are in auto mode, send it and update flow control.
%% If not, buffer it.
%% If not, buffer it.
%%
%% We always reset the expect field when we receive data,
%% since the client started sending the request body before
%% we could send a 100 continue response.
-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
-> {cowboy_stream:commands(), State} when State :: #state{}.
%% Stream isn't waiting for data.
data(StreamID, IsFin, Data, State = #state{
read_body_ref = undefined, read_body_buffer = Buffer, body_length = BodyLen}) ->
do_data(StreamID, IsFin, Data, [], State#state{
expect = undefined,
read_body_is_fin = IsFin,
read_body_buffer = <<Buffer/binary, Data/binary>>,
body_length = BodyLen + byte_size(Data)
});
%% Stream is waiting for data using auto mode.
%%
%% There is no buffering done in auto mode.
data(StreamID, IsFin, Data, State = #state{read_body_pid = Pid, read_body_ref = Ref,
read_body_length = auto, body_length = BodyLen}) ->
send_request_body(Pid, Ref, IsFin, BodyLen, Data),
do_data(StreamID, IsFin, Data, [{flow, byte_size(Data)}], State#state{
read_body_ref = undefined,
%% @todo This is wrong, it's missing byte_size(Data).
body_length = BodyLen
});
%% Stream is waiting for data but we didn't receive enough to send yet.
data(StreamID, IsFin = nofin, Data, State = #state{
read_body_length = ReadLen, read_body_buffer = Buffer, body_length = BodyLen})
when byte_size(Data) + byte_size(Buffer) < ReadLen ->
do_data(StreamID, IsFin, Data, [], State#state{
expect = undefined,
read_body_buffer = <<Buffer/binary, Data/binary>>,
body_length = BodyLen + byte_size(Data)
});
%% Stream is waiting for data and we received enough to send.
data(StreamID, IsFin, Data, State = #state{read_body_pid = Pid, read_body_ref = Ref,
read_body_timer_ref = TRef, read_body_buffer = Buffer, body_length = BodyLen0}) ->
BodyLen = BodyLen0 + byte_size(Data),
ok = erlang:cancel_timer(TRef, [{async, true}, {info, false}]),
send_request_body(Pid, Ref, IsFin, BodyLen, <<Buffer/binary, Data/binary>>),
do_data(StreamID, IsFin, Data, [], State#state{
expect = undefined,
read_body_ref = undefined,
read_body_timer_ref = undefined,
read_body_buffer = <<>>,
body_length = BodyLen
}).
do_data(StreamID, IsFin, Data, Commands1, State = #state{next = Next0}) ->
{Commands2, Next} = cowboy_stream:data(StreamID, IsFin, Data, Next0),
{Commands1 ++ Commands2, State#state{next = Next}}.
-spec info(cowboy_stream:streamid(), any(), State)
-> {cowboy_stream:commands(), State} when State :: #state{}.
info(StreamID, Info = {'EXIT', Pid, normal}, State = #state{pid = Pid}) ->
do_info(StreamID, Info, [stop], State);
info(StreamID, Info = {'EXIT', Pid, {{request_error, Reason, _HumanReadable}, _}},
State = #state{pid = Pid}) ->
Status = case Reason of
timeout -> 408;
payload_too_large -> 413;
_ -> 400
end,
%% @todo Headers? Details in body? Log the crash? More stuff in debug only?
do_info(StreamID, Info, [
{error_response, Status, #{<<"content-length">> => <<"0">>}, <<>>},
stop
], State);
info(StreamID, Exit = {'EXIT', Pid, {Reason, Stacktrace}}, State = #state{ref = Ref, pid = Pid}) ->
Commands0 = [{internal_error, Exit, 'Stream process crashed.'}],
Commands = case Reason of
normal -> Commands0;
shutdown -> Commands0;
{shutdown, _} -> Commands0;
_ -> [{log, error,
"Ranch listener ~p, connection process ~p, stream ~p "
"had its request process ~p exit with reason "
"~999999p and stacktrace ~999999p~n",
[Ref, self(), StreamID, Pid, Reason, Stacktrace]}
| Commands0]
end,
do_info(StreamID, Exit, [
{error_response, 500, #{<<"content-length">> => <<"0">>}, <<>>}
| Commands], State);
%% Request body, auto mode, no body buffered.
info(StreamID, Info = {read_body, Pid, Ref, auto, infinity}, State = #state{read_body_buffer = <<>>}) ->
do_info(StreamID, Info, [], State#state{
read_body_pid = Pid,
read_body_ref = Ref,
read_body_length = auto
});
%% Request body, auto mode, body buffered or complete.
info(StreamID, Info = {read_body, Pid, Ref, auto, infinity}, State = #state{
read_body_is_fin = IsFin, read_body_buffer = Buffer, body_length = BodyLen}) ->
send_request_body(Pid, Ref, IsFin, BodyLen, Buffer),
do_info(StreamID, Info, [{flow, byte_size(Buffer)}],
State#state{read_body_buffer = <<>>});
%% Request body, body buffered large enough or complete.
%%
%% We do not send a 100 continue response if the client
%% already started sending the body.
info(StreamID, Info = {read_body, Pid, Ref, Length, _}, State = #state{
read_body_is_fin = IsFin, read_body_buffer = Buffer, body_length = BodyLen})
when IsFin =:= fin; byte_size(Buffer) >= Length ->
send_request_body(Pid, Ref, IsFin, BodyLen, Buffer),
do_info(StreamID, Info, [], State#state{read_body_buffer = <<>>});
%% Request body, not enough to send yet.
info(StreamID, Info = {read_body, Pid, Ref, Length, Period}, State = #state{expect = Expect}) ->
Commands = case Expect of
continue -> [{inform, 100, #{}}, {flow, Length}];
undefined -> [{flow, Length}]
end,
TRef = erlang:send_after(Period, self(), {{self(), StreamID}, {read_body_timeout, Ref}}),
do_info(StreamID, Info, Commands, State#state{
read_body_pid = Pid,
read_body_ref = Ref,
read_body_timer_ref = TRef,
read_body_length = Length
});
%% Request body reading timeout; send what we got.
info(StreamID, Info = {read_body_timeout, Ref}, State = #state{read_body_pid = Pid, read_body_ref = Ref,
read_body_is_fin = IsFin, read_body_buffer = Buffer, body_length = BodyLen}) ->
send_request_body(Pid, Ref, IsFin, BodyLen, Buffer),
do_info(StreamID, Info, [], State#state{
read_body_ref = undefined,
read_body_timer_ref = undefined,
read_body_buffer = <<>>
});
info(StreamID, Info = {read_body_timeout, _}, State) ->
do_info(StreamID, Info, [], State);
%% Response.
%%
%% We reset the expect field when a 100 continue response
%% is sent or when any final response is sent.
info(StreamID, Inform = {inform, Status, _}, State0) ->
State = case cow_http:status_to_integer(Status) of
100 -> State0#state{expect = undefined};
_ -> State0
end,
do_info(StreamID, Inform, [Inform], State);
info(StreamID, Response = {response, _, _, _}, State) ->
do_info(StreamID, Response, [Response], State#state{expect = undefined});
info(StreamID, Headers = {headers, _, _}, State) ->
do_info(StreamID, Headers, [Headers], State#state{expect = undefined});
%% Sending data involves the data message, the stream_buffer_full alarm
%% and the connection_buffer_full alarm. We stop sending acks when an alarm is on.
%%
%% We only apply backpressure when the message includes a pid. Otherwise
%% it is a message from Cowboy, or the user circumventing the backpressure.
%%
%% We currently do not support sending data from multiple processes concurrently.
info(StreamID, Data = {data, _, _}, State) ->
do_info(StreamID, Data, [Data], State);
info(StreamID, Data0 = {data, Pid, _, _}, State0 = #state{stream_body_status = Status}) ->
State = case Status of
normal ->
Pid ! {data_ack, self()},
State0;
blocking ->
State0#state{stream_body_pid = Pid, stream_body_status = blocked};
blocked ->
State0
end,
Data = erlang:delete_element(2, Data0),
do_info(StreamID, Data, [Data], State);
info(StreamID, Alarm = {alarm, Name, on}, State0 = #state{stream_body_status = Status})
when Name =:= connection_buffer_full; Name =:= stream_buffer_full ->
State = case Status of
normal -> State0#state{stream_body_status = blocking};
_ -> State0
end,
do_info(StreamID, Alarm, [], State);
info(StreamID, Alarm = {alarm, Name, off}, State = #state{stream_body_pid = Pid, stream_body_status = Status})
when Name =:= connection_buffer_full; Name =:= stream_buffer_full ->
_ = case Status of
normal -> ok;
blocking -> ok;
blocked -> Pid ! {data_ack, self()}
end,
do_info(StreamID, Alarm, [], State#state{stream_body_pid = undefined, stream_body_status = normal});
info(StreamID, Trailers = {trailers, _}, State) ->
do_info(StreamID, Trailers, [Trailers], State);
info(StreamID, Push = {push, _, _, _, _, _, _, _}, State) ->
do_info(StreamID, Push, [Push], State);
info(StreamID, SwitchProtocol = {switch_protocol, _, _, _}, State) ->
do_info(StreamID, SwitchProtocol, [SwitchProtocol], State#state{expect = undefined});
%% Convert the set_options message to a command.
info(StreamID, SetOptions = {set_options, _}, State) ->
do_info(StreamID, SetOptions, [SetOptions], State);
%% Unknown message, either stray or meant for a handler down the line.
info(StreamID, Info, State) ->
do_info(StreamID, Info, [], State).
do_info(StreamID, Info, Commands1, State0 = #state{next = Next0}) ->
{Commands2, Next} = cowboy_stream:info(StreamID, Info, Next0),
{Commands1 ++ Commands2, State0#state{next = Next}}.
-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), #state{}) -> ok.
terminate(StreamID, Reason, #state{next = Next}) ->
cowboy_stream:terminate(StreamID, Reason, Next).
-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
when Resp :: cowboy_stream:resp_command().
early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).
send_request_body(Pid, Ref, nofin, _, Data) ->
Pid ! {request_body, Ref, nofin, Data},
ok;
send_request_body(Pid, Ref, fin, BodyLen, Data) ->
Pid ! {request_body, Ref, fin, BodyLen, Data},
ok.
%% Request process.
%% We add the stacktrace to exit exceptions here in order
%% to simplify the debugging of errors. The proc_lib library
%% already adds the stacktrace to other types of exceptions.
-spec request_process(cowboy_req:req(), cowboy_middleware:env(), [module()]) -> ok.
request_process(Req, Env, Middlewares) ->
try
execute(Req, Env, Middlewares)
catch
exit:Reason = {shutdown, _}:Stacktrace ->
erlang:raise(exit, Reason, Stacktrace);
exit:Reason:Stacktrace when Reason =/= normal, Reason =/= shutdown ->
erlang:raise(exit, {Reason, Stacktrace}, Stacktrace)
end.
execute(_, _, []) ->
ok;
execute(Req, Env, [Middleware | Tail]) ->
case Middleware:execute(Req, Env) of
{ok, Req2, Env2} ->
execute(Req2, Env2, Tail);
{suspend, Module, Function, Args} ->
proc_lib:hibernate(?MODULE, resume, [Env, Tail, Module, Function, Args]);
{stop, _Req2} ->
ok
end.
-spec resume(cowboy_middleware:env(), [module()], module(), atom(), [any()]) -> ok.
resume(Env, Tail, Module, Function, Args) ->
case apply(Module, Function, Args) of
{ok, Req2, Env2} ->
execute(Req2, Env2, Tail);
{suspend, Module2, Function2, Args2} ->
proc_lib:hibernate(?MODULE, resume, [Env, Tail, Module2, Function2, Args2]);
{stop, _Req2} ->
ok
end.

+ 0
- 24
src/wsSrv/cowboy_sub_protocol.erl Vedi File

@ -1,24 +0,0 @@
%% Copyright (c) 2013-2017, Loïc Hoguin <essen@ninenines.eu>
%% Copyright (c) 2013, James Fish <james@fishcakez.com>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_sub_protocol).
-callback upgrade(Req, Env, module(), any())
-> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
-callback upgrade(Req, Env, module(), any(), any())
-> {ok, Req, Env} | {suspend, module(), atom(), [any()]} | {stop, Req}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().

+ 0
- 30
src/wsSrv/cowboy_sup.erl Vedi File

@ -1,30 +0,0 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-spec init([])
-> {ok, {{supervisor:strategy(), 10, 10}, [supervisor:child_spec()]}}.
init([]) ->
Procs = [{cowboy_clock, {cowboy_clock, start_link, []},
permanent, 5000, worker, [cowboy_clock]}],
{ok, {{one_for_one, 10, 10}, Procs}}.

+ 0
- 56
src/wsSrv/cowboy_tls.erl Vedi File

@ -1,56 +0,0 @@
%% Copyright (c) 2015-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_tls).
-behavior(ranch_protocol).
-export([start_link/3]).
-export([start_link/4]).
-export([connection_process/4]).
%% Ranch 1.
-spec start_link(ranch:ref(), ssl:sslsocket(), module(), cowboy:opts()) -> {ok, pid()}.
start_link(Ref, _Socket, Transport, Opts) ->
start_link(Ref, Transport, Opts).
%% Ranch 2.
-spec start_link(ranch:ref(), module(), cowboy:opts()) -> {ok, pid()}.
start_link(Ref, Transport, Opts) ->
Pid = proc_lib:spawn_link(?MODULE, connection_process,
[self(), Ref, Transport, Opts]),
{ok, Pid}.
-spec connection_process(pid(), ranch:ref(), module(), cowboy:opts()) -> ok.
connection_process(Parent, Ref, Transport, Opts) ->
ProxyInfo = case maps:get(proxy_header, Opts, false) of
true ->
{ok, ProxyInfo0} = ranch:recv_proxy_header(Ref, 1000),
ProxyInfo0;
false ->
undefined
end,
{ok, Socket} = ranch:handshake(Ref),
case ssl:negotiated_protocol(Socket) of
{ok, <<"h2">>} ->
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, cowboy_http2);
_ -> %% http/1.1 or no protocol negotiated.
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, cowboy_http)
end.
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) ->
_ = case maps:get(connection_type, Opts, supervisor) of
worker -> ok;
supervisor -> process_flag(trap_exit, true)
end,
Protocol:init(Parent, Ref, Socket, Transport, ProxyInfo, Opts).

+ 0
- 192
src/wsSrv/cowboy_tracer_h.erl Vedi File

@ -1,192 +0,0 @@
%% Copyright (c) 2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(cowboy_tracer_h).
-behavior(cowboy_stream).
-export([init/3]).
-export([data/4]).
-export([info/3]).
-export([terminate/3]).
-export([early_error/5]).
-export([set_trace_patterns/0]).
-export([tracer_process/3]).
-export([system_continue/3]).
-export([system_terminate/4]).
-export([system_code_change/4]).
-type match_predicate()
:: fun((cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts()) -> boolean()).
-type tracer_match_specs() :: [match_predicate()
| {method, binary()}
| {host, binary()}
| {path, binary()}
| {path_start, binary()}
| {header, binary()}
| {header, binary(), binary()}
| {peer_ip, inet:ip_address()}
].
-export_type([tracer_match_specs/0]).
-type tracer_callback() :: fun((init | terminate | tuple(), any()) -> any()).
-export_type([tracer_callback/0]).
-spec init(cowboy_stream:streamid(), cowboy_req:req(), cowboy:opts())
-> {cowboy_stream:commands(), any()}.
init(StreamID, Req, Opts) ->
init_tracer(StreamID, Req, Opts),
cowboy_stream:init(StreamID, Req, Opts).
-spec data(cowboy_stream:streamid(), cowboy_stream:fin(), cowboy_req:resp_body(), State)
-> {cowboy_stream:commands(), State} when State :: any().
data(StreamID, IsFin, Data, Next) ->
cowboy_stream:data(StreamID, IsFin, Data, Next).
-spec info(cowboy_stream:streamid(), any(), State)
-> {cowboy_stream:commands(), State} when State :: any().
info(StreamID, Info, Next) ->
cowboy_stream:info(StreamID, Info, Next).
-spec terminate(cowboy_stream:streamid(), cowboy_stream:reason(), any()) -> any().
terminate(StreamID, Reason, Next) ->
cowboy_stream:terminate(StreamID, Reason, Next).
-spec early_error(cowboy_stream:streamid(), cowboy_stream:reason(),
cowboy_stream:partial_req(), Resp, cowboy:opts()) -> Resp
when Resp :: cowboy_stream:resp_command().
early_error(StreamID, Reason, PartialReq, Resp, Opts) ->
cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts).
%% API.
%% These trace patterns are most likely not suitable for production.
-spec set_trace_patterns() -> ok.
set_trace_patterns() ->
erlang:trace_pattern({'_', '_', '_'}, [{'_', [], [{return_trace}]}], [local]),
erlang:trace_pattern(on_load, [{'_', [], [{return_trace}]}], [local]),
ok.
%% Internal.
init_tracer(StreamID, Req, Opts = #{tracer_match_specs := List, tracer_callback := _}) ->
case match(List, StreamID, Req, Opts) of
false ->
ok;
true ->
start_tracer(StreamID, Req, Opts)
end;
%% When the options tracer_match_specs or tracer_callback
%% are not provided we do not enable tracing.
init_tracer(_, _, _) ->
ok.
match([], _, _, _) ->
true;
match([Predicate | Tail], StreamID, Req, Opts) when is_function(Predicate) ->
case Predicate(StreamID, Req, Opts) of
true -> match(Tail, StreamID, Req, Opts);
false -> false
end;
match([{method, Value} | Tail], StreamID, Req = #{method := Value}, Opts) ->
match(Tail, StreamID, Req, Opts);
match([{host, Value} | Tail], StreamID, Req = #{host := Value}, Opts) ->
match(Tail, StreamID, Req, Opts);
match([{path, Value} | Tail], StreamID, Req = #{path := Value}, Opts) ->
match(Tail, StreamID, Req, Opts);
match([{path_start, PathStart} | Tail], StreamID, Req = #{path := Path}, Opts) ->
Len = byte_size(PathStart),
case Path of
<<PathStart:Len/binary, _/bits>> -> match(Tail, StreamID, Req, Opts);
_ -> false
end;
match([{header, Name} | Tail], StreamID, Req = #{headers := Headers}, Opts) ->
case Headers of
#{Name := _} -> match(Tail, StreamID, Req, Opts);
_ -> false
end;
match([{header, Name, Value} | Tail], StreamID, Req = #{headers := Headers}, Opts) ->
case Headers of
#{Name := Value} -> match(Tail, StreamID, Req, Opts);
_ -> false
end;
match([{peer_ip, IP} | Tail], StreamID, Req = #{peer := {IP, _}}, Opts) ->
match(Tail, StreamID, Req, Opts);
match(_, _, _, _) ->
false.
%% We only start the tracer if one wasn't started before.
start_tracer(StreamID, Req, Opts) ->
case erlang:trace_info(self(), tracer) of
{tracer, []} ->
TracerPid = proc_lib:spawn_link(?MODULE, tracer_process, [StreamID, Req, Opts]),
%% The default flags are probably not suitable for production.
Flags = maps:get(tracer_flags, Opts, [
send, 'receive', call, return_to,
procs, ports, monotonic_timestamp,
%% The set_on_spawn flag is necessary to catch events
%% from request processes.
set_on_spawn
]),
erlang:trace(self(), true, [{tracer, TracerPid} | Flags]),
ok;
_ ->
ok
end.
%% Tracer process.
-spec tracer_process(_, _, _) -> no_return().
tracer_process(StreamID, Req = #{pid := Parent}, Opts = #{tracer_callback := Fun}) ->
%% This is necessary because otherwise the tracer could stop
%% before it has finished processing the events in its queue.
process_flag(trap_exit, true),
State = Fun(init, {StreamID, Req, Opts}),
tracer_loop(Parent, Opts, State).
tracer_loop(Parent, Opts = #{tracer_callback := Fun}, State0) ->
receive
Msg when element(1, Msg) =:= trace; element(1, Msg) =:= trace_ts ->
State = Fun(Msg, State0),
tracer_loop(Parent, Opts, State);
{'EXIT', Parent, Reason} ->
tracer_terminate(Reason, Opts, State0);
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {Opts, State0});
Msg ->
cowboy:log(warning, "~p: Tracer process received stray message ~9999p~n",
[?MODULE, Msg], Opts),
tracer_loop(Parent, Opts, State0)
end.
-spec tracer_terminate(_, _, _) -> no_return().
tracer_terminate(Reason, #{tracer_callback := Fun}, State) ->
_ = Fun(terminate, State),
exit(Reason).
%% System callbacks.
-spec system_continue(pid(), _, {cowboy:opts(), any()}) -> no_return().
system_continue(Parent, _, {Opts, State}) ->
tracer_loop(Parent, Opts, State).
-spec system_terminate(any(), _, _, _) -> no_return().
system_terminate(Reason, _, _, {Opts, State}) ->
tracer_terminate(Reason, Opts, State).
-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc :: any().
system_code_change(Misc, _, _, _) ->
{ok, Misc}.

+ 0
- 707
src/wsSrv/cowboy_websocket.erl Vedi File

@ -1,707 +0,0 @@
%% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
%% Cowboy supports versions 7 through 17 of the Websocket drafts.
%% It also supports RFC6455, the proposed standard for Websocket.
-module(cowboy_websocket).
-behaviour(cowboy_sub_protocol).
-export([is_upgrade_request/1]).
-export([upgrade/4]).
-export([upgrade/5]).
-export([takeover/7]).
-export([loop/3]).
-export([system_continue/3]).
-export([system_terminate/4]).
-export([system_code_change/4]).
-type commands() :: [cow_ws:frame()
| {active, boolean()}
| {deflate, boolean()}
| {set_options, map()}
| {shutdown_reason, any()}
].
-export_type([commands/0]).
-type call_result(State) :: {commands(), State} | {commands(), State, hibernate}.
-type deprecated_call_result(State) :: {ok, State}
| {ok, State, hibernate}
| {reply, cow_ws:frame() | [cow_ws:frame()], State}
| {reply, cow_ws:frame() | [cow_ws:frame()], State, hibernate}
| {stop, State}.
-type terminate_reason() :: normal | stop | timeout
| remote | {remote, cow_ws:close_code(), binary()}
| {error, badencoding | badframe | closed | atom()}
| {crash, error | exit | throw, any()}.
-callback init(Req, any())
-> {ok | module(), Req, any()}
| {module(), Req, any(), any()}
when Req :: cowboy_req:req().
-callback websocket_init(State)
-> call_result(State) | deprecated_call_result(State) when State :: any().
-optional_callbacks([websocket_init/1]).
-callback websocket_handle(ping | pong | {text | binary | ping | pong, binary()}, State)
-> call_result(State) | deprecated_call_result(State) when State :: any().
-callback websocket_info(any(), State)
-> call_result(State) | deprecated_call_result(State) when State :: any().
-callback terminate(any(), cowboy_req:req(), any()) -> ok.
-optional_callbacks([terminate/3]).
-type opts() :: #{
active_n => pos_integer(),
compress => boolean(),
deflate_opts => cow_ws:deflate_opts(),
idle_timeout => timeout(),
max_frame_size => non_neg_integer() | infinity,
req_filter => fun((cowboy_req:req()) -> map()),
validate_utf8 => boolean()
}.
-export_type([opts/0]).
-record(state, {
parent :: undefined | pid(),
ref :: ranch:ref(),
socket = undefined :: inet:socket() | {pid(), cowboy_stream:streamid()} | undefined,
transport = undefined :: module() | undefined,
opts = #{} :: opts(),
active = true :: boolean(),
handler :: module(),
key = undefined :: undefined | binary(),
timeout_ref = undefined :: undefined | reference(),
messages = undefined :: undefined | {atom(), atom(), atom()}
| {atom(), atom(), atom(), atom()},
hibernate = false :: boolean(),
frag_state = undefined :: cow_ws:frag_state(),
frag_buffer = <<>> :: binary(),
utf8_state :: cow_ws:utf8_state(),
deflate = true :: boolean(),
extensions = #{} :: map(),
req = #{} :: map(),
shutdown_reason = normal :: any()
}).
%% Because the HTTP/1.1 and HTTP/2 handshakes are so different,
%% this function is necessary to figure out whether a request
%% is trying to upgrade to the Websocket protocol.
-spec is_upgrade_request(cowboy_req:req()) -> boolean().
is_upgrade_request(#{version := 'HTTP/2', method := <<"CONNECT">>, protocol := Protocol}) ->
<<"websocket">> =:= cowboy_bstr:to_lower(Protocol);
is_upgrade_request(Req = #{version := 'HTTP/1.1', method := <<"GET">>}) ->
ConnTokens = cowboy_req:parse_header(<<"connection">>, Req, []),
case lists:member(<<"upgrade">>, ConnTokens) of
false ->
false;
true ->
UpgradeTokens = cowboy_req:parse_header(<<"upgrade">>, Req),
lists:member(<<"websocket">>, UpgradeTokens)
end;
is_upgrade_request(_) ->
false.
%% Stream process.
-spec upgrade(Req, Env, module(), any())
-> {ok, Req, Env}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
upgrade(Req, Env, Handler, HandlerState) ->
upgrade(Req, Env, Handler, HandlerState, #{}).
-spec upgrade(Req, Env, module(), any(), opts())
-> {ok, Req, Env}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
%% @todo Immediately crash if a response has already been sent.
upgrade(Req0 = #{version := Version}, Env, Handler, HandlerState, Opts) ->
FilteredReq = case maps:get(req_filter, Opts, undefined) of
undefined -> maps:with([method, version, scheme, host, port, path, qs, peer], Req0);
FilterFun -> FilterFun(Req0)
end,
Utf8State = case maps:get(validate_utf8, Opts, true) of
true -> 0;
false -> undefined
end,
State0 = #state{opts = Opts, handler = Handler, utf8_state = Utf8State, req = FilteredReq},
try websocket_upgrade(State0, Req0) of
{ok, State, Req} ->
websocket_handshake(State, Req, HandlerState, Env);
%% The status code 426 is specific to HTTP/1.1 connections.
{error, upgrade_required} when Version =:= 'HTTP/1.1' ->
{ok, cowboy_req:reply(426, #{
<<"connection">> => <<"upgrade">>,
<<"upgrade">> => <<"websocket">>
}, Req0), Env};
%% Use a generic 400 error for HTTP/2.
{error, upgrade_required} ->
{ok, cowboy_req:reply(400, Req0), Env}
catch _:_ ->
%% @todo Probably log something here?
%% @todo Test that we can have 2 /ws 400 status code in a row on the same connection.
%% @todo Does this even work?
{ok, cowboy_req:reply(400, Req0), Env}
end.
websocket_upgrade(State, Req = #{version := Version}) ->
case is_upgrade_request(Req) of
false ->
{error, upgrade_required};
true when Version =:= 'HTTP/1.1' ->
Key = cowboy_req:header(<<"sec-websocket-key">>, Req),
false = Key =:= undefined,
websocket_version(State#state{key = Key}, Req);
true ->
websocket_version(State, Req)
end.
websocket_version(State, Req) ->
WsVersion = cowboy_req:parse_header(<<"sec-websocket-version">>, Req),
case WsVersion of
7 -> ok;
8 -> ok;
13 -> ok
end,
websocket_extensions(State, Req#{websocket_version => WsVersion}).
websocket_extensions(State = #state{opts = Opts}, Req) ->
%% @todo We want different options for this. For example
%% * compress everything auto
%% * compress only text auto
%% * compress only binary auto
%% * compress nothing auto (but still enabled it)
%% * disable compression
Compress = maps:get(compress, Opts, false),
case {Compress, cowboy_req:parse_header(<<"sec-websocket-extensions">>, Req)} of
{true, Extensions} when Extensions =/= undefined ->
websocket_extensions(State, Req, Extensions, []);
_ ->
{ok, State, Req}
end.
websocket_extensions(State, Req, [], []) ->
{ok, State, Req};
websocket_extensions(State, Req, [], [<<", ">> | RespHeader]) ->
{ok, State, cowboy_req:set_resp_header(<<"sec-websocket-extensions">>, lists:reverse(RespHeader), Req)};
%% For HTTP/2 we ARE on the controlling process and do NOT want to update the owner.
websocket_extensions(State = #state{opts = Opts, extensions = Extensions},
Req = #{pid := Pid, version := Version},
[{<<"permessage-deflate">>, Params} | Tail], RespHeader) ->
DeflateOpts0 = maps:get(deflate_opts, Opts, #{}),
DeflateOpts = case Version of
'HTTP/1.1' -> DeflateOpts0#{owner => Pid};
_ -> DeflateOpts0
end,
try cow_ws:negotiate_permessage_deflate(Params, Extensions, DeflateOpts) of
{ok, RespExt, Extensions2} ->
websocket_extensions(State#state{extensions = Extensions2},
Req, Tail, [<<", ">>, RespExt | RespHeader]);
ignore ->
websocket_extensions(State, Req, Tail, RespHeader)
catch exit:{error, incompatible_zlib_version, _} ->
websocket_extensions(State, Req, Tail, RespHeader)
end;
websocket_extensions(State = #state{opts = Opts, extensions = Extensions},
Req = #{pid := Pid, version := Version},
[{<<"x-webkit-deflate-frame">>, Params} | Tail], RespHeader) ->
DeflateOpts0 = maps:get(deflate_opts, Opts, #{}),
DeflateOpts = case Version of
'HTTP/1.1' -> DeflateOpts0#{owner => Pid};
_ -> DeflateOpts0
end,
try cow_ws:negotiate_x_webkit_deflate_frame(Params, Extensions, DeflateOpts) of
{ok, RespExt, Extensions2} ->
websocket_extensions(State#state{extensions = Extensions2},
Req, Tail, [<<", ">>, RespExt | RespHeader]);
ignore ->
websocket_extensions(State, Req, Tail, RespHeader)
catch exit:{error, incompatible_zlib_version, _} ->
websocket_extensions(State, Req, Tail, RespHeader)
end;
websocket_extensions(State, Req, [_ | Tail], RespHeader) ->
websocket_extensions(State, Req, Tail, RespHeader).
-spec websocket_handshake(#state{}, Req, any(), Env)
-> {ok, Req, Env}
when Req :: cowboy_req:req(), Env :: cowboy_middleware:env().
websocket_handshake(State = #state{key = Key},
Req = #{version := 'HTTP/1.1', pid := Pid, streamid := StreamID},
HandlerState, Env) ->
Challenge = base64:encode(crypto:hash(sha,
<<Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11">>)),
%% @todo We don't want date and server headers.
Headers = cowboy_req:response_headers(#{
<<"connection">> => <<"Upgrade">>,
<<"upgrade">> => <<"websocket">>,
<<"sec-websocket-accept">> => Challenge
}, Req),
Pid ! {{Pid, StreamID}, {switch_protocol, Headers, ?MODULE, {State, HandlerState}}},
{ok, Req, Env};
%% For HTTP/2 we do not let the process die, we instead keep it
%% for the Websocket stream. This is because in HTTP/2 we only
%% have a stream, it doesn't take over the whole connection.
websocket_handshake(State, Req = #{ref := Ref, pid := Pid, streamid := StreamID},
HandlerState, _Env) ->
%% @todo We don't want date and server headers.
Headers = cowboy_req:response_headers(#{}, Req),
Pid ! {{Pid, StreamID}, {switch_protocol, Headers, ?MODULE, {State, HandlerState}}},
takeover(Pid, Ref, {Pid, StreamID}, undefined, undefined, <<>>,
{State, HandlerState}).
%% Connection process.
-record(ps_header, {
buffer = <<>> :: binary()
}).
-record(ps_payload, {
type :: cow_ws:frame_type(),
len :: non_neg_integer(),
mask_key :: cow_ws:mask_key(),
rsv :: cow_ws:rsv(),
close_code = undefined :: undefined | cow_ws:close_code(),
unmasked = <<>> :: binary(),
unmasked_len = 0 :: non_neg_integer(),
buffer = <<>> :: binary()
}).
-type parse_state() :: #ps_header{} | #ps_payload{}.
-spec takeover(pid(), ranch:ref(), inet:socket() | {pid(), cowboy_stream:streamid()},
module() | undefined, any(), binary(),
{#state{}, any()}) -> no_return().
takeover(Parent, Ref, Socket, Transport, _Opts, Buffer,
{State0 = #state{handler = Handler}, HandlerState}) ->
%% @todo We should have an option to disable this behavior.
ranch:remove_connection(Ref),
Messages = case Transport of
undefined -> undefined;
_ -> Transport:messages()
end,
State = loop_timeout(State0#state{parent = Parent,
ref = Ref, socket = Socket, transport = Transport,
key = undefined, messages = Messages}),
%% We call parse_header/3 immediately because there might be
%% some data in the buffer that was sent along with the handshake.
%% While it is not allowed by the protocol to send frames immediately,
%% we still want to process that data if any.
case erlang:function_exported(Handler, websocket_init, 1) of
true -> handler_call(State, HandlerState, #ps_header{buffer = Buffer},
websocket_init, undefined, fun after_init/3);
false -> after_init(State, HandlerState, #ps_header{buffer = Buffer})
end.
after_init(State = #state{active = true}, HandlerState, ParseState) ->
%% Enable active,N for HTTP/1.1, and auto read_body for HTTP/2.
%% We must do this only after calling websocket_init/1 (if any)
%% to give the handler a chance to disable active mode immediately.
setopts_active(State),
maybe_read_body(State),
parse_header(State, HandlerState, ParseState);
after_init(State, HandlerState, ParseState) ->
parse_header(State, HandlerState, ParseState).
%% We have two ways of reading the body for Websocket. For HTTP/1.1
%% we have full control of the socket and can therefore use active,N.
%% For HTTP/2 we are just a stream, and are instead using read_body
%% (automatic mode). Technically HTTP/2 will only go passive after
%% receiving the next data message, while HTTP/1.1 goes passive
%% immediately but there might still be data to be processed in
%% the message queue.
setopts_active(#state{transport = undefined}) ->
ok;
setopts_active(#state{socket = Socket, transport = Transport, opts = Opts}) ->
N = maps:get(active_n, Opts, 100),
Transport:setopts(Socket, [{active, N}]).
maybe_read_body(#state{socket = Stream = {Pid, _}, transport = undefined, active = true}) ->
%% @todo Keep Ref around.
ReadBodyRef = make_ref(),
Pid ! {Stream, {read_body, self(), ReadBodyRef, auto, infinity}},
ok;
maybe_read_body(_) ->
ok.
active(State) ->
setopts_active(State),
maybe_read_body(State),
State#state{active = true}.
passive(State = #state{transport = undefined}) ->
%% Unfortunately we cannot currently cancel read_body.
%% But that's OK, we will just stop reading the body
%% after the next message.
State#state{active = false};
passive(State = #state{socket = Socket, transport = Transport, messages = Messages}) ->
Transport:setopts(Socket, [{active, false}]),
flush_passive(Socket, Messages),
State#state{active = false}.
flush_passive(Socket, Messages) ->
receive
{Passive, Socket} when Passive =:= element(4, Messages);
%% Hardcoded for compatibility with Ranch 1.x.
Passive =:= tcp_passive; Passive =:= ssl_passive ->
flush_passive(Socket, Messages)
after 0 ->
ok
end.
before_loop(State = #state{hibernate = true}, HandlerState, ParseState) ->
proc_lib:hibernate(?MODULE, loop,
[State#state{hibernate = false}, HandlerState, ParseState]);
before_loop(State, HandlerState, ParseState) ->
loop(State, HandlerState, ParseState).
-spec loop_timeout(#state{}) -> #state{}.
loop_timeout(State = #state{opts = Opts, timeout_ref = PrevRef}) ->
_ = case PrevRef of
undefined -> ignore;
PrevRef -> erlang:cancel_timer(PrevRef)
end,
case maps:get(idle_timeout, Opts, 60000) of
infinity ->
State#state{timeout_ref = undefined};
Timeout ->
TRef = erlang:start_timer(Timeout, self(), ?MODULE),
State#state{timeout_ref = TRef}
end.
-spec loop(#state{}, any(), parse_state()) -> no_return().
loop(State = #state{parent = Parent, socket = Socket, messages = Messages,
timeout_ref = TRef}, HandlerState, ParseState) ->
receive
%% Socket messages. (HTTP/1.1)
{OK, Socket, Data} when OK =:= element(1, Messages) ->
State2 = loop_timeout(State),
parse(State2, HandlerState, ParseState, Data);
{Closed, Socket} when Closed =:= element(2, Messages) ->
terminate(State, HandlerState, {error, closed});
{Error, Socket, Reason} when Error =:= element(3, Messages) ->
terminate(State, HandlerState, {error, Reason});
{Passive, Socket} when Passive =:= element(4, Messages);
%% Hardcoded for compatibility with Ranch 1.x.
Passive =:= tcp_passive; Passive =:= ssl_passive ->
setopts_active(State),
loop(State, HandlerState, ParseState);
%% Body reading messages. (HTTP/2)
{request_body, _Ref, nofin, Data} ->
maybe_read_body(State),
State2 = loop_timeout(State),
parse(State2, HandlerState, ParseState, Data);
%% @todo We need to handle this case as if it was an {error, closed}
%% but not before we finish processing frames. We probably should have
%% a check in before_loop to let us stop looping if a flag is set.
{request_body, _Ref, fin, _, Data} ->
maybe_read_body(State),
State2 = loop_timeout(State),
parse(State2, HandlerState, ParseState, Data);
%% Timeouts.
{timeout, TRef, ?MODULE} ->
websocket_close(State, HandlerState, timeout);
{timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
before_loop(State, HandlerState, ParseState);
%% System messages.
{'EXIT', Parent, Reason} ->
%% @todo We should exit gracefully.
exit(Reason);
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent, ?MODULE, [],
{State, HandlerState, ParseState});
%% Calls from supervisor module.
{'$gen_call', From, Call} ->
cowboy_children:handle_supervisor_call(Call, From, [], ?MODULE),
before_loop(State, HandlerState, ParseState);
Message ->
handler_call(State, HandlerState, ParseState,
websocket_info, Message, fun before_loop/3)
end.
parse(State, HandlerState, PS = #ps_header{buffer = Buffer}, Data) ->
parse_header(State, HandlerState, PS#ps_header{
buffer = <<Buffer/binary, Data/binary>>});
parse(State, HandlerState, PS = #ps_payload{buffer = Buffer}, Data) ->
parse_payload(State, HandlerState, PS#ps_payload{buffer = <<>>},
<<Buffer/binary, Data/binary>>).
parse_header(State = #state{opts = Opts, frag_state = FragState, extensions = Extensions},
HandlerState, ParseState = #ps_header{buffer = Data}) ->
MaxFrameSize = maps:get(max_frame_size, Opts, infinity),
case cow_ws:parse_header(Data, Extensions, FragState) of
%% All frames sent from the client to the server are masked.
{_, _, _, _, undefined, _} ->
websocket_close(State, HandlerState, {error, badframe});
{_, _, _, Len, _, _} when Len > MaxFrameSize ->
websocket_close(State, HandlerState, {error, badsize});
{Type, FragState2, Rsv, Len, MaskKey, Rest} ->
parse_payload(State#state{frag_state = FragState2}, HandlerState,
#ps_payload{type = Type, len = Len, mask_key = MaskKey, rsv = Rsv}, Rest);
more ->
before_loop(State, HandlerState, ParseState);
error ->
websocket_close(State, HandlerState, {error, badframe})
end.
parse_payload(State = #state{frag_state = FragState, utf8_state = Incomplete, extensions = Extensions},
HandlerState, ParseState = #ps_payload{
type = Type, len = Len, mask_key = MaskKey, rsv = Rsv,
unmasked = Unmasked, unmasked_len = UnmaskedLen}, Data) ->
case cow_ws:parse_payload(Data, MaskKey, Incomplete, UnmaskedLen,
Type, Len, FragState, Extensions, Rsv) of
{ok, CloseCode, Payload, Utf8State, Rest} ->
dispatch_frame(State#state{utf8_state = Utf8State}, HandlerState,
ParseState#ps_payload{unmasked = <<Unmasked/binary, Payload/binary>>,
close_code = CloseCode}, Rest);
{ok, Payload, Utf8State, Rest} ->
dispatch_frame(State#state{utf8_state = Utf8State}, HandlerState,
ParseState#ps_payload{unmasked = <<Unmasked/binary, Payload/binary>>},
Rest);
{more, CloseCode, Payload, Utf8State} ->
before_loop(State#state{utf8_state = Utf8State}, HandlerState,
ParseState#ps_payload{len = Len - byte_size(Data), close_code = CloseCode,
unmasked = <<Unmasked/binary, Payload/binary>>,
unmasked_len = UnmaskedLen + byte_size(Data)});
{more, Payload, Utf8State} ->
before_loop(State#state{utf8_state = Utf8State}, HandlerState,
ParseState#ps_payload{len = Len - byte_size(Data),
unmasked = <<Unmasked/binary, Payload/binary>>,
unmasked_len = UnmaskedLen + byte_size(Data)});
Error = {error, _Reason} ->
websocket_close(State, HandlerState, Error)
end.
dispatch_frame(State = #state{opts = Opts, frag_state = FragState, frag_buffer = SoFar}, HandlerState,
#ps_payload{type = Type0, unmasked = Payload0, close_code = CloseCode0}, RemainingData) ->
MaxFrameSize = maps:get(max_frame_size, Opts, infinity),
case cow_ws:make_frame(Type0, Payload0, CloseCode0, FragState) of
%% @todo Allow receiving fragments.
{fragment, _, _, Payload} when byte_size(Payload) + byte_size(SoFar) > MaxFrameSize ->
websocket_close(State, HandlerState, {error, badsize});
{fragment, nofin, _, Payload} ->
parse_header(State#state{frag_buffer = <<SoFar/binary, Payload/binary>>},
HandlerState, #ps_header{buffer = RemainingData});
{fragment, fin, Type, Payload} ->
handler_call(State#state{frag_state = undefined, frag_buffer = <<>>}, HandlerState,
#ps_header{buffer = RemainingData},
websocket_handle, {Type, <<SoFar/binary, Payload/binary>>},
fun parse_header/3);
close ->
websocket_close(State, HandlerState, remote);
{close, CloseCode, Payload} ->
websocket_close(State, HandlerState, {remote, CloseCode, Payload});
Frame = ping ->
transport_send(State, nofin, frame(pong, State)),
handler_call(State, HandlerState,
#ps_header{buffer = RemainingData},
websocket_handle, Frame, fun parse_header/3);
Frame = {ping, Payload} ->
transport_send(State, nofin, frame({pong, Payload}, State)),
handler_call(State, HandlerState,
#ps_header{buffer = RemainingData},
websocket_handle, Frame, fun parse_header/3);
Frame ->
handler_call(State, HandlerState,
#ps_header{buffer = RemainingData},
websocket_handle, Frame, fun parse_header/3)
end.
handler_call(State = #state{handler = Handler}, HandlerState,
ParseState, Callback, Message, NextState) ->
try case Callback of
websocket_init -> Handler:websocket_init(HandlerState);
_ -> Handler:Callback(Message, HandlerState)
end of
{Commands, HandlerState2} when is_list(Commands) ->
handler_call_result(State,
HandlerState2, ParseState, NextState, Commands);
{Commands, HandlerState2, hibernate} when is_list(Commands) ->
handler_call_result(State#state{hibernate = true},
HandlerState2, ParseState, NextState, Commands);
%% The following call results are deprecated.
{ok, HandlerState2} ->
NextState(State, HandlerState2, ParseState);
{ok, HandlerState2, hibernate} ->
NextState(State#state{hibernate = true}, HandlerState2, ParseState);
{reply, Payload, HandlerState2} ->
case websocket_send(Payload, State) of
ok ->
NextState(State, HandlerState2, ParseState);
stop ->
terminate(State, HandlerState2, stop);
Error = {error, _} ->
terminate(State, HandlerState2, Error)
end;
{reply, Payload, HandlerState2, hibernate} ->
case websocket_send(Payload, State) of
ok ->
NextState(State#state{hibernate = true},
HandlerState2, ParseState);
stop ->
terminate(State, HandlerState2, stop);
Error = {error, _} ->
terminate(State, HandlerState2, Error)
end;
{stop, HandlerState2} ->
websocket_close(State, HandlerState2, stop)
catch Class:Reason:Stacktrace ->
websocket_send_close(State, {crash, Class, Reason}),
handler_terminate(State, HandlerState, {crash, Class, Reason}),
erlang:raise(Class, Reason, Stacktrace)
end.
-spec handler_call_result(#state{}, any(), parse_state(), fun(), commands()) -> no_return().
handler_call_result(State0, HandlerState, ParseState, NextState, Commands) ->
case commands(Commands, State0, []) of
{ok, State} ->
NextState(State, HandlerState, ParseState);
{stop, State} ->
terminate(State, HandlerState, stop);
{Error = {error, _}, State} ->
terminate(State, HandlerState, Error)
end.
commands([], State, []) ->
{ok, State};
commands([], State, Data) ->
Result = transport_send(State, nofin, lists:reverse(Data)),
{Result, State};
commands([{active, Active} | Tail], State0 = #state{active = Active0}, Data) when is_boolean(Active) ->
State = if
Active, not Active0 ->
active(State0);
Active0, not Active ->
passive(State0);
true ->
State0
end,
commands(Tail, State#state{active = Active}, Data);
commands([{deflate, Deflate} | Tail], State, Data) when is_boolean(Deflate) ->
commands(Tail, State#state{deflate = Deflate}, Data);
commands([{set_options, SetOpts} | Tail], State0 = #state{opts = Opts}, Data) ->
State = case SetOpts of
#{idle_timeout := IdleTimeout} ->
loop_timeout(State0#state{opts = Opts#{idle_timeout => IdleTimeout}});
_ ->
State0
end,
commands(Tail, State, Data);
commands([{shutdown_reason, ShutdownReason} | Tail], State, Data) ->
commands(Tail, State#state{shutdown_reason = ShutdownReason}, Data);
commands([Frame | Tail], State, Data0) ->
Data = [frame(Frame, State) | Data0],
case is_close_frame(Frame) of
true ->
_ = transport_send(State, fin, lists:reverse(Data)),
{stop, State};
false ->
commands(Tail, State, Data)
end.
transport_send(#state{socket = Stream = {Pid, _}, transport = undefined}, IsFin, Data) ->
Pid ! {Stream, {data, IsFin, Data}},
ok;
transport_send(#state{socket = Socket, transport = Transport}, _, Data) ->
Transport:send(Socket, Data).
-spec websocket_send(cow_ws:frame(), #state{}) -> ok | stop | {error, atom()}.
websocket_send(Frames, State) when is_list(Frames) ->
websocket_send_many(Frames, State, []);
websocket_send(Frame, State) ->
Data = frame(Frame, State),
case is_close_frame(Frame) of
true ->
_ = transport_send(State, fin, Data),
stop;
false ->
transport_send(State, nofin, Data)
end.
websocket_send_many([], State, Acc) ->
transport_send(State, nofin, lists:reverse(Acc));
websocket_send_many([Frame | Tail], State, Acc0) ->
Acc = [frame(Frame, State) | Acc0],
case is_close_frame(Frame) of
true ->
_ = transport_send(State, fin, lists:reverse(Acc)),
stop;
false ->
websocket_send_many(Tail, State, Acc)
end.
is_close_frame(close) -> true;
is_close_frame({close, _}) -> true;
is_close_frame({close, _, _}) -> true;
is_close_frame(_) -> false.
-spec websocket_close(#state{}, any(), terminate_reason()) -> no_return().
websocket_close(State, HandlerState, Reason) ->
websocket_send_close(State, Reason),
terminate(State, HandlerState, Reason).
websocket_send_close(State, Reason) ->
_ = case Reason of
Normal when Normal =:= stop; Normal =:= timeout ->
transport_send(State, fin, frame({close, 1000, <<>>}, State));
{error, badframe} ->
transport_send(State, fin, frame({close, 1002, <<>>}, State));
{error, badencoding} ->
transport_send(State, fin, frame({close, 1007, <<>>}, State));
{error, badsize} ->
transport_send(State, fin, frame({close, 1009, <<>>}, State));
{crash, _, _} ->
transport_send(State, fin, frame({close, 1011, <<>>}, State));
remote ->
transport_send(State, fin, frame(close, State));
{remote, Code, _} ->
transport_send(State, fin, frame({close, Code, <<>>}, State))
end,
ok.
%% Don't compress frames while deflate is disabled.
frame(Frame, #state{deflate = false, extensions = Extensions}) ->
cow_ws:frame(Frame, Extensions#{deflate => false});
frame(Frame, #state{extensions = Extensions}) ->
cow_ws:frame(Frame, Extensions).
-spec terminate(#state{}, any(), terminate_reason()) -> no_return().
terminate(State = #state{shutdown_reason = Shutdown}, HandlerState, Reason) ->
handler_terminate(State, HandlerState, Reason),
case Shutdown of
normal -> exit(normal);
_ -> exit({shutdown, Shutdown})
end.
handler_terminate(#state{handler = Handler, req = Req}, HandlerState, Reason) ->
cowboy_handler:terminate(Reason, Req, HandlerState, Handler).
%% System callbacks.
-spec system_continue(_, _, {#state{}, any(), parse_state()}) -> no_return().
system_continue(_, _, {State, HandlerState, ParseState}) ->
loop(State, HandlerState, ParseState).
-spec system_terminate(any(), _, _, {#state{}, any(), parse_state()}) -> no_return().
system_terminate(Reason, _, _, {State, HandlerState, _}) ->
%% @todo We should exit gracefully, if possible.
terminate(State, HandlerState, Reason).
-spec system_code_change(Misc, _, _, _)
-> {ok, Misc} when Misc :: {#state{}, any(), parse_state()}.
system_code_change(Misc, _, _, _) ->
{ok, Misc}.

+ 285
- 0
src/wsSrv/elli.erl Vedi File

@ -0,0 +1,285 @@
%% @doc: Elli acceptor manager
%%
%% This gen_server owns the listen socket and manages the processes
%% accepting on that socket. When a process waiting for accept gets a
%% request, it notifies this gen_server so we can start up another
%% acceptor.
%%
-module(elli).
-behaviour(gen_server).
-include("elli.hrl").
-include("elli_util.hrl").
%% API
-export([start_link/0,
start_link/1,
stop/1,
get_acceptors/1,
get_open_reqs/1,
get_open_reqs/2,
set_callback/3
]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-export_type([req/0, http_method/0, body/0, headers/0, response_code/0]).
%% @type req(). A record representing an HTTP request.
-type req() :: #req{}.
%% @type http_method(). An uppercase atom representing a known HTTP verb or a
%% binary for other verbs.
-type http_method() :: 'OPTIONS' | 'GET' | 'HEAD' | 'POST'
| 'PUT' | 'DELETE' | 'TRACE' | binary().
%% @type body(). A binary or iolist.
-type body() :: binary() | iolist().
-type header() :: {Key::binary(), Value::binary() | string()}.
-type headers() :: [header()].
-type response_code() :: 100..999.
-record(state, {socket :: elli_tcp:socket(),
acceptors :: ets:tid(),
open_reqs = 0 :: non_neg_integer(),
options = [] :: [{_, _}], % TODO: refine
callback :: elli_handler:callback()
}).
%% @type state(). Internal state.
-opaque state() :: #state{}.
-export_type([state/0]).
%%%===================================================================
%%% API
%%%===================================================================
-spec start_link() -> Result when
Result :: {ok, Pid} | ignore | {error, Error},
Pid :: pid(),
Error :: {already_started, Pid} | term().
%% @equiv start_link({@EXAMPLE_CONF})
%% @doc Create an Elli server process as part of a supervision tree, using the
%% default configuration.
start_link() -> start_link(?EXAMPLE_CONF).
-spec start_link(Opts) -> Result when
Opts :: [{_, _}], % TODO: refine
Result :: {ok, Pid} | ignore | {error, Error},
Pid :: pid(),
Error :: {already_started, Pid} | term().
start_link(Opts) ->
valid_callback(required_opt(callback, Opts))
orelse throw(invalid_callback),
case proplists:get_value(name, Opts) of
undefined ->
gen_server:start_link(?MODULE, [Opts], []);
Name ->
gen_server:start_link(Name, ?MODULE, [Opts], [])
end.
-spec get_acceptors(atom()) -> {reply, {ok, [ets:tid()]}, state()}.
get_acceptors(S) ->
gen_server:call(S, get_acceptors).
-spec get_open_reqs(S :: atom()) -> {reply, {ok, non_neg_integer()}, state()}.
%% @equiv get_open_reqs(S, 5000)
get_open_reqs(S) ->
get_open_reqs(S, 5000).
-spec get_open_reqs(S :: atom(), Timeout :: non_neg_integer()) -> Reply when
Reply :: {reply, {ok, non_neg_integer()}, state()}.
get_open_reqs(S, Timeout) ->
gen_server:call(S, get_open_reqs, Timeout).
-spec set_callback(S, Callback, CallbackArgs) -> Reply when
S :: atom(),
Callback :: elli_handler:callback_mod(),
CallbackArgs :: elli_handler:callback_args(),
Reply :: {reply, ok, state()}.
set_callback(S, Callback, CallbackArgs) ->
valid_callback(Callback) orelse throw(invalid_callback),
gen_server:call(S, {set_callback, Callback, CallbackArgs}).
%% @doc Stop `Server'.
-spec stop(Server :: atom()) -> {stop, normal, ok, state()}.
stop(S) ->
gen_server:call(S, stop).
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%% @hidden
-spec init([Opts :: [{_, _}]]) -> {ok, state()}.
init([Opts]) ->
%% Use the exit signal from the acceptor processes to know when
%% they exit
process_flag(trap_exit, true),
Callback = required_opt(callback, Opts),
CallbackArgs = proplists:get_value(callback_args, Opts),
IPAddress = proplists:get_value(ip, Opts, {0, 0, 0, 0}),
Port = proplists:get_value(port, Opts, 8080),
MinAcceptors = proplists:get_value(min_acceptors, Opts, 20),
UseSSL = proplists:get_value(ssl, Opts, false),
KeyFile = proplists:get_value(keyfile, Opts),
CertFile = proplists:get_value(certfile, Opts),
SockType = ?IF(UseSSL, ssl, plain),
SSLSockOpts = ?IF(UseSSL,
[{keyfile, KeyFile}, {certfile, CertFile}],
[]),
AcceptTimeout = proplists:get_value(accept_timeout, Opts, 10000),
RequestTimeout = proplists:get_value(request_timeout, Opts, 60000),
HeaderTimeout = proplists:get_value(header_timeout, Opts, 10000),
BodyTimeout = proplists:get_value(body_timeout, Opts, 30000),
MaxBodySize = proplists:get_value(max_body_size, Opts, 1024000),
Options = [{accept_timeout, AcceptTimeout},
{request_timeout, RequestTimeout},
{header_timeout, HeaderTimeout},
{body_timeout, BodyTimeout},
{max_body_size, MaxBodySize}],
%% Notify the handler that we are about to start accepting
%% requests, so it can create necessary supporting processes, ETS
%% tables, etc.
ok = Callback:handle_event(elli_startup, [], CallbackArgs),
{ok, Socket} = elli_tcp:listen(SockType, Port, [binary,
{ip, IPAddress},
{reuseaddr, true},
{backlog, 32768},
{packet, raw},
{active, false}
| SSLSockOpts]),
Acceptors = ets:new(acceptors, [private, set]),
[begin
Pid = elli_http:start_link(self(), Socket, Options,
{Callback, CallbackArgs}),
ets:insert(Acceptors, {Pid})
end
|| _ <- lists:seq(1, MinAcceptors)],
{ok, #state{socket = Socket,
acceptors = Acceptors,
open_reqs = 0,
options = Options,
callback = {Callback, CallbackArgs}}}.
%% @hidden
-spec handle_call(get_acceptors, {pid(), _Tag}, state()) ->
{reply, {ok, [ets:tid()]}, state()};
(get_open_reqs, {pid(), _Tag}, state()) ->
{reply, {ok, OpenReqs :: non_neg_integer()}, state()};
(stop, {pid(), _Tag}, state()) -> {stop, normal, ok, state()};
({set_callback, Mod, Args}, {pid(), _Tag}, state()) ->
{reply, ok, state()} when
Mod :: elli_handler:callback_mod(),
Args :: elli_handler:callback_args().
handle_call(get_acceptors, _From, State) ->
Acceptors = [Pid || {Pid} <- ets:tab2list(State#state.acceptors)],
{reply, {ok, Acceptors}, State};
handle_call(get_open_reqs, _From, State) ->
{reply, {ok, State#state.open_reqs}, State};
handle_call({set_callback, Callback, CallbackArgs}, _From, State) ->
ok = Callback:handle_event(elli_reconfigure, [], CallbackArgs),
{reply, ok, State#state{callback = {Callback, CallbackArgs}}};
handle_call(stop, _From, State) ->
{stop, normal, ok, State}.
%% @hidden
-spec handle_cast(accepted | _Msg, State0) -> {noreply, State1} when
State0 :: state(),
State1 :: state().
handle_cast(accepted, State) ->
{noreply, start_add_acceptor(State)};
handle_cast(_Msg, State) ->
{noreply, State}.
%% @hidden
-spec handle_info({'EXIT', _Pid, Reason}, State0) -> Result when
State0 :: state(),
Reason :: {error, emfile},
Result :: {stop, emfile, State0}
| {noreply, State1 :: state()}.
handle_info({'EXIT', _Pid, {error, emfile}}, State) ->
?LOG_ERROR("No more file descriptors, shutting down~n"),
{stop, emfile, State};
handle_info({'EXIT', Pid, normal}, State) ->
{noreply, remove_acceptor(State, Pid)};
handle_info({'EXIT', Pid, Reason}, State) ->
?LOG_ERROR("Elli request (pid ~p) unexpectedly crashed:~n~p~n", [Pid, Reason]),
{noreply, remove_acceptor(State, Pid)}.
%% @hidden
-spec terminate(_Reason, _State) -> ok.
terminate(_Reason, _State) ->
ok.
%% @hidden
-spec code_change(_OldVsn, State, _Extra) -> {ok, State} when State :: state().
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
-spec remove_acceptor(State0 :: state(), Pid :: pid()) -> State1 :: state().
remove_acceptor(State, Pid) ->
ets:delete(State#state.acceptors, Pid),
dec_open_reqs(State).
-spec start_add_acceptor(State0 :: state()) -> State1 :: state().
start_add_acceptor(State) ->
Pid = elli_http:start_link(self(), State#state.socket,
State#state.options, State#state.callback),
add_acceptor(State, Pid).
-spec add_acceptor(State0 :: state(), Pid :: pid()) -> State1 :: state().
add_acceptor(#state{acceptors = As} = State, Pid) ->
ets:insert(As, {Pid}),
inc_open_reqs(State).
-spec required_opt(Name, Opts) -> Value when
Name :: any(),
Opts :: proplists:proplist(),
Value :: term().
required_opt(Name, Opts) ->
case proplists:get_value(Name, Opts) of
undefined ->
throw(badarg);
Value ->
Value
end.
-spec valid_callback(Mod :: module()) -> Exported :: boolean().
valid_callback(Mod) ->
lists:member({handle, 2}, Mod:module_info(exports)) andalso
lists:member({handle_event, 3}, Mod:module_info(exports)).
-spec dec_open_reqs(State0 :: state()) -> State1 :: state().
dec_open_reqs(#state{open_reqs = OpenReqs} = State) ->
State#state{open_reqs = OpenReqs - 1}.
-spec inc_open_reqs(State0 :: state()) -> State1 :: state().
inc_open_reqs(#state{open_reqs = OpenReqs} = State) ->
State#state{open_reqs = OpenReqs + 1}.

+ 329
- 0
src/wsSrv/elli_example_callback.erl Vedi File

@ -0,0 +1,329 @@
%%% @doc: Elli example callback
%%%
%%% Your callback needs to implement two functions, {@link handle/2} and
%%% {@link handle_event/3}. For every request, Elli will call your handle
%%% function with the request. When an event happens, like Elli
%%% completed a request, there was a parsing error or your handler
%%% threw an error, {@link handle_event/3} is called.
-module(elli_example_callback).
-export([handle/2, handle_event/3]).
-export([chunk_loop/1]).
-include("elli.hrl").
-include("elli_util.hrl").
-behaviour(elli_handler).
-include_lib("kernel/include/file.hrl").
%%
%% ELLI REQUEST CALLBACK
%%
%% @doc Handle a `Req'uest.
%% Delegate to our handler function.
%% @see handle/3
-spec handle(Req, _Args) -> Result when
Req :: elli:req(),
_Args :: elli_handler:callback_args(),
Result :: elli_handler:result().
handle(Req, _Args) -> handle(Req#req.method, elli_request:path(Req), Req).
%% @doc Route `Method' and `Path' to the appropriate clause.
%%
%% `ok' can be used instead of `200' to signal success.
%%
%% If you return any of the following HTTP headers, you can
%% override the default behaviour of Elli:
%%
%% * **Connection**: By default Elli will use `keep-alive' if the protocol
%% supports it, setting `<<"close">>' will close the
%% connection immediately after Elli has sent the
%% response. If the client has already sent pipelined
%% requests, these will be discarded.
%%
%% * **Content-Length**: By default Elli looks at the size of the body you
%% returned to determine the `Content-Length' header.
%% Explicitly including your own `Content-Length' (with
%% the value as `integer()', `binary()' or `list()')
%% allows you to return an empty body. Useful for
%% implementing the `"304 Not Modified"' response.
%%
%% @see elli_request:get_arg/3
%% @see elli_request:post_arg/3
%% @see elli_request:post_arg_decoded/3
%% @see elli_request:get_header/3
%% @see elli_request:get_arg_decoded/3
%% @see elli_request:get_args_decoded/1
%% @see elli_util:file_size/1
%% @see elli_request:get_range/1
%% @see elli_request:normalize_range/2
%% @see elli_request:encode_range/2
%% @see elli_request:chunk_ref/1
%% @see chunk_loop/1
-spec handle(Method, Path, Req) -> elli_handler:result() when
Method :: elli:http_method(),
Path :: [binary()],
Req :: elli:req().
handle('GET', [<<"hello">>, <<"world">>], _Req) ->
%% Reply with a normal response.
timer:sleep(1000),
{ok, [], <<"Hello World!">>};
handle('GET', [<<"hello">>], Req) ->
%% Fetch a GET argument from the URL.
Name = elli_request:get_arg(<<"name">>, Req, <<"undefined">>),
{ok, [], <<"Hello ", Name/binary>>};
handle('POST', [<<"hello">>], Req) ->
%% Fetch a POST argument from the POST body.
Name = elli_request:post_arg(<<"name">>, Req, <<"undefined">>),
%% Fetch and decode
City = elli_request:post_arg_decoded(<<"city">>, Req, <<"undefined">>),
{ok, [], <<"Hello ", Name/binary, " of ", City/binary>>};
handle('GET', [<<"hello">>, <<"iolist">>], Req) ->
%% Iolists will be kept as iolists all the way to the socket.
Name = elli_request:get_arg(<<"name">>, Req),
{ok, [], [<<"Hello ">>, Name]};
handle('GET', [<<"type">>], Req) ->
Name = elli_request:get_arg(<<"name">>, Req),
%% Fetch a header.
case elli_request:get_header(<<"Accept">>, Req, <<"text/plain">>) of
<<"text/plain">> ->
{ok, [{<<"content-type">>, <<"text/plain; charset=ISO-8859-1">>}],
<<"name: ", Name/binary>>};
<<"application/json">> ->
{ok, [{<<"content-type">>,
<<"application/json; charset=ISO-8859-1">>}],
<<"{\"name\" : \"", Name/binary, "\"}">>}
end;
handle('GET', [<<"headers.html">>], _Req) ->
%% Set custom headers, for example 'Content-Type'
{ok, [{<<"X-Custom">>, <<"foobar">>}], <<"see headers">>};
%% See note in function doc re: overriding Elli's default behaviour
%% via Connection and Content-Length headers.
handle('GET', [<<"user">>, <<"defined">>, <<"behaviour">>], _Req) ->
{304, [{<<"Connection">>, <<"close">>},
{<<"Content-Length">>, <<"123">>}], <<"ignored">>};
handle('GET', [<<"user">>, <<"content-length">>], _Req) ->
{200, [{<<"Content-Length">>, 123}], <<"foobar">>};
handle('GET', [<<"crash">>], _Req) ->
%% Throwing an exception results in a 500 response and
%% request_throw being called
throw(foobar);
handle('GET', [<<"decoded-hello">>], Req) ->
%% Fetch a URI decoded GET argument from the URL.
Name = elli_request:get_arg_decoded(<<"name">>, Req, <<"undefined">>),
{ok, [], <<"Hello ", Name/binary>>};
handle('GET', [<<"decoded-list">>], Req) ->
%% Fetch a URI decoded GET argument from the URL.
[{<<"name">>, Name}, {<<"foo">>, true}] =
elli_request:get_args_decoded(Req),
{ok, [], <<"Hello ", Name/binary>>};
handle('GET', [<<"sendfile">>], _Req) ->
%% Returning {file, "/path/to/file"} instead of the body results
%% in Elli using sendfile.
F = "README.md",
{ok, [], {file, F}};
handle('GET', [<<"send_no_file">>], _Req) ->
%% Returning {file, "/path/to/file"} instead of the body results
%% in Elli using sendfile.
F = "README",
{ok, [], {file, F}};
handle('GET', [<<"sendfile">>, <<"error">>], _Req) ->
F = "test",
{ok, [], {file, F}};
handle('GET', [<<"sendfile">>, <<"range">>], Req) ->
%% Read the Range header of the request and use the normalized
%% range with sendfile, otherwise send the entire file when
%% no range is present, or respond with a 416 if the range is invalid.
F = "README.md",
{ok, [], {file, F, elli_request:get_range(Req)}};
handle('GET', [<<"compressed">>], _Req) ->
%% Body with a byte size over 1024 are automatically gzipped by
%% elli_middleware_compress
{ok, binary:copy(<<"Hello World!">>, 86)};
handle('GET', [<<"compressed-io_list">>], _Req) ->
%% Body with a iolist size over 1024 are automatically gzipped by
%% elli_middleware_compress
{ok, lists:duplicate(86, [<<"Hello World!">>])};
handle('HEAD', [<<"head">>], _Req) ->
{200, [], <<"body must be ignored">>};
handle('GET', [<<"chunked">>], Req) ->
%% Start a chunked response for streaming real-time events to the
%% browser.
%%
%% Calling elli_request:send_chunk(ChunkRef, Body) will send that
%% part to the client. elli_request:close_chunk(ChunkRef) will
%% close the response.
%%
%% Return immediately {chunk, Headers} to signal we want to chunk.
Ref = elli_request:chunk_ref(Req),
spawn(fun() -> ?MODULE:chunk_loop(Ref) end),
{chunk, [{<<"Content-Type">>, <<"text/event-stream">>}]};
handle('GET', [<<"shorthand">>], _Req) ->
{200, <<"hello">>};
handle('GET', [<<"ip">>], Req) ->
{<<"200 OK">>, elli_request:peer(Req)};
handle('GET', [<<"304">>], _Req) ->
%% A "Not Modified" response is exactly like a normal response (so
%% Content-Length is included), but the body will not be sent.
{304, [{<<"Etag">>, <<"foobar">>}], <<"Ignored">>};
handle('GET', [<<"302">>], _Req) ->
{302, [{<<"Location">>, <<"/hello/world">>}], <<>>};
handle('GET', [<<"403">>], _Req) ->
%% Exceptions formatted as return codes can be used to
%% short-circuit a response, for example in case of
%% authentication/authorization
throw({403, [], <<"Forbidden">>});
handle('GET', [<<"invalid_return">>], _Req) ->
{invalid_return};
handle(_, _, _Req) ->
{404, [], <<"Not Found">>}.
%% @doc Send 10 separate chunks to the client.
%% @equiv chunk_loop(Ref, 10)
chunk_loop(Ref) ->
chunk_loop(Ref, 10).
%% @doc If `N > 0', send a chunk to the client, checking for errors,
%% as the user might have disconnected.
%% When `N == 0', call {@link elli_request:close_chunk/1.
%% elli_request:close_chunk(Ref)}.
chunk_loop(Ref, 0) ->
elli_request:close_chunk(Ref);
chunk_loop(Ref, N) ->
timer:sleep(10),
case elli_request:send_chunk(Ref, [<<"chunk">>, integer_to_binary(N)]) of
ok -> ok;
{error, Reason} -> ?LOG_ERROR("error in sending chunk: ~p~n", [Reason])
end,
chunk_loop(Ref, N-1).
%%
%% ELLI EVENT CALLBACKS
%%
%% @doc Handle Elli events, fired throughout processing a request.
%%
%% `elli_startup' is sent when Elli is starting up. If you are
%% implementing a middleware, you can use it to spawn processes,
%% create ETS tables or start supervised processes in a supervisor
%% tree.
%%
%% `request_complete' fires *after* Elli has sent the response to the
%% client. `Timings' contains timestamps (native units) of events like when the
%% connection was accepted, when headers/body parsing finished, when the
%% user callback returns, response sent, etc. `Sizes' contains response sizes
%% like response headers size, response body or file size.
%% This allows you to collect performance statistics for monitoring your app.
%%
%% `request_throw', `request_error' and `request_exit' events are sent if
%% the user callback code throws an exception, has an error or
%% exits. After triggering this event, a generated response is sent to
%% the user.
%%
%% `invalid_return' is sent if the user callback code returns a term not
%% understood by elli, see {@link elli_http:execute_callback/1}.
%% After triggering this event, a generated response is sent to the user.
%%
%% `chunk_complete' fires when a chunked response is completely
%% sent. It's identical to the `request_complete' event, except instead
%% of the response body you get the atom `client' or `server'
%% depending on who closed the connection. `Sizes' will have the key `chunks',
%% which is the total size of all chunks plus encoding overhead.
%%
%% `request_closed' is sent if the client closes the connection when
%% Elli is waiting for the next request on a keep alive connection.
%%
%% `request_timeout' is sent if the client times out when
%% Elli is waiting for the request.
%%
%% `request_parse_error' fires if the request is invalid and cannot be parsed by
%% [`erlang:decode_packet/3`][decode_packet/3] or it contains a path Elli cannot
%% parse or does not support.
%%
%% [decode_packet/3]: http://erlang.org/doc/man/erlang.html#decode_packet-3
%%
%% `client_closed' can be sent from multiple parts of the request
%% handling. It's sent when the client closes the connection or if for
%% any reason the socket is closed unexpectedly. The `Where' atom
%% tells you in which part of the request processing the closed socket
%% was detected: `receiving_headers', `receiving_body' or `before_response'.
%%
%% `client_timeout' can as with `client_closed' be sent from multiple
%% parts of the request handling. If Elli tries to receive data from
%% the client socket and does not receive anything within a timeout,
%% this event fires and the socket is closed.
%%
%% `bad_request' is sent when Elli detects a request is not well
%% formatted or does not conform to the configured limits. Currently
%% the `Reason' variable can be `{too_many_headers, Headers}'
%% or `{body_size, ContentLength}'.
%%
%% `file_error' is sent when the user wants to return a file as a
%% response, but for some reason it cannot be opened.
-spec handle_event(Event, Args, Config) -> ok when
Event :: elli:event(),
Args :: elli_handler:callback_args(),
Config :: [tuple()].
handle_event(elli_startup, [], _) -> ok;
handle_event(request_complete, [_Request,
_ResponseCode, _ResponseHeaders, _ResponseBody,
{_Timings, _Sizes}], _) -> ok;
handle_event(request_throw, [_Request, _Exception, _Stacktrace], _) -> ok;
handle_event(request_error, [_Request, _Exception, _Stacktrace], _) -> ok;
handle_event(request_exit, [_Request, _Exception, _Stacktrace], _) -> ok;
handle_event(invalid_return, [_Request, _ReturnValue], _) -> ok;
handle_event(chunk_complete, [_Request,
_ResponseCode, _ResponseHeaders, _ClosingEnd,
{_Timings, _Sizes}], _) -> ok;
handle_event(request_closed, [], _) -> ok;
handle_event(request_timeout, [], _) -> ok;
handle_event(request_parse_error, [_], _) -> ok;
handle_event(client_closed, [_Where], _) -> ok;
handle_event(client_timeout, [_Where], _) -> ok;
handle_event(bad_request, [_Reason], _) -> ok;
handle_event(file_error, [_ErrorReason], _) -> ok.

+ 42
- 0
src/wsSrv/elli_example_callback_handover.erl Vedi File

@ -0,0 +1,42 @@
-module(elli_example_callback_handover).
-export([init/2, handle/2, handle_event/3]).
-include("elli_util.hrl").
-behaviour(elli_handler).
%% @doc Return `{ok, handover}' if `Req''s path is `/hello/world',
%% otherwise `ignore'.
init(Req, _Args) ->
case elli_request:path(Req) of
[<<"hello">>, <<"world">>] ->
{ok, handover};
_ ->
ignore
end.
%% TODO: write docstring
-spec handle(Req, Args) -> Result when
Req :: elli:req(),
Args :: elli_handler:callback_args(),
Result :: elli_handler:result().
handle(Req, Args) ->
handle(elli_request:method(Req), elli_request:path(Req), Req, Args).
handle('GET', [<<"hello">>, <<"world">>], Req, _Args) ->
Body = <<"Hello World!">>,
Size = integer_to_binary(size(Body)),
Headers = [{"Connection", "close"}, {"Content-Length", Size}],
elli_http:send_response(Req, 200, Headers, Body),
{close, <<>>};
handle('GET', [<<"hello">>], Req, _Args) ->
%% Fetch a GET argument from the URL.
Name = elli_request:get_arg(<<"name">>, Req, <<"undefined">>),
{ok, [], <<"Hello ", Name/binary>>}.
%% @hidden
handle_event(_, _, _) ->
ok.

+ 26
- 0
src/wsSrv/elli_example_middleware.erl Vedi File

@ -0,0 +1,26 @@
%% @hidden
-module(elli_example_middleware).
-export([handle/2, handle_event/3]).
-behaviour(elli_handler).
%%
%% ELLI
%%
handle(Req, _Args) ->
do_handle(elli_request:path(Req)).
do_handle([<<"middleware">>, <<"short-circuit">>]) ->
{200, [], <<"short circuit!">>};
do_handle(_) ->
ignore.
%%
%% ELLI EVENT CALLBACKS
%%
handle_event(_Event, _Data, _Args) ->
ok.

+ 57
- 0
src/wsSrv/elli_handler.erl Vedi File

@ -0,0 +1,57 @@
-module(elli_handler).
-optional_callbacks([init/2, preprocess/2, postprocess/3]).
-export_type([callback/0, callback_mod/0, callback_args/0, event/0, result/0]).
%% @type callback(). A tuple of a {@type callback_mod()} and {@type
%% callback_args()}.
-type callback() :: {callback_mod(), callback_args()}.
%% @type callback_mod(). A callback module.
-type callback_mod() :: module().
%% @type callback_args(). Arguments to pass to a {@type callback_mod()}.
-type callback_args() :: list().
%% @type event(). Fired throughout processing a request.
%% See {@link elli_example_callback:handle_event/3} for descriptions.
-type event() :: elli_startup
| bad_request | file_error
| chunk_complete | request_complete
| request_throw | request_error | request_exit
| request_closed | request_parse_error
| client_closed | client_timeout
| invalid_return.
-type result() :: {elli:response_code() | ok,
elli:headers(),
{file, file:name_all()}
| {file, file:name_all(), elli_util:range()}}
| {elli:response_code() | ok, elli:headers(), elli:body()}
| {elli:response_code() | ok, elli:body()}
| {chunk, elli:headers()}
| {chunk, elli:headers(), elli:body()}
| ignore.
-callback handle(Req :: elli:req(), callback_args()) -> result().
-callback handle_event(Event, Args, Config) -> ok when
Event :: event(),
Args :: callback_args(),
Config :: [tuple()].
-callback init(Req, Args) -> {ok, standard | handover} when
Req :: elli:req(),
Args :: callback_args().
-callback preprocess(Req1, Args) -> Req2 when
Req1 :: elli:req(),
Args :: callback_args(),
Req2 :: elli:req().
-callback postprocess(Req, Res1, Args) -> Res2 when
Req :: elli:req(),
Res1 :: result(),
Args :: callback_args(),
Res2 :: result().

+ 959
- 0
src/wsSrv/elli_http.erl Vedi File

@ -0,0 +1,959 @@
%% @doc: Elli HTTP request implementation
%%
%% An elli_http process blocks in elli_tcp:accept/2 until a client
%% connects. It then handles requests on that connection until it's
%% closed either by the client timing out or explicitly by the user.
-module(elli_http).
-include("elli.hrl").
-include("elli_util.hrl").
%% API
-export([start_link/4]).
-export([send_response/4]).
-export([send_file/5]).
-export([mk_req/8, mk_req/11]). %% useful when testing.
%% Exported for looping with a fully-qualified module name
-export([accept/4, handle_request/4, chunk_loop/1, split_args/1,
parse_path/1, keepalive_loop/3, keepalive_loop/5]).
%% Exported for correctly handling session keep-alive for handlers
%% operating in handler mode.
-export([close_or_keepalive/2]).
-export_type([version/0]).
%% @type version(). HTTP version as a tuple, i.e. `{0, 9} | {1, 0} | {1, 1}'.
-type version() :: {0, 9} | {1, 0} | {1, 1}.
-define(CONTENT_LENGTH_HEADER, <<"content-length">>).
-define(EXPECT_HEADER, <<"expect">>).
-define(CONNECTION_HEADER, <<"connection">>).
-define(TRANSFER_ENCODING_HEADER, <<"Transfer-Encoding">>).
%% TODO: use this
%% -type connection_token() :: keep_alive | close.
-spec start_link(Server, ListenSocket, Options, Callback) -> pid() when
Server :: pid(),
ListenSocket :: elli_tcp:socket(),
Options :: proplists:proplist(),
Callback :: elli_handler:callback().
start_link(Server, ListenSocket, Options, Callback) ->
proc_lib:spawn_link(?MODULE, accept,
[Server, ListenSocket, Options, Callback]).
%% @doc Accept on the socket until a client connects.
%% Handle the request, then loop if we're using keep alive or chunked transfer.
%% If {@link elli_tcp:accept/3} doesn't return a socket within a configurable
%% timeout, loop to allow code upgrades of this module.
-spec accept(Server, ListenSocket, Options, Callback) -> ok when
Server :: pid(),
ListenSocket :: elli_tcp:socket(),
Options :: proplists:proplist(),
Callback :: elli_handler:callback().
accept(Server, ListenSocket, Options, Callback) ->
case catch elli_tcp:accept(ListenSocket, Server, accept_timeout(Options)) of
{ok, Socket} ->
t(accepted),
?MODULE:keepalive_loop(Socket, Options, Callback);
{error, timeout} ->
?MODULE:accept(Server, ListenSocket, Options, Callback);
{error, econnaborted} ->
?MODULE:accept(Server, ListenSocket, Options, Callback);
{error, {tls_alert, _}} ->
?MODULE:accept(Server, ListenSocket, Options, Callback);
{error, closed} ->
ok;
{error, Other} ->
exit({error, Other})
end.
%% @doc Handle multiple requests on the same connection, i.e. `"keep alive"'.
keepalive_loop(Socket, Options, Callback) ->
keepalive_loop(Socket, 0, <<>>, Options, Callback).
keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) ->
case ?MODULE:handle_request(Socket, Buffer, Options, Callback) of
{keep_alive, NewBuffer} ->
?MODULE:keepalive_loop(Socket, NumRequests + 1,
NewBuffer, Options, Callback);
{close, _} ->
elli_tcp:close(Socket),
ok
end.
%% @doc Handle a HTTP request that will possibly come on the socket.
%% Returns the appropriate connection token and any buffer containing (parts of)
%% the next request.
-spec handle_request(Socket, PrevBin, Options, Callback) -> ConnToken when
Socket :: elli_tcp:socket(),
PrevBin :: binary(),
Options :: proplists:proplist(),
Callback :: elli_handler:callback(),
ConnToken :: {'keep_alive' | 'close', binary()}.
handle_request(S, PrevB, Opts, {Mod, Args} = Callback) ->
{Method, RawPath, V, B0} = get_request(S, PrevB, Opts, Callback),
t(headers_start),
{{RequestHeaders, ParsedRequestHeaders}, B1} = get_headers(S, V, B0, Opts, Callback),
t(headers_end),
Req = mk_req(Method, RawPath, RequestHeaders, ParsedRequestHeaders, <<>>, V, S, Callback),
case init(Req) of
{ok, standard} ->
t(body_start),
{RequestBody, B2} = get_body(S, ParsedRequestHeaders, B1, Opts, Callback),
t(body_end),
Req1 = Req#req{body = RequestBody},
t(user_start),
Response = execute_callback(Req1),
t(user_end),
handle_response(Req1, B2, Response);
{ok, handover} ->
Req1 = Req#req{body = B1},
t(user_start),
Response = Mod:handle(Req1, Args),
t(user_end),
t(request_end),
handle_event(Mod, request_complete,
[Req1, handover, [], <<>>, {get_timings(),
get_sizes()}], Args),
Response
end.
handle_response(Req, Buffer, {response, Code, UserHeaders, Body}) ->
#req{callback = {Mod, Args}} = Req,
Headers = [connection(Req, UserHeaders),
content_length(UserHeaders, Body)
| UserHeaders],
t(send_start),
send_response(Req, Code, Headers, Body),
t(send_end),
t(request_end),
handle_event(Mod, request_complete,
[Req, Code, Headers, Body, {get_timings(),
get_sizes()}], Args),
{close_or_keepalive(Req, UserHeaders), Buffer};
handle_response(Req, _Buffer, {chunk, UserHeaders, Initial}) ->
#req{callback = {Mod, Args}} = Req,
ResponseHeaders = [{?TRANSFER_ENCODING_HEADER, <<"chunked">>},
connection(Req, UserHeaders)
| UserHeaders],
send_response(Req, 200, ResponseHeaders, <<"">>),
t(send_start),
Initial =:= <<"">> orelse send_chunk(Req#req.socket, Initial),
ClosingEnd = case start_chunk_loop(Req#req.socket) of
{error, client_closed} -> client;
ok -> server
end,
t(send_end),
t(request_end),
handle_event(Mod, chunk_complete,
[Req, 200, ResponseHeaders, ClosingEnd, {get_timings(),
get_sizes()}],
Args),
{close, <<>>};
handle_response(Req, Buffer, {file, ResponseCode, UserHeaders,
Filename, Range}) ->
#req{callback = {Mod, Args}} = Req,
ResponseHeaders = [connection(Req, UserHeaders) | UserHeaders],
case elli_util:file_size(Filename) of
{error, FileError} ->
handle_event(Mod, file_error, [FileError], Args),
send_server_error(Req#req.socket),
elli_tcp:close(Req#req.socket),
exit(normal);
Size ->
t(send_start),
case elli_util:normalize_range(Range, Size) of
undefined ->
send_file(Req, ResponseCode,
[{<<"Content-Length">>, Size} |
ResponseHeaders],
Filename, {0, 0});
{Offset, Length} ->
ERange = elli_util:encode_range({Offset, Length}, Size),
send_file(Req, 206,
lists:append(ResponseHeaders,
[{<<"Content-Length">>, Length},
{<<"Content-Range">>, ERange}]),
Filename, {Offset, Length});
invalid_range ->
ERange = elli_util:encode_range(invalid_range, Size),
send_response(Req, 416,
lists:append(ResponseHeaders,
[{<<"Content-Length">>, 0},
{<<"Content-Range">>, ERange}]),
[])
end,
t(send_end),
t(request_end),
handle_event(Mod, request_complete,
[Req, ResponseCode, ResponseHeaders, <<>>,
{get_timings(),
get_sizes()}],
Args),
{close_or_keepalive(Req, UserHeaders), Buffer}
end.
%% @doc Generate a HTTP response and send it to the client.
send_response(Req, Code, Headers, UserBody) ->
ResponseHeaders = assemble_response_headers(Code, Headers),
Body = case {Req#req.method, Code} of
{'HEAD', _} -> <<>>;
{_, 304} -> <<>>;
{_, 204} -> <<>>;
_ -> UserBody
end,
s(resp_body, iolist_size(Body)),
Response = [ResponseHeaders,
Body],
case elli_tcp:send(Req#req.socket, Response) of
ok -> ok;
{error, Closed} when Closed =:= closed orelse Closed =:= enotconn ->
#req{callback = {Mod, Args}} = Req,
handle_event(Mod, client_closed, [before_response], Args),
ok
end.
%% @doc Send a HTTP response to the client where the body is the
%% contents of the given file. Assumes correctly set response code
%% and headers.
-spec send_file(Req, Code, Headers, Filename, Range) -> ok when
Req :: elli:req(),
Code :: elli:response_code(),
Headers :: elli:headers(),
Filename :: file:filename(),
Range :: elli_util:range().
send_file(#req{callback={Mod, Args}} = Req, Code, Headers, Filename, Range) ->
ResponseHeaders = assemble_response_headers(Code, Headers),
case file:open(Filename, [read, raw, binary]) of
{ok, Fd} -> do_send_file(Fd, Range, Req, ResponseHeaders);
{error, FileError} ->
handle_event(Mod, file_error, [FileError], Args),
send_server_error(Req#req.socket),
elli_tcp:close(Req#req.socket),
exit(normal)
end,
ok.
do_send_file(Fd, {Offset, Length}, #req{callback={Mod, Args}} = Req, Headers) ->
try elli_tcp:send(Req#req.socket, Headers) of
ok ->
case elli_tcp:sendfile(Fd, Req#req.socket, Offset, Length, []) of
{ok, BytesSent} -> s(file, BytesSent), ok;
{error, Closed} when Closed =:= closed orelse
Closed =:= enotconn ->
handle_event(Mod, client_closed, [before_response], Args)
end;
{error, Closed} when Closed =:= closed orelse
Closed =:= enotconn ->
handle_event(Mod, client_closed, [before_response], Args)
after
file:close(Fd)
end.
%% @doc To send a response, we must first have received everything the
%% client is sending. If this is not the case, {@link send_bad_request/1}
%% might reset the client connection.
send_bad_request(Socket) ->
send_rescue_response(Socket, 400, <<"Bad Request">>).
send_server_error(Socket) ->
send_rescue_response(Socket, 500, <<"Server Error">>).
send_rescue_response(Socket, Code, Body) ->
Response = http_response(Code, Body),
elli_tcp:send(Socket, Response).
%% @doc Execute the user callback, translating failure into a proper response.
execute_callback(#req{callback = {Mod, Args}} = Req) ->
try Mod:handle(Req, Args) of
%% {ok,...{file,...}}
{ok, Headers, {file, Filename}} ->
{file, 200, Headers, Filename, []};
{ok, Headers, {file, Filename, Range}} ->
{file, 200, Headers, Filename, Range};
%% ok simple
{ok, Headers, Body} -> {response, 200, Headers, Body};
{ok, Body} -> {response, 200, [], Body};
%% Chunk
{chunk, Headers} -> {chunk, Headers, <<"">>};
{chunk, Headers, Initial} -> {chunk, Headers, Initial};
%% File
{HttpCode, Headers, {file, Filename}} ->
{file, HttpCode, Headers, Filename, {0, 0}};
{HttpCode, Headers, {file, Filename, Range}} ->
{file, HttpCode, Headers, Filename, Range};
%% Simple
{HttpCode, Headers, Body} -> {response, HttpCode, Headers, Body};
{HttpCode, Body} -> {response, HttpCode, [], Body};
%% Unexpected
Unexpected ->
handle_event(Mod, invalid_return, [Req, Unexpected], Args),
{response, 500, [], <<"Internal server error">>}
catch
throw:{ResponseCode, Headers, Body} when is_integer(ResponseCode) ->
{response, ResponseCode, Headers, Body};
?WITH_STACKTRACE(throw, Exc, Stacktrace)
handle_event(Mod, request_throw,
[Req, Exc, Stacktrace],
Args),
{response, 500, [], <<"Internal server error">>};
?WITH_STACKTRACE(error, Error, Stacktrace)
handle_event(Mod, request_error,
[Req, Error, Stacktrace],
Args),
{response, 500, [], <<"Internal server error">>};
?WITH_STACKTRACE(exit, Exit, Stacktrace)
handle_event(Mod, request_exit,
[Req, Exit, Stacktrace],
Args),
{response, 500, [], <<"Internal server error">>}
end.
%%
%% CHUNKED-TRANSFER
%%
%% @doc The chunk loop is an intermediary between the socket and the
%% user. We forward anything the user sends until the user sends an
%% empty response, which signals that the connection should be
%% closed. When the client closes the socket, the loop exits.
start_chunk_loop(Socket) ->
%% Set the socket to active so we receive the tcp_closed message
%% if the client closes the connection
elli_tcp:setopts(Socket, [{active, once}]),
?MODULE:chunk_loop(Socket).
chunk_loop(Socket) ->
{_SockType, InnerSocket} = Socket,
receive
{tcp_closed, InnerSocket} ->
{error, client_closed};
{chunk, close} ->
case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of
ok ->
elli_tcp:close(Socket),
ok;
{error, Closed} when Closed =:= closed orelse
Closed =:= enotconn ->
{error, client_closed}
end;
{chunk, close, From} ->
case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of
ok ->
elli_tcp:close(Socket),
From ! {self(), ok},
ok;
{error, Closed} when Closed =:= closed orelse
Closed =:= enotconn ->
From ! {self(), {error, closed}},
ok
end;
{chunk, Data} ->
send_chunk(Socket, Data),
?MODULE:chunk_loop(Socket);
{chunk, Data, From} ->
case send_chunk(Socket, Data) of
ok ->
From ! {self(), ok};
{error, Closed} when Closed =:= closed orelse
Closed =:= enotconn ->
From ! {self(), {error, closed}}
end,
?MODULE:chunk_loop(Socket)
after 10000 ->
?MODULE:chunk_loop(Socket)
end.
send_chunk(Socket, Data) ->
case iolist_size(Data) of
0 -> ok;
Size ->
Response = [integer_to_list(Size, 16),
<<"\r\n">>, Data, <<"\r\n">>],
s(chunks, iolist_size(Response)),
elli_tcp:send(Socket, Response)
end.
%%
%% RECEIVE REQUEST
%%
%% @doc Retrieve the request line.
get_request(Socket, <<>>, Options, Callback) ->
NewBuffer = recv_request(Socket, <<>>, Options, Callback),
get_request(Socket, NewBuffer, Options, Callback);
get_request(Socket, Buffer, Options, Callback) ->
t(request_start),
get_request_(Socket, Buffer, Options, Callback).
get_request_(Socket, Buffer, Options, {Mod, Args} = Callback) ->
case erlang:decode_packet(http_bin, Buffer, []) of
{more, _} ->
NewBuffer = recv_request(Socket, Buffer, Options, Callback),
get_request_(Socket, NewBuffer, Options, Callback);
{ok, {http_request, Method, RawPath, Version}, Rest} ->
{Method, RawPath, Version, Rest};
{ok, {http_error, _}, _} ->
handle_event(Mod, request_parse_error, [Buffer], Args),
send_bad_request(Socket),
elli_tcp:close(Socket),
exit(normal);
{ok, {http_response, _, _, _}, _} ->
elli_tcp:close(Socket),
exit(normal)
end.
recv_request(Socket, Buffer, Options, {Mod, Args} = _Callback) ->
case elli_tcp:recv(Socket, 0, request_timeout(Options)) of
{ok, Data} ->
<<Buffer/binary, Data/binary>>;
{error, timeout} ->
handle_event(Mod, request_timeout, [], Args),
elli_tcp:close(Socket),
exit(normal);
{error, Closed} when Closed =:= closed orelse
Closed =:= enotconn ->
handle_event(Mod, request_closed, [], Args),
elli_tcp:close(Socket),
exit(normal)
end.
-spec get_headers(Socket, V, Buffer, Opts, Callback) -> Headers when
Socket :: elli_tcp:socket(),
V :: version(),
Buffer :: binary(),
Opts :: proplists:proplist(),
Callback :: elli_handler:callback(),
Headers :: {{elli:headers(), elli:headers()}, any()}. % TODO: refine
get_headers(_Socket, {0, 9}, _, _, _) ->
{{[], []}, <<>>};
get_headers(Socket, {1, _}, Buffer, Opts, Callback) ->
get_headers(Socket, Buffer, {[], []}, 0, Opts, Callback).
get_headers(Socket, _, {Headers, _}, HeadersCount, _Opts, {Mod, Args})
when HeadersCount >= 100 ->
handle_event(Mod, bad_request, [{too_many_headers, Headers}], Args),
send_bad_request(Socket),
elli_tcp:close(Socket),
exit(normal);
get_headers(Socket, Buffer, {Headers, ParsedHeaders}, Count, Opts, {Mod, Args} = Callback) ->
case erlang:decode_packet(httph_bin, Buffer, []) of
{ok, {http_header, _, Key, _, Value}, Rest} ->
BinKey = ensure_binary(Key),
NewHeaders = [{BinKey, Value} | Headers],
NewParsedHeaders = [{string:casefold(BinKey), Value} | ParsedHeaders],
get_headers(Socket, Rest, {NewHeaders, NewParsedHeaders}, Count + 1, Opts, Callback);
{ok, http_eoh, Rest} ->
{{Headers, ParsedHeaders}, Rest};
{ok, {http_error, _}, Rest} ->
get_headers(Socket, Rest, {Headers, ParsedHeaders}, Count, Opts, Callback);
{more, _} ->
case elli_tcp:recv(Socket, 0, header_timeout(Opts)) of
{ok, Data} ->
get_headers(Socket, <<Buffer/binary, Data/binary>>,
{Headers, ParsedHeaders}, Count, Opts, Callback);
{error, Closed} when Closed =:= closed orelse
Closed =:= enotconn ->
handle_event(Mod, client_closed, [receiving_headers], Args),
elli_tcp:close(Socket),
exit(normal);
{error, timeout} ->
handle_event(Mod, client_timeout,
[receiving_headers], Args),
elli_tcp:close(Socket),
exit(normal)
end
end.
%% @doc Fetch the full body of the request, if any is available.
%%
%% At the moment we don't need to handle large requests, so there is
%% no need for streaming or lazily fetching the body in the user
%% code. Fully receiving the body allows us to avoid the complex
%% request object threading in Cowboy and the caching in Mochiweb.
%%
%% As we are always receiving whatever the client sends, we might have
%% buffered too much and get parts of the next pipelined request. In
%% that case, push it back in the buffer and handle the first request.
-spec get_body(Socket, Headers, Buffer, Opts, Callback) -> FullBody when
Socket :: undefined | elli_tcp:socket(),
Headers :: elli:headers(),
Buffer :: binary(),
Opts :: proplists:proplist(),
Callback :: elli_handler:callback(),
FullBody :: {elli:body(), binary()}.
get_body(Socket, Headers, Buffer, Opts, Callback) ->
case get_header(?CONTENT_LENGTH_HEADER, Headers, undefined) of
undefined ->
{<<>>, Buffer};
ContentLengthBin ->
maybe_send_continue(Socket, Headers),
ContentLength = binary_to_integer(binary:replace(ContentLengthBin,
<<" ">>, <<>>,
[global])),
ok = check_max_size(Socket, ContentLength, Buffer, Opts, Callback),
Result = case ContentLength - byte_size(Buffer) of
0 ->
{Buffer, <<>>};
N when N > 0 ->
do_get_body(Socket, Buffer, Opts, N, Callback);
_ ->
<<Body:ContentLength/binary, R/binary>> = Buffer,
{Body, R}
end,
%% set the size here so if do_get_body exits it won't have
%% req_body in sizes
s(req_body, ContentLength),
Result
end.
do_get_body(Socket, Buffer, Opts, N, {Mod, Args}) ->
case elli_tcp:recv(Socket, N, body_timeout(Opts)) of
{ok, Data} ->
{<<Buffer/binary, Data/binary>>, <<>>};
{error, Closed} when Closed =:= closed orelse Closed =:= enotconn ->
handle_event(Mod, client_closed, [receiving_body], Args),
ok = elli_tcp:close(Socket),
exit(normal);
{error, timeout} ->
handle_event(Mod, client_timeout, [receiving_body], Args),
ok = elli_tcp:close(Socket),
exit(normal)
end.
ensure_binary(Bin) when is_binary(Bin) -> Bin;
ensure_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom, latin1).
maybe_send_continue(Socket, Headers) ->
% According to RFC2616 section 8.2.3 an origin server must respond with
% either a "100 Continue" or a final response code when the client
% headers contains "Expect:100-continue"
case get_header(?EXPECT_HEADER, Headers, undefined) of
<<"100-continue">> ->
Response = http_response(100),
elli_tcp:send(Socket, Response);
_Other ->
ok
end.
%% @doc To send a response, we must first receive anything the client is
%% sending. To avoid allowing clients to use all our bandwidth, if the request
%% size is too big, we simply close the socket.
check_max_size(Socket, ContentLength, Buffer, Opts, {Mod, Args}) ->
MaxSize = max_body_size(Opts),
do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args}).
do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args})
when ContentLength > MaxSize ->
handle_event(Mod, bad_request, [{body_size, ContentLength}], Args),
do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize),
elli_tcp:close(Socket),
exit(normal);
do_check_max_size(_, _, _, _, _) -> ok.
do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize)
when ContentLength < MaxSize * 2 ->
OnSocket = ContentLength - size(Buffer),
elli_tcp:recv(Socket, OnSocket, 60000),
Response = http_response(413),
elli_tcp:send(Socket, Response);
do_check_max_size_x2(_, _, _, _) -> ok.
-spec mk_req(Method, PathTuple, Headers, Headers, Body, V, Socket, Callback) -> Req when
Method :: elli:http_method(),
PathTuple :: {PathType :: atom(), RawPath :: binary()},
Headers :: elli:headers(),
Body :: elli:body(),
V :: version(),
Socket :: elli_tcp:socket() | undefined,
Callback :: elli_handler:callback(),
Req :: elli:req().
mk_req(Method, PathTuple, Headers, ParsedHeaders, Body, V, Socket, {Mod, Args} = Callback) ->
case parse_path(PathTuple) of
{ok, {Scheme, Host, Port}, {Path, URL, URLArgs}} ->
#req{method = Method, scheme = Scheme, host = Host,
port = Port, path = URL, args = URLArgs,
version = V, raw_path = Path, original_headers = Headers,
body = Body, pid = self(), socket = Socket,
callback = Callback, headers = ParsedHeaders};
{error, Reason} ->
handle_event(Mod, request_parse_error,
[{Reason, {Method, PathTuple}}], Args),
send_bad_request(Socket),
elli_tcp:close(Socket),
exit(normal)
end.
mk_req(Method, Scheme, Host, Port, PathTuple, Headers, ParsedHeaders, Body, V, Socket, Callback) ->
Req = mk_req(Method, PathTuple, Headers, ParsedHeaders, Body, V, Socket, Callback),
Req#req{scheme = Scheme, host = Host, port = Port}.
%%
%% HEADERS
%%
http_response(Code) ->
http_response(Code, <<>>).
http_response(Code, Body) ->
http_response(Code, [{?CONTENT_LENGTH_HEADER, size(Body)}], Body).
http_response(Code, Headers, <<>>) ->
[<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>,
encode_headers(Headers), <<"\r\n">>];
http_response(Code, Headers, Body) ->
[http_response(Code, Headers, <<>>), Body].
assemble_response_headers(Code, Headers) ->
ResponseHeaders = http_response(Code, Headers, <<>>),
s(resp_headers, iolist_size(ResponseHeaders)),
ResponseHeaders.
encode_headers([]) ->
[];
encode_headers([[] | H]) ->
encode_headers(H);
encode_headers([{K, V} | H]) ->
[encode_value(K), <<": ">>, encode_value(V), <<"\r\n">>, encode_headers(H)].
encode_value(V) when is_integer(V) -> integer_to_binary(V);
encode_value(V) when is_binary(V) -> V;
encode_value(V) when is_list(V) -> list_to_binary(V).
connection_token(#req{version = {1, 1}, headers = Headers}) ->
case get_header(?CONNECTION_HEADER, Headers) of
<<"close">> -> <<"close">>;
<<"Close">> -> <<"close">>;
_ -> <<"Keep-Alive">>
end;
connection_token(#req{version = {1, 0}, headers = Headers}) ->
case get_header(?CONNECTION_HEADER, Headers) of
<<"Keep-Alive">> -> <<"Keep-Alive">>;
_ -> <<"close">>
end;
connection_token(#req{version = {0, 9}}) ->
<<"close">>.
%% @doc Return the preferred session handling setting to close or keep the
%% current session alive based on the presence of a header or the standard
%% default based on the version of HTTP of the request.
-spec close_or_keepalive(Req, Headers) -> KeepaliveOpt when
Req :: elli:req(),
Headers :: elli:headers(),
KeepaliveOpt :: close | keep_alive.
close_or_keepalive(Req, UserHeaders) ->
case get_header(?CONNECTION_HEADER, UserHeaders, connection_token(Req)) of
<<"close">> -> close;
<<"Keep-Alive">> -> keep_alive
end.
%% @doc Add appropriate connection header if the user did not add one already.
connection(Req, UserHeaders) ->
case get_header(?CONNECTION_HEADER, UserHeaders) of
undefined ->
{?CONNECTION_HEADER, connection_token(Req)};
_ ->
[]
end.
content_length(Headers, Body)->
?IF(is_header_defined(?CONTENT_LENGTH_HEADER, Headers), [],
{?CONTENT_LENGTH_HEADER, iolist_size(Body)}).
is_header_defined(Key, Headers) ->
Key1 = string:casefold(Key),
lists:any(fun({X, _}) -> string:equal(Key1, X, true) end, Headers).
get_header(Key, Headers) ->
get_header(Key, Headers, undefined).
-ifdef(OTP_RELEASE).
get_header(Key, Headers, Default) ->
CaseFoldedKey = string:casefold(Key),
case lists:search(fun({N, _}) -> string:equal(CaseFoldedKey, N, true) end, Headers) of
{value, {_, Value}} ->
Value;
false ->
Default
end.
-else.
get_header(Key, Headers, Default) ->
CaseFoldedKey = string:casefold(Key),
case search(fun({N, _}) -> string:equal(CaseFoldedKey, N, true) end, Headers) of
{value, {_, Value}} ->
Value;
false ->
Default
end.
search(Pred, [Hd|Tail]) ->
case Pred(Hd) of
true -> {value, Hd};
false -> search(Pred, Tail)
end;
search(Pred, []) when is_function(Pred, 1) ->
false.
-endif.
%%
%% PATH HELPERS
%%
-ifdef(OTP_RELEASE).
-if(?OTP_RELEASE >= 22).
parse_path({abs_path, FullPath}) ->
URIMap = uri_string:parse(FullPath),
Host = maps:get(host, URIMap, undefined),
Scheme = maps:get(scheme, URIMap, undefined),
Path = maps:get(path, URIMap, <<>>),
Query = maps:get(query, URIMap, <<>>),
Port = maps:get(port, URIMap, case Scheme of http -> 80; https -> 443; _ -> undefined end),
{ok, {Scheme, Host, Port}, {Path, split_path(Path), uri_string:dissect_query(Query)}};
parse_path({absoluteURI, Scheme, Host, Port, Path}) ->
setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port});
parse_path(_) ->
{error, unsupported_uri}.
-else.
parse_path({abs_path, FullPath}) ->
Parsed = case binary:split(FullPath, [<<"?">>]) of
[URL] -> {FullPath, split_path(URL), []};
[URL, Args] -> {FullPath, split_path(URL), split_args(Args)}
end,
{ok, {undefined, undefined, undefined}, Parsed};
parse_path({absoluteURI, Scheme, Host, Port, Path}) ->
setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port});
parse_path(_) ->
{error, unsupported_uri}.
-endif.
-else.
%% same as else branch above. can drop this when only OTP 21+ is supported
parse_path({abs_path, FullPath}) ->
Parsed = case binary:split(FullPath, [<<"?">>]) of
[URL] -> {FullPath, split_path(URL), []};
[URL, Args] -> {FullPath, split_path(URL), split_args(Args)}
end,
{ok, {undefined, undefined, undefined}, Parsed};
parse_path({absoluteURI, Scheme, Host, Port, Path}) ->
setelement(2, parse_path({abs_path, Path}), {Scheme, Host, Port});
parse_path(_) ->
{error, unsupported_uri}.
-endif.
split_path(Path) ->
[P || P <- binary:split(Path, [<<"/">>], [global]),
P =/= <<>>].
%% @doc Split the URL arguments into a proplist.
%% Lifted from `cowboy_http:x_www_form_urlencoded/2'.
-spec split_args(binary()) -> list({binary(), binary() | true}).
split_args(<<>>) ->
[];
split_args(Qs) ->
Tokens = binary:split(Qs, <<"&">>, [global, trim]),
[case binary:split(Token, <<"=">>) of
[Token] -> {Token, true};
[Name, Value] -> {Name, Value}
end || Token <- Tokens].
%%
%% CALLBACK HELPERS
%%
init(#req{callback = {Mod, Args}} = Req) ->
?IF(erlang:function_exported(Mod, init, 2),
case Mod:init(Req, Args) of
ignore -> {ok, standard};
{ok, Behaviour} -> {ok, Behaviour}
end,
{ok, standard}).
handle_event(Mod, Name, EventArgs, ElliArgs) ->
try
Mod:handle_event(Name, EventArgs, ElliArgs)
catch
?WITH_STACKTRACE(EvClass, EvError, Stacktrace)
?LOG_ERROR("~p:handle_event/3 crashed ~p:~p~n~p",
[Mod, EvClass, EvError, Stacktrace])
end.
%%
%% TIMING HELPERS
%%
%% @doc Record the current monotonic time in the process dictionary.
%% This allows easily adding time tracing wherever,
%% without passing along any variables.
t(Key) ->
put({time, Key}, erlang:monotonic_time()).
get_timings() ->
lists:filtermap(fun get_timings/1, get()).
get_timings({{time, accepted}, Value}) ->
{true, {accepted, Value}};
get_timings({{time, Key}, Value}) ->
erase({time, Key}),
{true, {Key, Value}};
get_timings(_) ->
false.
%%
%% SIZE HELPERS
%%
%% @doc stores response part size in bytes
s(chunks, Size) ->
case get({size, chunks}) of
undefined ->
put({size, chunks}, Size);
Sum ->
put({size, chunks}, Size + Sum)
end;
s(Key, Size) ->
put({size, Key}, Size).
get_sizes() ->
lists:filtermap(fun get_sizes/1, get()).
get_sizes({{size, Key}, Value}) ->
erase({size, Key}),
{true, {Key, Value}};
get_sizes(_) ->
false.
%%
%% OPTIONS
%%
accept_timeout(Opts) -> proplists:get_value(accept_timeout, Opts).
request_timeout(Opts) -> proplists:get_value(request_timeout, Opts).
header_timeout(Opts) -> proplists:get_value(header_timeout, Opts).
body_timeout(Opts) -> proplists:get_value(body_timeout, Opts).
max_body_size(Opts) -> proplists:get_value(max_body_size, Opts).
%%
%% HTTP STATUS CODES
%%
%% @doc Response code string. Lifted from `cowboy_http_req.erl'.
status(100) -> <<"100 Continue">>;
status(101) -> <<"101 Switching Protocols">>;
status(102) -> <<"102 Processing">>;
status(200) -> <<"200 OK">>;
status(201) -> <<"201 Created">>;
status(202) -> <<"202 Accepted">>;
status(203) -> <<"203 Non-Authoritative Information">>;
status(204) -> <<"204 No Content">>;
status(205) -> <<"205 Reset Content">>;
status(206) -> <<"206 Partial Content">>;
status(207) -> <<"207 Multi-Status">>;
status(226) -> <<"226 IM Used">>;
status(300) -> <<"300 Multiple Choices">>;
status(301) -> <<"301 Moved Permanently">>;
status(302) -> <<"302 Found">>;
status(303) -> <<"303 See Other">>;
status(304) -> <<"304 Not Modified">>;
status(305) -> <<"305 Use Proxy">>;
status(306) -> <<"306 Switch Proxy">>;
status(307) -> <<"307 Temporary Redirect">>;
status(400) -> <<"400 Bad Request">>;
status(401) -> <<"401 Unauthorized">>;
status(402) -> <<"402 Payment Required">>;
status(403) -> <<"403 Forbidden">>;
status(404) -> <<"404 Not Found">>;
status(405) -> <<"405 Method Not Allowed">>;
status(406) -> <<"406 Not Acceptable">>;
status(407) -> <<"407 Proxy Authentication Required">>;
status(408) -> <<"408 Request Timeout">>;
status(409) -> <<"409 Conflict">>;
status(410) -> <<"410 Gone">>;
status(411) -> <<"411 Length Required">>;
status(412) -> <<"412 Precondition Failed">>;
status(413) -> <<"413 Request Entity Too Large">>;
status(414) -> <<"414 Request-URI Too Long">>;
status(415) -> <<"415 Unsupported Media Type">>;
status(416) -> <<"416 Requested Range Not Satisfiable">>;
status(417) -> <<"417 Expectation Failed">>;
status(418) -> <<"418 I'm a teapot">>;
status(422) -> <<"422 Unprocessable Entity">>;
status(423) -> <<"423 Locked">>;
status(424) -> <<"424 Failed Dependency">>;
status(425) -> <<"425 Unordered Collection">>;
status(426) -> <<"426 Upgrade Required">>;
status(428) -> <<"428 Precondition Required">>;
status(429) -> <<"429 Too Many Requests">>;
status(431) -> <<"431 Request Header Fields Too Large">>;
status(500) -> <<"500 Internal Server Error">>;
status(501) -> <<"501 Not Implemented">>;
status(502) -> <<"502 Bad Gateway">>;
status(503) -> <<"503 Service Unavailable">>;
status(504) -> <<"504 Gateway Timeout">>;
status(505) -> <<"505 HTTP Version Not Supported">>;
status(506) -> <<"506 Variant Also Negotiates">>;
status(507) -> <<"507 Insufficient Storage">>;
status(510) -> <<"510 Not Extended">>;
status(511) -> <<"511 Network Authentication Required">>;
status(I) when is_integer(I), I >= 100, I < 1000 -> list_to_binary(io_lib:format("~B Status", [I]));
status(B) when is_binary(B) -> B.
%%
%% UNIT TESTS
%%
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
get_body_test() ->
Socket = undefined,
Headers = [{<<"Content-Length">>, <<" 42 ">>}],
Buffer = binary:copy(<<".">>, 42),
Opts = [],
Callback = {no_mod, []},
?assertMatch({Buffer, <<>>},
get_body(Socket, Headers, Buffer, Opts, Callback)).
-endif.

+ 153
- 0
src/wsSrv/elli_middleware.erl Vedi File

@ -0,0 +1,153 @@
%%% @doc HTTP request processing middleware.
%%%
%%% This module offers both pre-processing of requests and post-processing of
%%% responses. It can also be used to allow multiple handlers, where the first
%%% handler to return a response short-circuits the request.
%%% It is implemented as a plain elli handler.
%%%
%%% Usage:
%%%
%%% ```
%%% Config = [
%%% {mods, [
%%% {elli_example_middleware, []},
%%% {elli_middleware_compress, []},
%%% {elli_example_callback, []}
%%% ]}
%%% ],
%%% elli:start_link([
%%% %% ...,
%%% {callback, elli_middleware},
%%% {callback_args, Config}
%%% ]).
%%% '''
%%%
%%% The configured modules may implement the elli behaviour, in which case all
%%% the callbacks will be used as normal. If {@link handle/2} returns `ignore',
%%% elli will continue on to the next callback in the list.
%%%
%%% Pre-processing and post-processing is implemented in {@link preprocess/2}
%%% and {@link postprocess/3}. {@link preprocess/2} is called for each
%%% middleware in the order specified, while {@link postprocess/3} is called in
%%% the reverse order.
%%%
%%% TODO: Don't call all postprocess middlewares when a middleware
%%% short-circuits the request.
%%%
%%% `elli_middleware' does not add any significant overhead.
-module(elli_middleware).
-behaviour(elli_handler).
-export([init/2, handle/2, handle_event/3]).
%% Macros.
-define(IF_NOT_EXPORTED(M, F, A, T, E),
case erlang:function_exported(M, F, A) of true -> E; false -> T end).
%%
%% ELLI CALLBACKS
%%
%% @hidden
-spec init(Req, Args) -> {ok, standard | handover} when
Req :: elli:req(),
Args :: elli_handler:callback_args().
init(Req, Args) ->
do_init(Req, callbacks(Args)).
%% @hidden
-spec handle(Req :: elli:req(), Config :: [tuple()]) -> elli_handler:result().
handle(CleanReq, Config) ->
Callbacks = callbacks(Config),
PreReq = preprocess(CleanReq, Callbacks),
Res = process(PreReq, Callbacks),
postprocess(PreReq, Res, lists:reverse(Callbacks)).
%% @hidden
-spec handle_event(Event, Args, Config) -> ok when
Event :: elli_handler:event(),
Args :: elli_handler:callback_args(),
Config :: [tuple()].
handle_event(elli_startup, Args, Config) ->
Callbacks = callbacks(Config),
[code:ensure_loaded(M) || {M, _} <- Callbacks],
forward_event(elli_startup, Args, Callbacks);
handle_event(Event, Args, Config) ->
forward_event(Event, Args, lists:reverse(callbacks(Config))).
%%
%% MIDDLEWARE LOGIC
%%
-spec do_init(Req, Callbacks) -> {ok, standard | handover} when
Req :: elli:req(),
Callbacks :: elli_handler:callbacks().
do_init(_, []) ->
{ok, standard};
do_init(Req, [{Mod, Args}|Mods]) ->
?IF_NOT_EXPORTED(Mod, init, 2, do_init(Req, Mods),
case Mod:init(Req, Args) of
ignore -> do_init(Req, Mods);
Result -> Result
end).
-spec process(Req, Callbacks) -> Result when
Req :: elli:req(),
Callbacks :: [Callback :: elli_handler:callback()],
Result :: elli_handler:result().
process(_Req, []) ->
{404, [], <<"Not Found">>};
process(Req, [{Mod, Args} | Mods]) ->
?IF_NOT_EXPORTED(Mod, handle, 2, process(Req, Mods),
case Mod:handle(Req, Args) of
ignore -> process(Req, Mods);
Response -> Response
end).
-spec preprocess(Req1, Callbacks) -> Req2 when
Req1 :: elli:req(),
Callbacks :: [elli_handler:callback()],
Req2 :: elli:req().
preprocess(Req, []) ->
Req;
preprocess(Req, [{Mod, Args} | Mods]) ->
?IF_NOT_EXPORTED(Mod, preprocess, 2, preprocess(Req, Mods),
preprocess(Mod:preprocess(Req, Args), Mods)).
-spec postprocess(Req, Res1, Callbacks) -> Res2 when
Req :: elli:req(),
Res1 :: elli_handler:result(),
Callbacks :: [elli_handler:callback()],
Res2 :: elli_handler:result().
postprocess(_Req, Res, []) ->
Res;
postprocess(Req, Res, [{Mod, Args} | Mods]) ->
?IF_NOT_EXPORTED(Mod, postprocess, 3, postprocess(Req, Res, Mods),
postprocess(Req, Mod:postprocess(Req, Res, Args), Mods)).
-spec forward_event(Event, Args, Callbacks) -> ok when
Event :: elli_handler:event(),
Args :: elli_handler:callback_args(),
Callbacks :: [elli_handler:callback()].
forward_event(Event, Args, Callbacks) ->
[?IF_NOT_EXPORTED(M, handle_event, 3, ok,
M:handle_event(Event, Args, XArgs))
|| {M, XArgs} <- Callbacks],
ok.
%%
%% INTERNAL HELPERS
%%
-spec callbacks(Config :: [{mod, Callbacks} | tuple()]) -> Callbacks when
Callbacks :: [elli_handler:callback()].
callbacks(Config) ->
proplists:get_value(mods, Config, []).

+ 57
- 0
src/wsSrv/elli_middleware_compress.erl Vedi File

@ -0,0 +1,57 @@
%%% @doc Response compression as Elli middleware.
-module(elli_middleware_compress).
-export([postprocess/3]).
-include("elli_util.hrl").
%%
%% Postprocess handler
%%
%%% @doc Postprocess all requests and compress bodies larger than
%%% `compress_byte_size' (`1024' by default).
-spec postprocess(Req, Result, Config) -> Result when
Req :: elli:req(),
Result :: elli_handler:result(),
Config :: [{compress_byte_size, non_neg_integer()} | tuple()].
postprocess(Req, {ResponseCode, Body}, Config)
when is_integer(ResponseCode) orelse ResponseCode =:= ok ->
postprocess(Req, {ResponseCode, [], Body}, Config);
postprocess(Req, {ResponseCode, Headers, Body} = Res, Config)
when is_integer(ResponseCode) orelse ResponseCode =:= ok ->
Threshold = proplists:get_value(compress_byte_size, Config, 1024),
?IF(not should_compress(Body, Threshold), Res,
case compress(Body, Req) of
no_compress ->
Res;
{CompressedBody, Encoding} ->
NewHeaders = [{<<"Content-Encoding">>, Encoding} | Headers],
{ResponseCode, NewHeaders, CompressedBody}
end);
postprocess(_, Res, _) ->
Res.
%%
%% INTERNALS
%%
%% NOTE: Algorithm is either `<<"gzip">>' or `<<"deflate">>'.
-spec compress(Body0 :: elli:body(), Req :: elli:req()) -> Body1 when
Body1 :: {Compressed :: binary(), Algorithm :: binary()} | no_compress.
compress(Body, Req) ->
case accepted_encoding(Req) of
<<"gzip">> = E -> {zlib:gzip(Body), E};
<<"deflate">> = E -> {zlib:compress(Body), E};
_ -> no_compress
end.
accepted_encoding(Req) ->
hd(binary:split(elli_request:get_header(<<"Accept-Encoding">>, Req, <<>>),
[<<",">>, <<";">>], [global])).
-spec should_compress(Body, Threshold) -> boolean() when
Body :: binary(),
Threshold :: non_neg_integer().
should_compress(Body, S) ->
is_binary(Body) andalso byte_size(Body) >= S orelse
is_list(Body) andalso iolist_size(Body) >= S.

+ 299
- 0
src/wsSrv/elli_request.erl Vedi File

@ -0,0 +1,299 @@
-module(elli_request).
-include("elli.hrl").
-include("elli_util.hrl").
-export([send_chunk/2
, async_send_chunk/2
, chunk_ref/1
, close_chunk/1
, path/1
, raw_path/1
, query_str/1
, get_header/2
, get_header/3
, get_arg_decoded/2
, get_arg_decoded/3
, get_arg/2
, get_arg/3
, get_args/1
, get_args_decoded/1
, post_arg/2
, post_arg/3
, post_arg_decoded/2
, post_arg_decoded/3
, post_args/1
, post_args_decoded/1
, body_qs/1
, original_headers/1
, headers/1
, peer/1
, method/1
, body/1
, scheme/1
, host/1
, port/1
, get_range/1
, to_proplist/1
, is_request/1
, uri_decode/1
]).
-export_type([http_range/0]).
-type http_range() :: {First::non_neg_integer(), Last::non_neg_integer()}
| {offset, Offset::non_neg_integer()}
| {suffix, Length::pos_integer()}.
%%
%% Helpers for working with a #req{}
%%
%% @doc Return `path' split into binary parts.
path(#req{path = Path}) -> Path.
%% @doc Return the `raw_path', i.e. not split or parsed for query params.
raw_path(#req{raw_path = Path}) -> Path.
%% @doc Return the `headers' that have had `string:casefold/1' run on each key.
headers(#req{headers = Headers}) -> Headers.
%% @doc Return the original `headers'.
original_headers(#req{original_headers = Headers}) -> Headers.
%% @doc Return the `method'.
method(#req{method = Method}) -> Method.
%% @doc Return the `body'.
body(#req{body = Body}) -> Body.
%% @doc Return the `scheme'.
scheme(#req{scheme = Scheme}) -> Scheme.
%% @doc Return the `host'.
host(#req{host = Host}) -> Host.
%% @doc Return the `port'.
port(#req{port = Port}) -> Port.
peer(#req{socket = Socket} = _Req) ->
case elli_tcp:peername(Socket) of
{ok, {Address, _}} ->
list_to_binary(inet_parse:ntoa(Address));
{error, _} ->
undefined
end.
get_header(Key, #req{headers = Headers}) ->
CaseFoldedKey = string:casefold(Key),
proplists:get_value(CaseFoldedKey, Headers).
get_header(Key, #req{headers = Headers}, Default) ->
CaseFoldedKey = string:casefold(Key),
proplists:get_value(CaseFoldedKey, Headers, Default).
%% @equiv get_arg(Key, Req, undefined)
get_arg(Key, #req{} = Req) ->
get_arg(Key, Req, undefined).
%% @equiv proplists:get_value(Key, Args, Default)
get_arg(Key, #req{args = Args}, Default) ->
proplists:get_value(Key, Args, Default).
%% @equiv get_arg_decoded(Key, Req, undefined)
get_arg_decoded(Key, #req{} = Req) ->
get_arg_decoded(Key, Req, undefined).
get_arg_decoded(Key, #req{args = Args}, Default) ->
case proplists:get_value(Key, Args) of
undefined -> Default;
true -> true;
EncodedValue ->
uri_decode(EncodedValue)
end.
%% @doc Parse `application/x-www-form-urlencoded' body into a proplist.
body_qs(#req{body = <<>>}) -> [];
body_qs(#req{body = Body} = Req) ->
case get_header(<<"Content-Type">>, Req) of
<<"application/x-www-form-urlencoded">> ->
elli_http:split_args(Body);
<<"application/x-www-form-urlencoded;", _/binary>> -> % ; charset=...
elli_http:split_args(Body);
_ ->
erlang:error(badarg)
end.
%% @equiv post_arg(Key, Req, undefined)
post_arg(Key, #req{} = Req) ->
post_arg(Key, Req, undefined).
post_arg(Key, #req{} = Req, Default) ->
proplists:get_value(Key, body_qs(Req), Default).
%% @equiv post_arg_decoded(Key, Req, undefined)
post_arg_decoded(Key, #req{} = Req) ->
post_arg_decoded(Key, Req, undefined).
post_arg_decoded(Key, #req{} = Req, Default) ->
case proplists:get_value(Key, body_qs(Req)) of
undefined -> Default;
true -> true;
EncodedValue ->
uri_decode(EncodedValue)
end.
%% @doc Return a proplist of keys and values of the original query string.
%% Both keys and values in the returned proplists will be binaries or the atom
%% `true' in case no value was supplied for the query value.
-spec get_args(elli:req()) -> QueryArgs :: proplists:proplist().
get_args(#req{args = Args}) -> Args.
get_args_decoded(#req{args = Args}) ->
lists:map(fun ({K, true}) ->
{K, true};
({K, V}) ->
{K, uri_decode(V)}
end, Args).
post_args(#req{} = Req) ->
body_qs(Req).
post_args_decoded(#req{} = Req) ->
lists:map(fun ({K, true}) ->
{K, true};
({K, V}) ->
{K, uri_decode(V)}
end, body_qs(Req)).
%% @doc Calculate the query string associated with a given `Request'
%% as a binary.
-spec query_str(elli:req()) -> QueryStr :: binary().
query_str(#req{raw_path = Path}) ->
case binary:split(Path, [<<"?">>]) of
[_, Qs] -> Qs;
[_] -> <<>>
end.
%% @doc Parse the `Range' header from the request.
%% The result is either a `byte_range_set()' or the atom `parse_error'.
%% Use {@link elli_util:normalize_range/2} to get a validated, normalized range.
-spec get_range(elli:req()) -> [http_range()] | parse_error.
get_range(#req{headers = Headers}) ->
case proplists:get_value(<<"range">>, Headers) of
<<"bytes=", RangeSetBin/binary>> ->
parse_range_set(RangeSetBin);
_ -> []
end.
-spec parse_range_set(Bin::binary()) -> [http_range()] | parse_error.
parse_range_set(<<ByteRangeSet/binary>>) ->
RangeBins = binary:split(ByteRangeSet, <<",">>, [global]),
Parsed = [parse_range(remove_whitespace(RangeBin))
|| RangeBin <- RangeBins],
case lists:member(parse_error, Parsed) of
true -> parse_error;
false -> Parsed
end.
-spec parse_range(Bin::binary()) -> http_range() | parse_error.
parse_range(<<$-, SuffixBin/binary>>) ->
%% suffix-byte-range
try {suffix, binary_to_integer(SuffixBin)}
catch
error:badarg -> parse_error
end;
parse_range(<<ByteRange/binary>>) ->
case binary:split(ByteRange, <<"-">>) of
%% byte-range without last-byte-pos
[FirstBytePosBin, <<>>] ->
try {offset, binary_to_integer(FirstBytePosBin)}
catch
error:badarg -> parse_error
end;
%% full byte-range
[FirstBytePosBin, LastBytePosBin] ->
try {bytes,
binary_to_integer(FirstBytePosBin),
binary_to_integer(LastBytePosBin)}
catch
error:badarg -> parse_error
end;
_ -> parse_error
end.
-spec remove_whitespace(binary()) -> binary().
remove_whitespace(Bin) ->
binary:replace(Bin, <<" ">>, <<>>, [global]).
%% @doc Serialize the `Req'uest record to a proplist.
%% Useful for logging.
to_proplist(#req{} = Req) ->
lists:zip(record_info(fields, req), tl(tuple_to_list(Req))).
%% @doc Return a reference that can be used to send chunks to the client.
%% If the protocol does not support it, return `{error, not_supported}'.
chunk_ref(#req{version = {1, 1}} = Req) ->
Req#req.pid;
chunk_ref(#req{}) ->
{error, not_supported}.
%% @doc Explicitly close the chunked connection.
%% Return `{error, closed}' if the client already closed the connection.
%% @equiv send_chunk(Ref, close)
close_chunk(Ref) ->
send_chunk(Ref, close).
%% @doc Send a chunk asynchronously.
async_send_chunk(Ref, Data) ->
Ref ! {chunk, Data}.
%% @doc Send a chunk synchronously.
%% If the referenced process is dead, return early with `{error, closed}',
%% instead of timing out.
send_chunk(Ref, Data) ->
?IF(is_ref_alive(Ref),
send_chunk(Ref, Data, 5000),
{error, closed}).
send_chunk(Ref, Data, Timeout) ->
Ref ! {chunk, Data, self()},
receive
{Ref, ok} ->
ok;
{Ref, {error, Reason}} ->
{error, Reason}
after Timeout ->
{error, timeout}
end.
is_ref_alive(Ref) ->
?IF(node(Ref) =:= node(),
is_process_alive(Ref),
rpc:call(node(Ref), erlang, is_process_alive, [Ref])).
is_request(#req{}) -> true;
is_request(_) -> false.
uri_decode(Bin) ->
case binary:match(Bin, [<<"+">>, <<"%">>]) of
nomatch -> Bin;
{Pos, _} ->
<<Prefix:Pos/binary, Rest/binary>> = Bin,
uri_decode(Rest, Prefix)
end.
uri_decode(<<>>, Acc) -> Acc;
uri_decode(<<$+, Rest/binary>>, Acc) ->
uri_decode(Rest, <<Acc/binary, $\s>>);
uri_decode(<<$%, H, L, Rest/binary>>, Acc) ->
uri_decode(Rest, <<Acc/binary, (hex_to_int(H)):4, (hex_to_int(L)):4>>);
uri_decode(<<C, Rest/binary>>, Acc) ->
uri_decode(Rest, <<Acc/binary, C>>).
-compile({inline, [hex_to_int/1]}).
hex_to_int(X) when X >= $0, X =< $9 -> X - $0;
hex_to_int(X) when X >= $a, X =< $f -> X - ($a-10);
hex_to_int(X) when X >= $A, X =< $F -> X - ($A-10).

+ 78
- 0
src/wsSrv/elli_sendfile.erl Vedi File

@ -0,0 +1,78 @@
%% Taken from https://github.com/ninenines/ranch/pull/41/files,
%% with permission from fishcakez
-module(elli_sendfile).
-export([sendfile/5]).
-type sendfile_opts() :: [{chunk_size, non_neg_integer()}].
%% @doc Send part of a file on a socket.
%%
%% Basically, @see file:sendfile/5 but for ssl (i.e. not raw OS sockets).
%% Originally from https://github.com/ninenines/ranch/pull/41/files
%%
%% @end
-spec sendfile(file:fd(), inet:socket() | ssl:sslsocket(),
non_neg_integer(), non_neg_integer(), sendfile_opts())
-> {ok, non_neg_integer()} | {error, atom()}.
sendfile(RawFile, Socket, Offset, Bytes, Opts) ->
ChunkSize = chunk_size(Opts),
Initial2 = case file:position(RawFile, {cur, 0}) of
{ok, Offset} ->
Offset;
{ok, Initial} ->
{ok, _} = file:position(RawFile, {bof, Offset}),
Initial
end,
case sendfile_loop(Socket, RawFile, Bytes, 0, ChunkSize) of
{ok, _Sent} = Result ->
{ok, _} = file:position(RawFile, {bof, Initial2}),
Result;
{error, _Reason} = Error ->
Error
end.
-spec chunk_size(sendfile_opts()) -> pos_integer().
chunk_size(Opts) ->
case lists:keyfind(chunk_size, 1, Opts) of
{chunk_size, ChunkSize}
when is_integer(ChunkSize) andalso ChunkSize > 0 ->
ChunkSize;
{chunk_size, 0} ->
16#1FFF;
false ->
16#1FFF
end.
-spec sendfile_loop(inet:socket() | ssl:sslsocket(), file:fd(), non_neg_integer(),
non_neg_integer(), pos_integer())
-> {ok, non_neg_integer()} | {error, term()}.
sendfile_loop(_Socket, _RawFile, Sent, Sent, _ChunkSize)
when Sent =/= 0 ->
%% All requested data has been read and sent, return number of bytes sent.
{ok, Sent};
sendfile_loop(Socket, RawFile, Bytes, Sent, ChunkSize) ->
ReadSize = read_size(Bytes, Sent, ChunkSize),
case file:read(RawFile, ReadSize) of
{ok, IoData} ->
case ssl:send(Socket, IoData) of
ok ->
Sent2 = iolist_size(IoData) + Sent,
sendfile_loop(Socket, RawFile, Bytes, Sent2,
ChunkSize);
{error, _Reason} = Error ->
Error
end;
eof ->
{ok, Sent};
{error, _Reason} = Error ->
Error
end.
-spec read_size(non_neg_integer(), non_neg_integer(), non_neg_integer()) ->
non_neg_integer().
read_size(0, _Sent, ChunkSize) ->
ChunkSize;
read_size(Bytes, Sent, ChunkSize) ->
min(Bytes - Sent, ChunkSize).

+ 97
- 0
src/wsSrv/elli_tcp.erl Vedi File

@ -0,0 +1,97 @@
%%% @doc Wrapper for plain and SSL sockets.
%%% Based on `mochiweb_socket.erl'.
-module(elli_tcp).
-export([listen/3, accept/3, recv/3, send/2, close/1,
setopts/2, sendfile/5, peername/1]).
-export_type([socket/0]).
-type socket() :: {plain, inet:socket()} | {ssl, ssl:sslsocket()}.
listen(plain, Port, Opts) ->
case gen_tcp:listen(Port, Opts) of
{ok, Socket} ->
{ok, {plain, Socket}};
{error, Reason} ->
{error, Reason}
end;
listen(ssl, Port, Opts) ->
case ssl:listen(Port, Opts) of
{ok, Socket} ->
{ok, {ssl, Socket}};
{error, Reason} ->
{error, Reason}
end.
accept({plain, Socket}, Server, Timeout) ->
case gen_tcp:accept(Socket, Timeout) of
{ok, S} ->
gen_server:cast(Server, accepted),
{ok, {plain, S}};
{error, Reason} ->
{error, Reason}
end;
accept({ssl, Socket}, Server, Timeout) ->
case ssl:transport_accept(Socket, Timeout) of
{ok, S} ->
handshake(S, Server, Timeout);
{error, Reason} ->
{error, Reason}
end.
-ifdef(post20).
handshake(S, Server, Timeout) ->
case ssl:handshake(S, Timeout) of
{ok, S1} ->
gen_server:cast(Server, accepted),
{ok, {ssl, S1}};
{error, closed} ->
{error, econnaborted};
{error, Reason} ->
{error, Reason}
end.
-else.
handshake(S, Server, Timeout) ->
case ssl:ssl_accept(S, Timeout) of
ok ->
gen_server:cast(Server, accepted),
{ok, {ssl, S}};
{error, closed} ->
{error, econnaborted};
{error, Reason} ->
{error, Reason}
end.
-endif.
recv({plain, Socket}, Size, Timeout) ->
gen_tcp:recv(Socket, Size, Timeout);
recv({ssl, Socket}, Size, Timeout) ->
ssl:recv(Socket, Size, Timeout).
send({plain, Socket}, Data) ->
gen_tcp:send(Socket, Data);
send({ssl, Socket}, Data) ->
ssl:send(Socket, Data).
close({plain, Socket}) ->
gen_tcp:close(Socket);
close({ssl, Socket}) ->
ssl:close(Socket).
setopts({plain, Socket}, Opts) ->
inet:setopts(Socket, Opts);
setopts({ssl, Socket}, Opts) ->
ssl:setopts(Socket, Opts).
sendfile(Fd, {plain, Socket}, Offset, Length, Opts) ->
file:sendfile(Fd, Socket, Offset, Length, Opts);
sendfile(Fd, {ssl, Socket}, Offset, Length, Opts) ->
elli_sendfile:sendfile(Fd, Socket, Offset, Length, Opts).
peername({plain, Socket}) ->
inet:peername(Socket);
peername({ssl, Socket}) ->
ssl:peername(Socket).

+ 47
- 0
src/wsSrv/elli_test.erl Vedi File

@ -0,0 +1,47 @@
%%% @author Andreas Hasselberg <andreas.hasselberg@gmail.com>
%%%
%%% @doc Helper for calling your Elli callback in unit tests.
%%% Only the callback specified is actually run. Elli's response handling is not
%%% used, so the headers will for example not include a content length and the
%%% return format is not standardized.
%%% The unit tests below test `elli_example_callback'.
-module(elli_test).
-include("elli.hrl").
-export([call/5]).
-spec call(Method, Path, Headers, Body, Opts) -> elli_handler:result() when
Method :: elli:http_method(),
Path :: binary(),
Headers :: elli:headers(),
Body :: elli:body(),
Opts :: proplists:proplist().
call(Method, Path, Headers, Body, Opts) ->
Callback = proplists:get_value(callback, Opts),
CallbackArgs = proplists:get_value(callback_args, Opts),
Req = elli_http:mk_req(Method, {abs_path, Path}, Headers, Headers,
Body, {1, 1}, undefined, {Callback, CallbackArgs}),
ok = Callback:handle_event(elli_startup, [], CallbackArgs),
Callback:handle(Req, CallbackArgs).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
hello_world_test() ->
?assertMatch({ok, [], <<"Hello World!">>},
elli_test:call('GET', <<"/hello/world/">>, [], <<>>,
?EXAMPLE_CONF)),
?assertMatch({ok, [], <<"Hello Test1">>},
elli_test:call('GET', <<"/hello/?name=Test1">>, [], <<>>,
?EXAMPLE_CONF)),
?assertMatch({ok,
[{<<"content-type">>,
<<"application/json; charset=ISO-8859-1">>}],
<<"{\"name\" : \"Test2\"}">>},
elli_test:call('GET', <<"/type?name=Test2">>,
[{<<"accept">>, <<"application/json">>}], <<>>,
?EXAMPLE_CONF)).
-endif. %% TEST

+ 71
- 0
src/wsSrv/elli_util.erl Vedi File

@ -0,0 +1,71 @@
-module(elli_util).
-include("elli.hrl").
-include("elli_util.hrl").
-include_lib("kernel/include/file.hrl").
-export([normalize_range/2
, encode_range/2
, file_size/1
]).
-export_type([range/0]).
-type range() :: {Offset::non_neg_integer(), Length::non_neg_integer()}.
-spec normalize_range(RangeOrSet, Size) -> Normalized when
RangeOrSet :: any(),
Size :: integer(),
Normalized :: range() | undefined | invalid_range.
%% @doc: If a valid byte-range, or byte-range-set of size 1
%% is supplied, returns a normalized range in the format
%% {Offset, Length}. Returns undefined when an empty byte-range-set
%% is supplied and the atom `invalid_range' in all other cases.
normalize_range({suffix, Length}, Size)
when is_integer(Length), Length > 0 ->
Length0 = erlang:min(Length, Size),
{Size - Length0, Length0};
normalize_range({offset, Offset}, Size)
when is_integer(Offset), Offset >= 0, Offset < Size ->
{Offset, Size - Offset};
normalize_range({bytes, First, Last}, Size)
when is_integer(First), is_integer(Last), First =< Last ->
normalize_range({First, Last - First + 1}, Size);
normalize_range({Offset, Length}, Size)
when is_integer(Offset), is_integer(Length),
Offset >= 0, Length >= 0, Offset < Size ->
Length0 = erlang:min(Length, Size - Offset),
{Offset, Length0};
normalize_range([ByteRange], Size) ->
normalize_range(ByteRange, Size);
normalize_range([], _Size) -> undefined;
normalize_range(_, _Size) -> invalid_range.
-spec encode_range(Range::range() | invalid_range,
Size::non_neg_integer()) -> ByteRange::iolist().
%% @doc: Encode Range to a Content-Range value.
encode_range(Range, Size) ->
[<<"bytes ">>, encode_range_bytes(Range),
<<"/">>, integer_to_binary(Size)].
encode_range_bytes({Offset, Length}) ->
[integer_to_binary(Offset),
<<"-">>,
integer_to_binary(Offset + Length - 1)];
encode_range_bytes(invalid_range) -> <<"*">>.
-spec file_size(Filename) -> Size | {error, Reason} when
Filename :: file:name_all(),
Size :: non_neg_integer(),
Reason :: file:posix() | badarg | invalid_file.
%% @doc: Get the size in bytes of the file.
file_size(Filename) ->
case file:read_file_info(Filename) of
{ok, #file_info{type = regular, access = Perm, size = Size}}
when Perm =:= read orelse Perm =:= read_write ->
Size;
{error, Reason} -> {error, Reason};
_ -> {error, invalid_file}
end.

+ 17
- 0
src/wsSrv/elli_util.hrl Vedi File

@ -0,0 +1,17 @@
-ifdef(OTP_RELEASE).
-include_lib("kernel/include/logger.hrl").
-else.
-define(LOG_ERROR(Str), error_logger:error_msg(Str)).
-define(LOG_ERROR(Format,Data), error_logger:error_msg(Format, Data)).
-define(LOG_INFO(Format,Data), error_logger:info_msg(Format, Data)).
-endif.
-ifdef(OTP_RELEASE).
-define(WITH_STACKTRACE(T, R, S), T:R:S ->).
-else.
-define(WITH_STACKTRACE(T, R, S), T:R -> S = erlang:get_stacktrace(),).
-endif.
%% Bloody useful
-define(IF(Test,True,False), case Test of true -> True; false -> False end).

Caricamento…
Annulla
Salva