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