Import cowlib.

Cowlib is the base library used by ranch and cowboy, respectively
the listener and the webserver used in the dudeswave stack.

URL: https://github.com/ninenines/cowlib
main
absc 2024-07-23 22:04:25 +02:00
commit 8ebac63dac
40 changed files with 19903 additions and 0 deletions

13
cowlib/LICENSE Normal file
View File

@ -0,0 +1,13 @@
Copyright (c) 2013-2024, 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.

7
cowlib/Makefile Normal file
View File

@ -0,0 +1,7 @@
.PHONY: all clean
all:
${MAKE} -C src
clean:
${MAKE} -C src clean

18
cowlib/README.asciidoc Normal file
View File

@ -0,0 +1,18 @@
= Cowlib
Cowlib is a support library for manipulating Web protocols.
== Goals
Cowlib provides libraries for parsing and building messages
for various Web protocols, including HTTP/1.1, HTTP/2 and
Websocket.
It is optimized for completeness rather than speed. No value
is ignored, they are all returned.
== Support
* Official IRC Channel: #ninenines on irc.freenode.net
* https://ninenines.eu/services[Commercial Support]
* https://github.com/sponsors/essen[Sponsor me!]

View File

@ -0,0 +1,114 @@
= cow_cookie(3)
== Name
cow_cookie - Cookies
== Description
The module `cow_cookie` provides functions for parsing
and manipulating cookie headers.
== Exports
* link:man:cow_cookie:parse_cookie(3)[cow_cookie:parse_cookie(3)] - Parse a cookie header
* link:man:cow_cookie:parse_set_cookie(3)[cow_cookie:parse_set_cookie(3)] - Parse a set-cookie header
* link:man:cow_cookie:cookie(3)[cow_cookie:cookie(3)] - Generate a cookie header
* link:man:cow_cookie:setcookie(3)[cow_cookie:setcookie(3)] - Generate a set-cookie header
== Types
=== cookie_attrs()
[source,erlang]
----
cookie_attrs() :: #{
expires => calendar:datetime(),
max_age => calendar:datetime(),
domain => binary(),
path => binary(),
secure => true,
http_only => true,
same_site => default | none | strict | lax
}
----
Cookie attributes parsed from the set-cookie header.
The attributes must be passed as-is to a cookie store
engine for processing, along with the cookie name and value.
More information about the attributes can be found in
https://tools.ietf.org/html/rfc6265[RFC 6265].
=== cookie_opts()
[source,erlang]
----
cookie_opts() :: #{
domain => binary(),
http_only => boolean(),
max_age => non_neg_integer(),
path => binary(),
same_site => default | none | strict | lax,
secure => boolean()
}
----
Options for the set-cookie header. They are added to the
header as attributes. More information about the options
can be found in https://tools.ietf.org/html/rfc6265[RFC 6265].
The following options are defined:
domain::
Hosts to which the cookie will be sent. By default it will
only be sent to the origin server.
http_only::
Whether the cookie should be restricted to HTTP requests, or
it should also be exposed to other APIs, for example Javascript.
By default there are no restrictions.
max_age::
Maximum lifetime of the cookie, in seconds. By default the
cookie is kept for the duration of the session.
path::
Path to which the cookie will be sent. By default it will
be sent to the current "directory" of the effective request URI.
same_site::
Whether the cookie should be sent along with cross-site
requests. This attribute is currently non-standard but is in
the process of being standardized. Please refer to the
https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7[RFC 6265 (bis) draft]
for details.
+
The default value for this attribute may vary depending on
user agent and configuration. Browsers are known to be more
strict over TCP compared to TLS.
secure::
Whether the cookie should be sent only on secure channels
(for example TLS). Note that this does not guarantee the
integrity of the cookie, only its confidentiality during
transfer. By default there are no restrictions.
== Changelog
* *2.12*: The `same_site` attribute and option may now be
set to `default`.
* *2.10*: The `same_site` attribute and option may now be
set to `none`.
* *2.9*: The `cookie_attrs` type was added.
* *1.0*: Module introduced.
== See also
link:man:cowlib(7)[cowlib(7)],
https://tools.ietf.org/html/rfc6265[RFC 6265]

View File

@ -0,0 +1,45 @@
= cow_cookie:cookie(3)
== Name
cow_cookie:cookie - Generate a cookie header
== Description
[source,erlang]
----
cookie(Cookies) -> iolist()
Cookies :: [{Name :: iodata(), Value :: iodata()}]
----
Generate a cookie header.
== Arguments
Cookies::
A list of pairs of cookie name and value.
== Return value
An iolist with the generated cookie header value.
== Changelog
* *2.9*: Function introduced.
== Examples
.Generate a cookie header
[source,erlang]
----
Cookie = cow_cookie:cookie([{<<"sessionid">>, ID}]).
----
== See also
link:man:cow_cookie(3)[cow_cookie(3)],
link:man:cow_cookie:parse_cookie(3)[cow_cookie:parse_cookie(3)],
link:man:cow_cookie:parse_set_cookie(3)[cow_cookie:parse_set_cookie(3)],
link:man:cow_cookie:setcookie(3)[cow_cookie:setcookie(3)]

