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/cowlibmain
commit
8ebac63dac
|
@ -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.
|
|
@ -0,0 +1,7 @@
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all:
|
||||||
|
${MAKE} -C src
|
||||||
|
|
||||||
|
clean:
|
||||||
|
${MAKE} -C src clean
|
|
@ -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!]
|
|
@ -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]
|
|
@ -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)]
|
|
@ -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)]
|
|
@ -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)]
|
|
@ -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)]
|
|
@ -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)]
|
|
@ -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, []}
|
||||||
|
]}.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
@ -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.
|
Binary file not shown.
|
@ -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.
|
Binary file not shown.
|
@ -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).
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
|
@ -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
File diff suppressed because it is too large
Load Diff
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
File diff suppressed because it is too large
Load Diff
|
@ -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">>, []}.
|
|
@ -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 >>).
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,181 @@
|
||||||
|
%% Zlib dictionary.
|
||||||
|
|
||||||
|
-define(ZDICT, <<
|
||||||
|
16#00, 16#00, 16#00, 16#07, 16#6f, 16#70, 16#74, 16#69,
|
||||||
|
16#6f, 16#6e, 16#73, 16#00, 16#00, 16#00, 16#04, 16#68,
|
||||||
|
16#65, 16#61, 16#64, 16#00, 16#00, 16#00, 16#04, 16#70,
|
||||||
|
16#6f, 16#73, 16#74, 16#00, 16#00, 16#00, 16#03, 16#70,
|
||||||
|
16#75, 16#74, 16#00, 16#00, 16#00, 16#06, 16#64, 16#65,
|
||||||
|
16#6c, 16#65, 16#74, 16#65, 16#00, 16#00, 16#00, 16#05,
|
||||||
|
16#74, 16#72, 16#61, 16#63, 16#65, 16#00, 16#00, 16#00,
|
||||||
|
16#06, 16#61, 16#63, 16#63, 16#65, 16#70, 16#74, 16#00,
|
||||||
|
16#00, 16#00, 16#0e, 16#61, 16#63, 16#63, 16#65, 16#70,
|
||||||
|
16#74, 16#2d, 16#63, 16#68, 16#61, 16#72, 16#73, 16#65,
|
||||||
|
16#74, 16#00, 16#00, 16#00, 16#0f, 16#61, 16#63, 16#63,
|
||||||
|
16#65, 16#70, 16#74, 16#2d, 16#65, 16#6e, 16#63, 16#6f,
|
||||||
|
16#64, 16#69, 16#6e, 16#67, 16#00, 16#00, 16#00, 16#0f,
|
||||||
|
16#61, 16#63, 16#63, 16#65, 16#70, 16#74, 16#2d, 16#6c,
|
||||||
|
16#61, 16#6e, 16#67, 16#75, 16#61, 16#67, 16#65, 16#00,
|
||||||
|
16#00, 16#00, 16#0d, 16#61, 16#63, 16#63, 16#65, 16#70,
|
||||||
|
16#74, 16#2d, 16#72, 16#61, 16#6e, 16#67, 16#65, 16#73,
|
||||||
|
16#00, 16#00, 16#00, 16#03, 16#61, 16#67, 16#65, 16#00,
|
||||||
|
16#00, 16#00, 16#05, 16#61, 16#6c, 16#6c, 16#6f, 16#77,
|
||||||
|
16#00, 16#00, 16#00, 16#0d, 16#61, 16#75, 16#74, 16#68,
|
||||||
|
16#6f, 16#72, 16#69, 16#7a, 16#61, 16#74, 16#69, 16#6f,
|
||||||
|
16#6e, 16#00, 16#00, 16#00, 16#0d, 16#63, 16#61, 16#63,
|
||||||
|
16#68, 16#65, 16#2d, 16#63, 16#6f, 16#6e, 16#74, 16#72,
|
||||||
|
16#6f, 16#6c, 16#00, 16#00, 16#00, 16#0a, 16#63, 16#6f,
|
||||||
|
16#6e, 16#6e, 16#65, 16#63, 16#74, 16#69, 16#6f, 16#6e,
|
||||||
|
16#00, 16#00, 16#00, 16#0c, 16#63, 16#6f, 16#6e, 16#74,
|
||||||
|
16#65, 16#6e, 16#74, 16#2d, 16#62, 16#61, 16#73, 16#65,
|
||||||
|
16#00, 16#00, 16#00, 16#10, 16#63, 16#6f, 16#6e, 16#74,
|
||||||
|
16#65, 16#6e, 16#74, 16#2d, 16#65, 16#6e, 16#63, 16#6f,
|
||||||
|
16#64, 16#69, 16#6e, 16#67, 16#00, 16#00, 16#00, 16#10,
|
||||||
|
16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 16#74, 16#2d,
|
||||||
|
16#6c, 16#61, 16#6e, 16#67, 16#75, 16#61, 16#67, 16#65,
|
||||||
|
16#00, 16#00, 16#00, 16#0e, 16#63, 16#6f, 16#6e, 16#74,
|
||||||
|
16#65, 16#6e, 16#74, 16#2d, 16#6c, 16#65, 16#6e, 16#67,
|
||||||
|
16#74, 16#68, 16#00, 16#00, 16#00, 16#10, 16#63, 16#6f,
|
||||||
|
16#6e, 16#74, 16#65, 16#6e, 16#74, 16#2d, 16#6c, 16#6f,
|
||||||
|
16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00,
|
||||||
|
16#00, 16#0b, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e,
|
||||||
|
16#74, 16#2d, 16#6d, 16#64, 16#35, 16#00, 16#00, 16#00,
|
||||||
|
16#0d, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e, 16#74,
|
||||||
|
16#2d, 16#72, 16#61, 16#6e, 16#67, 16#65, 16#00, 16#00,
|
||||||
|
16#00, 16#0c, 16#63, 16#6f, 16#6e, 16#74, 16#65, 16#6e,
|
||||||
|
16#74, 16#2d, 16#74, 16#79, 16#70, 16#65, 16#00, 16#00,
|
||||||
|
16#00, 16#04, 16#64, 16#61, 16#74, 16#65, 16#00, 16#00,
|
||||||
|
16#00, 16#04, 16#65, 16#74, 16#61, 16#67, 16#00, 16#00,
|
||||||
|
16#00, 16#06, 16#65, 16#78, 16#70, 16#65, 16#63, 16#74,
|
||||||
|
16#00, 16#00, 16#00, 16#07, 16#65, 16#78, 16#70, 16#69,
|
||||||
|
16#72, 16#65, 16#73, 16#00, 16#00, 16#00, 16#04, 16#66,
|
||||||
|
16#72, 16#6f, 16#6d, 16#00, 16#00, 16#00, 16#04, 16#68,
|
||||||
|
16#6f, 16#73, 16#74, 16#00, 16#00, 16#00, 16#08, 16#69,
|
||||||
|
16#66, 16#2d, 16#6d, 16#61, 16#74, 16#63, 16#68, 16#00,
|
||||||
|
16#00, 16#00, 16#11, 16#69, 16#66, 16#2d, 16#6d, 16#6f,
|
||||||
|
16#64, 16#69, 16#66, 16#69, 16#65, 16#64, 16#2d, 16#73,
|
||||||
|
16#69, 16#6e, 16#63, 16#65, 16#00, 16#00, 16#00, 16#0d,
|
||||||
|
16#69, 16#66, 16#2d, 16#6e, 16#6f, 16#6e, 16#65, 16#2d,
|
||||||
|
16#6d, 16#61, 16#74, 16#63, 16#68, 16#00, 16#00, 16#00,
|
||||||
|
16#08, 16#69, 16#66, 16#2d, 16#72, 16#61, 16#6e, 16#67,
|
||||||
|
16#65, 16#00, 16#00, 16#00, 16#13, 16#69, 16#66, 16#2d,
|
||||||
|
16#75, 16#6e, 16#6d, 16#6f, 16#64, 16#69, 16#66, 16#69,
|
||||||
|
16#65, 16#64, 16#2d, 16#73, 16#69, 16#6e, 16#63, 16#65,
|
||||||
|
16#00, 16#00, 16#00, 16#0d, 16#6c, 16#61, 16#73, 16#74,
|
||||||
|
16#2d, 16#6d, 16#6f, 16#64, 16#69, 16#66, 16#69, 16#65,
|
||||||
|
16#64, 16#00, 16#00, 16#00, 16#08, 16#6c, 16#6f, 16#63,
|
||||||
|
16#61, 16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, 16#00,
|
||||||
|
16#0c, 16#6d, 16#61, 16#78, 16#2d, 16#66, 16#6f, 16#72,
|
||||||
|
16#77, 16#61, 16#72, 16#64, 16#73, 16#00, 16#00, 16#00,
|
||||||
|
16#06, 16#70, 16#72, 16#61, 16#67, 16#6d, 16#61, 16#00,
|
||||||
|
16#00, 16#00, 16#12, 16#70, 16#72, 16#6f, 16#78, 16#79,
|
||||||
|
16#2d, 16#61, 16#75, 16#74, 16#68, 16#65, 16#6e, 16#74,
|
||||||
|
16#69, 16#63, 16#61, 16#74, 16#65, 16#00, 16#00, 16#00,
|
||||||
|
16#13, 16#70, 16#72, 16#6f, 16#78, 16#79, 16#2d, 16#61,
|
||||||
|
16#75, 16#74, 16#68, 16#6f, 16#72, 16#69, 16#7a, 16#61,
|
||||||
|
16#74, 16#69, 16#6f, 16#6e, 16#00, 16#00, 16#00, 16#05,
|
||||||
|
16#72, 16#61, 16#6e, 16#67, 16#65, 16#00, 16#00, 16#00,
|
||||||
|
16#07, 16#72, 16#65, 16#66, 16#65, 16#72, 16#65, 16#72,
|
||||||
|
16#00, 16#00, 16#00, 16#0b, 16#72, 16#65, 16#74, 16#72,
|
||||||
|
16#79, 16#2d, 16#61, 16#66, 16#74, 16#65, 16#72, 16#00,
|
||||||
|
16#00, 16#00, 16#06, 16#73, 16#65, 16#72, 16#76, 16#65,
|
||||||
|
16#72, 16#00, 16#00, 16#00, 16#02, 16#74, 16#65, 16#00,
|
||||||
|
16#00, 16#00, 16#07, 16#74, 16#72, 16#61, 16#69, 16#6c,
|
||||||
|
16#65, 16#72, 16#00, 16#00, 16#00, 16#11, 16#74, 16#72,
|
||||||
|
16#61, 16#6e, 16#73, 16#66, 16#65, 16#72, 16#2d, 16#65,
|
||||||
|
16#6e, 16#63, 16#6f, 16#64, 16#69, 16#6e, 16#67, 16#00,
|
||||||
|
16#00, 16#00, 16#07, 16#75, 16#70, 16#67, 16#72, 16#61,
|
||||||
|
16#64, 16#65, 16#00, 16#00, 16#00, 16#0a, 16#75, 16#73,
|
||||||
|
16#65, 16#72, 16#2d, 16#61, 16#67, 16#65, 16#6e, 16#74,
|
||||||
|
16#00, 16#00, 16#00, 16#04, 16#76, 16#61, 16#72, 16#79,
|
||||||
|
16#00, 16#00, 16#00, 16#03, 16#76, 16#69, 16#61, 16#00,
|
||||||
|
16#00, 16#00, 16#07, 16#77, 16#61, 16#72, 16#6e, 16#69,
|
||||||
|
16#6e, 16#67, 16#00, 16#00, 16#00, 16#10, 16#77, 16#77,
|
||||||
|
16#77, 16#2d, 16#61, 16#75, 16#74, 16#68, 16#65, 16#6e,
|
||||||
|
16#74, 16#69, 16#63, 16#61, 16#74, 16#65, 16#00, 16#00,
|
||||||
|
16#00, 16#06, 16#6d, 16#65, 16#74, 16#68, 16#6f, 16#64,
|
||||||
|
16#00, 16#00, 16#00, 16#03, 16#67, 16#65, 16#74, 16#00,
|
||||||
|
16#00, 16#00, 16#06, 16#73, 16#74, 16#61, 16#74, 16#75,
|
||||||
|
16#73, 16#00, 16#00, 16#00, 16#06, 16#32, 16#30, 16#30,
|
||||||
|
16#20, 16#4f, 16#4b, 16#00, 16#00, 16#00, 16#07, 16#76,
|
||||||
|
16#65, 16#72, 16#73, 16#69, 16#6f, 16#6e, 16#00, 16#00,
|
||||||
|
16#00, 16#08, 16#48, 16#54, 16#54, 16#50, 16#2f, 16#31,
|
||||||
|
16#2e, 16#31, 16#00, 16#00, 16#00, 16#03, 16#75, 16#72,
|
||||||
|
16#6c, 16#00, 16#00, 16#00, 16#06, 16#70, 16#75, 16#62,
|
||||||
|
16#6c, 16#69, 16#63, 16#00, 16#00, 16#00, 16#0a, 16#73,
|
||||||
|
16#65, 16#74, 16#2d, 16#63, 16#6f, 16#6f, 16#6b, 16#69,
|
||||||
|
16#65, 16#00, 16#00, 16#00, 16#0a, 16#6b, 16#65, 16#65,
|
||||||
|
16#70, 16#2d, 16#61, 16#6c, 16#69, 16#76, 16#65, 16#00,
|
||||||
|
16#00, 16#00, 16#06, 16#6f, 16#72, 16#69, 16#67, 16#69,
|
||||||
|
16#6e, 16#31, 16#30, 16#30, 16#31, 16#30, 16#31, 16#32,
|
||||||
|
16#30, 16#31, 16#32, 16#30, 16#32, 16#32, 16#30, 16#35,
|
||||||
|
16#32, 16#30, 16#36, 16#33, 16#30, 16#30, 16#33, 16#30,
|
||||||
|
16#32, 16#33, 16#30, 16#33, 16#33, 16#30, 16#34, 16#33,
|
||||||
|
16#30, 16#35, 16#33, 16#30, 16#36, 16#33, 16#30, 16#37,
|
||||||
|
16#34, 16#30, 16#32, 16#34, 16#30, 16#35, 16#34, 16#30,
|
||||||
|
16#36, 16#34, 16#30, 16#37, 16#34, 16#30, 16#38, 16#34,
|
||||||
|
16#30, 16#39, 16#34, 16#31, 16#30, 16#34, 16#31, 16#31,
|
||||||
|
16#34, 16#31, 16#32, 16#34, 16#31, 16#33, 16#34, 16#31,
|
||||||
|
16#34, 16#34, 16#31, 16#35, 16#34, 16#31, 16#36, 16#34,
|
||||||
|
16#31, 16#37, 16#35, 16#30, 16#32, 16#35, 16#30, 16#34,
|
||||||
|
16#35, 16#30, 16#35, 16#32, 16#30, 16#33, 16#20, 16#4e,
|
||||||
|
16#6f, 16#6e, 16#2d, 16#41, 16#75, 16#74, 16#68, 16#6f,
|
||||||
|
16#72, 16#69, 16#74, 16#61, 16#74, 16#69, 16#76, 16#65,
|
||||||
|
16#20, 16#49, 16#6e, 16#66, 16#6f, 16#72, 16#6d, 16#61,
|
||||||
|
16#74, 16#69, 16#6f, 16#6e, 16#32, 16#30, 16#34, 16#20,
|
||||||
|
16#4e, 16#6f, 16#20, 16#43, 16#6f, 16#6e, 16#74, 16#65,
|
||||||
|
16#6e, 16#74, 16#33, 16#30, 16#31, 16#20, 16#4d, 16#6f,
|
||||||
|
16#76, 16#65, 16#64, 16#20, 16#50, 16#65, 16#72, 16#6d,
|
||||||
|
16#61, 16#6e, 16#65, 16#6e, 16#74, 16#6c, 16#79, 16#34,
|
||||||
|
16#30, 16#30, 16#20, 16#42, 16#61, 16#64, 16#20, 16#52,
|
||||||
|
16#65, 16#71, 16#75, 16#65, 16#73, 16#74, 16#34, 16#30,
|
||||||
|
16#31, 16#20, 16#55, 16#6e, 16#61, 16#75, 16#74, 16#68,
|
||||||
|
16#6f, 16#72, 16#69, 16#7a, 16#65, 16#64, 16#34, 16#30,
|
||||||
|
16#33, 16#20, 16#46, 16#6f, 16#72, 16#62, 16#69, 16#64,
|
||||||
|
16#64, 16#65, 16#6e, 16#34, 16#30, 16#34, 16#20, 16#4e,
|
||||||
|
16#6f, 16#74, 16#20, 16#46, 16#6f, 16#75, 16#6e, 16#64,
|
||||||
|
16#35, 16#30, 16#30, 16#20, 16#49, 16#6e, 16#74, 16#65,
|
||||||
|
16#72, 16#6e, 16#61, 16#6c, 16#20, 16#53, 16#65, 16#72,
|
||||||
|
16#76, 16#65, 16#72, 16#20, 16#45, 16#72, 16#72, 16#6f,
|
||||||
|
16#72, 16#35, 16#30, 16#31, 16#20, 16#4e, 16#6f, 16#74,
|
||||||
|
16#20, 16#49, 16#6d, 16#70, 16#6c, 16#65, 16#6d, 16#65,
|
||||||
|
16#6e, 16#74, 16#65, 16#64, 16#35, 16#30, 16#33, 16#20,
|
||||||
|
16#53, 16#65, 16#72, 16#76, 16#69, 16#63, 16#65, 16#20,
|
||||||
|
16#55, 16#6e, 16#61, 16#76, 16#61, 16#69, 16#6c, 16#61,
|
||||||
|
16#62, 16#6c, 16#65, 16#4a, 16#61, 16#6e, 16#20, 16#46,
|
||||||
|
16#65, 16#62, 16#20, 16#4d, 16#61, 16#72, 16#20, 16#41,
|
||||||
|
16#70, 16#72, 16#20, 16#4d, 16#61, 16#79, 16#20, 16#4a,
|
||||||
|
16#75, 16#6e, 16#20, 16#4a, 16#75, 16#6c, 16#20, 16#41,
|
||||||
|
16#75, 16#67, 16#20, 16#53, 16#65, 16#70, 16#74, 16#20,
|
||||||
|
16#4f, 16#63, 16#74, 16#20, 16#4e, 16#6f, 16#76, 16#20,
|
||||||
|
16#44, 16#65, 16#63, 16#20, 16#30, 16#30, 16#3a, 16#30,
|
||||||
|
16#30, 16#3a, 16#30, 16#30, 16#20, 16#4d, 16#6f, 16#6e,
|
||||||
|
16#2c, 16#20, 16#54, 16#75, 16#65, 16#2c, 16#20, 16#57,
|
||||||
|
16#65, 16#64, 16#2c, 16#20, 16#54, 16#68, 16#75, 16#2c,
|
||||||
|
16#20, 16#46, 16#72, 16#69, 16#2c, 16#20, 16#53, 16#61,
|
||||||
|
16#74, 16#2c, 16#20, 16#53, 16#75, 16#6e, 16#2c, 16#20,
|
||||||
|
16#47, 16#4d, 16#54, 16#63, 16#68, 16#75, 16#6e, 16#6b,
|
||||||
|
16#65, 16#64, 16#2c, 16#74, 16#65, 16#78, 16#74, 16#2f,
|
||||||
|
16#68, 16#74, 16#6d, 16#6c, 16#2c, 16#69, 16#6d, 16#61,
|
||||||
|
16#67, 16#65, 16#2f, 16#70, 16#6e, 16#67, 16#2c, 16#69,
|
||||||
|
16#6d, 16#61, 16#67, 16#65, 16#2f, 16#6a, 16#70, 16#67,
|
||||||
|
16#2c, 16#69, 16#6d, 16#61, 16#67, 16#65, 16#2f, 16#67,
|
||||||
|
16#69, 16#66, 16#2c, 16#61, 16#70, 16#70, 16#6c, 16#69,
|
||||||
|
16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#2f, 16#78,
|
||||||
|
16#6d, 16#6c, 16#2c, 16#61, 16#70, 16#70, 16#6c, 16#69,
|
||||||
|
16#63, 16#61, 16#74, 16#69, 16#6f, 16#6e, 16#2f, 16#78,
|
||||||
|
16#68, 16#74, 16#6d, 16#6c, 16#2b, 16#78, 16#6d, 16#6c,
|
||||||
|
16#2c, 16#74, 16#65, 16#78, 16#74, 16#2f, 16#70, 16#6c,
|
||||||
|
16#61, 16#69, 16#6e, 16#2c, 16#74, 16#65, 16#78, 16#74,
|
||||||
|
16#2f, 16#6a, 16#61, 16#76, 16#61, 16#73, 16#63, 16#72,
|
||||||
|
16#69, 16#70, 16#74, 16#2c, 16#70, 16#75, 16#62, 16#6c,
|
||||||
|
16#69, 16#63, 16#70, 16#72, 16#69, 16#76, 16#61, 16#74,
|
||||||
|
16#65, 16#6d, 16#61, 16#78, 16#2d, 16#61, 16#67, 16#65,
|
||||||
|
16#3d, 16#67, 16#7a, 16#69, 16#70, 16#2c, 16#64, 16#65,
|
||||||
|
16#66, 16#6c, 16#61, 16#74, 16#65, 16#2c, 16#73, 16#64,
|
||||||
|
16#63, 16#68, 16#63, 16#68, 16#61, 16#72, 16#73, 16#65,
|
||||||
|
16#74, 16#3d, 16#75, 16#74, 16#66, 16#2d, 16#38, 16#63,
|
||||||
|
16#68, 16#61, 16#72, 16#73, 16#65, 16#74, 16#3d, 16#69,
|
||||||
|
16#73, 16#6f, 16#2d, 16#38, 16#38, 16#35, 16#39, 16#2d,
|
||||||
|
16#31, 16#2c, 16#75, 16#74, 16#66, 16#2d, 16#2c, 16#2a,
|
||||||
|
16#2c, 16#65, 16#6e, 16#71, 16#3d, 16#30, 16#2e >>).
|
|
@ -0,0 +1,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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
Loading…
Reference in New Issue