@ -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 |
@ -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. |
@ -0,0 +1,9 @@ | |||
eWSrv | |||
===== | |||
An OTP application | |||
Build | |||
----- | |||
$ rebar3 compile |
@ -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. |
@ -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. |
@ -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]} | |||
]}. |
@ -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, []} | |||
]}. |
@ -0,0 +1,11 @@ | |||
-module(eWSrv_app). | |||
-behaviour(application). | |||
-export([start/2, stop/1]). | |||
start(_StartType, _StartArgs) -> | |||
eWSrv_sup:start_link(). | |||
stop(_State) -> | |||
ok. |
@ -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}}. | |||
@ -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. |
@ -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. |
@ -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). |
@ -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. |
@ -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. |
@ -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. |
@ -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. |
@ -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. |
@ -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. |
@ -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">>, []}. |
@ -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 >>). |
@ -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. |
@ -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. |
@ -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 >>). |
@ -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. |
@ -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. |
@ -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. |
@ -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. |
@ -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). |
@ -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. |
@ -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). |
@ -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. |
@ -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). |
@ -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}}. |
@ -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. |
@ -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]}}. |
@ -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}}. |
@ -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()}. |
@ -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]}. |
@ -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. |
@ -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. |
@ -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}}. |
@ -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. |
@ -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). |
@ -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). |
@ -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. |
@ -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. |
@ -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} | |||
]. |
@ -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). |
@ -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. |
@ -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. |
@ -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. |
@ -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. |
@ -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}. |
@ -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). |
@ -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(). |
@ -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. |
@ -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}. |
@ -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]}. |
@ -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. |
@ -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(). |
@ -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}}. |
@ -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). |
@ -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}. |
@ -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}. |