View File

@ -0,0 +1,50 @@
= cow_cookie:parse_cookie(3)
== Name
cow_cookie:parse_cookie - Parse a cookie header
== Description
[source,erlang]
----
parse_cookie(Cookie :: binary())
-> [{binary(), binary()}]
----
Parse a cookie header.
== Arguments
Cookie::
The cookie header value.
== Return value
A list of cookie name/value pairs is returned on success.
An exception is thrown in the event of a parse error.
== Changelog
* *2.9*: Fixes to the parser may lead to potential incompatibilities.
A cookie name starting with `$` is no longer ignored.
A cookie without a `=` will be parsed as the value of
the cookie named `<<>>` (empty name).
* *1.0*: Function introduced.
== Examples
.Parse a cookie header
[source,erlang]
----
Cookies = cow_cookie:parse_cookie(CookieHd).
----
== See also
link:man:cow_cookie(3)[cow_cookie(3)],
link:man:cow_cookie:parse_set_cookie(3)[cow_cookie:parse_set_cookie(3)],
link:man:cow_cookie:cookie(3)[cow_cookie:cookie(3)],
link:man:cow_cookie:setcookie(3)[cow_cookie:setcookie(3)]

View File

@ -0,0 +1,57 @@
= cow_cookie:parse_set_cookie(3)
== Name
cow_cookie:parse_set_cookie - Parse a set-cookie header
== Description
[source,erlang]
----
parse_set_cookie(SetCookie :: binary())
-> {ok, Name, Value, Attrs} | ignore
Name :: binary()
Value :: binary()
Attrs :: cow_cookie:cookie_attrs()
----
Parse a set-cookie header.
== Arguments
SetCookie::
The set-cookie header value.
== Return value
An `ok` tuple with the cookie name, value and attributes
is returned on success.
An atom `ignore` is returned when the cookie has both
an empty name and an empty value, and must be ignored.
== Changelog
* *2.9*: Function introduced.
== Examples
.Parse a cookie header
[source,erlang]
----
case cow_cookie:parse_set_cookie(SetCookieHd) of
{ok, Name, Value, Attrs} ->
cookie_engine_set_cookie(Name, Value, Attrs);
ignore ->
do_nothing()
end.
----
== See also
link:man:cow_cookie(3)[cow_cookie(3)],
link:man:cow_cookie:parse_cookie(3)[cow_cookie:parse_cookie(3)],
link:man:cow_cookie:cookie(3)[cow_cookie:cookie(3)],
link:man:cow_cookie:setcookie(3)[cow_cookie:setcookie(3)]

View File

@ -0,0 +1,58 @@
= cow_cookie:setcookie(3)
== Name
cow_cookie:setcookie - Generate a set-cookie header
== Description
[source,erlang]
----
setcookie(Name :: iodata(),
Value :: iodata(),
Opts :: cow_cookie:cookie_opts())
-> iolist()
----
Generate a set-cookie header.
== Arguments
Name::
Cookie name.
Value::
Cookie value.
Opts::
Options added to the set-cookie header as attributes.
== Return value
An iolist with the generated set-cookie header value.
== Changelog
* *2.12*: The `Version` attribute is no longer generated.
* *1.0*: Function introduced.
== Examples
.Generate a set-cookie header
[source,erlang]
----
SetCookie = cow_cookie:setcookie(<<"sessionid">>, ID, #{
http_only => true,
secure => true
}).
----
== See also
link:man:cow_cookie(3)[cow_cookie(3)],
link:man:cow_cookie:parse_cookie(3)[cow_cookie:parse_cookie(3)],
link:man:cow_cookie:parse_set_cookie(3)[cow_cookie:parse_set_cookie(3)],
link:man:cow_cookie:cookie(3)[cow_cookie:cookie(3)]

View File

@ -0,0 +1,40 @@
= cowlib(7)
== Name
cowlib - Support library for manipulating Web protocols
== Description
Cowlib provides libraries for parsing and building messages
for various Web protocols, including HTTP/1.1, HTTP/2 and
Websocket.
It is optimized for completeness rather than speed. No value
is ignored, they are all returned.
== Modules
* link:man:cow_cookie(3)[cow_cookie(3)] - Cookies
== Dependencies
* crypto - Crypto functions
All these applications must be started before the `cowlib`
application. To start Cowlib and all dependencies at once:
[source,erlang]
----
{ok, _} = application:ensure_all_started(cowlib).
----
== Environment
The `cowlib` application does not define any application
environment configuration parameters.
== See also
link:man:cowboy(7)[cowboy(7)],
link:man:gun(7)[gun(7)]

8
cowlib/ebin/cowlib.app Normal file
View File

