Browse Source

ft: 初始化提交

master
SisMaker 3 years ago
commit
df58b46de2
74 changed files with 32829 additions and 0 deletions
  1. +29
    -0
      .gitignore
  2. +21
    -0
      LICENSE
  3. +9
    -0
      README.md
  4. +447
    -0
      include/cow_inline.hrl
  5. +83
    -0
      include/cow_parse.hrl
  6. +21
    -0
      rebar.config
  7. +11
    -0
      src/eWSrv.app.src
  8. +11
    -0
      src/eWSrv_app.erl
  9. +29
    -0
      src/eWSrv_sup.erl
  10. +81
    -0
      src/wsLib/cow_base64url.erl
  11. +428
    -0
      src/wsLib/cow_cookie.erl
  12. +434
    -0
      src/wsLib/cow_date.erl
  13. +1449
    -0
      src/wsLib/cow_hpack.erl
  14. +4132
    -0
      src/wsLib/cow_hpack_dec_huffman_lookup.hrl
  15. +426
    -0
      src/wsLib/cow_http.erl
  16. +483
    -0
      src/wsLib/cow_http2.erl
  17. +1647
    -0
      src/wsLib/cow_http2_machine.erl
  18. +3622
    -0
      src/wsLib/cow_http_hd.erl
  19. +420
    -0
      src/wsLib/cow_http_struct_hd.erl
  20. +373
    -0
      src/wsLib/cow_http_te.erl
  21. +95
    -0
      src/wsLib/cow_iolists.erl
  22. +445
    -0
      src/wsLib/cow_link.erl
  23. +1045
    -0
      src/wsLib/cow_mimetypes.erl
  24. +61
    -0
      src/wsLib/cow_mimetypes.erl.src
  25. +775
    -0
      src/wsLib/cow_multipart.erl
  26. +563
    -0
      src/wsLib/cow_qs.erl
  27. +313
    -0
      src/wsLib/cow_spdy.erl
  28. +181
    -0
      src/wsLib/cow_spdy.hrl
  29. +348
    -0
      src/wsLib/cow_sse.erl
  30. +339
    -0
      src/wsLib/cow_uri.erl
  31. +356
    -0
      src/wsLib/cow_uri_template.erl
  32. +741
    -0
      src/wsLib/cow_ws.erl
  33. +625
    -0
      src/wsNet/ranch.erl
  34. +72
    -0
      src/wsNet/ranch_acceptor.erl
  35. +103
    -0
      src/wsNet/ranch_acceptors_sup.erl
  36. +48
    -0
      src/wsNet/ranch_app.erl
  37. +508
    -0
      src/wsNet/ranch_conns_sup.erl
  38. +42
    -0
      src/wsNet/ranch_conns_sup_sup.erl
  39. +115
    -0
      src/wsNet/ranch_crc32c.erl
  40. +36
    -0
      src/wsNet/ranch_embedded_sup.erl
  41. +48
    -0
      src/wsNet/ranch_listener_sup.erl
  42. +23
    -0
      src/wsNet/ranch_protocol.erl
  43. +1007
    -0
      src/wsNet/ranch_proxy_header.erl
  44. +279
    -0
      src/wsNet/ranch_server.erl
  45. +67
    -0
      src/wsNet/ranch_server_proxy.erl
  46. +341
    -0
      src/wsNet/ranch_ssl.erl
  47. +39
    -0
      src/wsNet/ranch_sup.erl
  48. +287
    -0
      src/wsNet/ranch_tcp.erl
  49. +157
    -0
      src/wsNet/ranch_transport.erl
  50. +105
    -0
      src/wsSrv/cowboy.erl
  51. +27
    -0
      src/wsSrv/cowboy_app.erl
  52. +123
    -0
      src/wsSrv/cowboy_bstr.erl
  53. +192
    -0
      src/wsSrv/cowboy_children.erl
  54. +60
    -0
      src/wsSrv/cowboy_clear.erl
  55. +221
    -0
      src/wsSrv/cowboy_clock.erl
  56. +249
    -0
      src/wsSrv/cowboy_compress_h.erl
  57. +174
    -0
      src/wsSrv/cowboy_constraints.erl
  58. +57
    -0
      src/wsSrv/cowboy_handler.erl
  59. +1523
    -0
      src/wsSrv/cowboy_http.erl
  60. +1220
    -0
      src/wsSrv/cowboy_http2.erl
  61. +108
    -0
      src/wsSrv/cowboy_loop.erl
  62. +331
    -0
      src/wsSrv/cowboy_metrics_h.erl
  63. +24
    -0
      src/wsSrv/cowboy_middleware.erl
  64. +1016
    -0
      src/wsSrv/cowboy_req.erl
  65. +1637
    -0
      src/wsSrv/cowboy_rest.erl
  66. +603
    -0
      src/wsSrv/cowboy_router.erl
  67. +418
    -0
      src/wsSrv/cowboy_static.erl
  68. +193
    -0
      src/wsSrv/cowboy_stream.erl
  69. +324
    -0
      src/wsSrv/cowboy_stream_h.erl
  70. +24
    -0
      src/wsSrv/cowboy_sub_protocol.erl
  71. +30
    -0
      src/wsSrv/cowboy_sup.erl
  72. +56
    -0
      src/wsSrv/cowboy_tls.erl
  73. +192
    -0
      src/wsSrv/cowboy_tracer_h.erl
  74. +707
    -0
      src/wsSrv/cowboy_websocket.erl

