Delete ejabberd_auth_http dependency dirs (#115)

This commit is contained in:
Badlop 2022-07-26 16:14:16 +02:00
parent e76b0877d6
commit 2ee0a514a5
38 changed files with 0 additions and 5119 deletions

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,16 +0,0 @@
.PHONY: all reload compile clean
all: compile
compile: rebar
./rebar get-deps compile
reload: compile
zsh -c './reload.erl $$(foreach f (src/*.erl); basename $$f .erl; end)'
rebar:
wget -c http://cloud.github.com/downloads/basho/rebar/rebar
chmod +x $@
clean: rebar
./rebar clean

View File

@ -1,3 +0,0 @@
Copyright 2011 Erlang Solutions Ltd.
Use `git shortlog --summary` to get the list of developers.

View File

@ -1,38 +0,0 @@
Summary
=======
Cuesport is a simple pool of workers, meant mainly to be used with things like DB connections.
Use when poolboy's elaborate checkin/checkout seems to be overkill for your task.
Usage
-----
Suggested usage is to create your own wrapper module which wraps cuesport and provides
wrapper functions to hide pool's usage. Something in the lines of:
```erlang
-module(myproduct_redis).
-export([start_link/0, q/1, q/2]).
-define(POOL_NAME, myproduct_redis_pool).
%%%===================================================================
%%% API functions
%%%===================================================================
start_link() ->
PoolSize = get_config(redis_pool_size),
EredisOpts = get_config(redis_worker_config),
ChildMods = [eredis, eredis_client, eredis_parser],
ChildMFA = {eredis, start_link, EredisOpts},
cuesport:start_link(?POOL_NAME, PoolSize, ChildMods, ChildMFA).
q(Query) ->
eredis:q(cuesport:get_worker(?POOL_NAME), Query).
q(Query, Opts) ->
eredis:q(cuesport:get_worker(?POOL_NAME), Query, Opts).
```
Then add `myproduct_redis` to your supervision tree.

View File

@ -1,7 +0,0 @@
{application, cuesport, [
{description, "Simple pool of workers"},
{vsn, "0.1"},
{registered, []},
{applications, [kernel, stdlib]},
{env, []}
]}.

View File

@ -1,85 +0,0 @@
%%%===================================================================
%%% @copyright (C) 2011, Erlang Solutions Ltd.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%% http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.
%%%
%%% @doc Utility for using a pool of workers easily
%%% Obvious pun is obvious.
%%% @end
%%%===================================================================
-module(cuesport).
-behaviour(supervisor).
%% API
-export([start_link/4,
start_link/5,
get_worker/1]).
%% Supervisor callbacks
-export([init/1]).
%% Internal exports
-export([start_worker/3]).
-define(SERVER, ?MODULE).
%%%===================================================================
%%% API functions
%%%===================================================================
-spec start_link(atom(), integer(), [atom()], {atom(), atom(), [term()]}) ->
{ok, pid()} | ignore | {error, term()}.
start_link(PoolName, PoolSize, ChildMods, ChildMFA) ->
start_link(PoolName, PoolSize, 2*PoolSize, ChildMods, ChildMFA).
-spec start_link(atom(), integer(), integer(), [atom()], {atom(), atom(), [term()]}) ->
{ok, pid()} | ignore | {error, term()}.
start_link(PoolName, PoolSize, MaxRestarts, ChildMods, ChildMFA) ->
Args = [PoolName, PoolSize, MaxRestarts, ChildMods, ChildMFA],
SupName = list_to_atom("cuesport_" ++ atom_to_list(PoolName) ++ "_sup"),
supervisor:start_link({local, SupName}, ?MODULE, Args).
-spec get_worker(atom()) -> pid().
get_worker(PoolName) ->
[{pool_size, PoolSize}] = ets:lookup(PoolName, pool_size),
N = ets:update_counter(PoolName, seq, {2, 1, PoolSize, 1}),
[{N, Worker}] = ets:lookup(PoolName, N),
Worker.
%%%===================================================================
%%% Supervisor callbacks
%%%===================================================================
init([PoolName, PoolSize, MaxRestarts, ChildMods, ChildMFA]) ->
PoolTable = ets:new(PoolName, [named_table, public]),
ets:insert(PoolTable, {pool_size, PoolSize}),
ets:insert(PoolTable, {seq, 0}),
MFA = fun(Id) ->
{?MODULE, start_worker, [Id, PoolTable, ChildMFA]}
end,
Children = [{N, MFA(N), transient, 2000, worker, ChildMods}
|| N <- lists:seq(1, PoolSize)],
{ok, {{one_for_one, MaxRestarts, PoolSize}, Children}}.
%%%===================================================================
%%% non-API exports
%%%===================================================================
-spec start_worker(integer(), atom(), {module(), atom(), [term()]}) -> {ok, pid()}.
start_worker(Id, PoolTable, {M, F, A}) ->
{ok, Pid} = apply(M, F, A),
ets:insert(PoolTable, {Id, Pid}),
{ok, Pid}.

View File

@ -1,177 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@ -1,34 +0,0 @@
.PHONY: all release compile test clean rel doc build-plt dialyzer
PROJECT = fusco
REBAR := ./rebar
DIALYZER = dialyzer
APPS = kernel stdlib sasl inets ssl public_key crypto compiler
all: compile doc
compile:
$(REBAR) compile
doc:
$(REBAR) doc
test: compile
$(REBAR) eunit ct
release: all dialyze test
$(REBAR) release
clean:
$(REBAR) clean
build-plt: compile
@$(DIALYZER) --build_plt --output_plt .$(PROJECT).plt \
--apps $(APPS)
dialyzer:
@$(DIALYZER) --fullpath --src ./src \
--plt .$(PROJECT).plt --no_native \
-Werror_handling #-Wrace_conditions

View File

@ -1,9 +0,0 @@
FUSCO
=====
Fast and Ultra Slim Connection Oriented HTTP Client
Fusco is an Erlang HTTP client for high performance applications.
For all those who need a generic HTTP client, check [lhttpc](https://github.com/esl/lhttpc) in which development is based Fusco. Not all the functionalities of lhttpc are present here.
Fusco is still in a very early stage of development, be aware of that!

View File

@ -1,3 +0,0 @@
{import, [".eunit/eunit.coverdata"]}.
{export, ["logs/total.coverdata"]}.
{incl_dirs, ["ebin"]}.

View File

@ -1,66 +0,0 @@
%%% ----------------------------------------------------------------------------
%%% Copyright (c) 2009, Erlang Training and Consulting Ltd.
%%% All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions are met:
%%% * Redistributions of source code must retain the above copyright
%%% notice, this list of conditions and the following disclaimer.
%%% * Redistributions in binary form must reproduce the above copyright
%%% notice, this list of conditions and the following disclaimer in the
%%% documentation and/or other materials provided with the distribution.
%%% * Neither the name of Erlang Training and Consulting Ltd. nor the
%%% names of its contributors may be used to endorse or promote products
%%% derived from this software without specific prior written permission.
%%%
%%% THIS SOFTWARE IS PROVIDED BY Erlang Training and Consulting Ltd. ''AS IS''
%%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
%%% ARE DISCLAIMED. IN NO EVENT SHALL Erlang Training and Consulting Ltd. BE
%%% LIABLE SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
%%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
%%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
%%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
%%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
%%% ----------------------------------------------------------------------------
-record(fusco_url, {
host :: string(),
port :: integer(),
path :: string(),
is_ssl:: boolean(),
user = "" :: string(),
password = "" :: string()
}).
-record(fusco_cookie, {
name :: binary(),
value :: binary(),
expires :: {{integer(), integer(), integer()},
{integer(), integer(), integer()}} | atom(),
path :: binary(),
path_tokens :: [binary()],
max_age :: integer() | atom(),
domain :: binary()
}).
-record(response, {socket,
ssl,
version,
status_code,
reason,
headers = [],
connection,
cookies = [],
content_length = 0,
size,
in_timestamp,
transfer_encoding,
body}).
-ifdef(no_binary_to_integer).
-import(fusco_binary, [binary_to_integer/1,
integer_to_binary/1]).
-endif.

View File

@ -1,65 +0,0 @@
%%% ----------------------------------------------------------------------------
%%% Copyright (c) 2009, Erlang Training and Consulting Ltd.
%%% All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions are met:
%%% * Redistributions of source code must retain the above copyright
%%% notice, this list of conditions and the following disclaimer.
%%% * Redistributions in binary form must reproduce the above copyright
%%% notice, this list of conditions and the following disclaimer in the
%%% documentation and/or other materials provided with the distribution.
%%% * Neither the name of Erlang Training and Consulting Ltd. nor the
%%% names of its contributors may be used to endorse or promote products
%%% derived from this software without specific prior written permission.
%%%
%%% THIS SOFTWARE IS PROVIDED BY Erlang Training and Consulting Ltd. ''AS IS''
%%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
%%% ARE DISCLAIMED. IN NO EVENT SHALL Erlang Training and Consulting Ltd. BE
%%% LIABLE SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
%%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
%%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
%%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
%%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
%%% ----------------------------------------------------------------------------
-type header() :: binary().
-type headers() :: [{header(), iodata()}].
-type method() :: string().
-type pos_timeout() :: pos_integer() | 'infinity'.
-type socket() :: _.
-type port_num() :: 1..65535.
-type invalid_option() :: any().
-type destination() :: {string(), pos_integer(), boolean()}.
-type option() ::
{'connect_timeout', timeout()} |
{'send_retry', non_neg_integer()} |
{'connect_options', socket_options()} |
{'use_cookies', boolean()} |
{'proxy', string()} |
{'proxy_ssl_options', socket_options()} |
invalid_option().
-type options() :: [option()].
-type host() :: string() | {integer(), integer(), integer(), integer()}.
-type socket_options() :: [{atom(), term()} | atom()].
-type body() :: binary() |
'undefined' | % HEAD request.
pid(). % When partial_download option is used.
-type result() ::
{ok, {pos_integer(), headers(), body(),
non_neg_integer(), pos_timeout()}} |
{error, atom()}.

View File

@ -1,57 +0,0 @@
=============================================================================
9fedc1ee82661eb6cbf54153198542fcbe51f771 - With http packets on sockets
HIPE compilation
Process | RPS
-------------------
20 | 16891
40 | 17048
Without HIPE compilation
Process | RPS
-------------------
20 | 14249
40 | 13507
=============================================================================
1cb83bb70fffa37f3b51d874f15f0a1d21b72378 - With raw packets on sockets
HIPE compilation
Process | RPS
-------------------
20 | 19651
40 | 20088
Without HIPE compilation
Process | RPS
-------------------
20 | 18577
40 | 18601
Comparison between gen_tcp:recv on http packets and decoding from lhttpc_protocol
Gen_tcp
Process | RPS
-------------------
20 | 32214
40 | 31731
lhttpc_protocol - without HIPE
Process | RPS
-------------------
20 | 25658
40 | 27448
lhttpc_protocol -HIPE
Process | RPS
-------------------
20 | 55814
40 | 49963

Binary file not shown.

View File

@ -1,6 +0,0 @@
{erl_opts, [debug_info,
{platform_define, "R1[45]", no_binary_to_integer}]}.
{eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}.
{cover_enabled, true}.
{cover_export_enabled, true}.

View File

@ -1,13 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%%=============================================================================
{application, fusco,
[{description, "Fast and Ultra Slim Connection Oriented HTTP Client"},
{vsn, "0.0.0"},
{modules, []},
{registered, []},
{applications, [kernel, stdlib, ssl]},
{env, []}
]}.

View File

@ -1,642 +0,0 @@
%%% ----------------------------------------------------------------------------
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Oscar Hellström <oscar@hellstrom.st>
%%% @author Diana Parra Corbacho <diana.corbacho@erlang-solutions.com>
%%% @author Ramon Lastres Guerrero <ramon.lastres@erlang-solutions.com>
%%% @doc Fast and Ultra Slim Connection Oriented HTTP Client
%%%
%%% @end
%%%-----------------------------------------------------------------------------
-module(fusco).
-copyright("2013, Erlang Solutions Ltd.").
%exported functions
-export([start/2,
start_link/2,
connect/1,
request/6,
request/7,
disconnect/1]).
%% gen_server callbacks
-export([init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3]).
-include("fusco_types.hrl").
-include("fusco.hrl").
-export_type([header/0,
headers/0,
method/0,
pos_timeout/0,
socket/0,
port_num/0,
invalid_option/0,
destination/0,
option/0,
options/0,
host/0,
socket_options/0,
body/0,
result/0]).
-define(HTTP_LINE_END, "\r\n").
-record(client_state, {
host :: string(),
port = 80 :: port_num(),
ssl = false :: boolean(),
socket,
connect_timeout = 'infinity' :: timeout(),
connect_options = [] :: [any()],
%% next fields are specific to particular requests
request :: iolist() | undefined,
connection_header,
requester,
cookies = [] :: [#fusco_cookie{}],
use_cookies = false :: boolean(),
%% in case of infinity we read whatever data we can get from
%% the wire at that point
attempts = 0 :: integer(),
proxy :: undefined | #fusco_url{},
proxy_ssl_options = [] :: [any()],
host_header,
out_timestamp,
in_timestamp,
on_connect,
recv_timeout = 'infinity' :: timeout()
}).
%%==============================================================================
%% Exported functions
%%==============================================================================
start(Destination, Options) ->
verify_options(Options),
gen_server:start(?MODULE, {Destination, Options}, []).
start_link(Destination, Options) ->
verify_options(Options),
gen_server:start_link(?MODULE, {Destination, Options}, []).
%%------------------------------------------------------------------------------
%% @doc Starts a Client.
%% @end
%%------------------------------------------------------------------------------
connect(Client) ->
gen_server:call(Client, connect).
%%------------------------------------------------------------------------------
%% @doc Stops a Client.
%% @end
%%------------------------------------------------------------------------------
-spec disconnect(pid()) -> ok.
disconnect(Client) ->
gen_server:cast(Client, stop).
%%------------------------------------------------------------------------------
%% @doc Makes a request using a client already connected.
%% @end
%%------------------------------------------------------------------------------
-spec request(pid(), string(), method(), headers(), iodata(), pos_timeout()) -> result().
request(Client, Path, Method, Hdrs, Body, Timeout) ->
request(Client, Path, Method, Hdrs, Body, 1, Timeout).
%%------------------------------------------------------------------------------
%% @spec (Client, Host, Method, Hdrs, RequestBody, RetryCount, Timeout) -> Result
%% Host = string()
%% Method = string() | atom()
%% Hdrs = [{Header, Value}]
%% Header = string() | binary() | atom()
%% Value = string() | binary()
%% RequestBody = iodata()
%% RetryCount = integer()
%% Timeout = integer() | infinity
%% Result = {ok, {{StatusCode, ReasonPhrase}, Hdrs, ResponseBody}}
%% | {error, Reason}
%% StatusCode = integer()
%% ReasonPhrase = string()
%% ResponseBody = binary() | pid() | undefined
%% Reason = connection_closed | connect_timeout | timeout
%% @doc Sends a request with a body.
%%
%% Instead of building and parsing URLs the target server is specified with
%% a host, port, weither SSL should be used or not and a path on the server.
%% For instance, if you want to request http://example.com/foobar you would
%% use the following:<br/>
%% `Host' = `"example.com"'<br/>
%% `Port' = `80'<br/>
%% `Ssl' = `false'<br/>
%% `Path' = `"/foobar"'<br/>
%% `Path' must begin with a forward slash `/'.
%%
%% `Method' is either a string, stating the HTTP method exactly as in the
%% protocol, i.e: `"POST"' or `"GET"'. It could also be an atom, which is
%% then coverted to an uppercase (if it isn't already) string.
%%
%% `Hdrs' is a list of headers to send. Mandatory headers such as
%% `Host', `Content-Length' or `Transfer-Encoding' (for some requests)
%% are added automatically.
%%
%% `Body' is the entity to send in the request. Please don't include entity
%% bodies where there shouldn't be any (such as for `GET').
%%
%% `Timeout' is the timeout for the request in milliseconds.
%%
%% `Options' is a list of options.
%%
%% Options:
%%
%% `{connect_timeout, Milliseconds}' specifies how many milliseconds the
%% client can spend trying to establish a connection to the server. This
%% doesn't affect the overall request timeout. However, if it's longer than
%% the overall timeout it will be ignored. Also note that the TCP layer my
%% choose to give up earlier than the connect timeout, in which case the
%% client will also give up. The default value is infinity, which means that
%% it will either give up when the TCP stack gives up, or when the overall
%% request timeout is reached.
%%
%% `{connect_options, Options}' specifies options to pass to the socket at
%% connect time. This makes it possible to specify both SSL options and
%% regular socket options, such as which IP/Port to connect from etc.
%% Some options must not be included here, namely the mode, `binary'
%% or `list', `{active, boolean()}', `{active, once}' or `{packet, Packet}'.
%% These options would confuse the client if they are included.
%% Please note that these options will only have an effect on *new*
%% connections, and it isn't possible for different requests
%% to the same host uses different options unless the connection is closed
%% between the requests. Using HTTP/1.0 or including the "Connection: close"
%% header would make the client close the connection after the first
%% response is received.
%%
%% `{send_retry, N}' specifies how many times the client should retry
%% sending a request if the connection is closed after the data has been
%% sent. The default value is `1'.
%%
%% `{proxy, ProxyUrl}' if this option is specified, a proxy server is used as
%% an intermediary for all communication with the destination server. The link
%% to the proxy server is established with the HTTP CONNECT method (RFC2817).
%% Example value: {proxy, "http://john:doe@myproxy.com:3128"}
%%
%% `{proxy_ssl_options, SslOptions}' this is a list of SSL options to use for
%% the SSL session created after the proxy connection is established. For a
%% list of all available options, please check OTP's ssl module manpage.
%% @end
%%------------------------------------------------------------------------------
-spec request(pid(), string(), method(), headers(), iodata(), integer(), pos_timeout()) -> result().
request(Client, Path, Method, Hdrs, Body, SendRetry, Timeout) when is_binary(Path) ->
gen_server:call(Client, {request, Path, Method, Hdrs, Body, SendRetry, Timeout}, infinity);
request(_, _, _, _, _, _, _) ->
{error, badarg}.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
init({Destination, Options}) ->
ConnectTimeout = fusco_lib:get_value(connect_timeout, Options, infinity),
ConnectOptions = fusco_lib:get_value(connect_options, Options, []),
UseCookies = fusco_lib:get_value(use_cookies, Options, false),
ProxyInfo = fusco_lib:get_value(proxy, Options, false),
ProxySsl = fusco_lib:get_value(proxy_ssl_options, Options, []),
OnConnectFun = fusco_lib:get_value(on_connect, Options, fun(_) -> ok end),
{Host, Port, Ssl} = case Destination of
{H, P, S} ->
{H, P, S};
URL ->
#fusco_url{host = H, port = P,
is_ssl = S} = fusco_lib:parse_url(URL),
{H, P, S}
end,
Proxy = case ProxyInfo of
false ->
undefined;
{proxy, ProxyUrl} when is_list(ProxyUrl), not Ssl ->
%% The point of HTTP CONNECT proxying is to use TLS tunneled in
%% a plain HTTP/1.1 connection to the proxy (RFC2817).
throw(origin_server_not_https);
{proxy, ProxyUrl} when is_list(ProxyUrl) ->
fusco_lib:parse_url(ProxyUrl)
end,
State = #client_state{host = Host, port = Port, ssl = Ssl,
connect_timeout = ConnectTimeout,
connect_options = ConnectOptions,
use_cookies = UseCookies,
host_header = fusco_lib:host_header(Host, Port),
proxy = Proxy,
proxy_ssl_options = ProxySsl,
on_connect = OnConnectFun},
{ok, State}.
%%------------------------------------------------------------------------------
%% @doc This function fills in the Client record used in the requests and obtains
%% the socket.
%% @end
%%------------------------------------------------------------------------------
handle_call(connect, _From, #client_state{socket = undefined} = State) ->
% if we dont get a keep alive from the previous request, the socket is undefined.
case connect_socket(State) of
{ok, NewState} ->
{reply, ok, NewState};
{Error, NewState} ->
{reply, Error, NewState}
end;
handle_call(connect, _From, State) ->
{reply, ok, State};
handle_call({request, Path, Method, Hdrs, Body, SendRetry, Timeout}, From,
State = #client_state{host_header = Host,
use_cookies = UseCookies}) ->
Cookies = delete_expired_cookies(State),
{Request, ConHeader} =
fusco_lib:format_request(Path, Method, Hdrs, Host, Body, {UseCookies, Cookies}),
send_request(State#client_state{
request = Request,
requester = From,
connection_header = ConHeader,
attempts = SendRetry + 1,
recv_timeout = Timeout}).
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast(_Msg, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, #client_state{socket = Socket, ssl = Ssl}) ->
case Socket of
undefined ->
ok;
_ ->
fusco_sock:close(Socket, Ssl),
ok
end.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%==============================================================================
%% Internal functions
%%==============================================================================
%%------------------------------------------------------------------------------
%% @private
%% @doc This function creates a new socket connection if needed, and it also
%% handles the proxy connection.
%% @end
%%------------------------------------------------------------------------------
send_request(#client_state{attempts = 0} = State) ->
{reply, {error, connection_closed}, State};
send_request(#client_state{socket = undefined} = State) ->
% if we dont get a keep alive from the previous request, the socket is undefined.
case connect_socket(State) of
{ok, NewState} ->
send_request(NewState);
{Error, NewState} ->
{reply, Error, NewState}
end;
send_request(#client_state{socket = Socket, ssl = Ssl, request = Request,
attempts = Attempts, recv_timeout = RecvTimeout} = State) ->
Out = os:timestamp(),
%If we have a timeout set then we need to ensure a timeout on sending too
fusco_sock:setopts(Socket, [{send_timeout, RecvTimeout}, {send_timeout_close, true}], Ssl),
case fusco_sock:send(Socket, Request, Ssl) of
ok ->
read_response(State#client_state{out_timestamp = Out});
{error, closed} ->
fusco_sock:close(Socket, Ssl),
send_request(State#client_state{socket = undefined, attempts = Attempts - 1});
{error, _Reason} ->
fusco_sock:close(Socket, Ssl),
{reply, {error, connection_closed}, State#client_state{socket = undefined}}
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
request_first_destination(#client_state{proxy = #fusco_url{} = Proxy}) ->
{Proxy#fusco_url.host, Proxy#fusco_url.port, Proxy#fusco_url.is_ssl};
request_first_destination(#client_state{host = Host, port = Port, ssl = Ssl}) ->
{Host, Port, Ssl}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
read_proxy_connect_response(#client_state{recv_timeout = RecvTimeout} = State) ->
Socket = State#client_state.socket,
ProxyIsSsl = (State#client_state.proxy)#fusco_url.is_ssl,
case fusco_protocol:recv(Socket, ProxyIsSsl, RecvTimeout) of
#response{status_code = <<$1,_,_>>} ->
%% RFC 2616, section 10.1:
%% A client MUST be prepared to accept one or more
%% 1xx status responses prior to a regular
%% response, even if the client does not expect a
%% 100 (Continue) status message. Unexpected 1xx
%% status responses MAY be ignored by a user agent.
read_proxy_connect_response(State);
#response{status_code = <<$2,_,_>>} ->
%% RFC2817, any 2xx code means success.
ConnectOptions = State#client_state.connect_options,
SslOptions = State#client_state.proxy_ssl_options,
Timeout = State#client_state.connect_timeout,
case ssl:connect(Socket, SslOptions ++ ConnectOptions, Timeout) of
{ok, SslSocket} ->
{ok, SslSocket};
{error, Reason} ->
fusco_sock:close(State#client_state.socket, State#client_state.ssl),
{error, {proxy_connection_failed, Reason}}
end;
#response{status_code = StatusCode, reason = Reason} ->
{error, {proxy_connection_refused, StatusCode, Reason}};
{error, closed} ->
fusco_sock:close(Socket, ProxyIsSsl),
{error, proxy_connection_closed};
{error, Reason} ->
{error, {proxy_connection_failed, Reason}}
end.
%%------------------------------------------------------------------------------
%% @private
%% @doc @TODO This does not handle redirects at the moment.
%% @end
%%------------------------------------------------------------------------------
-spec read_response(#client_state{}) -> {any(), socket()} | no_return().
read_response(#client_state{socket = Socket, ssl = Ssl, use_cookies = UseCookies,
connection_header = ConHdr, cookies = Cookies,
requester = From, out_timestamp = Out, attempts = Attempts,
recv_timeout = RecvTimeout} = State) ->
case fusco_protocol:recv(Socket, Ssl, RecvTimeout) of
#response{status_code = <<$1,_,_>>} ->
%% RFC 2616, section 10.1:
%% A client MUST be prepared to accept one or more
%% 1xx status responses prior to a regular
%% response, even if the client does not expect a
%% 100 (Continue) status message. Unexpected 1xx
%% status responses MAY be ignored by a user agent.
read_response(State);
#response{version = Vsn, cookies = NewCookies, connection = Connection,
status_code = Status, reason = Reason, headers = Headers,
body = Body, size = Size, in_timestamp = In}->
gen_server:reply(
From,
{ok, {{Status, Reason}, Headers, Body, Size,
timer:now_diff(In, Out)}}),
case maybe_close_socket(Connection, State, Vsn, ConHdr) of
undefined ->
case UseCookies of
true ->
{noreply, State#client_state{socket = undefined,
cookies = fusco_lib:update_cookies(NewCookies, Cookies),
in_timestamp = In}};
false ->
{noreply, State#client_state{socket = undefined}}
end;
_ ->
case UseCookies of
true ->
{noreply, State#client_state{cookies = fusco_lib:update_cookies(NewCookies, Cookies),
in_timestamp = In}};
_ ->
{noreply, State}
end
end;
{error, closed} ->
% Either we only noticed that the socket was closed after we
% sent the request, the server closed it just after we put
% the request on the wire or the server has some isses and is
% closing connections without sending responses.
% If this the first attempt to send the request, we will try again.
fusco_sock:close(Socket, Ssl),
send_request(State#client_state{socket = undefined, attempts = Attempts - 1});
{error, Reason} ->
fusco_sock:close(Socket, Ssl),
{reply, {error, Reason}, State#client_state{socket = undefined}}
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
maybe_close_socket(<<"close">>, #client_state{socket = Socket} = State, {1, 1}, _) ->
fusco_sock:close(Socket, State#client_state.ssl),
undefined;
maybe_close_socket(_, #client_state{socket = Socket}, {1, 1}, undefined) ->
Socket;
maybe_close_socket(_, #client_state{socket = Socket} = State, {1, 1}, ConHdr) ->
ClientConnection = fusco_lib:is_close(ConHdr),
if
ClientConnection ->
fusco_sock:close(Socket, State#client_state.ssl),
undefined;
(not ClientConnection) ->
Socket
end;
maybe_close_socket(<<"keep-alive">>, #client_state{socket = Socket}, _, undefined) ->
Socket;
maybe_close_socket(C, #client_state{socket = Socket} = State, _, _)
when C =/= <<"keep-alive">> ->
fusco_sock:close(Socket, State#client_state.ssl),
undefined;
maybe_close_socket(_, #client_state{socket = Socket} = State, _, ConHdr) ->
ClientConnection = fusco_lib:is_close(ConHdr),
if
ClientConnection ->
fusco_sock:close(Socket, State#client_state.ssl),
undefined;
(not ClientConnection) ->
Socket
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec is_ipv6_host(host()) -> boolean().
is_ipv6_host(Host) ->
case inet_parse:address(Host) of
{ok, {_, _, _, _, _, _, _, _}} ->
true;
{ok, {_, _, _, _}} ->
false;
_ ->
% Prefer IPv4 over IPv6.
case inet:getaddr(Host, inet) of
{ok, _} ->
false;
_ ->
case inet:getaddr(Host, inet6) of
{ok, _} ->
true;
_ ->
false
end
end
end.
% What about the timeout?
%%------------------------------------------------------------------------------
%% @private
%% Creates a new socket.
%% @end
%%------------------------------------------------------------------------------
connect_socket(State) ->
case ensure_proxy_tunnel(new_socket(State), State) of
{ok, Socket, _} ->
{ok, State#client_state{socket = Socket}};
Error ->
{Error, State}
end.
%%------------------------------------------------------------------------------
%% @private
%% @doc Creates a new socket using the options included in the client state.
%% end
%%------------------------------------------------------------------------------
new_socket(#client_state{connect_timeout = Timeout, connect_options = ConnectOptions,
on_connect = OnConnectFun} = State) ->
{Host, Port, Ssl} = request_first_destination(State),
ConnectOptions2 = case (not lists:member(inet, ConnectOptions)) andalso
(not lists:member(inet6, ConnectOptions)) andalso
is_ipv6_host(Host) of
true ->
[inet6 | ConnectOptions];
false ->
ConnectOptions
end,
SocketOptions = [binary, {packet, raw}, {nodelay, true}, {reuseaddr, true},
{active, false} | ConnectOptions2],
Reply = connect(Host, Port, SocketOptions, Timeout, Ssl),
OnConnectFun(Reply),
Reply.
connect(Host, Port, SocketOptions, Timeout, Ssl) ->
TimeB = os:timestamp(),
try fusco_sock:connect(Host, Port, SocketOptions, Timeout, Ssl) of
{ok, Socket} ->
TimeA = os:timestamp(),
ConnectionTime = timer:now_diff(TimeA, TimeB),
{ok, Socket, ConnectionTime};
{error, etimedout} ->
%% TCP stack decided to give up
{error, connect_timeout};
{error, timeout} ->
{error, connect_timeout};
{error, 'record overflow'} ->
{error, ssl_error};
{error, _} = Error ->
Error
catch
exit:{{{badmatch, {error, {asn1, _}}}, _}, _} ->
{error, ssl_decode_error};
Type:Error ->
error_logger:error_msg("Socket connection error: ~p ~p, ~p",
[Type, Error, erlang:get_stacktrace()]),
{error, connection_error}
end.
ensure_proxy_tunnel({error, _} = Error, _State) ->
Error;
ensure_proxy_tunnel({ok, Socket}, #client_state{proxy = #fusco_url{user = User,
password = Passwd,
is_ssl = Ssl},
host = DestHost, port = Port} = State) ->
%% Proxy tunnel connection http://tools.ietf.org/html/rfc2817#section-5.2
%% Draft http://www.web-cache.com/Writings/Internet-Drafts/draft-luotonen-web-proxy-tunneling-01.txt
%% IPv6 address literals are enclosed by square brackets (RFC2732)
Host = [fusco_lib:maybe_ipv6_enclose(DestHost), $:, integer_to_list(Port)],
ConnectRequest = [
<<"CONNECT ">>, Host, <<" HTTP/1.1">>, ?HTTP_LINE_END,
<<"Host: ">>, Host, ?HTTP_LINE_END,
case User of
[] ->
[];
_ ->
[<<"Proxy-Authorization: Basic ">>,
base64:encode(User ++ ":" ++ Passwd), ?HTTP_LINE_END]
end,
?HTTP_LINE_END],
case fusco_sock:send(Socket, ConnectRequest, Ssl) of
ok ->
read_proxy_connect_response(State#client_state{socket = Socket});
{error, closed} ->
fusco_sock:close(Socket, Ssl),
{error, proxy_connection_closed};
{error, _Reason} ->
fusco_sock:close(Socket, Ssl),
{error, proxy_connection_closed}
end;
ensure_proxy_tunnel(Socket, _State) ->
Socket.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
-spec verify_options(options()) -> ok | any().
verify_options([{connect_timeout, infinity} | Options]) ->
verify_options(Options);
verify_options([{connect_timeout, MS} | Options])
when is_integer(MS), MS >= 0 ->
verify_options(Options);
verify_options([{connect_options, List} | Options]) when is_list(List) ->
verify_options(Options);
verify_options([{proxy, List} | Options]) when is_list(List) ->
verify_options(Options);
verify_options([{proxy_ssl_options, List} | Options]) when is_list(List) ->
verify_options(Options);
verify_options([{use_cookies, B} | Options]) when is_boolean(B) ->
verify_options(Options);
verify_options([{on_connect, F} | Options]) when is_function(F) ->
verify_options(Options);
verify_options([Option | _Rest]) ->
erlang:error({bad_option, Option});
verify_options([]) ->
ok.
delete_expired_cookies(#client_state{use_cookies = false}) ->
[];
delete_expired_cookies(#client_state{in_timestamp = undefined,
cookies = Cookies}) ->
Cookies;
delete_expired_cookies(#client_state{in_timestamp = In,
cookies = Cookies}) ->
fusco_lib:delete_expired_cookies(Cookies, In).

View File

@ -1,14 +0,0 @@
-module(fusco_binary).
-ifdef(no_binary_to_integer).
-export([binary_to_integer/1,
integer_to_binary/1]).
binary_to_integer(B) ->
catch list_to_integer(binary_to_list(B)).
integer_to_binary(I) ->
catch list_to_binary(integer_to_list(I)).
-endif.

View File

@ -1,219 +0,0 @@
%%% ----------------------------------------------------------------------------
%%% @copyright (C) 2013, Erlang Solutions Ltd
%%% @author Diana Parra Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc Fusco Client Pool
%%%
%%% Pool of clients connected to the same server. Clients do not share state
%%% Recommended for BOSH where connections do not share cookies or any other
%%% headers state
%%%
%%% @end
%%%-----------------------------------------------------------------------------
-module(fusco_cp).
-behaviour(gen_server).
%% API
-export([start/3,
start_link/3,
get_client/1,
free_client/2,
stop/1
]).
-export([request/7]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-record(state, {destination,
options,
max,
total,
free,
busy,
queue
}).
%%%===================================================================
%%% API
%%%===================================================================
start(Destination, Options, MaxClients) ->
verify_max(MaxClients),
gen_server:start(?MODULE, [Destination, Options, MaxClients], []).
start_link(Destination, Options, MaxClients) ->
verify_max(MaxClients),
gen_server:start_link(?MODULE, [Destination, Options, MaxClients], []).
get_client(Pool) ->
try
gen_server:call(Pool, get_client)
catch
exit:{timeout, _} ->
{error, timeout}
end.
free_client(Pool, Client) ->
gen_server:cast(Pool, {free_client, Client}).
stop(Pool) ->
gen_server:cast(Pool, stop).
request(Pool, Path, Method, Hdrs, Body, SendRetry, Timeout) ->
case get_client(Pool) of
{error, _} = Error ->
Error;
Client ->
Reply = fusco:request(Client, Path, Method, Hdrs, Body, SendRetry,
Timeout),
free_client(Pool, Client),
Reply
end.
%%%===================================================================
%%% gen_server callbacks
%%%===================================================================
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% @end
%%--------------------------------------------------------------------
init([Destination, Options, MaxClients]) ->
process_flag(trap_exit, true),
{ok, #state{destination = Destination,
options = Options,
total = 0,
max = MaxClients,
free = [],
busy = [],
queue = queue:new()
}, 0}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @spec handle_call(Request, From, State) ->
%% {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_call(get_client, _From, State = #state{free = [Client | Free],
busy = Busy}) ->
{reply, Client, State#state{free = Free,
busy = [Client | Busy]}};
handle_call(get_client, _From, State = #state{destination = Destination,
options = Options,
free = [],
max = M,
total = T,
busy = Busy})
when M > T ->
{ok, Pid} = fusco:start_link(Destination, Options),
{reply, Pid, State#state{total = T + 1,
busy = [Pid | Busy]}};
handle_call(get_client, From, State = #state{free = [],
max = M,
total = T,
queue = Queue})
when M == T ->
{noreply, State#state{queue = queue:in(From, Queue)}}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @spec handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_cast({free_client, Pid}, State = #state{free = Free,
busy = Busy,
queue = Queue}) ->
case queue:is_empty(Queue) of
true ->
{noreply, State#state{free = [Pid | Free],
busy = lists:delete(Pid, Busy)}};
false ->
{{value, From}, Q2} = queue:out(Queue),
gen_server:reply(From, Pid),
{noreply, State#state{queue = Q2}}
end;
handle_cast(stop, State) ->
{stop, normal, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
handle_info({'EXIT', From, _Reason}, State = #state{free = Free,
busy = Busy,
total = Total}) ->
{noreply, State#state{free = lists:delete(From, Free),
busy = lists:delete(From, Busy),
total = Total - 1}};
handle_info(timeout, #state{free = [], busy = [],
destination = Destination,
options = Options} = State) ->
{ok, Pid} = fusco:start_link(Destination, Options),
{noreply, State#state{free = [Pid], total = 1}};
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
terminate(_Reason, #state{free = Free, busy = Busy}) ->
[fusco:disconnect(F) || F <- Free],
[fusco:disconnect(B) || B <- Busy],
ok.
%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%%===================================================================
%%% Internal functions
%%%===================================================================
verify_max(Integer) when is_integer(Integer), Integer > 0 ->
ok;
verify_max(_) ->
throw(invalid_parameter).

View File

@ -1,498 +0,0 @@
%%%-----------------------------------------------------------------------------
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Oscar Hellström <oscar@hellstrom.st>
%%% @author Diana Parra Corbacho <diana.corbacho@erlang-solutions.com>
%%% @author Ramon Lastres Guerrero <ramon.lastres@erlang-solutions.com>
%%% @doc This module implements various library functions used in fusco
%%%
%%% @end
%%%-----------------------------------------------------------------------------
-module(fusco_lib).
-copyright("2013, Erlang Solutions Ltd.").
-export([parse_url/1,
format_request/6,
header_value/2,
update_cookies/2,
delete_expired_cookies/2,
to_lower/1,
get_value/2,
get_value/3,
host_header/2,
is_close/1,
maybe_ipv6_enclose/1]).
-include("fusco_types.hrl").
-include("fusco.hrl").
-define(HTTP_LINE_END, <<"\r\n">>).
%%==============================================================================
%% Exported functions
%%==============================================================================
%%------------------------------------------------------------------------------
%% @spec header_value(Header, Headers) -> undefined | term()
%% Header = string()
%% Headers = [{header(), term()}]
%% Value = term()
%% @doc
%% Returns the value associated with the `Header' in `Headers'.
%% `Header' must be a lowercase string, since every header is mangled to
%% check the match.
%% @end
%%------------------------------------------------------------------------------
-spec header_value(string(), headers()) -> undefined | term().
header_value(Hdr, Hdrs) ->
%% TODO ensure headers and values are stripped
case lists:keyfind(Hdr, 1, Hdrs) of
false ->
undefined;
{Hdr, Value} ->
Value
end.
%%------------------------------------------------------------------------------
%% @spec (URL) -> #fusco_url{}
%% URL = string()
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec parse_url(string()) -> #fusco_url{}.
parse_url(URL) ->
% XXX This should be possible to do with the re module?
{Scheme, CredsHostPortPath} = split_scheme(URL),
{User, Passwd, HostPortPath} = split_credentials(CredsHostPortPath),
{Host, PortPath} = split_host(HostPortPath, []),
{Port, Path} = split_port(Scheme, PortPath, []),
#fusco_url{host = fusco_lib:to_lower(Host), port = Port, path = Path,
user = User, password = Passwd, is_ssl = (Scheme =:= https)}.
%%------------------------------------------------------------------------------
%% @spec (Path, Method, Headers, Host, Body, Cookies) ->
%% Request
%% Path = iolist()
%% Method = atom() | string()
%% Headers = [{atom() | string(), string()}]
%% Host = string()
%% Body = iolist()
%% Cookies = [#fusco_cookie{}]
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec format_request(iolist(), method(), headers(), string(), iolist(),
{boolean(), [#fusco_cookie{}]}) -> {iodata(), iodata()}.
format_request(Path, Method, Hdrs, Host, Body, Cookies) ->
{AllHdrs, ConHdr} =
add_mandatory_hdrs(Path, Hdrs, Host, Body, Cookies),
{[Method, <<" ">>, Path, <<" HTTP/1.1">>, ?HTTP_LINE_END, AllHdrs,
?HTTP_LINE_END, Body], ConHdr}.
%%------------------------------------------------------------------------------
%% @private
%% @doc Updated the state of the cookies. after we receive a response.
%% @end
%%------------------------------------------------------------------------------
-spec update_cookies(headers(), [#fusco_cookie{}]) -> [#fusco_cookie{}].
update_cookies([], []) ->
[];
update_cookies([], StateCookies) ->
StateCookies;
update_cookies(ReceivedCookies, []) ->
ReceivedCookies;
update_cookies(ReceivedCookies, StateCookies) ->
%% substitute the cookies with the same name, add the others, delete.
%% http://tools.ietf.org/search/rfc6265#section-4.1.2
%% If a user agent receives a Set-Cookie response header whose NAME is
%% the same as a pre-existing cookie, and whose Domain and Path
%% attribute values exactly (string) match those of a pre-existing
%% cookie, the new cookie supersedes the old.
lists:foldl(fun(NewCookie, Acc) ->
OldCookie =
lists:keyfind(NewCookie#fusco_cookie.name,
#fusco_cookie.name, Acc),
replace_or_add_cookie(OldCookie, NewCookie, Acc)
end, StateCookies, ReceivedCookies).
%% http://tools.ietf.org/search/rfc6265#section-4.1.2
replace_or_add_cookie(false, NewCookie, List) ->
%% Add new cookie
[NewCookie | List];
replace_or_add_cookie(#fusco_cookie{domain = Domain, path = Path},
#fusco_cookie{domain = Domain,
path = Path} = NewCookie, List) ->
%% Replace previous cookie
lists:keystore(NewCookie#fusco_cookie.name, #fusco_cookie.name, List,
NewCookie);
replace_or_add_cookie(_, NewCookie, List) ->
%% Add new cookie, path and/or domain are different
[NewCookie | List].
%%------------------------------------------------------------------------------
%% @doc Converts characters in a string ro lower case.
%% @end
%%------------------------------------------------------------------------------
-spec to_lower(string()) -> string().
to_lower(String) when is_list(String) ->
[char_to_lower(X) || X <- String].
bin_to_lower(Bin) ->
<< <<(char_to_lower(B))>> || <<B>> <= Bin >>.
%%------------------------------------------------------------------------------
%% @doc Compares header values to pre-defined values
%% Faster than string:to_lower and then compare
%% @end
%%------------------------------------------------------------------------------
is_close(<<"close">>) ->
true;
is_close(<<"Close">>) ->
true;
is_close(<<"keep-alive">>) ->
false;
is_close(<<"Keep-Alive">>) ->
false;
is_close(C) ->
is_close(C, "close").
is_close(<<C, Rest1/bits>>, [C | Rest2]) ->
is_close(Rest1, Rest2);
is_close(<<C1, Rest1/bits>>, [C2 | Rest2]) ->
case close_to_lower(C1) == C2 of
true ->
is_close(Rest1, Rest2);
false ->
false
end;
is_close(<<>>, _) ->
false;
is_close(_, []) ->
false.
close_to_lower($C) ->
$c;
close_to_lower($L) ->
$l;
close_to_lower($O) ->
$o;
close_to_lower($S) ->
$s;
close_to_lower($E) ->
$e;
close_to_lower(C) ->
C.
%%------------------------------------------------------------------------------
%% @doc Gets value from tuple list
%% @end
%%------------------------------------------------------------------------------
-spec get_value(Key, List) -> term() when
Key :: term(),
List :: [term()].
get_value(Key, List) ->
case lists:keyfind(Key, 1, List) of
{Key, Value} ->
Value;
false ->
undefined
end.
%%------------------------------------------------------------------------------
%% @doc Gets value from tuple list. If it is not present, returns default value.
%% @end
%%------------------------------------------------------------------------------
-spec get_value(Key, List, Default) -> term() when
Key :: term(),
List :: [term()],
Default :: term().
get_value(Key, List, Default) ->
case lists:keyfind(Key, 1, List) of
{Key, Value} ->
Value;
false ->
Default
end.
%%------------------------------------------------------------------------------
%% @private
%% @doc Delete the cookies that are expired (check max-age and expire fields).
%% @end
%%------------------------------------------------------------------------------
-spec delete_expired_cookies([#fusco_cookie{}], erlang:timestamp()) -> [#fusco_cookie{}].
delete_expired_cookies([], _InTimestamp) ->
[];
delete_expired_cookies(Cookies, InTimestamp) ->
[ X || X <- Cookies, not expires(X, InTimestamp)].
%%==============================================================================
%% Internal functions
%%==============================================================================
%% http://tools.ietf.org/search/rfc6265#section-4.1.2.2
%% The Max-Age attribute indicates the maximum lifetime of the cookie,
%% represented as the number of seconds until the cookie expires.
%%
%% If a cookie has both the Max-Age and the Expires attribute,
%% the Max-Age attribute has precedence and controls the expiration date of the
%% cookie. If a cookie has neither the Max-Age nor the Expires attribute,
%% the user agent will retain the cookie until "the current session is over"
expires(#fusco_cookie{max_age = 0}, _) ->
true;
expires(#fusco_cookie{max_age = Max}, InTimestamp) when Max =/= undefined ->
timer:now_diff(os:timestamp(), InTimestamp) > Max;
expires(#fusco_cookie{expires = Exp}, _) when Exp =/= undefined ->
calendar:universal_time() > Exp;
expires(_, _) ->
false.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
split_scheme("http://" ++ HostPortPath) ->
{http, HostPortPath};
split_scheme("https://" ++ HostPortPath) ->
{https, HostPortPath}.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
split_credentials(CredsHostPortPath) ->
case string:tokens(CredsHostPortPath, "@") of
[HostPortPath] ->
{"", "", HostPortPath};
[Creds, HostPortPath] ->
% RFC1738 (section 3.1) says:
% "The user name (and password), if present, are followed by a
% commercial at-sign "@". Within the user and password field, any ":",
% "@", or "/" must be encoded."
% The mentioned encoding is the "percent" encoding.
case string:tokens(Creds, ":") of
[User] ->
% RFC1738 says ":password" is optional
{http_uri:decode(User), "", HostPortPath};
[User, Passwd] ->
{http_uri:decode(User), http_uri:decode(Passwd), HostPortPath}
end
end.
%%------------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec split_host(string(), string()) -> {string(), string()}.
split_host("[" ++ Rest, []) ->
% IPv6 address literals are enclosed by square brackets (RFC2732)
case string:str(Rest, "]") of
0 ->
split_host(Rest, "[");
N ->
{IPv6Address, "]" ++ PortPath0} = lists:split(N - 1, Rest),
case PortPath0 of
":" ++ PortPath ->
{IPv6Address, PortPath};
_ ->
{IPv6Address, PortPath0}
end
end;
split_host([$: | PortPath], Host) ->
{lists:reverse(Host), PortPath};
split_host([$/ | _] = PortPath, Host) ->
{lists:reverse(Host), PortPath};
split_host([$? | _] = Query, Host) ->
%% The query string follows the hostname, without a slash. The
%% path is empty, but for HTTP an empty path is equivalent to "/"
%% (RFC 3986, section 6.2.3), so let's add the slash ourselves.
{lists:reverse(Host), "/" ++ Query};
split_host([H | T], Host) ->
split_host(T, [H | Host]);
split_host([], Host) ->
{lists:reverse(Host), []}.
%%------------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%%------------------------------------------------------------------------------
split_port(http, [$/ | _] = Path, []) ->
{80, Path};
split_port(https, [$/ | _] = Path, []) ->
{443, Path};
split_port(http, [], []) ->
{80, "/"};
split_port(https, [], []) ->
{443, "/"};
split_port(_, [], Port) ->
{list_to_integer(lists:reverse(Port)), "/"};
split_port(_,[$/ | _] = Path, Port) ->
{list_to_integer(lists:reverse(Port)), Path};
split_port(Scheme, [P | T], Port) ->
split_port(Scheme, T, [P | Port]).
%%------------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec add_mandatory_hdrs(string(), headers(), host(),
iolist(), {boolean(), [#fusco_cookie{}]}) -> {iodata(), iodata()}.
add_mandatory_hdrs(_Path, Hdrs, Host, Body, {_, []}) ->
add_headers(Hdrs, Body, Host, undefined, []);
add_mandatory_hdrs(_Path, Hdrs, Host, Body, {false, _}) ->
add_headers(Hdrs, Body, Host, undefined, []);
add_mandatory_hdrs(Path, Hdrs, Host, Body, {true, Cookies}) ->
Result = {ContentHdrs, ConHdr} =
add_headers(Hdrs, Body, Host, undefined, []),
%% http://tools.ietf.org/search/rfc6265#section-4.1.2.4
%% only include cookies if the cookie path is a prefix of the request path
%% TODO optimize cookie handling
case lists:filter(
fun(#fusco_cookie{path_tokens = undefined}) ->
true;
(#fusco_cookie{path_tokens = CookiePath}) ->
SubPath = binary:split(Path, <<"/">>, [global]),
is_prefix(CookiePath, SubPath)
end, Cookies)
of
[] ->
Result;
IncludeCookies ->
{add_cookie_headers(ContentHdrs, IncludeCookies), ConHdr}
end.
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
%% http://tools.ietf.org/search/rfc6265#section-4.2.1
add_cookie_headers(Hdrs, Cookies) ->
[[<<"Cookie: ">>, make_cookie_string(Cookies, []), ?HTTP_LINE_END]
| Hdrs].
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
make_cookie_string([], Acc) ->
Acc;
make_cookie_string([Cookie | Rest], []) ->
make_cookie_string(Rest, cookie_string(Cookie));
make_cookie_string([Cookie | Rest], Acc) ->
make_cookie_string(Rest, [cookie_string(Cookie), "; " | Acc]).
%%------------------------------------------------------------------------------
%% @private
%%------------------------------------------------------------------------------
cookie_string(#fusco_cookie{name = Name, value = Value}) ->
[Name, <<"=">>, Value].
%%------------------------------------------------------------------------------
%% @private
%% Host header: http://tools.ietf.org/html/rfc2616#section-14.23
%%------------------------------------------------------------------------------
add_headers([{H, V} | T], undefined, undefined, Connection, Acc)
when Connection =/= undefined ->
add_headers(T, undefined, undefined, Connection,
[[H, <<": ">>, V, ?HTTP_LINE_END] | Acc]);
add_headers([{H, V} | T], Body, Host, Connection, Acc) ->
case bin_to_lower(H) of
<<"connection">> ->
add_headers(T, Body, Host, V,
[[H, <<": ">>, V, ?HTTP_LINE_END] | Acc]);
<<"host">> ->
add_headers(T, Body, undefined, Connection,
[[H, <<": ">>, V, ?HTTP_LINE_END] | Acc]);
<<"content-length">> ->
add_headers(T, undefined, Host, Connection,
[[H, <<": ">>, V, ?HTTP_LINE_END] | Acc]);
_ ->
add_headers(T, Body, Host, Connection,
[[H, <<": ">>, V, ?HTTP_LINE_END] | Acc])
end;
add_headers([], undefined, Host, Connection, Headers) ->
case Host of
undefined ->
{Headers, Connection};
_ ->
{[[<<"Host: ">>, Host, ?HTTP_LINE_END] | Headers], Connection}
end;
add_headers([], Body, Host, Connection, Headers) ->
ContentLength = integer_to_list(iolist_size(Body)),
case ContentLength > 0 of
true ->
add_headers([], undefined, Host, Connection,
[[<<"Content-Length: ">>, ContentLength, ?HTTP_LINE_END]
| Headers]);
_ ->
add_headers([], undefined, Host, Connection, Headers)
end.
%%------------------------------------------------------------------------------
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec host_header(host(), port_num()) -> any().
host_header(Host, 80) -> maybe_ipv6_enclose(Host);
% When proxying after an HTTP CONNECT session is established, squid doesn't
% like the :443 suffix in the Host header.
host_header(Host, 443) -> maybe_ipv6_enclose(Host);
host_header(Host, Port) -> [maybe_ipv6_enclose(Host), $:, integer_to_list(Port)].
%%------------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%%------------------------------------------------------------------------------
-spec maybe_ipv6_enclose(host()) -> host().
maybe_ipv6_enclose(Host) ->
case inet_parse:address(Host) of
{ok, {_, _, _, _, _, _, _, _}} ->
% IPv6 address literals are enclosed by square brackets (RFC2732)
[$[, Host, $]];
_ ->
Host
end.
%%------------------------------------------------------------------------------
%% @private
%% @doc
%% @end
%%------------------------------------------------------------------------------
char_to_lower($A) -> $a;
char_to_lower($B) -> $b;
char_to_lower($C) -> $c;
char_to_lower($D) -> $d;
char_to_lower($E) -> $e;
char_to_lower($F) -> $f;
char_to_lower($G) -> $g;
char_to_lower($H) -> $h;
char_to_lower($I) -> $i;
char_to_lower($J) -> $j;
char_to_lower($K) -> $k;
char_to_lower($L) -> $l;
char_to_lower($M) -> $m;
char_to_lower($N) -> $n;
char_to_lower($O) -> $o;
char_to_lower($P) -> $p;
char_to_lower($Q) -> $q;
char_to_lower($R) -> $r;
char_to_lower($S) -> $s;
char_to_lower($T) -> $t;
char_to_lower($U) -> $u;
char_to_lower($V) -> $v;
char_to_lower($W) -> $w;
char_to_lower($X) -> $x;
char_to_lower($Y) -> $y;
char_to_lower($Z) -> $z;
char_to_lower(Ch) -> Ch.
is_prefix([<<>>], _) ->
true;
is_prefix([H | T1], [H | T2]) ->
is_prefix(T1, T2);
is_prefix([], _) ->
true;
is_prefix(_, []) ->
false;
is_prefix(_, _) ->
false.

View File

@ -1,567 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 2013, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc
%%%
%%% @end
%%%=============================================================================
-module(fusco_protocol).
-copyright("2013, Erlang Solutions Ltd.").
-include("fusco.hrl").
-define(SIZE(Data, Response), Response#response{size = Response#response.size + byte_size(Data)}).
-define(RECEPTION(Data, Response), Response#response{size = byte_size(Data),
in_timestamp = os:timestamp()}).
-define(TOUT, 1000).
%% Latency is here defined as the time from the start of packet transmission to the start of packet reception
%% API
-export([recv/2, recv/3,
decode_cookie/1]).
%% TEST
-export([decode_header_value/5, decode_header_value/6,
decode_header/3, decode_header/4]).
%% TODO handle partial downloads
recv(Socket, Ssl) ->
recv(Socket, Ssl, infinity).
recv(Socket, Ssl, Timeout) ->
case fusco_sock:recv(Socket, Ssl, Timeout) of
{ok, Data} ->
decode_status_line(<< Data/binary >>,
?RECEPTION(Data, #response{socket = Socket, ssl = Ssl}), Timeout);
{error, Reason} ->
{error, Reason}
end.
decode_status_line(<<"HTTP/1.0\s",C1,C2,C3,$\s,Rest/bits>>, Response, Timeout) ->
decode_reason_phrase(Rest, <<>>, Response#response{version = {1,0},
status_code = <<C1,C2,C3>>}, Timeout);
decode_status_line(<<"HTTP/1.1\s",C1,C2,C3,$\s,Rest/bits>>, Response, Timeout) ->
decode_reason_phrase(Rest, <<>>, Response#response{version = {1,1},
status_code = <<C1,C2,C3>>}, Timeout);
decode_status_line(Bin, Response = #response{size = Size}, Timeout) when Size < 13 ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_status_line(<<Bin/binary, Data/binary>>, ?SIZE(Data, Response), Timeout);
{error, Reason} ->
{error, Reason}
end;
decode_status_line(_, _, _) ->
{error, status_line}.
decode_reason_phrase(<<>>, Acc, Response, Timeout) ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_reason_phrase(Data, Acc, ?SIZE(Data, Response), Timeout);
{error, Reason} ->
{error, Reason}
end;
decode_reason_phrase(<<$\r>>, Acc, Response, Timeout) ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_reason_phrase(<<$\r, Data/binary>>, Acc, ?SIZE(Data, Response), Timeout);
{error, Reason} ->
{error, Reason}
end;
decode_reason_phrase(<<$\n, Rest/bits>>, Acc, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{reason = Acc}, Timeout);
decode_reason_phrase(<<$\r,$\n, Rest/bits>>, Acc, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{reason = Acc}, Timeout);
decode_reason_phrase(<<C, Rest/bits>>, Acc, Response, Timeout) ->
decode_reason_phrase(Rest, <<Acc/binary, C>>, Response, Timeout).
decode_header(Data, Acc, Response) ->
decode_header(Data, Acc, Response, infinity).
decode_header(<<>>, Acc, Response, Timeout) ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_header(Data, Acc, ?SIZE(Data, Response), Timeout);
{error, closed} ->
case Acc of
<<>> ->
decode_body(<<>>, Response, Timeout);
_ ->
{error, closed}
end;
{error, Reason} ->
{error, Reason}
end;
decode_header(<<$\r>>, Acc, Response, Timeout) ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_header(<<$\r, Data/binary>>, Acc, ?SIZE(Data, Response), Timeout);
{error, Reason} ->
{error, Reason}
end;
decode_header(<<$\s, Rest/bits>>, Acc, Response, Timeout) ->
decode_header(Rest, Acc, Response, Timeout);
decode_header(<<$:, Rest/bits>>, Header, Response, Timeout) ->
decode_header_value_ws(Rest, Header, Response, Timeout);
decode_header(<<$\n, Rest/bits>>, <<>>, Response, Timeout) ->
decode_body(Rest, Response, Timeout);
decode_header(<<$\r, $\n, Rest/bits>>, <<>>, Response, Timeout) ->
decode_body(Rest, Response, Timeout);
decode_header(<<$\r, $\n, _Rest/bits>>, _, _Response, _Timeout) ->
{error, header};
decode_header(<<$A, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $a>>, Response, Timeout);
decode_header(<<$B, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $b>>, Response, Timeout);
decode_header(<<$C, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $c>>, Response, Timeout);
decode_header(<<$D, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $d>>, Response, Timeout);
decode_header(<<$E, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $e>>, Response, Timeout);
decode_header(<<$F, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $f>>, Response, Timeout);
decode_header(<<$G, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $g>>, Response, Timeout);
decode_header(<<$H, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $h>>, Response, Timeout);
decode_header(<<$I, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $i>>, Response, Timeout);
decode_header(<<$J, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $j>>, Response, Timeout);
decode_header(<<$K, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $k>>, Response, Timeout);
decode_header(<<$L, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $l>>, Response, Timeout);
decode_header(<<$M, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $m>>, Response, Timeout);
decode_header(<<$N, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $n>>, Response, Timeout);
decode_header(<<$O, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $o>>, Response, Timeout);
decode_header(<<$P, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $p>>, Response, Timeout);
decode_header(<<$Q, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $q>>, Response, Timeout);
decode_header(<<$R, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $r>>, Response, Timeout);
decode_header(<<$S, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $s>>, Response, Timeout);
decode_header(<<$T, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $t>>, Response, Timeout);
decode_header(<<$U, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $u>>, Response, Timeout);
decode_header(<<$V, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $v>>, Response, Timeout);
decode_header(<<$W, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $w>>, Response, Timeout);
decode_header(<<$X, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $x>>, Response, Timeout);
decode_header(<<$Y, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $y>>, Response, Timeout);
decode_header(<<$Z, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, $z>>, Response, Timeout);
decode_header(<<C, Rest/bits>>, Header, Response, Timeout) ->
decode_header(Rest, <<Header/binary, C>>, Response, Timeout).
decode_header_value_ws(<<$\s, Rest/bits>>, H, S, Timeout) ->
decode_header_value_ws(Rest, H, S, Timeout);
decode_header_value_ws(<<$\t, Rest/bits>>, H, S, Timeout) ->
decode_header_value_ws(Rest, H, S, Timeout);
decode_header_value_ws(Rest, <<"connection">> = H, S, Timeout) ->
decode_header_value_lc(Rest, H, <<>>, <<>>, S, Timeout);
decode_header_value_ws(Rest, <<"transfer-encoding">> = H, S, Timeout) ->
decode_header_value_lc(Rest, H, <<>>, <<>>, S, Timeout);
decode_header_value_ws(Rest, H, S, Timeout) ->
decode_header_value(Rest, H, <<>>, <<>>, S, Timeout).
decode_header_value(Data, H, V, T, Response) ->
decode_header_value(Data, H, V, T, Response, infinity).
decode_header_value(<<>>, H, V, T, Response, Timeout) ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_header_value(Data, H, V, T, ?SIZE(Data, Response), Timeout);
{error, Reason} ->
{error, Reason}
end;
decode_header_value(<<$\r>>, H, V, T, Response, Timeout) ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_header_value(<<$\r, Data/binary>>, H, V, T, ?SIZE(Data, Response), Timeout);
{error, Reason} ->
{error, Reason}
end;
decode_header_value(<<$\n, Rest/bits>>, <<"content-length">> = H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{headers = [{H, V} | Response#response.headers],
content_length = binary_to_integer(V)}, Timeout);
decode_header_value(<<$\n, Rest/bits>>, <<"set-cookie">> = H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{cookies = [decode_cookie(V)
| Response#response.cookies],
headers = [{H, V} | Response#response.headers]}, Timeout);
decode_header_value(<<$\n, Rest/bits>>, H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{headers = [{H, V} | Response#response.headers]}, Timeout);
decode_header_value(<<$\r, $\n, Rest/bits>>, <<"set-cookie">> = H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{cookies = [decode_cookie(V)
| Response#response.cookies],
headers = [{H, V} | Response#response.headers]}, Timeout);
decode_header_value(<<$\r,$\n, Rest/bits>>, <<"content-length">> = H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{headers = [{H, V} | Response#response.headers],
content_length = binary_to_integer(V)}, Timeout);
decode_header_value(<<$\r, $\n, Rest/bits>>, H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{headers = [{H, V} | Response#response.headers]}, Timeout);
decode_header_value(<<$\s, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value(Rest, H, V, <<T/binary, $\s>>, Response, Timeout);
decode_header_value(<<$\t, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value(Rest, H, V, <<T/binary, $\t>>, Response, Timeout);
decode_header_value(<<C, Rest/bits>>, H, V, <<>>, Response, Timeout) ->
decode_header_value(Rest, H, <<V/binary, C>>, <<>>, Response, Timeout);
decode_header_value(<<C, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value(Rest, H, <<V/binary, T/binary, C>>, <<>>, Response, Timeout).
decode_header_value_lc(<<>>, H, V, T, Response, Timeout) ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_header_value_lc(Data, H, V, T, ?SIZE(Data, Response), Timeout);
{error, Reason} ->
{error, Reason}
end;
decode_header_value_lc(<<$\r>>, H, V, T, Response, Timeout) ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_header_value_lc(<<$\r, Data/binary>>, H, V, T, ?SIZE(Data, Response), Timeout);
{error, Reason} ->
{error, Reason}
end;
decode_header_value_lc(<<$\n, Rest/bits>>, <<"transfer-encoding">> = H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{headers = [{H, V} | Response#response.headers],
transfer_encoding = V}, Timeout);
decode_header_value_lc(<<$\n, Rest/bits>>, H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{headers = [{H, V} | Response#response.headers],
connection = V}, Timeout);
decode_header_value_lc(<<$\r, $\n, Rest/bits>>, <<"transfer-encoding">> = H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{headers = [{H, V} | Response#response.headers],
transfer_encoding = V}, Timeout);
decode_header_value_lc(<<$\r, $\n, Rest/bits>>, H, V, _T, Response, Timeout) ->
decode_header(Rest, <<>>, Response#response{headers = [{H, V} | Response#response.headers],
connection = V}, Timeout);
decode_header_value_lc(<<$\s, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, V, <<T/binary, $\s>>, Response, Timeout);
decode_header_value_lc(<<$\t, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, V, <<T/binary, $\t>>, Response, Timeout);
decode_header_value_lc(<<$A, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $a>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$B, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $b>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$C, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $c>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$D, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $d>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$E, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $e>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$F, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $f>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$G, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $g>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$H, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $h>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$I, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $i>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$J, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $j>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$K, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $k>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$L, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $l>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$M, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $m>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$N, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $n>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$O, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $o>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$P, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $p>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$Q, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $q>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$R, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $r>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$S, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $s>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$T, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $t>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$U, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $u>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$V, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $v>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$W, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $w>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$X, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $x>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$Y, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $y>>, <<>>, Response, Timeout);
decode_header_value_lc(<<$Z, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, $z>>, <<>>, Response, Timeout);
decode_header_value_lc(<<C, Rest/bits>>, H, V, T, Response, Timeout) ->
decode_header_value_lc(Rest, H, <<V/binary, T/binary, C>>, <<>>, Response, Timeout).
%% RFC 6265
%% TODO decode cookie values, this only accepts 'a=b'
decode_cookie(Cookie) ->
decode_cookie_name(Cookie, <<>>).
decode_cookie_name(<<$\s, Rest/bits>>, N) ->
decode_cookie_name(Rest, N);
decode_cookie_name(<<$\t, Rest/bits>>, N) ->
decode_cookie_name(Rest, N);
decode_cookie_name(<<$=, Rest/bits>>, N) ->
decode_cookie_value(Rest, N, <<>>);
decode_cookie_name(<<C, Rest/bits>>, N) ->
decode_cookie_name(Rest, <<N/binary, C>>).
decode_cookie_value(<<$\s, Rest/bits>>, N, V) ->
decode_cookie_value(Rest, N, V);
decode_cookie_value(<<$\t, Rest/bits>>, N, V) ->
decode_cookie_value(Rest, N, V);
decode_cookie_value(<<$;, Rest/bits>>, N, V) ->
decode_cookie_av_ws(Rest, #fusco_cookie{name = N, value = V});
decode_cookie_value(<<C, Rest/bits>>, N, V) ->
decode_cookie_value(Rest, N, <<V/binary, C>>);
decode_cookie_value(<<>>, N, V) ->
#fusco_cookie{name = N, value = V}.
decode_cookie_av_ws(<<$\s, Rest/bits>>, C) ->
decode_cookie_av_ws(Rest, C);
decode_cookie_av_ws(<<$\t, Rest/bits>>, C) ->
decode_cookie_av_ws(Rest, C);
%% We are only interested on Expires, Max-Age, Path, Domain
decode_cookie_av_ws(<<$e, Rest/bits>>, C) ->
decode_cookie_av(Rest, C, <<$e>>);
decode_cookie_av_ws(<<$E, Rest/bits>>, C) ->
decode_cookie_av(Rest, C, <<$e>>);
decode_cookie_av_ws(<<$m, Rest/bits>>, C) ->
decode_cookie_av(Rest, C, <<$m>>);
decode_cookie_av_ws(<<$M, Rest/bits>>, C) ->
decode_cookie_av(Rest, C, <<$m>>);
decode_cookie_av_ws(<<$p, Rest/bits>>, C) ->
decode_cookie_av(Rest, C, <<$p>>);
decode_cookie_av_ws(<<$P, Rest/bits>>, C) ->
decode_cookie_av(Rest, C, <<$p>>);
decode_cookie_av_ws(<<$d, Rest/bits>>, C) ->
decode_cookie_av(Rest, C, <<$d>>);
decode_cookie_av_ws(<<$D, Rest/bits>>, C) ->
decode_cookie_av(Rest, C, <<$d>>);
decode_cookie_av_ws(Rest, C) ->
ignore_cookie_av(Rest, C).
ignore_cookie_av(<<$;, Rest/bits>>, Co) ->
decode_cookie_av_ws(Rest, Co);
ignore_cookie_av(<<_, Rest/bits>>, Co) ->
ignore_cookie_av(Rest, Co);
ignore_cookie_av(<<>>, Co) ->
Co.
%% Match only uppercase chars on Expires, Max-Age, Path, Domain
decode_cookie_av(<<$=, Rest/bits>>, Co, AV) ->
decode_cookie_av_value(Rest, Co, AV, <<>>);
decode_cookie_av(<<$D, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $d>>);
decode_cookie_av(<<$O, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $o>>);
decode_cookie_av(<<$N, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $n>>);
decode_cookie_av(<<$E, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $e>>);
decode_cookie_av(<<$X, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $x>>);
decode_cookie_av(<<$P, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $p>>);
decode_cookie_av(<<$I, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $i>>);
decode_cookie_av(<<$R, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $r>>);
decode_cookie_av(<<$S, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $s>>);
decode_cookie_av(<<$M, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $m>>);
decode_cookie_av(<<$A, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $a>>);
decode_cookie_av(<<$G, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $g>>);
decode_cookie_av(<<$T, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $t>>);
decode_cookie_av(<<$H, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, $h>>);
decode_cookie_av(<<$;, Rest/bits>>, Co, _AV) ->
decode_cookie_av_ws(Rest, Co);
decode_cookie_av(<<C, Rest/bits>>, Co, AV) ->
decode_cookie_av(Rest, Co, <<AV/binary, C>>);
decode_cookie_av(<<>>, Co, _AV) ->
ignore_cookie_av(<<>>, Co).
decode_cookie_av_value(<<>>, Co, <<"path">>, Value) ->
Co#fusco_cookie{path_tokens = binary:split(Value, <<"/">>, [global]),
path = Value};
decode_cookie_av_value(<<>>, Co, <<"max-age">>, Value) ->
Co#fusco_cookie{max_age = max_age(Value)};
decode_cookie_av_value(<<>>, Co, <<"expires">>, Value) ->
Co#fusco_cookie{expires = expires(Value)};
decode_cookie_av_value(<<>>, Co, <<"domain">>, Value) ->
Co#fusco_cookie{domain = Value};
decode_cookie_av_value(<<$;, Rest/bits>>, Co, <<"path">>, Value) ->
Path = binary:split(Value, <<"/">>, [global]),
decode_cookie_av_ws(Rest, Co#fusco_cookie{path_tokens = Path,
path = Value});
decode_cookie_av_value(<<$;, Rest/bits>>, Co, <<"max-age">>, Value) ->
decode_cookie_av_ws(Rest, Co#fusco_cookie{
max_age = max_age(Value)});
decode_cookie_av_value(<<$;, Rest/bits>>, Co, <<"expires">>, Value) ->
%% TODO parse expires
decode_cookie_av_ws(Rest, Co#fusco_cookie{expires = expires(Value)});
decode_cookie_av_value(<<$;, Rest/bits>>, Co, <<"domain">>, Value) ->
decode_cookie_av_ws(Rest, Co#fusco_cookie{domain = Value});
decode_cookie_av_value(<<$;, Rest/bits>>, Co, _, _) ->
decode_cookie_av_ws(Rest, Co);
decode_cookie_av_value(<<C, Rest/bits>>, Co, AV, Value) ->
decode_cookie_av_value(Rest, Co, AV, <<Value/binary, C>>).
decode_body(<<>>, Response = #response{status_code = <<$1, _, _>>,
transfer_encoding = TE}, _Timeout)
when TE =/= <<"chunked">> ->
return(<<>>, Response);
decode_body(<<$\r, $\n, Rest/bits>>, Response, Timeout) ->
decode_body(Rest, Response, Timeout);
decode_body(Rest, Response = #response{status_code = <<$1, _, _>>,
transfer_encoding = TE}, Timeout)
when TE =/= <<"chunked">> ->
decode_status_line(Rest, #response{socket = Response#response.socket,
ssl = Response#response.ssl,
in_timestamp = Response#response.in_timestamp}, Timeout);
decode_body(Rest, Response = #response{transfer_encoding = <<"chunked">>}, Timeout) ->
decode_chunked_body(Rest, <<>>, <<>>, Response, Timeout);
decode_body(Rest, Response, Timeout) ->
case byte_size(Rest) >= Response#response.content_length of
true ->
return(Rest, Response);
false ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_body(<<Rest/binary, Data/binary>>, ?SIZE(Data, Response), Timeout);
_ ->
%% NOTE: Return what we have so far
return(Rest, Response)
end
end.
download_chunked_body(Rest, Acc, Size, Response, Timeout) ->
case fusco_sock:recv(Response#response.socket, Response#response.ssl, Timeout) of
{ok, Data} ->
decode_chunked_body(<<Rest/bits, Data/bits>>, Acc, Size,
?SIZE(Data, Response), Timeout);
_ ->
return(Acc, Response)
end.
decode_chunked_body(<<$0,$\r,$\n,$\r,$\n>>, Acc, _, Response, _Timeout) ->
return(Acc, Response);
decode_chunked_body(<<$0, Rest/bits>> = R, Acc, Size, Response, Timeout)
when is_binary(Size), byte_size(Rest) < 4 ->
download_chunked_body(R, Acc, Size, Response, Timeout);
decode_chunked_body(<<$\r>> = R, Acc, Size, Response, Timeout) when is_binary(Size) ->
download_chunked_body(R, Acc, Size, Response, Timeout);
decode_chunked_body(<<$\r,$\n, Rest/bits>>, Acc, <<>>, Response, Timeout) ->
decode_chunked_body(Rest, Acc, <<>>, Response, Timeout);
decode_chunked_body(<<$\r,$\n, Rest/bits>>, Acc, Size, Response, Timeout) when is_binary(Size) ->
IntSize = erlang:binary_to_integer(Size, 16),
decode_chunked_body(Rest, Acc, IntSize, Response, Timeout);
decode_chunked_body(<<C, Rest/bits>>, Acc, Size, Response, Timeout) when is_binary(Size) ->
decode_chunked_body(Rest, Acc, <<Size/bits, C>>, Response, Timeout);
decode_chunked_body(<<>> = R, Acc, Size, Response, Timeout) when is_binary(Size) ->
download_chunked_body(R, Acc, Size, Response, Timeout);
decode_chunked_body(Rest, Acc, Size, Response, Timeout) when is_integer(Size) ->
case byte_size(Rest) of
S when S == Size ->
decode_chunked_body(<<>>, <<Acc/bits, Rest/bits>>, <<>>, Response, Timeout);
S when S < Size ->
download_chunked_body(Rest, Acc, Size, Response, Timeout);
S when S > Size ->
Current = binary:part(Rest, 0, Size),
Next = binary:part(Rest, Size, S - Size),
decode_chunked_body(Next, <<Acc/bits, Current/bits>>, <<>>, Response, Timeout)
end.
return(Body, Response) ->
Response#response{body = Body}.
max_age(Value) ->
binary_to_integer(Value) * 1000000.
%% http://tools.ietf.org/html/rfc2616#section-3.3.1
%% Supports some non-standard datetime (Tomcat) Tue, 06-Nov-1994 08:49:37 GMT
expires(<<_,_,_,$,,$\s,D1,D2,$\s,M1,M2,M3,$\s,Y1,Y2,Y3,Y4,$\s,Rest/bits>>) ->
expires(Rest, {list_to_integer([Y1,Y2,Y3,Y4]),month(<<M1,M2,M3>>),list_to_integer([D1,D2])});
expires(<<_,_,_,$\s,Mo1,Mo2,Mo3,$\s,D1,D2,$\s,H1,H2,$:,M1,M2,$:,S1,S2,$\s,Y1,Y2,Y3,Y4,_Rest/bits>>) ->
{{list_to_integer([Y1,Y2,Y3,Y4]),month(<<Mo1,Mo2,Mo3>>),list_to_integer([D1,D2])},
{list_to_integer([H1,H2]), list_to_integer([M1,M2]), list_to_integer([S1,S2])}};
expires(<<_,_,_,$,,$\s,Rest/bits>>) ->
expires(Rest);
expires(<<"Monday",$,,$\s,Rest/bits>>) ->
expires(Rest);
expires(<<"Tuesday",$,,$\s,Rest/bits>>) ->
expires(Rest);
expires(<<"Wednesday",$,,$\s,Rest/bits>>) ->
expires(Rest);
expires(<<"Thursday",$,,$\s,Rest/bits>>) ->
expires(Rest);
expires(<<"Friday",$,,$\s,Rest/bits>>) ->
expires(Rest);
expires(<<"Saturday",$,,$\s,Rest/bits>>) ->
expires(Rest);
expires(<<"Sunday",$,,$\s,Rest/bits>>) ->
expires(Rest);
expires(<<D1,D2,$\-,M1,M2,M3,$\-,Y1,Y2,Y3,Y4,$\s,Rest/bits>>) ->
expires(Rest, {list_to_integer([Y1,Y2,Y3,Y4]),month(<<M1,M2,M3>>),list_to_integer([D1,D2])});
expires(<<D1,D2,$\-,M1,M2,M3,$\-,Y3,Y4,$\s,Rest/bits>>) ->
%% http://tools.ietf.org/html/rfc2616#section-19.3
%% HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date
%% which appears to be more than 50 years in the future is in fact
%% in the past (this helps solve the "year 2000" problem).
expires(Rest, {to_year([Y3, Y4]),month(<<M1,M2,M3>>),list_to_integer([D1,D2])}).
to_year(List) ->
Int = list_to_integer(List),
{Y, _, _} = date(),
case (2000 + Int - Y) > 50 of
true ->
1900 + Int;
false ->
2000 + Int
end.
expires(<<H1,H2,$:,M1,M2,$:,S1,S2,_Rest/bits>>, Date) ->
{Date, {list_to_integer([H1,H2]), list_to_integer([M1,M2]), list_to_integer([S1,S2])}}.
month(<<$J,$a,$n>>) ->
1;
month(<<$F,$e,$b>>) ->
2;
month(<<$M,$a,$r>>) ->
3;
month(<<$A,$p,$r>>) ->
4;
month(<<$M,$a,$y>>) ->
5;
month(<<$J,$u,$n>>) ->
6;
month(<<$J,$u,$l>>) ->
7;
month(<<$A,$u,$g>>) ->
8;
month(<<$S,$e,$p>>) ->
9;
month(<<$O,$c,$t>>) ->
10;
month(<<$N,$o,$v>>) ->
11;
month(<<$D,$e,$c>>) ->
12.

View File

@ -1,123 +0,0 @@
%%%-----------------------------------------------------------------------------
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Oscar Hellström <oscar@hellstrom.st>
%%% @author Diana Parra Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc This module implements wrappers for socket operations.
%%%
%%% Makes it possible to have the same interface to ssl and tcp sockets.
%%% @end
%%%-----------------------------------------------------------------------------
-module(fusco_sock).
-export([connect/5,
recv/2,
recv/3,
send/3,
close/2,
setopts/3]).
-include("fusco_types.hrl").
%%==============================================================================
%% Exported functions
%%==============================================================================
%%------------------------------------------------------------------------------
%% @spec (Host, Port, Options, Timeout, SslFlag) -> {ok, Socket} | {error, Reason}
%% Host = string() | ip_address()
%% Port = integer()
%% Options = [{atom(), term()} | atom()]
%% Timeout = infinity | integer()
%% SslFlag = boolean()
%% Socket = socket()
%% Reason = atom()
%% @doc
%% Connects to `Host' and `Port'.
%% Will use the `ssl' module if `SslFlag' is `true' and gen_tcp otherwise.
%% `Options' are the normal `gen_tcp' or `ssl' Options.
%% @end
%%------------------------------------------------------------------------------
-spec connect(host(), integer(), socket_options(), timeout(), boolean()) ->
{ok, socket()} | {error, atom()}.
connect(Host, Port, Options, Timeout, true) ->
ssl:connect(Host, Port, Options, Timeout);
connect(Host, Port, Options, Timeout, false) ->
gen_tcp:connect(Host, Port, Options, Timeout).
%%------------------------------------------------------------------------------
%% @spec (Socket, SslFlag) -> {ok, Data} | {error, Reason}
%% Socket = socket()
%% Length = integer()
%% SslFlag = boolean()
%% Data = term()
%% Reason = atom()
%% @doc
%% Reads available bytes from `Socket'.
%% Will block untill data is available on the socket and return the first
%% packet.
%% @end
%%------------------------------------------------------------------------------
-spec recv(socket(), boolean()) ->
{ok, any()} | {error, atom()} | {error, {http_error, string()}}.
recv(Socket, true) ->
ssl:recv(Socket, 0);
recv(Socket, false) ->
prim_inet:recv(Socket, 0).
-spec recv(socket(), boolean(), timeout()) ->
{ok, any()} | {error, atom()} | {error, {http_error, string()}}.
recv(Socket, true, Timeout) ->
ssl:recv(Socket, 0, Timeout);
recv(Socket, false, Timeout) ->
prim_inet:recv(Socket, 0, Timeout).
%%------------------------------------------------------------------------------
%% @spec (Socket, Data, SslFlag) -> ok | {error, Reason}
%% Socket = socket()
%% Data = iolist()
%% SslFlag = boolean()
%% Reason = atom()
%% @doc
%% Sends data on a socket.
%% Will use the `ssl' module if `SslFlag' is set to `true', otherwise the
%% gen_tcp module.
%% @end
%%------------------------------------------------------------------------------
-spec send(socket(), iolist() | binary(), boolean()) -> ok | {error, atom()}.
send(Socket, Request, true) ->
ssl:send(Socket, Request);
send(Socket, Request, false) ->
prim_inet:send(Socket, Request, []).
%%------------------------------------------------------------------------------
%% @spec (Socket, SslFlag) -> ok | {error, Reason}
%% Socket = socket()
%% SslFlag = boolean()
%% Reason = atom()
%% @doc
%% Closes a socket.
%% @end
%%------------------------------------------------------------------------------
-spec close(socket(), boolean()) -> ok | {error, atom()}.
close(Socket, true) ->
ssl:close(Socket);
close(Socket, false) ->
gen_tcp:close(Socket).
%%------------------------------------------------------------------------------
%% @spec (Socket, Opts, SslFlag) -> ok | {error, Reason}
%% Socket = socket()
%% Opts = list()
%% SslFlag = boolean()
%% Reason = atom()
%% @doc
%% Sets options for a socket.
%% Will use the `ssl' module if `SslFlag' is set to `true', otherwise the
%% inets module.
%% @end
%%------------------------------------------------------------------------------
-spec setopts(socket(), list(), boolean()) -> ok | {error, atom()}.
setopts(Socket, Opts, true) ->
ssl:setopts(Socket, Opts);
setopts(Socket, Opts, false) ->
inet:setopts(Socket, Opts).

View File

@ -1,17 +0,0 @@
-----BEGIN CERTIFICATE-----
MIICqzCCAhQCCQCDYWGxYNaErjANBgkqhkiG9w0BAQUFADCBmTELMAkGA1UEBhMC
VUsxDzANBgNVBAgTBkVybGFuZzEPMA0GA1UEBxMGTG9uZG9uMSwwKgYDVQQKEyNF
cmxhbmcgVHJhaW5pbmcgYW5kIENvbnN1bHRpbmcgTHRkLjEPMA0GA1UEAxMGbGh0
dHBjMSkwJwYJKoZIhvcNAQkBFhpjb2RlQGVybGFuZy1jb25zdWx0aW5nLmNvbTAe
Fw0wOTA2MDYxNjA2MThaFw0xMDA2MDYxNjA2MThaMIGZMQswCQYDVQQGEwJVSzEP
MA0GA1UECBMGRXJsYW5nMQ8wDQYDVQQHEwZMb25kb24xLDAqBgNVBAoTI0VybGFu
ZyBUcmFpbmluZyBhbmQgQ29uc3VsdGluZyBMdGQuMQ8wDQYDVQQDEwZsaHR0cGMx
KTAnBgkqhkiG9w0BCQEWGmNvZGVAZXJsYW5nLWNvbnN1bHRpbmcuY29tMIGfMA0G
CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6YrA5HIBj817qplKlRaU3dkCnnZtKS666
lRbqsdj3Fug7ezANmUrUZIGMTDOAwYg3E2JPAL1VOiPmi/ENlanLTyOp2SkqYLfR
59Z5wr1nE/iAC+es7WT2OPjXG7MIBvnM7FNpHY17F4MM0FWWm+LJyJRucUZHL964
nw0c2xZ3fwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAHRPJK72okG7u//YgJ1zIHzi
P8xtoUqwRikNNK1Zf1//xrgMFdsX4M7ZrX+SJ5ArXXpvy8iUbx81m9w+tuEyqdnl
VbBherAqLi2XmwJFu2n6TIfJB2eUjZg95lUbIJsNuiqL05/LNthFdkVAvyi8nTC4
KAqPDQhDdvpmaBZDKE1L
-----END CERTIFICATE-----

View File

@ -1,57 +0,0 @@
%%% ----------------------------------------------------------------------------
%%% @copyright (C) 2013, Erlang Solutions Ltd
%%% @author Diana Parra Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc Fusco Client Pool tests
%%% @end
%%%-----------------------------------------------------------------------------
-module(fusco_cp_tests).
-include_lib("eunit/include/eunit.hrl").
-define(POOL, fusco_pool).
client_pool_test_() ->
{foreach,
fun() ->
{ok, Pid} = fusco_cp:start({"127.0.0.1", 5050, false}, [], 3),
erlang:register(?POOL, Pid),
Pid
end,
fun(Pid) ->
fusco_cp:stop(Pid)
end,
[
{timeout, 60000, {"Get client", fun get_client/0}},
{"Free client", fun free_client/0},
{"Unblock client", fun unblock_client/0}
]
}.
get_client() ->
?assertEqual(true, is_pid(fusco_cp:get_client(?POOL))),
?assertEqual(true, is_pid(fusco_cp:get_client(?POOL))),
?assertEqual(true, is_pid(fusco_cp:get_client(?POOL))),
?assertEqual({error, timeout}, fusco_cp:get_client(?POOL)).
free_client() ->
Pid = fusco_cp:get_client(?POOL),
?assertEqual(true, is_pid(Pid)),
?assertEqual(ok, fusco_cp:free_client(?POOL, Pid)),
?assertEqual(Pid, fusco_cp:get_client(?POOL)).
unblock_client() ->
Client = fusco_cp:get_client(?POOL),
?assertEqual(true, is_pid(Client)),
?assertEqual(true, is_pid(fusco_cp:get_client(?POOL))),
?assertEqual(true, is_pid(fusco_cp:get_client(?POOL))),
To = self(),
spawn(fun() ->
Pid = fusco_cp:get_client(?POOL),
To ! {client, Pid}
end),
?assertEqual(ok, fusco_cp:free_client(?POOL, Client)),
?assertEqual({client, Client}, receive
{client, _} = R ->
R
end).

View File

@ -1,207 +0,0 @@
%%%-----------------------------------------------------------------------------
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Oscar Hellström <oscar@hellstrom.st>
%%% @author Diana Parra Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc
%%% @end
%%%-----------------------------------------------------------------------------
-module(fusco_lib_tests).
-copyright("2013, Erlang Solutions Ltd.").
-include("../include/fusco_types.hrl").
-include("../include/fusco.hrl").
-include_lib("eunit/include/eunit.hrl").
parse_url_test_() ->
[
?_assertEqual(#fusco_url{
host = "host",
port = 80,
path = "/",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://host")),
?_assertEqual(#fusco_url{
host = "host",
port = 80,
path = "/",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://host/")),
?_assertEqual(#fusco_url{
host = "host",
port = 443,
path = "/",
is_ssl = true,
user = "",
password = ""
},
fusco_lib:parse_url("https://host")),
?_assertEqual(#fusco_url{
host = "host",
port = 443,
path = "/",
is_ssl = true,
user = "",
password = ""
},
fusco_lib:parse_url("https://host/")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://host:180")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://host:180/")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/foo",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://host:180/foo")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://host:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "joe",
password = "erlang"
},
fusco_lib:parse_url("http://joe:erlang@host:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "joe",
password = "erl@ng"
},
fusco_lib:parse_url("http://joe:erl%40ng@host:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "joe",
password = ""
},
fusco_lib:parse_url("http://joe@host:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://@host:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "joe:arm",
password = "erlang"
},
fusco_lib:parse_url("http://joe%3Aarm:erlang@host:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "host",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "joe:arm",
password = "erlang/otp"
},
fusco_lib:parse_url("http://joe%3aarm:erlang%2Fotp@host:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "::1",
port = 80,
path = "/foo/bar",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://[::1]/foo/bar")),
?_assertEqual(#fusco_url{
host = "::1",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://[::1]:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "::1",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "joe",
password = "erlang"
},
fusco_lib:parse_url("http://joe:erlang@[::1]:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "1080:0:0:0:8:800:200c:417a",
port = 180,
path = "/foo/bar",
is_ssl = false,
user = "joe",
password = "erlang"
},
fusco_lib:parse_url("http://joe:erlang@[1080:0:0:0:8:800:200C:417A]:180/foo/bar")),
?_assertEqual(#fusco_url{
host = "www.example.com",
port = 80,
path = "/?a=b",
is_ssl = false,
user = "",
password = ""
},
fusco_lib:parse_url("http://www.example.com?a=b"))
].

View File

@ -1,38 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc
%%% @end
%%%=============================================================================
-module(fusco_protocol_SUITE).
-copyright("2013, Erlang Solutions Ltd.").
-compile(export_all).
all() ->
[prop_http_response_close_connection,
prop_http_response_keep_alive,
prop_chunked_http_response_keep_alive].
%%==============================================================================
%% Test cases
%%==============================================================================
prop_http_response_close_connection(_) ->
do_prop(prop_http_response_close_connection).
prop_http_response_keep_alive(_) ->
do_prop(prop_http_response_keep_alive).
prop_chunked_http_response_keep_alive(_) ->
do_prop(prop_chunked_http_response_keep_alive).
%%==============================================================================
%% Internal functions
%%==============================================================================
do_prop(Case) ->
case eqc:counterexample(erlang:apply(fusco_protocol_eqc, Case, [])) of
true ->
true;
Value ->
exit(Value)
end.

View File

@ -1,173 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc
%%% @end
%%%=============================================================================
-module(fusco_protocol_eqc).
-copyright("2013, Erlang Solutions Ltd.").
-include_lib("eqc/include/eqc.hrl").
-include("fusco.hrl").
-export([prop_http_response_close_connection/0,
prop_http_response_keep_alive/0,
prop_chunked_http_response_keep_alive/0]).
%%==============================================================================
%% Quickcheck generators
%%==============================================================================
valid_http_message() ->
?LET({StatusLine, Headers, Cookies},
{http_eqc_gen:status_line(), http_eqc_gen:headers(),
list(http_eqc_gen:set_cookie())},
?LET(Body, http_eqc_encoding:body(StatusLine),
{StatusLine, http_eqc_encoding:add_content_length(Headers, Body),
Cookies, Body})).
valid_http_chunked_message() ->
?LET({StatusLine, Headers, Cookies},
{http_eqc_gen:status_line(), http_eqc_gen:headers(),
list(http_eqc_gen:set_cookie())},
?LET(Body, http_eqc_gen:chunked_body(),
{StatusLine, http_eqc_encoding:add_transfer_encoding(
Headers, <<"chunked">>),
Cookies, Body})).
%%==============================================================================
%% Quickcheck properties
%%==============================================================================
prop_http_response_close_connection() ->
%% Connection is closed just after send the response
prop_http_response(close).
prop_http_response_keep_alive() ->
%% Connection stays open after send the response
prop_http_response(keepalive).
prop_chunked_http_response_keep_alive() ->
%% Connection stays open after send the response
prop_chunked_http_response(keepalive).
prop_http_response(ConnectionState) ->
eqc:numtests(
500,
?FORALL(ValidMessage, valid_http_message(),
decode_valid_message(ConnectionState, ValidMessage))).
decode_valid_message(ConnectionState, {StatusLine, Headers, Cookies, Body}) ->
Msg = http_eqc_encoding:build_valid_response(StatusLine, Headers, Cookies, Body),
L = {_, _, Socket} =
test_utils:start_listener({fragmented, Msg}, ConnectionState),
test_utils:send_message(Socket),
Recv = fusco_protocol:recv(Socket, false),
test_utils:stop_listener(L),
Expected = expected_output(StatusLine, Headers, Cookies, Body, Msg),
Cleared = clear_record(clear_connection(Recv)),
?WHENFAIL(io:format("Message:~n=======~n~s~n=======~nResponse:"
" ~p~nCleared: ~p~nExpected: ~p~n",
[binary:list_to_bin(Msg), Recv, Cleared, Expected]),
case Cleared of
Expected ->
true;
_ ->
false
end).
prop_chunked_http_response(ConnectionState) ->
eqc:numtests(
500,
?FORALL(ValidMessage, valid_http_chunked_message(),
decode_valid_message(ConnectionState, ValidMessage))).
%%==============================================================================
%% Internal functions
%%==============================================================================
expected_output({HttpVersion, StatusCode, Reason}, Headers, Cookies, Body, Msg) ->
Version = http_version(HttpVersion),
OCookies = [{Name, list_to_binary(http_eqc_encoding:build_cookie(Cookie))}
|| {Name, Cookie} <- Cookies],
LowerHeaders = lists:reverse(headers_to_lower(Headers ++ OCookies)),
CookiesRec = output_cookies(Cookies),
#response{version = Version,
status_code = StatusCode,
reason = Reason,
cookies = CookiesRec,
headers = LowerHeaders,
connection = to_lower(proplists:get_value(<<"connection">>,
LowerHeaders)),
body = expected_body(Body),
content_length = content_length(Body),
transfer_encoding = to_lower(proplists:get_value(<<"transfer-encoding">>,
LowerHeaders)),
size = byte_size(list_to_binary(Msg))}.
expected_body(Body) when is_binary(Body) ->
Body;
expected_body(List) ->
list_to_binary([Bin || {_, Bin} <- List]).
content_length(Body) when is_binary(Body) ->
byte_size(Body);
content_length(_) ->
0.
output_cookies(Cookies) ->
output_cookies(Cookies, []).
output_cookies([{_SetCookie, {{K, V}, Avs}} | Rest], Acc) ->
MaxAge = output_max_age(proplists:get_value(<<"Max-Age">>, Avs)),
Path = proplists:get_value(<<"Path">>, Avs),
PathTokens = case Path of
Bin when is_binary(Bin) ->
binary:split(Bin, <<"/">>, [global]);
undefined ->
undefined
end,
Expires = http_eqc_encoding:expires_datetime(proplists:get_value(<<"Expires">>, Avs)),
Cookie = #fusco_cookie{name = K, value = V, max_age = MaxAge, path = Path,
path_tokens = PathTokens,
expires = Expires,
domain = proplists:get_value(<<"Domain">>, Avs)},
output_cookies(Rest, [Cookie | Acc]);
output_cookies([], Acc) ->
Acc.
output_max_age(undefined) ->
undefined;
output_max_age(Age) ->
list_to_integer(binary_to_list(Age)) * 1000000.
clear_record(Response) when is_record(Response, response) ->
Response#response{socket = undefined,
ssl = undefined,
in_timestamp = undefined};
clear_record(Error) ->
Error.
clear_connection(Response) when is_record(Response, response) ->
Response#response{connection = to_lower(Response#response.connection)};
clear_connection(Error) ->
Error.
http_version(<<"HTTP/1.1">>) ->
{1, 1};
http_version(<<"HTTP/1.0">>) ->
{1, 0}.
headers_to_lower(Headers) ->
[begin
He = to_lower(H),
case He of
LH when LH == <<"connection">>; LH == <<"transfer-encoding">> ->
{He, to_lower(V)};
_ ->
{He, V}
end
end || {H, V} <- Headers].
to_lower(undefined) ->
undefined;
to_lower(Bin) ->
list_to_binary(string:to_lower(binary_to_list(Bin))).

View File

@ -1,66 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc
%%% @end
%%%=============================================================================
-module(fusco_protocol_tests).
-copyright("2013, Erlang Solutions Ltd.").
-include_lib("eunit/include/eunit.hrl").
-include("fusco.hrl").
-export([test_decode_header/0]).
fusco_protocol_test_() ->
[{"HTTP version", ?_test(http_version())},
{"Cookies", ?_test(cookies())},
{"Decode header", ?_test(decode_header())}].
http_version() ->
L = {_, _, Socket} = test_utils:start_listener(cookie_message()),
test_utils:send_message(Socket),
?assertMatch(#response{version = {1,1},
status_code = <<"200">>,
reason = <<"OK">>,
body = <<"Great success!">>},
fusco_protocol:recv(Socket, false)),
test_utils:stop_listener(L).
cookies() ->
L = {_, _, Socket} = test_utils:start_listener(cookie_message()),
test_utils:send_message(Socket),
Recv = fusco_protocol:recv(Socket, false),
test_utils:stop_listener(L),
?assertMatch(#response{version = {1,1},
status_code = <<"200">>,
reason = <<"OK">>,
headers = [{<<"set-cookie">>,<<"name2=value2; Expires=Wed, 09 Jun 2021 10:18:14 GMT">>},
{<<"set-cookie">>,<<"name=value">>} | _],
body = <<"Great success!">>},
Recv).
decode_header() ->
?assertMatch(#response{
headers = [{<<"set-cookie">>,<<"name2=value2; Expires=Wed, 09 Jun 2021 10:18:14 GMT">>},
{<<"set-cookie">>,<<"name=value">>},
{<<"content-length">>, <<"14">>},
{<<"content-type">>,<<"text/plain">>}],
body = <<"Great success!">>},
test_decode_header()).
test_decode_header() ->
fusco_protocol:decode_header(header(), <<>>, #response{}).
header() ->
<<"Content-type: text/plain\r\nContent-length: 14\r\nSet-Cookie: name=value\r\nSet-Cookie: name2=value2; Expires=Wed, 09 Jun 2021 10:18:14 GMT\r\n\r\nGreat success!">>.
cookie_message() ->
[
"HTTP/1.1 200 OK\r\n"
"Content-type: text/plain\r\nContent-length: 14\r\n"
"Set-Cookie: name=value\r\n"
"Set-Cookie: name2=value2; Expires=Wed, 09 Jun 2021 10:18:14 GMT\r\n"
"\r\n"
"Great success!"
].

View File

@ -1,201 +0,0 @@
%%% ----------------------------------------------------------------------------
%%% Copyright (c) 2009, Erlang Training and Consulting Ltd.
%%% All rights reserved.
%%%
%%% Redistribution and use in source and binary forms, with or without
%%% modification, are permitted provided that the following conditions are met:
%%% * Redistributions of source code must retain the above copyright
%%% notice, this list of conditions and the following disclaimer.
%%% * Redistributions in binary form must reproduce the above copyright
%%% notice, this list of conditions and the following disclaimer in the
%%% documentation and/or other materials provided with the distribution.
%%% * Neither the name of Erlang Training and Consulting Ltd. nor the
%%% names of its contributors may be used to endorse or promote products
%%% derived from this software without specific prior written permission.
%%%
%%% THIS SOFTWARE IS PROVIDED BY Erlang Training and Consulting Ltd. ''AS IS''
%%% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
%%% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
%%% ARE DISCLAIMED. IN NO EVENT SHALL Erlang Training and Consulting Ltd. BE
%%% LIABLE SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
%%% BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
%%% WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
%%% OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
%%% ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
%%% ----------------------------------------------------------------------------
%%% @author Oscar Hellström <oscar@hellstrom.st>
-module(fusco_tests).
-export([test_no/2]).
-include_lib("eunit/include/eunit.hrl").
test_no(N, Tests) ->
setelement(2, Tests,
setelement(4, element(2, Tests),
lists:nth(N, element(4, element(2, Tests))))).
%%% Eunit setup stuff
start_app() ->
[application:start(App) || App <- apps()].
apps() ->
[crypto, asn1, public_key, ssl].
stop_app(_) ->
[application:stop(App) || App <- lists:reverse(apps())].
tcp_test_() ->
{inorder,
{setup, fun start_app/0, fun stop_app/1, [
?_test(get_with_connect_options()),
?_test(no_content_length()),
?_test(no_content_length_1_0()),
?_test(pre_1_1_server_connection()),
?_test(pre_1_1_server_keep_alive()),
?_test(post_100_continue()),
?_test(request_timeout()),
?_test(trailing_space_header()),
?_test(closed_after_timeout())
]}
}.
options_test() ->
invalid_options().
get_with_connect_options() ->
{ok, _, _, Port} = webserver:start(gen_tcp, [fun webserver_utils:empty_body/5]),
URL = url(Port),
Options = [{connect_options, [{ip, {127, 0, 0, 1}}, {port, 0}]}],
{ok, Client} = fusco:start(URL, Options),
{ok, Response} = fusco:request(Client, <<"/empty">>, "GET", [], [], 1, 1000),
?assertEqual({<<"200">>, <<"OK">>}, status(Response)),
?assertEqual(<<>>, body(Response)).
no_content_length() ->
{ok, _, _, Port} = webserver:start(gen_tcp, [fun webserver_utils:no_content_length/5]),
URL = url(Port),
{ok, Client} = fusco:start(URL, []),
{ok, Response} = fusco:request(Client, <<"/no_cl">>, "GET", [], [], 1000),
?assertEqual({<<"200">>, <<"OK">>}, status(Response)),
?assertEqual(list_to_binary(webserver_utils:default_string()), body(Response)).
no_content_length_1_0() ->
{ok, _, _, Port} = webserver:start(gen_tcp, [fun webserver_utils:no_content_length_1_0/5]),
URL = url(Port),
{ok, Client} = fusco:start(URL, []),
{ok, Response} = fusco:request(Client, <<"/no_cl">>, "GET", [], [], 1000),
?assertEqual({<<"200">>, <<"OK">>}, status(Response)),
?assertEqual(list_to_binary(webserver_utils:default_string()), body(Response)).
%% Check the header value is trimming spaces on header values
%% which can cause crash in fusco_client:body_type when Content-Length
%% is converted from list to integer
trailing_space_header() ->
{ok, _, _, Port} = webserver:start(gen_tcp, [fun webserver_utils:trailing_space_header/5]),
URL = url(Port),
{ok, Client} = fusco:start(URL, []),
{ok, Response} = fusco:request(Client, <<"/no_cl">>, "GET", [], [], 1000),
Headers = headers(Response),
ContentLength = fusco_lib:header_value(<<"content-length">>, Headers),
?assertEqual(<<"14">>, ContentLength).
pre_1_1_server_connection() ->
{ok, _, _, Port} = webserver:start(gen_tcp, [fun webserver_utils:pre_1_1_server/5]),
URL = url(Port),
Body = pid_to_list(self()),
{ok, Client} = fusco:start(URL, []),
{ok, _} = fusco:request(Client, <<"/close">>, "PUT", [], Body, 1000),
% Wait for the server to see that socket has been closed.
% The socket should be closed by us since the server responded with a
% 1.0 version, and not the Connection: keep-alive header.
receive closed -> ok end.
pre_1_1_server_keep_alive() ->
{ok, _, _, Port} = webserver:start(gen_tcp,
[
fun webserver_utils:pre_1_1_server_keep_alive/5,
fun webserver_utils:pre_1_1_server/5
]),
URL = url(Port),
Body = pid_to_list(self()),
{ok, Client} = fusco:start(URL, []),
{ok, Response1} = fusco:request(Client, <<"/close">>, "GET", [], [], 1000),
{ok, Response2} = fusco:request(Client, <<"/close">>, "PUT", [], Body, 1000),
?assertEqual({<<"200">>, <<"OK">>}, status(Response1)),
?assertEqual({<<"200">>, <<"OK">>}, status(Response2)),
?assertEqual(list_to_binary(webserver_utils:default_string()), body(Response1)),
?assertEqual(list_to_binary(webserver_utils:default_string()), body(Response2)),
% Wait for the server to see that socket has been closed.
% The socket should be closed by us since the server responded with a
% 1.0 version, and not the Connection: keep-alive header.
receive closed -> ok end.
post_100_continue() ->
{ok, _, _, Port} = webserver:start(gen_tcp, [fun webserver_utils:copy_body_100_continue/5]),
URL = url(Port),
{X, Y, Z} = now(),
Body = [
"This is a rather simple post :)",
integer_to_list(X),
integer_to_list(Y),
integer_to_list(Z)
],
{ok, Client} = fusco:start(URL, []),
{ok, Response} = fusco:request(Client, <<"/post">>, "POST", [], Body, 1000),
{StatusCode, ReasonPhrase} = status(Response),
?assertEqual(<<"200">>, StatusCode),
?assertEqual(<<"OK">>, ReasonPhrase),
?assertEqual(iolist_to_binary(Body), body(Response)).
request_timeout() ->
{ok, _, _, Port} = webserver:start(gen_tcp, [fun webserver_utils:very_slow_response/5]),
URL = url(Port),
{ok, Client} = fusco:start(URL, []),
?assertEqual({error, timeout}, fusco:request(Client, <<"/slow">>, "GET", [], [], 50)).
invalid_options() ->
URL = url(5050),
?assertError({bad_option, bad_option},
fusco:start(URL, [bad_option, {foo, bar}])),
?assertError({bad_option, {foo, bar}},
fusco:start(URL, [{foo, bar}, bad_option])).
closed_after_timeout() ->
{ok, _, _, Port} = webserver:start(gen_tcp, [fun webserver_utils:no_response/5, stay_open]),
URL = url(Port),
{ok, Client} = fusco:start(URL, []),
fusco:request(Client, <<"/slow">>, "GET", [], [], 50),
fusco:disconnect(Client),
wait_for_exit(10, Client),
?assertEqual(false,erlang:is_process_alive(Client)).
wait_for_exit(0, _) ->
ok;
wait_for_exit(N, Proc) ->
timer:sleep(50),
case is_process_alive(Proc) of
false ->
ok;
true ->
wait_for_exit(N - 1, Proc)
end.
url(Port) ->
url(inet, Port).
url(inet, Port) ->
"http://localhost:" ++ integer_to_list(Port);
url(inet6, Port) ->
"http://[::1]:" ++ integer_to_list(Port).
status({Status, _, _, _, _}) ->
Status.
body({_, _, Body, _, _}) ->
Body.
headers({_, Headers, _, _, _}) ->
Headers.

View File

@ -1,102 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc
%%% @end
%%%=============================================================================
-module(fusco_tests_SUITE).
-copyright("2013, Erlang Solutions Ltd.").
-include_lib("common_test/include/ct.hrl").
-compile(export_all).
all() ->
[{group, ipv4}, {group, ipv6}, {group, ipv4ssl}, {group, ipv6ssl},
{group, independent}].
init_per_group(ipv4, Config) ->
[{fusco_parameters, {"127.0.0.1", inet, false}} | Config];
init_per_group(ipv6, Config) ->
[{fusco_parameters, {"::1", inet6, false}} | Config];
init_per_group(ipv4ssl, Config) ->
[ok = application:start(App) || App <- apps()],
[{fusco_parameters, {"127.0.0.1", inet, true}} | Config];
init_per_group(ipv6ssl, Config) ->
[ok = application:start(App) || App <- apps()],
[{fusco_parameters, {"::1", inet6, true}} | Config];
init_per_group(independent, Config) ->
[{fusco_parameters, {"127.0.0.1", inet, false}} | Config].
end_per_group(ipv4, _Config) ->
ok;
end_per_group(ipv6, _Config) ->
ok;
end_per_group(ipv4ssl, _Config) ->
[application:stop(App) || App <- lists:reverse(apps())],
ok;
end_per_group(ipv6ssl, _Config) ->
[application:stop(App) || App <- lists:reverse(apps())],
ok;
end_per_group(independent, _Config) ->
ok.
apps() ->
[asn1, crypto, public_key, ssl, fusco].
groups() ->
[{ipv4, [], all_tests()},
{ipv6, [], all_tests()},
{ipv4ssl, [], all_tests()},
{ipv6ssl, [], all_tests()},
{independent, [], independent_tests()}].
all_tests() ->
[prop_http_request, prop_persistent_connection, prop_reconnect,
prop_client_close_connection, prop_connection_refused].
independent_tests() ->
[prop_http_request_cookie_path, prop_http_request_supersede_cookie,
prop_http_request_max_age, prop_http_request_expires].
%%==============================================================================
%% Test cases
%%==============================================================================
prop_http_request(Config) ->
do_prop(prop_http_request_per_family, Config).
prop_persistent_connection(Config) ->
do_prop(prop_persistent_connection_per_family, Config).
prop_reconnect(Config) ->
do_prop(prop_reconnect_per_family, Config).
prop_client_close_connection(Config) ->
do_prop(prop_client_close_connection_per_family, Config).
prop_connection_refused(Config) ->
do_prop(prop_connection_refused_per_family, Config).
prop_http_request_cookie_path(Config) ->
do_prop(prop_http_request_cookie_path, Config).
prop_http_request_supersede_cookie(Config) ->
do_prop(prop_http_request_supersede_cookie, Config).
prop_http_request_max_age(Config) ->
do_prop(prop_http_request_max_age, Config).
prop_http_request_expires(Config) ->
do_prop(prop_http_request_expires, Config).
%%==============================================================================
%% Internal functions
%%==============================================================================
do_prop(Case, Config) ->
{Ip, Family, Ssl} = ?config(fusco_parameters, Config),
case eqc:counterexample(erlang:apply(fusco_tests_eqc, Case, [Ip, Family, Ssl])) of
true ->
true;
Value ->
exit(Value)
end.

View File

@ -1,676 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc
%%% @end
%%%=============================================================================
-module(fusco_tests_eqc).
-copyright("2013, Erlang Solutions Ltd.").
-include_lib("eqc/include/eqc.hrl").
-include("fusco.hrl").
-define(TWO_OK, <<"HTTP/1.1 200 OK\r\n\r\n">>).
-define(FOUR_BAD_REQUEST, <<"HTTP/1.1 400 Bad Request\r\n">>).
-define(TWO_OK(V), case V of
undefined ->
?TWO_OK;
_ ->
<<"HTTP/1.1 200 OK\r\nConnection: ",
V/binary,"\r\n\r\n">>
end).
-export([prop_http_request_per_family/3,
prop_persistent_connection_per_family/3,
prop_reconnect_per_family/3,
prop_client_close_connection_per_family/3,
prop_connection_refused_per_family/3,
prop_http_request_cookie_path/3,
prop_http_request_supersede_cookie/3,
prop_http_request_max_age/3,
prop_http_request_expires/3]).
%%==============================================================================
%% Quickcheck generators
%%==============================================================================
valid_http_request() ->
?LET({RequestLine, Headers},
{http_eqc_gen:request_line(), http_eqc_gen:request_headers()},
?LET(Body, http_eqc_encoding:body(any),
{RequestLine, http_eqc_encoding:add_content_length(Headers, Body),
Body})).
valid_http_response() ->
?LET({StatusLine, Headers},
{status_line(), http_eqc_gen:headers()},
?LET(Body, http_eqc_encoding:body(StatusLine),
{StatusLine, Headers, Body}
)
).
status_line() ->
%% Discard CONTINUE for cookie testing, client waits for next messages
?SUCHTHAT({_, S, _}, http_eqc_gen:status_line(),
not lists:member(S, [<<"100">>, <<"101">>])).
token() ->
non_empty(list(choose($A, $z))).
path() ->
?LET({Path, Slash}, {non_empty(list(token())), slash()},
Path ++ Slash).
domain() ->
?LET(Domain, path(), list_to_binary(string:join(Domain, "."))).
slash() ->
oneof([["/"], []]).
subpath(Path, true) ->
?LET({Length, Slash}, {choose(1, length(Path)), slash()},
begin
{H, _} = lists:split(Length, Path),
case lists:last(H) of
"/" ->
H;
_ ->
H ++ Slash
end
end);
subpath(Path, false) ->
?SUCHTHAT(SubPath, path(), hd(SubPath) =/= hd(Path)).
max_age() ->
%% Make cookie expire on nat/0 and not expire on largeint/0
%% Otherwise, in black box testing we lose the control to make cookie
%% expire on values near the current time. It needs unit testing
%% to verify the expiration is precise.
?LET({Expires, MaxAge},
oneof([{true, nat()}, {false, largeint()}]),
case MaxAge of
0 ->
{true, MaxAge};
_ ->
{Expires, abs(MaxAge)}
end).
past() ->
?SUCHTHAT(Date, http_eqc_gen:sane_cookie_date(), is_past(Date)).
future() ->
?SUCHTHAT(Date, http_eqc_gen:sane_cookie_date(), is_future(Date)).
expires() ->
oneof([{true, past()}, {false, future()}]).
set_cookie(Path) ->
{<<"Set-Cookie">>, {http_eqc_gen:cookie_pair(),
[{<<"Path">>, encode_path(Path)}]}}.
set_cookie(Path, MaxAge) when is_integer(MaxAge) ->
{<<"Set-Cookie">>, {http_eqc_gen:cookie_pair(),
[{<<"Path">>, encode_path(Path)},
{<<"Max-Age">>, integer_to_binary(MaxAge)}]}};
set_cookie(Path, Expires) ->
{<<"Set-Cookie">>,
{http_eqc_gen:cookie_pair(),
[{<<"Path">>, encode_path(Path)},
{<<"Expires">>, Expires}]}}.
set_cookie(Path, Domain, Name, Value) ->
{<<"Set-Cookie">>, {{Name, Value}, [{<<"Path">>, encode_path(Path)},
{<<"Domain">>, Domain}]}}.
cookie_path() ->
%% Cookie rejected if the value for the Path attribute
%% is not a prefix of the requested-URI
?LET({Path, IsSubPath}, {path(), bool()},
?LET(SubPath, subpath(Path, IsSubPath),
?LET(Cookie, set_cookie(SubPath),
{Cookie, encode_path(Path), IsSubPath}
)
)
).
cookie_max_age() ->
%% Path is needed in the test setup to ensure cookie is not rejected
?LET({Path, {Expires, MaxAge}}, {path(), max_age()},
?LET(Cookie, set_cookie(Path, MaxAge),
{Cookie, encode_path(Path), Expires, MaxAge}
)
).
cookie_expires() ->
%% Path is needed in the test setup to ensure cookie is not rejected
?LET({Path, {Expires, Date}}, {path(), expires()},
?LET(Cookie, set_cookie(Path, Date),
{Cookie, encode_path(Path), Expires}
)
).
maybe_different_cookie_data(true, Path, Domain, Name) ->
{Path, Domain, Name};
maybe_different_cookie_data(false, Path, Domain, Name) ->
%% Generates a combination of 1 or more mutations on Path, Domain and Name
?LET([ChangePath, ChangeDomain, ChangeName],
?SUCHTHAT(V, vector(3, bool()),
lists:any(fun(X) -> X == true end, V)),
{change_path(ChangePath, Path),
change_domain(ChangeDomain, Domain),
change_name(ChangeName, Name)}
).
change_path(false, Path) ->
Path;
change_path(true, Path) ->
?SUCHTHAT(P, path(), hd(P) =/= hd(Path)).
change_domain(false, Domain) ->
Domain;
change_domain(true, Domain) ->
?SUCHTHAT(D, domain(), D =/= Domain).
change_name(false, Name) ->
Name;
change_name(true, Name) ->
?SUCHTHAT(N, http_eqc_gen:small_valid_bin(), N =/= Name).
%%==============================================================================
%% Quickcheck properties
%%==============================================================================
prop_http_request_per_family(Host, Family, Ssl) ->
eqc:numtests(
500,
?FORALL({{Method, Uri, _Version}, Headers, Body} = Msg,
valid_http_request(),
begin
Module = select_module(Ssl),
{ok, Listener, LS, Port} =
webserver:start(Module, [validate_msg(Msg)], Family),
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
{ok, {Status, _, _, _, _}}
= fusco:request(Client, Uri, Method, Headers, Body, 10000),
ok = fusco:disconnect(Client),
webserver:stop(Module, Listener, LS),
Expected = {<<"200">>, <<"OK">>},
?WHENFAIL(io:format("Status: ~p~nExpected: ~p~n",
[Status, Expected]),
case Status of
Expected ->
true;
_ ->
false
end)
end)).
prop_persistent_connection_per_family(Host, Family, Ssl) ->
%% Fusco must keep the connection alive and be able to reconnect
%% Individual properties defined for reconnect and keep-alive
?FORALL(
Msgs,
non_empty(list({valid_http_request(), http_eqc_gen:connection_header()})),
begin
Module = select_module(Ssl),
{ok, Listener, LS, Port} =
webserver:start(Module,
[reply_msg(?TWO_OK(ConHeader)) || {_, ConHeader} <- Msgs],
Family),
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
Replies = lists:map(fun({{{Method, Uri, _Version}, Headers, Body}, _}) ->
fusco:request(Client, Uri, Method, Headers, Body, 10000)
end, Msgs),
ok = fusco:disconnect(Client),
webserver:stop(Module, Listener, LS),
?WHENFAIL(io:format("Replies: ~p~nExpected: 200 OK~n", [Replies]),
lists:all(fun({ok, {{<<"200">>, <<"OK">>}, _, _, _, _}}) ->
true;
(_) ->
false
end, Replies))
end).
prop_reconnect_per_family(Host, Family, Ssl) ->
%% Connection is always closed in the server and fusco must reconnect
eqc:numtests(
50,
?FORALL(
Msgs,
non_empty(list(valid_http_request())),
begin
Module = select_module(Ssl),
{ok, Listener, LS, Port} =
webserver:start(Module,
[reply_and_close_msg(?TWO_OK) || _ <- Msgs],
Family),
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
Replies = lists:map(fun({{Method, Uri, _Version}, Headers, Body}) ->
Hdrs = lists:keydelete(<<"Connection">>, 1, Headers),
fusco:request(Client, Uri, Method, Hdrs, Body, 10000)
end, Msgs),
ok = fusco:disconnect(Client),
webserver:stop(Module, Listener, LS),
?WHENFAIL(io:format("Replies: ~p~nExpected: 200 OK~n", [Replies]),
lists:all(fun({ok, {{<<"200">>, <<"OK">>}, _, _, _, _}}) ->
true;
(_) ->
false
end, Replies))
end)).
prop_client_close_connection_per_family(Host, Family, Ssl) ->
%% Fusco must close the connection if requested by the server
eqc:numtests(
25,
?FORALL({{{Method, Uri, _Version}, Headers, Body}, Connection},
{valid_http_request(), http_eqc_gen:connection_header()},
begin
Id = erlang:now(),
Module = select_module(Ssl),
{ok, Listener, LS, Port} =
webserver:start(Module,
[reply_msg_and_check(Id, ?TWO_OK(Connection))],
Family),
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
{ok, {Status, _, _, _, _}}
= fusco:request(Client, Uri, Method, Headers, Body, 10000),
Closed = receive
{Id, closed} ->
true
after 1000 ->
false
end,
ok = fusco:disconnect(Client),
webserver:stop(Module, Listener, LS),
Expected = {<<"200">>, <<"OK">>},
MustClose = must_close(Headers, Connection),
?WHENFAIL(io:format("Connection: ~p~nStatus: ~p~nExpected:"
" ~p~nMust close: ~p~nClosed: ~p~n",
[Connection, Status, Expected, MustClose, Closed]),
case Status of
Expected ->
MustClose == Closed;
_ ->
false
end)
end)).
prop_connection_refused_per_family(Host, Family, Ssl) ->
eqc:numtests(1,
begin
Module = select_module(Ssl),
{ok, Listener, LS, Port} =
webserver:start(Module, [reply_msg(<<>>)], Family),
webserver:stop(Module, Listener, LS),
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
Reply = fusco:connect(Client),
Expected = {error, econnrefused},
?WHENFAIL(io:format("Reply: ~p~nExpected: ~p~n",
[Reply, Expected]),
case Reply of
Expected ->
true;
_ ->
false
end)
end).
%% Cookie rejected if the value for the Path attribute
%% is not a prefix of the requested-URI
prop_http_request_cookie_path(Host, Family, Ssl) ->
?FORALL(
{Request, {Cookie, Path, IsSubPath},
{{_, Status, Reason}, _, _} = Response},
{valid_http_request(), cookie_path(), valid_http_response()},
begin
ResponseBin = build_response(Response, [Cookie]),
ValidationFun = validate_cookie(ResponseBin, fun check_cookie_deleted/3,
[not IsSubPath, Cookie]),
Responses =
send_cookie_requests(Host, Ssl, Family, ValidationFun, Path, Request,
[<<"first">>, <<"second">>], 0),
check_responses(Status, Reason, Responses)
end
).
%% Supersed old cookie if Name is the same as existing cookie,
%% and Domain and Path exactly match pre-existing ones
prop_http_request_supersede_cookie(Host, Family, Ssl) ->
?FORALL(
{Request, Path, Domain, {Name, Value}, Supersede,
{{_, Status, Reason}, _, _} = Response},
{valid_http_request(), path(), domain(), http_eqc_gen:cookie_pair(),
bool(), valid_http_response()},
%% Generate a second cookie that could supersed or not the previous one
%% Uses 'Supersede' as generation parameter
?LET(
{{SPath, SDomain, SName}, SValue},
{maybe_different_cookie_data(Supersede, Path, Domain, Name),
?SUCHTHAT(V, http_eqc_gen:small_valid_bin(), V =/= Value)},
begin
FirstCookie = set_cookie(Path, Domain, Name, Value),
SecondCookie = set_cookie(SPath, SDomain, SName, SValue),
FirstServerResponse = build_response(Response, [FirstCookie]),
SecondServerResponse = build_response(Response, [SecondCookie]),
ValidationFun = validate_cookie_supersede(
FirstServerResponse, SecondServerResponse,
Supersede, {Name, Value, encode_path(Path), Domain},
{SName, SValue, encode_path(SPath), SDomain}),
%% Three requests to supersede the value
%% First get cookie
%% Second get second cookie
%% Third checks received cookies
Responses =
send_cookie_requests(Host, Ssl, Family, ValidationFun,
encode_path(Path), Request,
[<<"first">>, <<"second">>, <<"third">>], 0),
check_responses(Status, Reason, Responses)
end
)
).
prop_http_request_max_age(Host, Family, Ssl) ->
eqc:numtests(
25,
?FORALL(
{Request, {Cookie, Path, Expires, MaxAge},
{{_, Status, Reason}, _, _} = Response},
{valid_http_request(), cookie_max_age(), valid_http_response()},
begin
ResponseBin = build_response(Response, [Cookie]),
ValidationFun = validate_cookie(ResponseBin, fun check_cookie_deleted/3,
[Expires, Cookie]),
WaitTime = expiration_time(Expires, MaxAge),
Responses =
send_cookie_requests(Host, Ssl, Family, ValidationFun, Path,
Request, [<<"first">>, <<"second">>],
WaitTime),
check_responses(Status, Reason, Responses)
end
)
).
prop_http_request_expires(Host, Family, Ssl) ->
?FORALL(
{Request, {Cookie, Path, Expires},
{{_, Status, Reason}, _, _} = Response},
{valid_http_request(), cookie_expires(), valid_http_response()},
begin
ResponseBin = build_response(Response, [Cookie]),
ValidationFun = validate_cookie(ResponseBin, fun check_cookie_deleted/3,
[Expires, Cookie]),
Responses =
send_cookie_requests(Host, Ssl, Family, ValidationFun, Path,
Request, [<<"first">>, <<"second">>], 0),
check_responses(Status, Reason, Responses)
end
).
%%==============================================================================
%% Internal functions
%%==============================================================================
validate_msg({{_Method, _Uri, _Version}, SentHeaders, SentBody}) ->
fun(Module, Socket, _Request, GotHeaders, GotBody) when SentBody == GotBody ->
case validate_headers(SentBody, GotHeaders, SentHeaders) of
true ->
Module:send(Socket, ?TWO_OK);
false ->
Module:send(Socket, ?FOUR_BAD_REQUEST)
end;
(Module, Socket, _Request, _GotHeaders, _) ->
Module:send(Socket, ?FOUR_BAD_REQUEST)
end.
validate_cookie(Response, Fun, Params) ->
fun(Module, Socket, _Request, Headers, _Body) ->
case proplists:get_value("Test", Headers) of
"first" ->
Module:send(Socket, Response);
"second" ->
case erlang:apply(Fun, [Headers | Params]) of
true ->
Module:send(Socket, ?TWO_OK);
false ->
Module:send(Socket, ?FOUR_BAD_REQUEST)
end
end
end.
validate_cookie_supersede(FirstResponse, SecondResponse, Supersede, FirstPair,
SecondPair) ->
fun(Module, Socket, _Request, Headers, _Body) ->
case proplists:get_value("Test", Headers) of
"first" ->
Module:send(Socket, FirstResponse);
"second" ->
Module:send(Socket, SecondResponse);
"third" ->
case check_cookie_supersede(Headers, Supersede, FirstPair, SecondPair) of
true ->
Module:send(Socket, ?TWO_OK);
false ->
Module:send(Socket, ?FOUR_BAD_REQUEST)
end
end
end.
build_cookie(N, V) ->
binary_to_list(<<N/binary,"=",V/binary>>).
check_cookie_deleted(Headers, true, _) ->
undefined == proplists:get_value("Cookie", Headers);
check_cookie_deleted(Headers, false, {_, {{N, V}, _}}) ->
build_cookie(N, V) == proplists:get_value("Cookie", Headers).
%% http://tools.ietf.org/search/rfc6265#section-4.1.2
check_cookie_supersede(Headers, true, {Name, _, _, _}, {_, NewValue, _, _}) ->
build_cookie(Name, NewValue) == proplists:get_value("Cookie", Headers);
check_cookie_supersede(Headers, false, {Name, Value, Path, _},
{NewName, NewValue, Path, _}) ->
lists:sort([build_cookie(Name, Value), build_cookie(NewName, NewValue)])
== lists:sort(string:tokens(proplists:get_value("Cookie", Headers), "; "));
check_cookie_supersede(Headers, false, {Name, Value, _, _}, _) ->
build_cookie(Name, Value)
== proplists:get_value("Cookie", Headers).
verify_host(GotHeaders, SentHeaders) ->
%% Host must be added by the client if it is not part of the headers list
%% http://tools.ietf.org/html/rfc2616#section-14.23
Key = "host",
case lists:keytake(Key, 1, GotHeaders) of
{value, {_, Value}, NewGotHeaders} ->
case lists:keytake(Key, 1, SentHeaders) of
%% The user sent the 'Host' header, value must match
{value, {_, Value}, NewSentHeaders} ->
{NewGotHeaders, NewSentHeaders};
false ->
{NewGotHeaders, SentHeaders};
_ ->
false
end;
false ->
false
end.
verify_content_length(Body, GotHeaders, SentHeaders) ->
%% Must be updated when the code supports transfer-encoding
%% http://tools.ietf.org/html/rfc2616#section-14.13
Key = "content-length",
ContentLength = iolist_size(Body),
{NewGotHeaders, GotContentLength}
= case lists:keytake(Key, 1, GotHeaders) of
{value, {_, Value}, H} ->
{H, list_to_integer(Value)};
false ->
{GotHeaders, 0}
end,
case ContentLength == GotContentLength of
true ->
{NewGotHeaders, lists:keydelete(Key, 1, SentHeaders)};
false ->
false
end.
validate_headers(Body, GotHeaders, SentHeaders) ->
CleanGotHeaders = lists:keysort(1, [{string:to_lower(K), V}
|| {K, V} <- GotHeaders]),
CleanSentHeaders = lists:keysort(1, [{string:to_lower(binary_to_list(K)),
binary_to_list(V)}
|| {K, V} <- SentHeaders]),
case verify_host(CleanGotHeaders, CleanSentHeaders) of
false ->
false;
{GotHeaders1, Headers1} ->
case verify_content_length(Body, GotHeaders1, Headers1) of
false ->
false;
{GotHeaders2, Headers2} ->
GotHeaders2 == Headers2
end
end.
reply_msg(Msg) ->
fun(Module, Socket, _Request, _Headers, _Body) ->
Module:send(Socket, Msg)
end.
reply_msg_and_check(Id, Msg) ->
Parent = self(),
fun(Module, Socket, _Request, _Headers, _Body) ->
Module:send(Socket, Msg),
case Module:recv(Socket, 0) of
{error, closed} ->
Parent ! {Id, closed};
_ ->
ok
end
end.
reply_and_close_msg(Msg) ->
fun(Module, Socket, _Request, _Headers, _Body) ->
Module:send(Socket, Msg),
Module:close(Socket)
end.
must_close(Headers, Connection) ->
case proplists:get_value(<<"Connection">>, Headers) of
<<"close">> ->
true;
_ ->
case Connection of
<<"close">> ->
true;
_ ->
false
end
end.
select_module(Ssl) ->
case Ssl of
true ->
ssl;
false ->
gen_tcp
end.
encode_path(Path) ->
case lists:split(length(Path) - 1, Path) of
{P, ["/"]} ->
list_to_binary(["/", string:join(P, "/"), "/"]);
_ ->
list_to_binary(["/", string:join(Path, "/")])
end.
build_response({StatusLine, Headers, Body}, Cookies) ->
http_eqc_encoding:build_valid_response(
StatusLine,
http_eqc_encoding:add_content_length(Headers, Body),
Cookies, Body).
send_cookie_requests(Host, Ssl, Family, ValidationFun, Path,
{{Method, _Uri, _Version}, Headers, Body},
RequestTags, WaitTime) ->
Module = select_module(Ssl),
{ok, Listener, LS, Port} =
webserver:start(Module, [ValidationFun], Family),
{ok, Client} = fusco:start({Host, Port, Ssl}, [{use_cookies, true}]),
%% Use header "test" to distinguish requests in the server side
Responses = lists:map(
fun(Header) ->
{ok, {Response, _, _, _, _}}
= fusco:request(Client, Path, Method,
[{<<"test">>, Header} | Headers],
Body, 10000),
timer:sleep(WaitTime),
Response
end, RequestTags),
ok = fusco:disconnect(Client),
webserver:stop(Module, Listener, LS),
Responses.
expiration_time(Expires, MaxAge) ->
case Expires of
true ->
MaxAge*1000;
false ->
0
end.
check_responses(Status, Reason, [First, Second]) ->
Expected = {Status, Reason},
?WHENFAIL(io:format("First: ~p~nExpected: ~p~nSecond: ~p~n",
[First, Expected, Second]),
{First, Second} == {Expected, {<<"200">>, <<"OK">>}}
);
check_responses(Status, Reason, [First, Second, Third]) ->
Expected = {Status, Reason},
?WHENFAIL(io:format("First: ~p~nSecond: ~p~nExpected: ~p~nThird ~p~n",
[First, Second, Expected, Third]),
{First, Second, Third}
== {Expected, Expected, {<<"200">>, <<"OK">>}}
).
diff(A, A) ->
eq;
diff(A, B) when A < B ->
lt;
diff(_, _) ->
gt.
is_past(DateTime) ->
{Date, {H, M, _}} = http_eqc_encoding:expires_datetime(DateTime),
case diff(Date, date()) of
lt ->
true;
gt ->
false;
eq ->
{HH, MM, _} = time(),
%% Set 30 min margin, safe side
case diff((HH*60+MM) - (H*60+M), 30) of
gt ->
true;
eq ->
true;
lt ->
false
end
end.
is_future(DateTime) ->
{Date, {H, M, _}} = http_eqc_encoding:expires_datetime(DateTime),
case diff(Date, date()) of
gt ->
true;
lt ->
false;
eq ->
{HH, MM, _} = time(),
%% Set 30 min margin, safe side
case diff((H*60+M) - (HH*60+MM), 30) of
gt ->
true;
eq ->
true;
lt ->
false
end
end.

View File

@ -1,153 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 1999-2014, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc
%%% @end
%%%=============================================================================
-module(http_eqc_encoding).
-copyright("2014, Erlang Solutions Ltd.").
-export([add_content_length/2,
add_transfer_encoding/2,
body/1]).
-export([build_valid_response/4,
build_cookie/1,
build_expires_date/1,
expires_datetime/1]).
%%==============================================================================
%% API
%%==============================================================================
add_content_length(Headers, <<>>) ->
Headers;
add_content_length(Headers, Body) ->
ContentLength = list_to_binary(integer_to_list(byte_size(Body))),
[{<<"Content-Length">>, ContentLength} | Headers].
body({_, <<$1, _, _>>, _}) ->
<<>>;
body({_, <<$2,$0,$4>>, _}) ->
<<>>;
body({_, <<$3,$0,$4>>, _}) ->
<<>>;
body(_) ->
http_eqc_gen:body().
add_transfer_encoding(Headers, <<>>) ->
Headers;
add_transfer_encoding(Headers, Encoding) ->
lists:keystore(<<"Transfer-Encoding">>, 1,
remove_transfer_encoding(Headers),
{<<"Transfer-Encoding">>, Encoding}).
build_valid_response({HttpVersion, StatusCode, Reason}, Headers, Cookies, Body) ->
SL = [HttpVersion, sp(), StatusCode, sp(), Reason, crlf()],
HS = [[Name, colon(), Value, crlf()] || {Name, Value} <- Headers],
CS = [[Name, colon(), build_cookie(Cookie), crlf()] || {Name, Cookie} <- Cookies],
[SL, HS ++ CS, crlf(), build_body(Body)].
build_cookie({{K, V}, Avs}) ->
CookiePair = [K, eq(), V],
CookieAvs = build_cookie_avs(Avs),
CookiePair ++ CookieAvs.
build_expires_date({rfc1123date, {Wkday, Date1, Time}}) ->
Date = build_date(Date1),
BTime = build_time(Time),
[Wkday, $,, $\s, Date, $\s, BTime, $\s, "GMT"];
build_expires_date({rfc850date, {Weekday, Date2, Time}}) ->
Date = build_date(Date2),
BTime = build_time(Time),
[Weekday, $,, $\s, Date, $\s, BTime, $\s, "GMT"];
build_expires_date({asctimedate, {Wkday, Date3, Time, Year}}) ->
BTime = build_time(Time),
Date = build_date(Date3),
[Wkday, $\s, Date, $\s, BTime, $\s, Year].
expires_datetime({rfc1123date, {_, {date1, {Day, Month, Year}}, {H, M, S}}}) ->
{{st_to_int(Year), month(Month), st_to_int(Day)},
{st_to_int(H), st_to_int(M), st_to_int(S)}};
expires_datetime({rfc850date, {_, {date2, {Day, Month, Year}}, {H, M, S}}}) ->
{{to_year(Year), month(Month), st_to_int(Day)},
{st_to_int(H), st_to_int(M), st_to_int(S)}};
expires_datetime({asctimedate, {_, {date3, {Day, Month}}, {H, M, S}, Year}}) ->
{{st_to_int(Year), month(Month), st_to_int(Day)},
{st_to_int(H), st_to_int(M), st_to_int(S)}};
expires_datetime(undefined) ->
undefined.
%%==============================================================================
%% Internal functions
%%==============================================================================
remove_transfer_encoding(Headers) ->
lists:filter(fun({H, _}) -> H =/= <<"Transfer-Encoding">> end, Headers).
build_body(Body) when is_binary(Body) ->
Body;
build_body(List) ->
list_to_binary(
io_lib:format("~s0\r\n\r\n",
[[io_lib:format("~s\r\n~s\r\n",
[erlang:integer_to_list(Nat, 16), Body])
|| {Nat, Body} <- List]])).
build_cookie_avs(Avs) ->
build_cookie_avs(Avs, []).
build_cookie_avs([{<<"Expires">> = K, Date} | Rest], Acc) ->
V = build_expires_date(Date),
build_cookie_avs(Rest, [[semicolon(), sp(), K, eq(), V] | Acc]);
build_cookie_avs([{K, V} | Rest], Acc) ->
build_cookie_avs(Rest, [[semicolon(), sp(), K, eq(), V] | Acc]);
build_cookie_avs([K | Rest], Acc) ->
build_cookie_avs(Rest, [[semicolon(), sp(), K] | Acc]);
build_cookie_avs([], Acc) ->
Acc.
build_date({date1, {Day, Month, Year}}) ->
[Day, $\s, Month, $\s, Year];
build_date({date2, {Day, Month, Year}}) ->
[Day, $-, Month, $-, Year];
build_date({date3, {Day, Month}}) ->
[Month, $\s, Day].
build_time({H, M, S}) ->
[H, $:, M, $:, S].
colon() ->
<<$:>>.
semicolon() ->
<<$;>>.
sp() ->
<<$\s>>.
crlf() ->
<<$\r,$\n>>.
eq() ->
<<$=>>.
months() ->
[{"Jan", 1}, {"Feb", 2}, {"Mar", 3}, {"Apr", 4},
{"May", 5}, {"Jun", 6}, {"Jul", 7}, {"Aug", 8},
{"Sep", 9}, {"Oct", 10}, {"Nov", 11}, {"Dec", 12}].
st_to_int(L) ->
list_to_integer(L).
month(Month) ->
proplists:get_value(Month, months()).
to_year(Year) when length(Year) == 4 ->
st_to_int(Year);
to_year(Year) ->
IntY = list_to_integer(Year),
{Y, _, _} = date(),
case (2000 + IntY - Y) > 50 of
true ->
1900 + IntY;
false ->
2000 + IntY
end.

View File

@ -1,262 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc Quickcheck generators for HTTP messages
%%% @end
%%%=============================================================================
-module(http_eqc_gen).
-copyright("2013, Erlang Solutions Ltd.").
-include_lib("eqc/include/eqc.hrl").
-compile(export_all).
%% RFC 2616
general_header() ->
[{<<"Cache-Control">>, small_valid_bin()},
{<<"Connection">>, connection_header()},
{<<"Date">>, small_valid_bin()},
{<<"Pragma">>, small_valid_bin()},
{<<"Trailer">>, small_valid_bin()},
{<<"Transfer-Encoding">>, small_valid_bin()},
{<<"Upgrade">>, small_valid_bin()},
{<<"Via">>, small_valid_bin()},
{<<"Warning">>, small_valid_bin()}].
connection_header() ->
oneof([<<"close">>, <<"keep-alive">>, small_valid_bin()]).
%% RFC 2616
entity_header() ->
[{<<"Allow">>, small_valid_bin()},
{<<"Content-Encoding">>, small_valid_bin()},
{<<"Content-Language">>, small_valid_bin()},
{<<"Content-Location">>, small_valid_bin()},
{<<"Content-MD5">>, small_valid_bin()},
{<<"Content-Range">>, small_valid_bin()},
{<<"Content-Type">>, small_valid_bin()},
{<<"Expires">>, small_valid_bin()},
{<<"Last-Modified">>, small_valid_bin()}].
%% RFC 2616
response_header() ->
[{<<"Accept-Ranges">>, small_valid_bin()},
{<<"Age">>, small_valid_bin()},
{<<"ETag">>, small_valid_bin()},
{<<"Location">>, small_valid_bin()},
{<<"Proxy-Authenticate">>, small_valid_bin()},
{<<"Retry-After">>, small_valid_bin()},
{<<"Server">>, small_valid_bin()},
{<<"Vary">>, small_valid_bin()},
{<<"WWW-Authenticate">>, small_valid_bin()}].
%% http://tools.ietf.org/html/rfc2616#section-5.3
request_header() ->
[{<<"Accept">>, small_valid_bin()},
{<<"Accept-Charset">>, small_valid_bin()},
{<<"Accept-Encoding">>, small_valid_bin()},
{<<"Accept-Language">>, small_valid_bin()},
{<<"Authorization">>, authorization()},
{<<"Expect">>, small_valid_bin()},
{<<"From">>, small_valid_bin()},
{<<"Host">>, small_valid_bin()},
{<<"If-Match">>, small_valid_bin()},
{<<"If-Modified-Since">>, small_valid_bin()},
{<<"If-None-Match">>, small_valid_bin()},
{<<"If-Range">>, small_valid_bin()},
{<<"If-Unmodified-Since">>, small_valid_bin()},
{<<"Max-Forwards">>, small_valid_bin()},
{<<"Proxy-Authorization">>, small_valid_bin()},
{<<"Range">>, small_valid_bin()},
{<<"Referer">>, small_valid_bin()},
{<<"TE">>, small_valid_bin()},
{<<"User-Agent">>, small_valid_bin()}
].
authorization() ->
?LET({User, Pass}, {small_valid_bin(), small_valid_bin()},
begin
Encoded = base64:encode(<<User/binary, $:, Pass/binary>>),
<<"Basic ", Encoded/binary>>
end).
header() ->
lists:append([general_header(), entity_header(), response_header()]).
req_headers() ->
lists:append([general_header(), entity_header(), request_header()]).
request_headers() ->
?LET(Headers, list(oneof(req_headers())), Headers).
headers() ->
?LET(Headers, list(oneof(header())), Headers).
http_version() ->
[<<"HTTP/1.0">>, <<"HTTP/1.1">>].
informational_code() ->
[{<<"100">>, <<"Continue">>},
{<<"101">>, <<"Switching protocols">>}].
success_code() ->
[{<<"200">>, <<"OK">>},
{<<"201">>, <<"Created">>},
{<<"202">>, <<"Accepted">>},
{<<"203">>, <<"Non-Authoritative Information">>},
{<<"204">>, <<"No Content">>},
{<<"205">>, <<"Reset Content">>},
{<<"206">>, <<"Partial Content">>}].
redirection_code() ->
[{<<"300">>, <<"Multiple Choices">>},
{<<"301">>, <<"Moved Permanently">>},
{<<"302">>, <<"Found">>},
{<<"303">>, <<"See Other">>},
{<<"304">>, <<"Not Modified">>},
{<<"305">>, <<"Use Proxy">>},
{<<"307">>, <<"Temporary Redirect">>}].
client_error_code() ->
[{<<"400">>, <<"Bad Request">>},
{<<"401">>, <<"Unauthorized">>},
{<<"402">>, <<"Payment Required">>},
{<<"403">>, <<"Forbidden">>},
{<<"404">>, <<"Not Found">>},
{<<"405">>, <<"Method Not Allowed">>},
{<<"406">>, <<"Not Acceptable">>},
{<<"407">>, <<"Proxy Authentication Required">>},
{<<"408">>, <<"Request Time-out">>},
{<<"409">>, <<"Conflict">>},
{<<"410">>, <<"Gone">>},
{<<"411">>, <<"Length Required">>},
{<<"412">>, <<"Precondition Failed">>},
{<<"413">>, <<"Request Entity Too Large">>},
{<<"414">>, <<"Request-URI Too Large">>},
{<<"415">>, <<"Unsupported Media Type">>},
{<<"416">>, <<"Requested range not satisfiable">>},
{<<"417">>, <<"Expectation Failed">>}].
server_error_code() ->
[{<<"500">>, <<"Internal Server Error">>},
{<<"501">>, <<"Not Implemented">>},
{<<"502">>, <<"Bad Gateway">>},
{<<"503">>, <<"Service Unavailable">>},
{<<"504">>, <<"Gateway Time-out">>},
{<<"505">>, <<"HTTP Version not supported">>}].
%% RFC 6265
set_cookie() ->
{<<"Set-Cookie">>, set_cookie_string()}.
set_cookie_string() ->
{cookie_pair(), cookie_avs()}.
cookie_pair() ->
{small_valid_bin(), small_valid_bin()}.
cookie_avs() ->
list(oneof(cookie_av())).
cookie_av() ->
[{<<"Expires">>, sane_cookie_date()},
{<<"Max-Age">>, max_age()},
{<<"Domain">>, small_valid_bin()},
{<<"Path">>, small_valid_bin()},
{<<"Secure">>, small_valid_bin()},
<<"HttpOnly">>,
small_valid_bin() %% extension
].
sane_cookie_date() ->
?LET(Date, oneof([rfc1123date(), rfc850date(), asctimedate()]), Date).
max_age() ->
?LET(Age, ?SUCHTHAT(Nat, nat(), Nat > 0), list_to_binary(integer_to_list(Age))).
rfc1123date() ->
{rfc1123date, {wkday(), date1(), timeb()}}.
rfc850date() ->
{rfc850date, {weekday(), date2(), timeb()}}.
asctimedate() ->
{asctimedate, {wkday(), date3(), timeb(), year4()}}.
date1() ->
{date1, {day(), month(), year4()}}.
date2() ->
{date2, {day(), month(), year2()}}.
date3() ->
{date3, {day(), month()}}.
timeb() ->
?LET({H, M, S}, {choose(0, 23), choose(0,59), choose(0, 59)},
{twod(H), twod(M), twod(S)}).
twod(Integer) ->
string:right(integer_to_list(Integer), 2, $0).
day() ->
?LET(Day, choose(1, 31), twod(Day)).
year4() ->
?LET(Year, choose(1983, 2083), integer_to_list(Year)).
year2() ->
?LET(Year, choose(0, 99), twod(Year)).
wkday() ->
?LET(Wkday, oneof(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]), Wkday).
weekday() ->
?LET(Weekday, oneof(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
"Saturday", "Sunday"]),
Weekday).
month() ->
?LET(Month,
oneof(["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
"Oct", "Nov", "Dec"]),
Month).
status_code() ->
lists:append([informational_code(), success_code(), redirection_code(),
client_error_code(), server_error_code()]).
status_line() ->
?LET({HttpVersion, {StatusCode, Reason}},
{oneof(http_version()), oneof(status_code())},
{HttpVersion, StatusCode, Reason}).
http_method() ->
["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"].
request_uri() ->
[<<"*">>, <<"http://www.w3.org/pub/WWW/TheProject.html">>,
<<"/pub/WWW/TheProject.html">>].
request_line() ->
?LET({Method, RequestUri},
{oneof(http_method()), oneof(request_uri())},
{Method, RequestUri, "HTTP/1.1"}).
small_valid_bin() ->
?LET(String, vector(5, choose($A, $z)),
list_to_binary(String)).
body() ->
?LET(String, list(choose($A, $z)),
list_to_binary(String)).
body(Size) ->
?LET(String, vector(Size, choose($A, $z)), String).
chunked_body() ->
?LET(PosNats, non_empty(list(?SUCHTHAT(Nat, nat(), Nat > 0))),
?LET(Bins, [body(Size) || Size <- PosNats],
lists:zip(PosNats, Bins)
)
).

View File

@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQC6YrA5HIBj817qplKlRaU3dkCnnZtKS666lRbqsdj3Fug7ezAN
mUrUZIGMTDOAwYg3E2JPAL1VOiPmi/ENlanLTyOp2SkqYLfR59Z5wr1nE/iAC+es
7WT2OPjXG7MIBvnM7FNpHY17F4MM0FWWm+LJyJRucUZHL964nw0c2xZ3fwIDAQAB
AoGAWpvnd5w3fl+t4P0CaH43F4NRYyrnd3LberFH9siG5XgpZeE5NyMykZZatE3H
K+zpv3yY6jc909Tz5vxZL3V2mR5r5O8PulteyZ8gHKVMD3c//xhPjcVMOg2j/wB1
QLDvL8qL7tDv53uYKyy3fVud9ao8xbNtoL+pzpMGE3CdlqECQQDxTyrQ0OE9+/1Q
xM0Oa7b48R3ncL/zv5sXB7Melr2j27CIxm04DXi9CnWVYMvlgWYZ5nBBMOZixTZA
YSq7tkmFAkEAxbuHNGeCSffccv5LsVMIHLRYZr/WBRqkbim8K76XaSD/ao9BR/T/
ZobOiYTD36eKw4TzKXfKkMiHcPyRGb4qMwJBAIXFmHu4QBWnmy9qWi7TYdSxfh1u
cMsEfkqPFyou8KRkxoGcVrHLLhLGOJb92Sq6yEo1aCeLnzxEDaY094amMC0CQQCo
JlVQJ0YRCQsbb02HOokHgAY9Op4SMRnr5On0eVvhNwJ9590oCBy5X6J8J786jwve
QU1X0lsczKsBVoc+UQ0pAkBO/+utKl2jt0LYHi+ZOsvLgLoYKy7y+304MsId3Hfi
oZpxA0a5ctG1tHpBAjJJfO1at4Dpy67nkQNc3CAhrsSn
-----END RSA PRIVATE KEY-----

View File

@ -1,73 +0,0 @@
%%%=============================================================================
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc
%%% @end
%%%=============================================================================
-module(test_utils).
-copyright("2013, Erlang Solutions Ltd.").
-export([start_listener/1,
start_listener/2,
send_message/1,
stop_listener/1]).
start_listener(Msg) ->
start_listener(Msg, close).
start_listener({fragmented, Msg}, ConnectionState) ->
random:seed(erlang:now()),
start_listener(Msg, fun fragmented_user_response/1, ConnectionState);
start_listener(Msg, ConnectionState) ->
start_listener(Msg, fun user_response/1, ConnectionState).
start_listener(Msg, Fun, ConnectionState) ->
Responders = case ConnectionState of
close ->
[Fun(Msg)];
keepalive ->
[Fun(Msg), user_response(message())]
end,
{ok, Listener, LS, Port} = webserver:start(gen_tcp, Responders),
{ok, Socket} = gen_tcp:connect("127.0.0.1", Port, [binary, {packet, raw},
{nodelay, true},
{reuseaddr, true},
{active, false}], 5000),
{Listener, LS, Socket}.
send_message(Socket) ->
gen_tcp:send(Socket, message()).
send_fragmented_message(Module, Socket, L) when is_list(L) ->
send_fragmented_message(Module, Socket, list_to_binary(L));
send_fragmented_message(_, _, <<>>) ->
ok;
send_fragmented_message(Module, Socket, Msg) ->
Length = erlang:byte_size(Msg),
R = random(Length),
Bin = binary:part(Msg, 0, R),
Module:send(Socket, Bin),
send_fragmented_message(Module, Socket, binary:part(Msg, R, erlang:byte_size(Msg) - R)).
random(Length) when Length =< 5 ->
random:uniform(Length);
random(_Length) ->
random:uniform(5).
user_response(Message) ->
fun(Module, Socket, _, _, _) ->
Module:send(Socket, Message)
end.
fragmented_user_response(Message) ->
fun(Module, Socket, _, _, _) ->
send_fragmented_message(Module, Socket, Message)
end.
message() ->
<<"GET /blabla HTTP/1.1\r\nhost: 127.0.0.1:5050\r\nuser-agent: Cow\r\nAccept: */*\r\n\r\n">>.
stop_listener({Listener, LS, Socket}) ->
webserver:stop(Listener, LS),
gen_tcp:close(Socket).

View File

@ -1,147 +0,0 @@
%%%-----------------------------------------------------------------------------
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
%%% @author Oscar Hellström <oscar@hellstrom.st>
%%% @author Magnus Henoch <magnus@erlang-consulting.com>
%%% @author Diana Parra Corbacho <diana.corbacho@erlang-solutions.com>
%%% @doc Simple web server for testing purposes
%%% @end
%%%-----------------------------------------------------------------------------
-module(webserver).
-copyright("2013, Erlang Solutions Ltd.").
-export([start/2, start/3, stop/2, stop/3]).
-export([acceptor/3]).
start(Module, Responders) ->
start(Module, Responders, inet).
start(Module, Responders, Family) ->
case get_addr("localhost", Family) of
{ok, Addr} ->
LS = listen(Module, Addr, Family),
Pid = spawn(?MODULE, acceptor, [Module, LS, Responders]),
{ok, Pid, LS, port(Module, LS)};
Error ->
Error
end.
stop(Listener, LS) ->
stop(gen_tcp, Listener, LS).
stop(Module, Listener, LS) ->
(catch exit(kill, Listener)),
Module:close(LS).
acceptor(Module, ListenSocket, Responders) ->
case accept(Module, ListenSocket) of
error ->
ok;
Socket ->
spawn_link(fun() ->
acceptor(Module, ListenSocket, Responders)
end),
server_loop(Module, Socket, nil, [], Responders)
end.
server_loop(Module, Socket, _, _, []) ->
Module:close(Socket);
server_loop(Module, Socket, Request, Headers, [H | T] = Responders) ->
receive
stop ->
Module:close(Socket)
after 0 ->
case Module:recv(Socket, 0, 500) of
{ok, {http_request, _, _, _} = NewRequest} ->
server_loop(Module, Socket, NewRequest, Headers, Responders);
{ok, {http_header, _, Field, _, Value}} when is_atom(Field) ->
NewHeaders = [{atom_to_list(Field), Value} | Headers],
server_loop(Module, Socket, Request, NewHeaders, Responders);
{ok, {http_header, _, Field, _, Value}} when is_list(Field) ->
NewHeaders = [{Field, Value} | Headers],
server_loop(Module, Socket, Request, NewHeaders, Responders);
{ok, http_eoh} ->
RequestBody = case proplists:get_value("Content-Length", Headers) of
undefined ->
<<>>;
"0" ->
<<>>;
SLength ->
Length = list_to_integer(SLength),
setopts(Module, Socket, [{packet, raw}]),
{ok, Body} = Module:recv(Socket, Length),
setopts(Module, Socket, [{packet, http}]),
Body
end,
H(Module, Socket, Request, Headers, RequestBody),
case proplists:get_value("Connection", Headers) of
"close" ->
Module:close(Socket);
_ ->
server_loop(Module, Socket, none, [], T)
end;
{error, timeout} ->
server_loop(Module, Socket, Request, Headers, Responders);
{error, closed} ->
Module:close(Socket)
end
end.
listen(ssl, Addr, Family) ->
Root = code:lib_dir(fusco, test),
Opts = [
Family,
{packet, http},
binary,
{active, false},
{ip, Addr},
{verify,0},
{keyfile, filename:join(Root, "key.pem")},
{certfile, filename:join(Root,"crt.pem")}
],
{ok, LS} = ssl:listen(0, Opts),
LS;
listen(Module, Addr, Family) ->
{ok, LS} = Module:listen(0, [
Family,
{packet, http},
binary,
{active, false},
{ip, Addr}
]),
LS.
get_addr(Host, Family) ->
case inet:getaddr(Host, Family) of
{ok, Addr} ->
{ok, Addr};
_ ->
{error, family_not_supported}
end.
accept(ssl, ListenSocket) ->
case ssl:transport_accept(ListenSocket, 1000000) of
{ok, Socket} ->
ok = ssl:ssl_accept(Socket),
Socket;
{error, _} ->
error
end;
accept(Module, ListenSocket) ->
case Module:accept(ListenSocket, 100000) of
{ok, Socket} ->
Socket;
{error, _} ->
error
end.
setopts(ssl, Socket, Options) ->
ssl:setopts(Socket, Options);
setopts(_, Socket, Options) ->
inet:setopts(Socket, Options).
port(ssl, Socket) ->
{ok, {_, Port}} = ssl:sockname(Socket),
Port;
port(_, Socket) ->
{ok, Port} = inet:port(Socket),
Port.

View File

@ -1,88 +0,0 @@
-module(webserver_utils).
-compile(export_all).
-define(DEFAULT_STRING, "Great success!").
default_string() ->
?DEFAULT_STRING.
empty_body(Module, Socket, _, _, _) ->
Module:send(
Socket,
"HTTP/1.1 200 OK\r\n"
"Content-type: text/plain\r\nContent-length: 0\r\n\r\n"
).
copy_body_100_continue(Module, Socket, _, _, Body) ->
Module:send(
Socket,
[
"HTTP/1.1 100 Continue\r\n\r\n"
"HTTP/1.1 200 OK\r\n"
"Content-type: text/plain\r\nContent-length: "
++ integer_to_list(size(Body)) ++ "\r\n\r\n",
Body
]
).
pre_1_1_server(Module, Socket, _, _, Body) ->
Pid = list_to_pid(binary_to_list(Body)),
Module:send(
Socket,
"HTTP/1.0 200 OK\r\n"
"Content-type: text/plain\r\nContent-length: 14\r\n\r\n"
?DEFAULT_STRING
),
% We didn't signal a connection close, but we want the client to do that
% any way since we're 1.0 now
{error, closed} = Module:recv(Socket, 0),
Pid ! closed,
Module:close(Socket).
pre_1_1_server_keep_alive(Module, Socket, _, _, _) ->
Module:send(
Socket,
"HTTP/1.0 200 OK\r\n"
"Content-type: text/plain\r\n"
"Connection: Keep-Alive\r\n"
"Content-length: 14\r\n\r\n"
?DEFAULT_STRING
).
very_slow_response(Module, Socket, _, _, _) ->
timer:sleep(1000),
Module:send(
Socket,
"HTTP/1.1 200 OK\r\n"
"Content-type: text/plain\r\nContent-length: 14\r\n\r\n"
?DEFAULT_STRING
).
no_content_length(Module, Socket, _, _, _) ->
Module:send(
Socket,
"HTTP/1.1 200 OK\r\n"
"Content-type: text/plain\r\nConnection: close\r\n\r\n"
?DEFAULT_STRING
).
no_content_length_1_0(Module, Socket, _, _, _) ->
Module:send(
Socket,
"HTTP/1.0 200 OK\r\n"
"Content-type: text/plain\r\n\r\n"
?DEFAULT_STRING
).
trailing_space_header(Module, Socket, _, _, _) ->
Module:send(
Socket,
"HTTP/1.1 200 OK\r\n"
"Content-type: text/plain\r\n"
"Content-Length: 14 \r\n\r\n"
?DEFAULT_STRING
).
no_response(_, _, _, _, _) ->
ok.