@ -0,0 +1,8 @@
{application, 'cowlib', [
{description, "Support library for manipulating Web protocols."},
{vsn, "2.13.0"},
{modules, ['cow_base64url','cow_cookie','cow_date','cow_hpack','cow_http','cow_http2','cow_http2_machine','cow_http_hd','cow_http_struct_hd','cow_http_te','cow_iolists','cow_link','cow_mimetypes','cow_multipart','cow_qs','cow_spdy','cow_sse','cow_uri','cow_uri_template','cow_ws']},
{registered, []},
{applications, [kernel,stdlib,crypto]},
{env, []}
]}.

View File

@ -0,0 +1,447 @@
%% Copyright (c) 2014-2023, 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.

View File

@ -0,0 +1,83 @@
%% Copyright (c) 2015-2023, 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.

22
cowlib/src/Makefile Normal file
View File

@ -0,0 +1,22 @@
.PHONY: all
.SUFFIXES: .erl .beam
ERLC?= erlc -server
ERLFLAGS+= -I ../include
OBJS+= cow_base64url.beam cow_cookie.beam cow_date.beam
OBJS+= cow_hpack.beam cow_http.beam cow_http2.beam
OBJS+= cow_http2_machine.beam cow_http_hd.beam
OBJS+= cow_http_struct_hd.beam cow_http_te.beam cow_iolists.beam
OBJS+= cow_link.beam cow_mimetypes.beam cow_multipart.beam
OBJS+= cow_qs.beam cow_spdy.beam cow_sse.beam cow_uri.beam
OBJS+= cow_uri_template.beam cow_ws.beam
all: ${OBJS}
.erl.beam:
${ERLC} ${ERLOPTS} ${ERLFLAGS} $<
clean:
rm -f *.beam

Binary file not shown.

View File

@ -0,0 +1,81 @@
%% Copyright (c) 2017-2023, 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.

BIN
cowlib/src/cow_cookie.beam Normal file

Binary file not shown.

456
cowlib/src/cow_cookie.erl Normal file
View File