+ 29
- 0
.gitignore View File

@ -0,0 +1,29 @@
.eunit
*.o
*.beam
*.plt
erl_crash.dump
.concrete/DEV_MODE
# rebar 2.x
.rebar
rel/example_project
ebin/*
deps
# rebar 3
.rebar3
_build/
_checkouts/
rebar.lock
# idea
.idea
*.iml
cmake-build*
CMakeLists.txt
# nif compile temp file
*.pdb
*.d
compile_commands.json

+ 21
- 0
LICENSE View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 AICells
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

+ 9
- 0
README.md View File

@ -0,0 +1,9 @@
eWSrv
=====
An OTP application
Build
-----
$ rebar3 compile

+ 447
- 0
include/cow_inline.hrl View File

@ -0,0 +1,447 @@
%% 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.

+ 83
- 0
include/cow_parse.hrl View File

@ -0,0 +1,83 @@
%% 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.

+ 21
- 0
rebar.config View File

@ -0,0 +1,21 @@
{erl_opts, [
debug_info,
verbose,
warn_export_vars,
warn_shadow_vars,
warn_obsolete_guard,
warn_missing_spec,
warn_untyped_record
]}.
{deps, [
{eFmt, ".*", {git, "http://sismaker.tpddns.cn:53000/SisMaker/eFmt.git", {branch, "master"}}},
{eGbh, ".*", {git, "http://sismaker.tpddns.cn:53000/SisMaker/eGbh.git", {branch, "master"}}},
{eSync, ".*", {git, "http://sismaker.tpddns.cn:53000/SisMaker/eSync.git", {branch, "master"}}},
{jiffy, ".*", {git, "http://sismaker.tpddns.cn:53000/SisMaker/jiffy.git", {branch, "master"}}}
]}.
{shell, [
% {config, "config/sys.config"},
{apps, [eWSrv]}
]}.

+ 11
- 0
src/eWSrv.app.src View File

@ -0,0 +1,11 @@
{application, eWSrv,
[{description, "erlang web server"},
{vsn, "0.1.0"},
{registered, []},
{mod, {eWSrv_app, []}},
{applications, [kernel, stdlib, crypto, ssl]},
{env, []},
{modules, []},
{licenses, ["MIT"]},
{links, []}
]}.

+ 11
- 0
src/eWSrv_app.erl View File

@ -0,0 +1,11 @@
-module(eWSrv_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
eWSrv_sup:start_link().
stop(_State) ->
ok.

+ 29
- 0
src/eWSrv_sup.erl View File

@ -0,0 +1,29 @@
-module(eWSrv_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
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,
period => 1},
ChildSpecs = [],
{ok, {SupFlags, ChildSpecs}}.

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

@ -0,0 +1,81 @@
%% 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.

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

@ -0,0 +1,428 @@
%% 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.

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

@ -0,0 +1,434 @@
%% 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).

+ 1449
- 0
src/wsLib/cow_hpack.erl
File diff suppressed because it is too large
View File


+ 4132
- 0
src/wsLib/cow_hpack_dec_huffman_lookup.hrl
File diff suppressed because it is too large
View File


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

@ -0,0 +1,426 @@
%% 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.

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

@ -0,0 +1,483 @@
%% 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.

+ 1647
- 0
src/wsLib/cow_http2_machine.erl
File diff suppressed because it is too large
View File


+ 3622
- 0
src/wsLib/cow_http_hd.erl
File diff suppressed because it is too large
View File


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

@ -0,0 +1,420 @@
%% 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.

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

@ -0,0 +1,373 @@
%% 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.

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

@ -0,0 +1,95 @@
%% 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.

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

@ -0,0 +1,445 @@
%% 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.

+ 1045
- 0
src/wsLib/cow_mimetypes.erl
File diff suppressed because it is too large
View File


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

@ -0,0 +1,61 @@
%% 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">>, []}.

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

@ -0,0 +1,775 @@
%% 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 >>).

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

@ -0,0 +1,563 @@
%% 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.

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

@ -0,0 +1,313 @@
%% 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.

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

@ -0,0 +1,181 @@
%% 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 >>).

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

@ -0,0 +1,348 @@
%% 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.

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

@ -0,0 +1,339 @@
%% 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.

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

@ -0,0 +1,356 @@
%% 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.

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

@ -0,0 +1,741 @@
%% 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.

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

@ -0,0 +1,625 @@
%% 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).

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

@ -0,0 +1,72 @@
%% 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.

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

@ -0,0 +1,103 @@
%% 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).

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

@ -0,0 +1,48 @@
%% 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.

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

@ -0,0 +1,508 @@
%% 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).

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

@ -0,0 +1,42 @@
%% 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}}.

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

@ -0,0 +1,115 @@
%% 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.

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

@ -0,0 +1,36 @@
%% 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]}}.

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

@ -0,0 +1,48 @@
%% 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}}.

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

@ -0,0 +1,23 @@
%% 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()}.

+ 1007
- 0
src/wsNet/ranch_proxy_header.erl
File diff suppressed because it is too large
View File


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

@ -0,0 +1,279 @@
%% 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]}.

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

@ -0,0 +1,67 @@
%% 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.

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

@ -0,0 +1,341 @@
%% 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.

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

@ -0,0 +1,39 @@
%% 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}}.

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

@ -0,0 +1,287 @@
%% 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.

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

@ -0,0 +1,157 @@
%% 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).

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

@ -0,0 +1,105 @@
%% 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).

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

@ -0,0 +1,27 @@
%% 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.

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

@ -0,0 +1,123 @@
%% 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.

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

@ -0,0 +1,192 @@
%% 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}
].

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

@ -0,0 +1,60 @@
%% 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).

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

@ -0,0 +1,221 @@
%% 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.

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

@ -0,0 +1,249 @@
%% 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.

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

@ -0,0 +1,174 @@
%% 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.

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

@ -0,0 +1,57 @@
%% 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.

+ 1523
- 0
src/wsSrv/cowboy_http.erl
File diff suppressed because it is too large
View File


+ 1220
- 0
src/wsSrv/cowboy_http2.erl
File diff suppressed because it is too large
View File


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

@ -0,0 +1,108 @@
%% 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}.

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

@ -0,0 +1,331 @@
%% 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).

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

@ -0,0 +1,24 @@
%% 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().

+ 1016
- 0
src/wsSrv/cowboy_req.erl
File diff suppressed because it is too large
View File


+ 1637
- 0
src/wsSrv/cowboy_rest.erl
File diff suppressed because it is too large
View File


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

@ -0,0 +1,603 @@
%% 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.

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

@ -0,0 +1,418 @@
%% 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}.

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

@ -0,0 +1,193 @@
%% 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]}.

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

@ -0,0 +1,324 @@
%% 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.

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

@ -0,0 +1,24 @@
%% 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().

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

@ -0,0 +1,30 @@
%% 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}}.

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

@ -0,0 +1,56 @@
%% 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).

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

@ -0,0 +1,192 @@
%% 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}.

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

@ -0,0 +1,707 @@
%% 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}.

Loading…
Cancel
Save