@ -0,0 +1,456 @@
%% Copyright (c) 2013-2023, 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 => default | none | strict | lax
}.
-export_type([cookie_attrs/0]).
-type cookie_opts() :: #{
domain => binary(),
http_only => boolean(),
max_age => non_neg_integer(),
path => binary(),
same_site => default | none | strict | lax,
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) ->
case has_non_ws_ctl(SetCookie) of
true ->
ignore;
false ->
{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
end.
has_non_ws_ctl(<<>>) ->
false;
has_non_ws_ctl(<<C,R/bits>>) ->
if
C =< 16#08 -> true;
C >= 16#0A, C =< 16#1F -> true;
C =:= 16#7F -> true;
true -> has_non_ws_ctl(R)
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,
if
byte_size(Value) > 1024 ->
parse_set_cookie_attrs(Rest, Attrs);
true ->
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
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
<<"none">> ->
{ok, same_site, none};
<<"strict">> ->
{ok, same_site, strict};
<<"lax">> ->
{ok, same_site, lax};
%% Unknown values and lack of value are equivalent.
_ ->
{ok, same_site, default}
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=UnknownValue">>, {ok, <<"a">>, <<"b">>, #{same_site => default}}},
{<<"a=b; SameSite=None">>, {ok, <<"a">>, <<"b">>, #{same_site => none}}},
{<<"a=b; SameSite=Lax">>, {ok, <<"a">>, <<"b">>, #{same_site => lax}}},
{<<"a=b; SameSite=Strict">>, {ok, <<"a">>, <<"b">>, #{same_site => strict}}},
{<<"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, 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, default}|Tail]) -> attributes(Tail);
attributes([{same_site, none}|Tail]) -> [<<"; SameSite=None">>|attributes(Tail)];
attributes([{same_site, lax}|Tail]) -> [<<"; SameSite=Lax">>|attributes(Tail)];
attributes([{same_site, strict}|Tail]) -> [<<"; SameSite=Strict">>|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; "
"Domain=acme.com; HttpOnly">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{path => <<"/acme">>},
<<"Customer=WILE_E_COYOTE; Path=/acme">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{secure => true},
<<"Customer=WILE_E_COYOTE; Secure">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{secure => false, http_only => false},
<<"Customer=WILE_E_COYOTE">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{same_site => default},
<<"Customer=WILE_E_COYOTE">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{same_site => none},
<<"Customer=WILE_E_COYOTE; SameSite=None">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{same_site => lax},
<<"Customer=WILE_E_COYOTE; SameSite=Lax">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{same_site => strict},
<<"Customer=WILE_E_COYOTE; SameSite=Strict">>},
{<<"Customer">>, <<"WILE_E_COYOTE">>,
#{path => <<"/acme">>, badoption => <<"negatory">>},
<<"Customer=WILE_E_COYOTE; 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">>,
<<" 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">>,
<<" 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.

BIN
cowlib/src/cow_date.beam Normal file

Binary file not shown.

434
cowlib/src/cow_date.erl Normal file
View File

@ -0,0 +1,434 @@
%% Copyright (c) 2013-2023, 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).

BIN
cowlib/src/cow_hpack.beam Normal file

Binary file not shown.

1449
cowlib/src/cow_hpack.erl Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

426
cowlib/src/cow_http.erl Normal file
View File

@ -0,0 +1,426 @@
%% Copyright (c) 2013-2023, 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.

482
cowlib/src/cow_http2.erl Normal file
View File

@ -0,0 +1,482 @@
%% Copyright (c) 2015-2023, 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 non-fatal frame size errors; if we have everything
%% then OK 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};
parse(<< BadLen:24, 3:8, _:9, _:31, _/bits >>) when BadLen =/= 4 ->
{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.

File diff suppressed because it is too large Load Diff

3642
cowlib/src/cow_http_hd.erl Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,522 @@
%% Copyright (c) 2019-2023, 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()
%% Inner list: {list, [item()], params()}
%% Dictionary: [{binary(), item()}]
%% There is no distinction between empty list and empty dictionary.
%% Item with parameters: {item, bare_item(), params()}
%% Parameters: [{binary(), bare_item()}]
%% Bare item: one bare_item() that can be of type:
%% Integer: integer()
%% Decimal: {decimal, {integer(), integer()}}
%% String: {string, binary()}
%% Token: {token, binary()}
%% Byte sequence: {binary, binary()}
%% Boolean: boolean()
-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() :: {list, [sh_item()], sh_params()}.
-type sh_params() :: [{binary(), sh_bare_item()}].
-type sh_dictionary() :: [{binary(), sh_item() | sh_inner_list()}].
-type sh_item() :: {item, sh_bare_item(), sh_params()}.
-type sh_bare_item() :: integer() | sh_decimal() | boolean()
| {string | token | binary, binary()}.
-type sh_decimal() :: {decimal, {integer(), integer()}}.
-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) or (C =:= $*) ->
parse_dict_key(R, [], <<C>>).
parse_dict_key(<<$=,$(,R0/bits>>, Acc, K) ->
{Item, R} = parse_inner_list(R0, []),
parse_dict_before_sep(R, lists:keystore(K, 1, Acc, {K, Item}));
parse_dict_key(<<$=,R0/bits>>, Acc, K) ->
{Item, R} = parse_item1(R0),
parse_dict_before_sep(R, lists:keystore(K, 1, Acc, {K, Item}));
parse_dict_key(<<C,R/bits>>, Acc, K)
when ?IS_LC_ALPHA(C) or ?IS_DIGIT(C)
or (C =:= $_) or (C =:= $-) or (C =:= $.) or (C =:= $*) ->
parse_dict_key(R, Acc, <<K/binary,C>>);
parse_dict_key(<<$;,R0/bits>>, Acc, K) ->
{Params, R} = parse_before_param(R0, []),
parse_dict_before_sep(R, lists:keystore(K, 1, Acc, {K, {item, true, Params}}));
parse_dict_key(R, Acc, K) ->
parse_dict_before_sep(R, lists:keystore(K, 1, Acc, {K, {item, true, []}})).
parse_dict_before_sep(<<$\s,R/bits>>, Acc) ->
parse_dict_before_sep(R, Acc);
parse_dict_before_sep(<<$\t,R/bits>>, Acc) ->
parse_dict_before_sep(R, Acc);
parse_dict_before_sep(<<C,R/bits>>, Acc) when C =:= $, ->
parse_dict_before_member(R, Acc);
parse_dict_before_sep(<<>>, Acc) ->
Acc.
parse_dict_before_member(<<$\s,R/bits>>, Acc) ->
parse_dict_before_member(R, Acc);
parse_dict_before_member(<<$\t,R/bits>>, Acc) ->
parse_dict_before_member(R, Acc);
parse_dict_before_member(<<C,R/bits>>, Acc) when ?IS_LC_ALPHA(C) or (C =:= $*) ->
parse_dict_key(R, Acc, <<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, []),
{{item, Item, Params}, Rest};
{Item, Rest} ->
{{item, 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(<<$\s,R/bits>>, Acc) ->
parse_list_before_sep(R, Acc);
parse_list_before_sep(<<$\t,R/bits>>, Acc) ->
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(<<$\s,R/bits>>, Acc) ->
parse_list_before_member(R, Acc);
parse_list_before_member(<<$\t,R/bits>>, Acc) ->
parse_list_before_member(R, Acc);
parse_list_before_member(R, Acc) ->
parse_list_member(R, Acc).
%% Internal.
parse_inner_list(<<$\s,R/bits>>, Acc) ->
parse_inner_list(R, Acc);
parse_inner_list(<<$),$;,R0/bits>>, Acc) ->
{Params, R} = parse_before_param(R0, []),
{{list, lists:reverse(Acc), Params}, R};
parse_inner_list(<<$),R/bits>>, Acc) ->
{{list, 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(<<$\s,R/bits>>, Acc) ->
parse_before_param(R, Acc);
parse_before_param(<<C,R/bits>>, Acc) when ?IS_LC_ALPHA(C) or (C =:= $*) ->
parse_param(R, Acc, <<C>>).
parse_param(<<$;,R/bits>>, Acc, K) ->
parse_before_param(R, lists:keystore(K, 1, Acc, {K, true}));
parse_param(<<$=,R0/bits>>, Acc, K) ->
case parse_bare_item(R0) of
{Item, <<$;,R/bits>>} ->
parse_before_param(R, lists:keystore(K, 1, Acc, {K, Item}));
{Item, R} ->
{lists:keystore(K, 1, 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 =:= $.) or (C =:= $*) ->
parse_param(R, Acc, <<K/binary,C>>);
parse_param(R, Acc, K) ->
{lists:keystore(K, 1, Acc, {K, true}), R}.
%% Integer or decimal.
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) or (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(<<$.,R/bits>>, L, Acc) ->
parse_decimal(R, L, 0, Acc, <<>>);
parse_number(R, L, Acc) when L =< 15 ->
{binary_to_integer(Acc), R}.
parse_decimal(<<C,R/bits>>, L1, L2, IntAcc, FracAcc) when ?IS_DIGIT(C) ->
parse_decimal(R, L1, L2+1, IntAcc, <<FracAcc/binary,C>>);
parse_decimal(R, L1, L2, IntAcc, FracAcc0) when L1 =< 12, L2 >= 1, L2 =< 3 ->
%% While not strictly required this gives a more consistent representation.
FracAcc = case FracAcc0 of
<<$0>> -> <<>>;
<<$0,$0>> -> <<>>;
<<$0,$0,$0>> -> <<>>;
<<A,B,$0>> -> <<A,B>>;
<<A,$0,$0>> -> <<A>>;
<<A,$0>> -> <<A>>;
_ -> FracAcc0
end,
Mul = case byte_size(FracAcc) of
3 -> 1000;
2 -> 100;
1 -> 10;
0 -> 1
end,
Int = binary_to_integer(IntAcc),
Frac = case FracAcc of
<<>> -> 0;
%% Mind the sign.
_ when Int < 0 -> -binary_to_integer(FracAcc);
_ -> binary_to_integer(FracAcc)
end,
{{decimal, {Int * Mul + Frac, -byte_size(FracAcc)}}, 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),
io:format("must fail ~p~nexpected json ~0p~n",
[MustFail, maps:get(<<"expected">>, Test, undefined)]),
Expected = case MustFail of
true -> undefined;
false -> expected_to_term(maps:get(<<"expected">>, Test))
end,
io:format("expected term: ~0p", [Expected]),
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 = (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]).
%% The tests JSON use arrays for almost everything. Identifying
%% what is what requires looking deeper in the values:
%%
%% dict: [["k", v], ["k2", v2]] (values may have params)
%% params: [["k", v], ["k2", v2]] (no params for values)
%% list: [e1, e2, e3]
%% inner-list: [[ [items...], params]]
%% item: [bare, params]
%% Item.
expected_to_term([Bare, []])
when is_boolean(Bare); is_number(Bare); is_binary(Bare); is_map(Bare) ->
{item, e2tb(Bare), []};
expected_to_term([Bare, Params = [[<<_/bits>>, _]|_]])
when is_boolean(Bare); is_number(Bare); is_binary(Bare); is_map(Bare) ->
{item, e2tb(Bare), e2tp(Params)};
%% Empty list or dictionary.
expected_to_term([]) ->
[];
%% Dictionary.
%%
%% We exclude empty list from values because that could
%% be confused with an outer list of strings. There is
%% currently no conflicts in the tests thankfully.
expected_to_term(Dict = [[<<_/bits>>, V]|_]) when V =/= [] ->
e2t(Dict);
%% Outer list.
expected_to_term(List) when is_list(List) ->
[e2t(E) || E <- List].
%% Dictionary.
e2t(Dict = [[<<_/bits>>, _]|_]) ->
[{K, e2t(V)} || [K, V] <- Dict];
%% Inner list.
e2t([List, Params]) when is_list(List) ->
{list, [e2t(E) || E <- List], e2tp(Params)};
%% Item.
e2t([Bare, Params]) ->
{item, e2tb(Bare), e2tp(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(V) when is_float(V) ->
%% There should be no rounding needed for the test cases.
{decimal, decimal:to_decimal(V, #{precision => 3, rounding => round_down})};
e2tb(V) ->
V.
%% Params.
e2tp([]) ->
[];
e2tp(Params) ->
[{K, e2tb(V)} || [K, V] <- Params].
%% 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(<<$\s,R/bits>>) -> 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);
_ ->
S = N + 1,
<< Value2:S/binary, _/bits >> = Value,
Value2
end.
-endif.
%% Building.
-spec dictionary(#{binary() => sh_item() | sh_inner_list()} | sh_dictionary())
-> iolist().
dictionary(Map) when is_map(Map) ->
dictionary(maps:to_list(Map));
dictionary(KVList) when is_list(KVList) ->
lists:join(<<", ">>, [
case Value of
true -> Key;
_ -> [Key, $=, item_or_inner_list(Value)]
end
|| {Key, Value} <- KVList]).
-spec item(sh_item()) -> iolist().
item({item, 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 = {list, _, _}) ->
inner_list(Value);
item_or_inner_list(Value) ->
item(Value).
inner_list({list, List, Params}) ->
[$(, lists:join($\s, [item(Value) || Value <- List]), $), params(Params)].
bare_item({string, String}) ->
[$", escape_string(String, <<>>), $"];
%% @todo Must fail if Token has invalid characters.
bare_item({token, Token}) ->
Token;
bare_item({binary, Binary}) ->
[$:, base64:encode(Binary), $:];
bare_item({decimal, {Base, Exp}}) when Exp >= 0 ->
Mul = case Exp of
0 -> 1;
1 -> 10;
2 -> 100;
3 -> 1000;
4 -> 10000;
5 -> 100000;
6 -> 1000000;
7 -> 10000000;
8 -> 100000000;
9 -> 1000000000;
10 -> 10000000000;
11 -> 100000000000;
12 -> 1000000000000
end,
MaxLenWithSign = if
Base < 0 -> 13;
true -> 12
end,
Bin = integer_to_binary(Base * Mul),
true = byte_size(Bin) =< MaxLenWithSign,
[Bin, <<".0">>];
bare_item({decimal, {Base, -1}}) ->
Int = Base div 10,
Frac = abs(Base) rem 10,
[integer_to_binary(Int), $., integer_to_binary(Frac)];
bare_item({decimal, {Base, -2}}) ->
Int = Base div 100,
Frac = abs(Base) rem 100,
[integer_to_binary(Int), $., integer_to_binary(Frac)];
bare_item({decimal, {Base, -3}}) ->
Int = Base div 1000,
Frac = abs(Base) rem 1000,
[integer_to_binary(Int), $., integer_to_binary(Frac)];
bare_item({decimal, {Base, Exp}}) ->
Div = exp_div(Exp),
Int0 = Base div Div,
true = abs(Int0) < 1000000000000,
Frac0 = abs(Base) rem Div,
DivFrac = Div div 1000,
Frac1 = Frac0 div DivFrac,
{Int, Frac} = if
(Frac0 rem DivFrac) > (DivFrac div 2) ->
case Frac1 of
999 when Int0 < 0 -> {Int0 - 1, 0};
999 -> {Int0 + 1, 0};
_ -> {Int0, Frac1 + 1}
end;
true ->
{Int0, Frac1}
end,
[integer_to_binary(Int), $., if
Frac < 10 -> [$0, $0, integer_to_binary(Frac)];
Frac < 100 -> [$0, integer_to_binary(Frac)];
true -> integer_to_binary(Frac)
end];
bare_item(Integer) when is_integer(Integer) ->
integer_to_binary(Integer);
bare_item(true) ->
<<"?1">>;
bare_item(false) ->
<<"?0">>.
exp_div(0) -> 1;
exp_div(N) -> 10 * exp_div(N + 1).
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) ->
[case Param of
{Key, true} -> [$;, Key];
{Key, Value} -> [$;, Key, $=, bare_item(Value)]
end || Param <- 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() ->
io:format("expected json ~0p~n", [Expected0]),
Expected = expected_to_term(Expected0),
io:format("expected term: ~0p", [Expected]),
case HeaderType of
<<"dictionary">> ->
Expected = parse_dictionary(iolist_to_binary(dictionary(Expected)));
<<"item">> ->
Expected = parse_item(iolist_to_binary(item(Expected)));
<<"list">> ->
Expected = parse_list(iolist_to_binary(list(Expected)))
end
end}
|| #{
<<"name">> := Name,
<<"header_type">> := HeaderType,
%% We only run tests that must not fail.
<<"expected">> := Expected0
} <- Tests]
end || File <- Files]).
-endif.

373
cowlib/src/cow_http_te.erl Normal file
View File

@ -0,0 +1,373 @@
%% Copyright (c) 2014-2023, 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.

View File

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

445
cowlib/src/cow_link.erl Normal file
View File

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

1045
cowlib/src/cow_mimetypes.erl Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
%% Copyright (c) 2013-2023, 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">>, []}.

View File

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

563
cowlib/src/cow_qs.erl Normal file
View File

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

313
cowlib/src/cow_spdy.erl Normal file
View File

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

181
cowlib/src/cow_spdy.hrl Normal file
View File

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

349
cowlib/src/cow_sse.erl Normal file
View File

@ -0,0 +1,349 @@
%% Copyright (c) 2017-2023, 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}
when State::state().
parse(Data0, State=#state{state_name=bom, buffer=Buffer}) ->
Data1 = case Buffer of
<<>> -> Data0;
_ -> << Buffer/binary, Data0/binary >>
end,
case Data1 of
%% Skip the BOM.
<< 16#fe, 16#ff, Data/bits >> ->
parse_event(Data, State#state{state_name=events, buffer= <<>>});
%% Not enough data to know wether we have a BOM.
<< 16#fe >> ->
{more, State#state{buffer=Data1}};
<<>> ->
{more, State};
%% No BOM.
_ ->
parse_event(Data1, State#state{state_name=events, buffer= <<>>})
end;
%% Try to process data from the buffer if there is no new input.
parse(<<>>, State=#state{buffer=Buffer}) ->
parse_event(Buffer, State#state{buffer= <<>>});
%% Otherwise process the input data as-is.
parse(Data0, State=#state{buffer=Buffer}) ->
Data = case Buffer of
<<>> -> Data0;
_ -> << Buffer/binary, Data0/binary >>
end,
parse_event(Data, State).
parse_event(Data, State0) ->
case binary:split(Data, [<<"\r\n">>, <<"\r">>, <<"\n">>]) of
[Line, Rest] ->
case parse_line(Line, State0) of
{ok, State} ->
parse_event(Rest, State);
{event, Event, State} ->
{event, Event, State#state{buffer=Rest}}
end;
[_] ->
{more, State0#state{buffer=Data}}
end.
%% Dispatch events on empty line.
parse_line(<<>>, State) ->
dispatch_event(State);
%% Ignore comments.
parse_line(<< $:, _/bits >>, State) ->
{ok, State};
%% Normal line.
parse_line(Line, State) ->
case binary:split(Line, [<<":\s">>, <<":">>]) of
[Field, Value] ->
process_field(Field, Value, State);
[Field] ->
process_field(Field, <<>>, State)
end.
process_field(<<"event">>, Value, State) ->
{ok, State#state{event_type=Value}};
process_field(<<"data">>, Value, State=#state{data=Data}) ->
{ok, State#state{data=[<<$\n>>, Value|Data]}};
process_field(<<"id">>, Value, State) ->
{ok, State#state{last_event_id=Value, last_event_id_set=true}};
process_field(<<"retry">>, Value, State) ->
try
{ok, State#state{retry=binary_to_integer(Value)}}
catch _:_ ->
{ok, State}
end;
process_field(_, _, State) ->
{ok, State}.
%% Data is an empty string; abort.
dispatch_event(State=#state{last_event_id_set=false, data=[]}) ->
{ok, State#state{event_type= <<>>}};
%% Data is an empty string but we have a last_event_id:
%% propagate it on its own so that the caller knows the
%% most recent ID.
dispatch_event(State=#state{last_event_id=LastEventID, data=[]}) ->
{event, #{
last_event_id => LastEventID
}, State#state{last_event_id_set=false, event_type= <<>>}};
%% Dispatch the event.
%%
%% Always remove the last linebreak from the data.
dispatch_event(State=#state{last_event_id=LastEventID,
event_type=EventType, data=[_|Data]}) ->
{event, #{
last_event_id => LastEventID,
event_type => case EventType of
<<>> -> <<"message">>;
_ -> EventType
end,
data => lists:reverse(Data)
}, State#state{last_event_id_set=false, event_type= <<>>, data=[]}}.
-ifdef(TEST).
parse_example1_test() ->
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data
}, State} = parse(<<
"data: YHOO\n"
"data: +2\n"
"data: 10\n"
"\n">>, init()),
<<"YHOO\n+2\n10">> = iolist_to_binary(Data),
{more, _} = parse(<<>>, State),
ok.
parse_example2_test() ->
{event, #{
event_type := <<"message">>,
last_event_id := <<"1">>,
data := Data1
}, State0} = parse(<<
": test stream\n"
"\n"
"data: first event\n"
"id: 1\n"
"\n"
"data:second event\n"
"id\n"
"\n"
"data: third event\n"
"\n">>, init()),
<<"first event">> = iolist_to_binary(Data1),
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data2
}, State1} = parse(<<>>, State0),
<<"second event">> = iolist_to_binary(Data2),
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data3
}, State} = parse(<<>>, State1),
<<" third event">> = iolist_to_binary(Data3),
{more, _} = parse(<<>>, State),
ok.
parse_example3_test() ->
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data1
}, State0} = parse(<<
"data\n"
"\n"
"data\n"
"data\n"
"\n"
"data:\n">>, init()),
<<>> = iolist_to_binary(Data1),
{event, #{
event_type := <<"message">>,
last_event_id := <<>>,
data := Data2
}, State} = parse(<<>>, State0),
<<"\n">> = iolist_to_binary(Data2),
{more, _} = parse(<<>>, State),
ok.
parse_example4_test() ->
{event, Event, State0} = parse(<<
"data:test\n"
"\n"
"data: test\n"
"\n">>, init()),
{event, Event, State} = parse(<<>>, State0),
{more, _} = parse(<<>>, State),
ok.
parse_id_without_data_test() ->
{event, Event1, State0} = parse(<<
"id: 1\n"
"\n"
"data: data\n"
"\n"
"id: 2\n"
"\n">>, init()),
1 = maps:size(Event1),
#{last_event_id := <<"1">>} = Event1,
{event, #{
event_type := <<"message">>,
last_event_id := <<"1">>,
data := Data
}, State1} = parse(<<>>, State0),
<<"data">> = iolist_to_binary(Data),
{event, Event2, State} = parse(<<>>, State1),
1 = maps:size(Event2),
#{last_event_id := <<"2">>} = Event2,
{more, _} = parse(<<>>, State),
ok.
parse_repeated_id_without_data_test() ->
{event, Event1, State0} = parse(<<
"id: 1\n"
"\n"
"event: message\n" %% This will be ignored since there's no data.
"\n"
"id: 1\n"
"\n"
"id: 2\n"
"\n">>, init()),
{event, Event1, State1} = parse(<<>>, State0),
1 = maps:size(Event1),
#{last_event_id := <<"1">>} = Event1,
{event, Event2, State} = parse(<<>>, State1),
1 = maps:size(Event2),
#{last_event_id := <<"2">>} = Event2,
{more, _} = parse(<<>>, State),
ok.
parse_split_event_test() ->
{more, State} = parse(<<
"data: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA">>, init()),
{event, _, _} = parse(<<"==\n\n">>, State),
ok.
-endif.
-spec events([event()]) -> iolist().
events(Events) ->
[event(Event) || Event <- Events].
-spec event(event()) -> iolist().
event(Event) ->
[
event_comment(Event),
event_id(Event),
event_name(Event),
event_data(Event),
event_retry(Event),
$\n
].
event_comment(#{comment := Comment}) ->
prefix_lines(Comment, <<>>);
event_comment(_) ->
[].
event_id(#{id := ID}) ->
nomatch = binary:match(iolist_to_binary(ID), <<"\n">>),
[<<"id: ">>, ID, $\n];
event_id(_) ->
[].
event_name(#{event := Name0}) ->
Name = if
is_atom(Name0) -> atom_to_binary(Name0, utf8);
true -> iolist_to_binary(Name0)
end,
nomatch = binary:match(Name, <<"\n">>),
[<<"event: ">>, Name, $\n];
event_name(_) ->
[].
event_data(#{data := Data}) ->
prefix_lines(Data, <<"data">>);
event_data(_) ->
[].
event_retry(#{retry := Retry}) ->
[<<"retry: ">>, integer_to_binary(Retry), $\n];
event_retry(_) ->
[].
prefix_lines(IoData, Prefix) ->
Lines = binary:split(iolist_to_binary(IoData), <<"\n">>, [global]),
[[Prefix, <<": ">>, Line, $\n] || Line <- Lines].
-ifdef(TEST).
event_test() ->
_ = event(#{}),
_ = event(#{comment => "test"}),
_ = event(#{data => "test"}),
_ = event(#{data => "test\ntest\ntest"}),
_ = event(#{data => "test\ntest\ntest\n"}),
_ = event(#{data => <<"test\ntest\ntest">>}),
_ = event(#{data => [<<"test">>, $\n, <<"test">>, [$\n, "test"]]}),
_ = event(#{event => test}),
_ = event(#{event => "test"}),
_ = event(#{id => "test"}),
_ = event(#{retry => 5000}),
_ = event(#{event => "test", data => "test"}),
_ = event(#{id => "test", event => "test", data => "test"}),
ok.
-endif.

339
cowlib/src/cow_uri.erl Normal file
View File

@ -0,0 +1,339 @@
%% Copyright (c) 2016-2023, 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.

View File

@ -0,0 +1,360 @@
%% Copyright (c) 2019-2023, 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(<<$%,H,L,R/bits>>, Acc)
when ?IS_HEX(H), ?IS_HEX(L) ->
urlencode_reserved(R, <<Acc/binary,$%,H,L>>);
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() ->
io:format("expected: ~0p", [Expected]),
case Expected of
false ->
{'EXIT', _} = (catch expand(URITemplate, Vars));
[_|_] ->
Result = iolist_to_binary(expand(URITemplate, Vars)),
io:format("~p", [Result]),
true = lists:member(Result, Expected);
_ ->
Expected = iolist_to_binary(expand(URITemplate, Vars))
end
end}
|| [URITemplate, Expected] <- Cases]
end || {Section, #{
<<"variables">> := Vars0,
<<"testcases">> := Cases
}} <- maps:to_list(Tests)]
end || File <- Files]).
-endif.

741
cowlib/src/cow_ws.erl Normal file
View File

@ -0,0 +1,741 @@
%% Copyright (c) 2015-2023, 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.