Ver código fonte

Added support for tunnelling through a proxy. Other minor changes as indicated in the README

pull/16/head
Chandrashekhar Mullaparthi 15 anos atrás
pai
commit
8a4653d4d4
11 arquivos alterados com 1325 adições e 1050 exclusões
  1. +16
    -3
      README
  2. +47
    -29
      doc/ibrowse.html
  3. +1
    -1
      ebin/ibrowse.app
  4. +246
    -234
      src/ibrowse.erl
  5. +0
    -1
      src/ibrowse_app.erl
  6. +894
    -683
      src/ibrowse_http_client.erl
  7. +0
    -2
      src/ibrowse_lb.erl
  8. +120
    -93
      src/ibrowse_lib.erl
  9. +0
    -2
      src/ibrowse_sup.erl
  10. +0
    -1
      src/ibrowse_test.erl
  11. +1
    -1
      vsn.mk

+ 16
- 3
README Ver arquivo

@ -18,12 +18,25 @@ ibrowse is available under two different licenses. LGPL and the BSD license.
Comments to : Chandrashekhar.Mullaparthi@gmail.com
Version : 1.5.6
Version : 1.5.7
Latest version : git://github.com/cmullaparthi/ibrowse.git
CONTRIBUTIONS & CHANGE HISTORY
==============================
12-05-2010 - * Added support for the CONNECT method to tunnel HTTPS through
a proxy. When a https URL is requested through a proxy,
ibrowse will automatically use the CONNECT method to first
setup a tunnel through the proxy. Once this succeeds, the
actual request is dispatched. Successfully tested with the
new SSL implementation in R13B-03
* Added SSL support for direct connections.
See ibrowse:spawn_worker_process/1 and
ibrowse:spawn_link_worker_process/1
* Added option to return raw status line and raw unparsed headers
23-04-2010 - * Fixes to URL parsing by Karol Skocik
08-11-2009 - * Added option headers_as_is
04-10-2009 - * Patch from Kostis Sagonas to cleanup some code and suppress
@ -132,7 +145,7 @@ CONTRIBUTIONS & CHANGE HISTORY
12-01-2007 - Derek Upham sent in a bug fix. The reset_state function was not
behaving correctly when the transfer encoding was not chunked.
13-11-2006 - Youn�s Hafri reported a bug where ibrowse was not returning the
13-11-2006 - Youn�s Hafri reported a bug where ibrowse was not returning the
temporary filename when the server was closing the connection
after sending the data (as in HTTP/1.0).
Released ibrowse under the BSD license
@ -151,7 +164,7 @@ CONTRIBUTIONS & CHANGE HISTORY
22-Nov-2005 - Added ability to generate requests using the Chunked
Transfer-Encoding.
08-May-2005 - Youn�s Hafri made a CRUX LINUX port of ibrowse.
08-May-2005 - Youn�s Hafri made a CRUX LINUX port of ibrowse.
http://yhafri.club.fr/crux/index.html
Here are some usage examples. Enjoy!

+ 47
- 29
doc/ibrowse.html Ver arquivo

@ -10,9 +10,9 @@
<h1>Module ibrowse</h1>
<ul class="index"><li><a href="#description">Description</a></li><li><a href="#index">Function Index</a></li><li><a href="#functions">Function Details</a></li></ul>The ibrowse application implements an HTTP 1.1 client.
<p>Copyright © 2005-2009 Chandrashekhar Mullaparthi</p>
<p>Copyright © 2005-2010 Chandrashekhar Mullaparthi</p>
<p><b>Version:</b> 1.5.2</p>
<p><b>Version:</b> 1.6.0</p>
<p><b>Behaviours:</b> <a href="gen_server.html"><tt>gen_server</tt></a>.</p>
<p><b>Authors:</b> Chandrashekhar Mullaparthi (<a href="mailto:chandrashekhar dot mullaparthi at gmail dot com"><tt>chandrashekhar dot mullaparthi at gmail dot com</tt></a>).</p>
@ -28,23 +28,23 @@ send_req/4, send_req/5, send_req/6.

<p>Here are a few sample invocations.</p>
<p><code>
<code>
ibrowse:send_req("http://intranet/messenger/", [], get).
<br><br>
ibrowse:send_req("http://www.google.com/", [], get, [],
[{proxy_user, "XXXXX"},
{proxy_password, "XXXXX"},
{proxy_host, "proxy"},
{proxy_port, 8080}], 1000).
[{proxy_user, "XXXXX"},
{proxy_password, "XXXXX"},
{proxy_host, "proxy"},
{proxy_port, 8080}], 1000).
<br><br>
ibrowse:send_req("http://www.erlang.org/download/otp_src_R10B-3.tar.gz", [], get, [],
[{proxy_user, "XXXXX"},
{proxy_password, "XXXXX"},
{proxy_host, "proxy"},
{proxy_port, 8080},
{save_response_to_file, true}], 1000).
[{proxy_user, "XXXXX"},
{proxy_password, "XXXXX"},
{proxy_host, "proxy"},
{proxy_port, 8080},
{save_response_to_file, true}], 1000).
<br><br>
ibrowse:send_req("http://www.erlang.org", [], head).
@ -58,11 +58,8 @@ send_req/4, send_req/5, send_req/6.

<br><br>
ibrowse:send_req("http://www.google.com", [], get, [],
[{stream_to, self()}]).
</code></p>
</code>
<p>A driver exists which implements URL encoding in C, but the
speed achieved using only erlang has been good enough, so the
driver isn't actually used.</p>
<h2><a name="index">Function Index</a></h2>
<table width="100%" border="1" cellspacing="0" cellpadding="2" summary="function index"><tr><td valign="top"><a href="#all_trace_off-0">all_trace_off/0</a></td><td>Turn Off ALL tracing.</td></tr>
<tr><td valign="top"><a href="#code_change-3">code_change/3</a></td><td></td></tr>
@ -93,10 +90,12 @@ send_req/4, send_req/5, send_req/6.

<tr><td valign="top"><a href="#show_dest_status-0">show_dest_status/0</a></td><td></td></tr>
<tr><td valign="top"><a href="#show_dest_status-2">show_dest_status/2</a></td><td>Shows some internal information about load balancing to a
specified Host:Port.</td></tr>
<tr><td valign="top"><a href="#spawn_link_worker_process-2">spawn_link_worker_process/2</a></td><td>Same as spawn_worker_process/2 except the the calling process
<tr><td valign="top"><a href="#spawn_link_worker_process-1">spawn_link_worker_process/1</a></td><td>Same as spawn_worker_process/1 except the the calling process
is linked to the worker process which is spawned.</td></tr>
<tr><td valign="top"><a href="#spawn_worker_process-2">spawn_worker_process/2</a></td><td>Creates a HTTP client process to the specified Host:Port which
<tr><td valign="top"><a href="#spawn_link_worker_process-2">spawn_link_worker_process/2</a></td><td></td></tr>
<tr><td valign="top"><a href="#spawn_worker_process-1">spawn_worker_process/1</a></td><td>Creates a HTTP client process to the specified Host:Port which
is not part of the load balancing pool.</td></tr>
<tr><td valign="top"><a href="#spawn_worker_process-2">spawn_worker_process/2</a></td><td></td></tr>
<tr><td valign="top"><a href="#start-0">start/0</a></td><td>Starts the ibrowse process without linking.</td></tr>
<tr><td valign="top"><a href="#start_link-0">start_link/0</a></td><td>Starts the ibrowse process linked to the calling process.</td></tr>
<tr><td valign="top"><a href="#stop-0">stop/0</a></td><td>Stop the ibrowse process.</td></tr>
@ -203,7 +202,7 @@ send_req/4, send_req/5, send_req/6.

<div class="spec">
<p><tt>send_req(Url::string(), Headers::<a href="#type-headerList">headerList()</a>, Method::<a href="#type-method">method()</a>, Body::<a href="#type-body">body()</a>, Options::<a href="#type-optionList">optionList()</a>) -&gt; <a href="#type-response">response()</a></tt>
<ul class="definitions"><li><tt><a name="type-optionList">optionList()</a> = [<a href="#type-option">option()</a>]</tt></li>
<li><tt><a name="type-option">option()</a> = {max_sessions, integer()} | {response_format, <a href="#type-response_format">response_format()</a>} | {stream_chunk_size, integer()} | {max_pipeline_size, integer()} | {trace, <a href="#type-boolean">boolean()</a>} | {is_ssl, <a href="#type-boolean">boolean()</a>} | {ssl_options, [SSLOpt]} | {pool_name, atom()} | {proxy_host, string()} | {proxy_port, integer()} | {proxy_user, string()} | {proxy_password, string()} | {use_absolute_uri, <a href="#type-boolean">boolean()</a>} | {basic_auth, {<a href="#type-username">username()</a>, <a href="#type-password">password()</a>}} | {cookie, string()} | {content_length, integer()} | {content_type, string()} | {save_response_to_file, <a href="#type-srtf">srtf()</a>} | {stream_to, <a href="#type-stream_to">stream_to()</a>} | {http_vsn, {MajorVsn, MinorVsn}} | {host_header, string()} | {inactivity_timeout, integer()} | {connect_timeout, integer()} | {socket_options, Sock_opts} | {transfer_encoding, {chunked, ChunkSize}}</tt></li>
<li><tt><a name="type-option">option()</a> = {max_sessions, integer()} | {response_format, <a href="#type-response_format">response_format()</a>} | {stream_chunk_size, integer()} | {max_pipeline_size, integer()} | {trace, <a href="#type-boolean">boolean()</a>} | {is_ssl, <a href="#type-boolean">boolean()</a>} | {ssl_options, [SSLOpt]} | {pool_name, atom()} | {proxy_host, string()} | {proxy_port, integer()} | {proxy_user, string()} | {proxy_password, string()} | {use_absolute_uri, <a href="#type-boolean">boolean()</a>} | {basic_auth, {<a href="#type-username">username()</a>, <a href="#type-password">password()</a>}} | {cookie, string()} | {content_length, integer()} | {content_type, string()} | {save_response_to_file, <a href="#type-srtf">srtf()</a>} | {stream_to, <a href="#type-stream_to">stream_to()</a>} | {http_vsn, {MajorVsn, MinorVsn}} | {host_header, string()} | {inactivity_timeout, integer()} | {connect_timeout, integer()} | {socket_options, Sock_opts} | {transfer_encoding, {chunked, ChunkSize}} | {headers_as_is, <a href="#type-boolean">boolean()</a>} | {give_raw_headers, <a href="#type-boolean">boolean()</a>}</tt></li>
<li><tt><a name="type-stream_to">stream_to()</a> = <a href="#type-process">process()</a> | {<a href="#type-process">process()</a>, once}</tt></li>
<li><tt><a name="type-process">process()</a> = pid() | atom()</tt></li>
<li><tt><a name="type-username">username()</a> = string()</tt></li>
@ -220,14 +219,14 @@ send_req/4, send_req/5, send_req/6.

For a description of SSL Options, look in the <a href="http://www.erlang.org/doc/apps/ssl/index.html">ssl</a> manpage. If the
HTTP Version to use is not specified, the default is 1.1.
<br>
<p>The <code>host_header</code> option is useful in the case where ibrowse is
<ul>
<li>The <code>host_header</code> option is useful in the case where ibrowse is
connecting to a component such as <a href="http://www.stunnel.org">stunnel</a> which then sets up a
secure connection to a webserver. In this case, the URL supplied to
ibrowse must have the stunnel host/port details, but that won't
make sense to the destination webserver. This option can then be
used to specify what should go in the <code>Host</code> header in
the request.</p>
<ul>
the request.</li>
<li>The <code>stream_to</code> option can be used to have the HTTP
response streamed to a process as messages as data arrives on the
socket. If the calling process wishes to control the rate at which
@ -272,11 +271,20 @@ send_req/4, send_req/5, send_req/6.

request to complete will be 1000 milliseconds minus the time taken
for connection setup.
</li>
</ul>
<li> The <code>socket_options</code> option can be used to set
specific options on the socket. The <code>{active, true | false | once}</code>
and <code>{packet_type, Packet_type}</code> will be filtered out by ibrowse. </li>
<li> The <code>headers_as_is</code> option is to enable the caller
to send headers exactly as specified in the request without ibrowse
adding some of its own. Required for some picky servers apparently. </li>
<li>The <code>give_raw_headers</code> option is to enable the
caller to get access to the raw status line and raw unparsed
headers. Not quite sure why someone would want this, but one of my
users asked for it, so here it is. </li>
</ul>
</p>
<h3 class="function"><a name="send_req-6">send_req/6</a></h3>
@ -340,15 +348,20 @@ send_req/4, send_req/5, send_req/6.

spawn_worker_process/2 or spawn_link_worker_process/2 is not
included.</p>
<h3 class="function"><a name="spawn_link_worker_process-2">spawn_link_worker_process/2</a></h3>
<h3 class="function"><a name="spawn_link_worker_process-1">spawn_link_worker_process/1</a></h3>
<div class="spec">
<p><tt>spawn_link_worker_process(Host, Port) -&gt; any()</tt></p>
</div><p>Same as spawn_worker_process/2 except the the calling process
<p><tt>spawn_link_worker_process(Url::string()) -&gt; {ok, pid()}</tt></p>
</div><p>Same as spawn_worker_process/1 except the the calling process
is linked to the worker process which is spawned.</p>
<h3 class="function"><a name="spawn_worker_process-2">spawn_worker_process/2</a></h3>
<h3 class="function"><a name="spawn_link_worker_process-2">spawn_link_worker_process/2</a></h3>
<div class="spec">
<p><tt>spawn_worker_process(Host::string(), Port::integer()) -&gt; {ok, pid()}</tt></p>
<p><tt>spawn_link_worker_process(Host::string(), Port::integer()) -&gt; {ok, pid()}</tt></p>
</div>
<h3 class="function"><a name="spawn_worker_process-1">spawn_worker_process/1</a></h3>
<div class="spec">
<p><tt>spawn_worker_process(Url::string()) -&gt; {ok, pid()}</tt></p>
</div><p>Creates a HTTP client process to the specified Host:Port which
is not part of the load balancing pool. This is useful in cases
where some requests to a webserver might take a long time whereas
@ -362,6 +375,11 @@ send_req/4, send_req/5, send_req/6.

pipeline size on such connections.
</p>
<h3 class="function"><a name="spawn_worker_process-2">spawn_worker_process/2</a></h3>
<div class="spec">
<p><tt>spawn_worker_process(Host::string(), Port::integer()) -&gt; {ok, pid()}</tt></p>
</div>
<h3 class="function"><a name="start-0">start/0</a></h3>
<div class="spec">
<p><tt>start() -&gt; any()</tt></p>
@ -423,6 +441,6 @@ send_req/4, send_req/5, send_req/6.

<hr>
<div class="navbar"><a name="#navbar_bottom"></a><table width="100%" border="0" cellspacing="0" cellpadding="2" summary="navigation bar"><tr><td><a href="overview-summary.html" target="overviewFrame">Overview</a></td><td><a href="http://www.erlang.org/"><img src="erlang.png" align="right" border="0" alt="erlang logo"></a></td></tr></table></div>
<p><i>Generated by EDoc, Sep 5 2009, 23:59:48.</i></p>
<p><i>Generated by EDoc, May 17 2010, 23:21:42.</i></p>
</body>
</html>

+ 1
- 1
ebin/ibrowse.app Ver arquivo

@ -1,6 +1,6 @@
{application, ibrowse,
[{description, "HTTP client application"},
{vsn, "1.5.3"},
{vsn, "1.6.0"},
{modules, [ ibrowse,
ibrowse_http_client,
ibrowse_app,

+ 246
- 234
src/ibrowse.erl Ver arquivo

@ -6,8 +6,8 @@
%%% Created : 11 Oct 2003 by Chandrashekhar Mullaparthi <chandrashekhar.mullaparthi@t-mobile.co.uk>
%%%-------------------------------------------------------------------
%% @author Chandrashekhar Mullaparthi <chandrashekhar dot mullaparthi at gmail dot com>
%% @copyright 2005-2009 Chandrashekhar Mullaparthi
%% @version 1.5.4
%% @copyright 2005-2010 Chandrashekhar Mullaparthi
%% @version 1.6.0
%% @doc The ibrowse application implements an HTTP 1.1 client. This
%% module implements the API of the HTTP client. There is one named
%% process called 'ibrowse' which assists in load balancing and maintaining configuration. There is one load balancing process per unique webserver. There is
@ -25,18 +25,18 @@
%% <br/><br/>
%%
%% ibrowse:send_req("http://www.google.com/", [], get, [],
%% [{proxy_user, "XXXXX"},
%% {proxy_password, "XXXXX"},
%% {proxy_host, "proxy"},
%% {proxy_port, 8080}], 1000).
%% [{proxy_user, "XXXXX"},
%% {proxy_password, "XXXXX"},
%% {proxy_host, "proxy"},
%% {proxy_port, 8080}], 1000).
%% <br/><br/>
%%
%%ibrowse:send_req("http://www.erlang.org/download/otp_src_R10B-3.tar.gz", [], get, [],
%% [{proxy_user, "XXXXX"},
%% {proxy_password, "XXXXX"},
%% {proxy_host, "proxy"},
%% {proxy_port, 8080},
%% {save_response_to_file, true}], 1000).
%% [{proxy_user, "XXXXX"},
%% {proxy_password, "XXXXX"},
%% {proxy_host, "proxy"},
%% {proxy_port, 8080},
%% {save_response_to_file, true}], 1000).
%% <br/><br/>
%%
%% ibrowse:send_req("http://www.erlang.org", [], head).
@ -52,13 +52,8 @@
%% [{stream_to, self()}]).
%% </code>
%%
%% <p>A driver exists which implements URL encoding in C, but the
%% speed achieved using only erlang has been good enough, so the
%% driver isn't actually used.</p>
-module(ibrowse).
-vsn('$Id: ibrowse.erl,v 1.8 2009/07/01 22:43:19 chandrusf Exp $ ').
-behaviour(gen_server).
%%--------------------------------------------------------------------
%% Include files
@ -70,48 +65,50 @@
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
terminate/2, code_change/3]).
%% API interface
-export([
rescan_config/0,
rescan_config/1,
get_config_value/1,
get_config_value/2,
spawn_worker_process/2,
spawn_link_worker_process/2,
stop_worker_process/1,
send_req/3,
send_req/4,
send_req/5,
send_req/6,
send_req_direct/4,
send_req_direct/5,
send_req_direct/6,
send_req_direct/7,
stream_next/1,
set_max_sessions/3,
set_max_pipeline_size/3,
set_dest/3,
trace_on/0,
trace_off/0,
trace_on/2,
trace_off/2,
all_trace_off/0,
show_dest_status/0,
show_dest_status/2
]).
rescan_config/0,
rescan_config/1,
get_config_value/1,
get_config_value/2,
spawn_worker_process/1,
spawn_worker_process/2,
spawn_link_worker_process/1,
spawn_link_worker_process/2,
stop_worker_process/1,
send_req/3,
send_req/4,
send_req/5,
send_req/6,
send_req_direct/4,
send_req_direct/5,
send_req_direct/6,
send_req_direct/7,
stream_next/1,
set_max_sessions/3,
set_max_pipeline_size/3,
set_dest/3,
trace_on/0,
trace_off/0,
trace_on/2,
trace_off/2,
all_trace_off/0,
show_dest_status/0,
show_dest_status/2
]).
-ifdef(debug).
-compile(export_all).
-endif.
-import(ibrowse_lib, [
parse_url/1,
get_value/3,
do_trace/2
]).
parse_url/1,
get_value/3,
do_trace/2
]).
-record(state, {trace = false}).
-include("ibrowse.hrl").
@ -173,15 +170,15 @@ send_req(Url, Headers, Method, Body) ->
%% For a description of SSL Options, look in the <a href="http://www.erlang.org/doc/apps/ssl/index.html">ssl</a> manpage. If the
%% HTTP Version to use is not specified, the default is 1.1.
%% <br/>
%% <p>The <code>host_header</code> option is useful in the case where ibrowse is
%% <ul>
%% <li>The <code>host_header</code> option is useful in the case where ibrowse is
%% connecting to a component such as <a
%% href="http://www.stunnel.org">stunnel</a> which then sets up a
%% secure connection to a webserver. In this case, the URL supplied to
%% ibrowse must have the stunnel host/port details, but that won't
%% make sense to the destination webserver. This option can then be
%% used to specify what should go in the <code>Host</code> header in
%% the request.</p>
%% <ul>
%% the request.</li>
%% <li>The <code>stream_to</code> option can be used to have the HTTP
%% response streamed to a process as messages as data arrives on the
%% socket. If the calling process wishes to control the rate at which
@ -234,6 +231,11 @@ send_req(Url, Headers, Method, Body) ->
%% <li> The <code>headers_as_is</code> option is to enable the caller
%% to send headers exactly as specified in the request without ibrowse
%% adding some of its own. Required for some picky servers apparently. </li>
%%
%% <li>The <code>give_raw_headers</code> option is to enable the
%% caller to get access to the raw status line and raw unparsed
%% headers. Not quite sure why someone would want this, but one of my
%% users asked for it, so here it is. </li>
%% </ul>
%%
%% @spec send_req(Url::string(), Headers::headerList(), Method::method(), Body::body(), Options::optionList()) -> response()
@ -263,7 +265,8 @@ send_req(Url, Headers, Method, Body) ->
%% {connect_timeout, integer()} |
%% {socket_options, Sock_opts} |
%% {transfer_encoding, {chunked, ChunkSize}} |
%% {headers_as_is, boolean()}
%% {headers_as_is, boolean()} |
%% {give_raw_headers, boolean()}
%%
%% stream_to() = process() | {process(), once}
%% process() = pid() | atom()
@ -285,48 +288,48 @@ send_req(Url, Headers, Method, Body, Options) ->
%% Timeout = integer() | infinity
send_req(Url, Headers, Method, Body, Options, Timeout) ->
case catch parse_url(Url) of
#url{host = Host,
port = Port,
protocol = Protocol} = Parsed_url ->
Lb_pid = case ets:lookup(ibrowse_lb, {Host, Port}) of
[] ->
get_lb_pid(Parsed_url);
[#lb_pid{pid = Lb_pid_1}] ->
Lb_pid_1
end,
Max_sessions = get_max_sessions(Host, Port, Options),
Max_pipeline_size = get_max_pipeline_size(Host, Port, Options),
Options_1 = merge_options(Host, Port, Options),
{SSLOptions, IsSSL} =
case (Protocol == https) orelse
get_value(is_ssl, Options_1, false) of
false -> {[], false};
true -> {get_value(ssl_options, Options_1, []), true}
end,
case ibrowse_lb:spawn_connection(Lb_pid, Parsed_url,
Max_sessions,
Max_pipeline_size,
{SSLOptions, IsSSL}) of
{ok, Conn_Pid} ->
do_send_req(Conn_Pid, Parsed_url, Headers,
Method, Body, Options_1, Timeout);
Err ->
Err
end;
Err ->
{error, {url_parsing_failed, Err}}
#url{host = Host,
port = Port,
protocol = Protocol} = Parsed_url ->
Lb_pid = case ets:lookup(ibrowse_lb, {Host, Port}) of
[] ->
get_lb_pid(Parsed_url);
[#lb_pid{pid = Lb_pid_1}] ->
Lb_pid_1
end,
Max_sessions = get_max_sessions(Host, Port, Options),
Max_pipeline_size = get_max_pipeline_size(Host, Port, Options),
Options_1 = merge_options(Host, Port, Options),
{SSLOptions, IsSSL} =
case (Protocol == https) orelse
get_value(is_ssl, Options_1, false) of
false -> {[], false};
true -> {get_value(ssl_options, Options_1, []), true}
end,
case ibrowse_lb:spawn_connection(Lb_pid, Parsed_url,
Max_sessions,
Max_pipeline_size,
{SSLOptions, IsSSL}) of
{ok, Conn_Pid} ->
do_send_req(Conn_Pid, Parsed_url, Headers,
Method, Body, Options_1, Timeout);
Err ->
Err
end;
Err ->
{error, {url_parsing_failed, Err}}
end.
merge_options(Host, Port, Options) ->
Config_options = get_config_value({options, Host, Port}, []),
lists:foldl(
fun({Key, Val}, Acc) ->
case lists:keysearch(Key, 1, Options) of
false ->
[{Key, Val} | Acc];
_ ->
Acc
end
case lists:keysearch(Key, 1, Options) of
false ->
[{Key, Val} | Acc];
_ ->
Acc
end
end, Options, Config_options).
get_lb_pid(Url) ->
@ -334,11 +337,11 @@ get_lb_pid(Url) ->
get_max_sessions(Host, Port, Options) ->
get_value(max_sessions, Options,
get_config_value({max_sessions, Host, Port}, ?DEF_MAX_SESSIONS)).
get_config_value({max_sessions, Host, Port}, ?DEF_MAX_SESSIONS)).
get_max_pipeline_size(Host, Port, Options) ->
get_value(max_pipeline_size, Options,
get_config_value({max_pipeline_size, Host, Port}, ?DEF_MAX_PIPELINE_SIZE)).
get_config_value({max_pipeline_size, Host, Port}, ?DEF_MAX_PIPELINE_SIZE)).
%% @doc Deprecated. Use set_max_sessions/3 and set_max_pipeline_size/3
%% for achieving the same effect.
@ -368,21 +371,21 @@ set_max_pipeline_size(Host, Port, Max) when is_integer(Max), Max > 0 ->
do_send_req(Conn_Pid, Parsed_url, Headers, Method, Body, Options, Timeout) ->
case catch ibrowse_http_client:send_req(Conn_Pid, Parsed_url,
Headers, Method, ensure_bin(Body),
Options, Timeout) of
{'EXIT', {timeout, _}} ->
{error, req_timedout};
{'EXIT', Reason} ->
{error, {'EXIT', Reason}};
{ok, St_code, Headers, Body} = Ret when is_binary(Body) ->
case get_value(response_format, Options, list) of
list ->
{ok, St_code, Headers, binary_to_list(Body)};
binary ->
Ret
end;
Ret ->
Ret
Headers, Method, ensure_bin(Body),
Options, Timeout) of
{'EXIT', {timeout, _}} ->
{error, req_timedout};
{'EXIT', Reason} ->
{error, {'EXIT', Reason}};
{ok, St_code, Headers, Body} = Ret when is_binary(Body) ->
case get_value(response_format, Options, list) of
list ->
{ok, St_code, Headers, binary_to_list(Body)};
binary ->
Ret
end;
Ret ->
Ret
end.
ensure_bin(L) when is_list(L) -> list_to_binary(L);
@ -403,12 +406,21 @@ ensure_bin({Fun, _} = Body) when is_function(Fun) -> Body.
%% <b>Note:</b> It is the responsibility of the calling process to control
%% pipeline size on such connections.
%%
%% @spec spawn_worker_process(Url::string()) -> {ok, pid()}
spawn_worker_process(Url) ->
ibrowse_http_client:start(Url).
%% @spec spawn_worker_process(Host::string(), Port::integer()) -> {ok, pid()}
spawn_worker_process(Host, Port) ->
ibrowse_http_client:start({Host, Port}).
%% @doc Same as spawn_worker_process/2 except the the calling process
%% @doc Same as spawn_worker_process/1 except the the calling process
%% is linked to the worker process which is spawned.
%% @spec spawn_link_worker_process(Url::string()) -> {ok, pid()}
spawn_link_worker_process(Url) ->
ibrowse_http_client:start_link(Url).
%% @spec spawn_link_worker_process(Host::string(), Port::integer()) -> {ok, pid()}
spawn_link_worker_process(Host, Port) ->
ibrowse_http_client:start_link({Host, Port}).
@ -438,17 +450,17 @@ send_req_direct(Conn_pid, Url, Headers, Method, Body, Options) ->
%% returned by spawn_worker_process/2 or spawn_link_worker_process/2
send_req_direct(Conn_pid, Url, Headers, Method, Body, Options, Timeout) ->
case catch parse_url(Url) of
#url{host = Host,
port = Port} = Parsed_url ->
Options_1 = merge_options(Host, Port, Options),
case do_send_req(Conn_pid, Parsed_url, Headers, Method, Body, Options_1, Timeout) of
{error, {'EXIT', {noproc, _}}} ->
{error, worker_is_dead};
Ret ->
Ret
end;
Err ->
{error, {url_parsing_failed, Err}}
#url{host = Host,
port = Port} = Parsed_url ->
Options_1 = merge_options(Host, Port, Options),
case do_send_req(Conn_pid, Parsed_url, Headers, Method, Body, Options_1, Timeout) of
{error, {'EXIT', {noproc, _}}} ->
{error, worker_is_dead};
Ret ->
Ret
end;
Err ->
{error, {url_parsing_failed, Err}}
end.
%% @doc Tell ibrowse to stream the next chunk of data to the
@ -457,11 +469,11 @@ send_req_direct(Conn_pid, Url, Headers, Method, Body, Options, Timeout) ->
%% @spec stream_next(Req_id :: req_id()) -> ok | {error, unknown_req_id}
stream_next(Req_id) ->
case ets:lookup(ibrowse_stream, {req_id_pid, Req_id}) of
[] ->
{error, unknown_req_id};
[{_, Pid}] ->
catch Pid ! {stream_next, Req_id},
ok
[] ->
{error, unknown_req_id};
[{_, Pid}] ->
catch Pid ! {stream_next, Req_id},
ok
end.
%% @doc Turn tracing on for the ibrowse process
@ -495,75 +507,75 @@ all_trace_off() ->
show_dest_status() ->
Dests = lists:filter(fun({lb_pid, {Host, Port}, _}) when is_list(Host),
is_integer(Port) ->
true;
(_) ->
false
end, ets:tab2list(ibrowse_lb)),
is_integer(Port) ->
true;
(_) ->
false
end, ets:tab2list(ibrowse_lb)),
All_ets = ets:all(),
io:format("~-40.40s | ~-5.5s | ~-10.10s | ~s~n",
["Server:port", "ETS", "Num conns", "LB Pid"]),
["Server:port", "ETS", "Num conns", "LB Pid"]),
io:format("~80.80.=s~n", [""]),
lists:foreach(fun({lb_pid, {Host, Port}, Lb_pid}) ->
case lists:dropwhile(
fun(Tid) ->
ets:info(Tid, owner) /= Lb_pid
end, All_ets) of
[] ->
io:format("~40.40s | ~-5.5s | ~-5.5s | ~s~n",
[Host ++ ":" ++ integer_to_list(Port),
"",
"",
io_lib:format("~p", [Lb_pid])]
);
[Tid | _] ->
catch (
begin
Size = ets:info(Tid, size),
io:format("~40.40s | ~-5.5s | ~-5.5s | ~s~n",
[Host ++ ":" ++ integer_to_list(Port),
integer_to_list(Tid),
integer_to_list(Size),
io_lib:format("~p", [Lb_pid])]
)
end
)
end
end, Dests).
case lists:dropwhile(
fun(Tid) ->
ets:info(Tid, owner) /= Lb_pid
end, All_ets) of
[] ->
io:format("~40.40s | ~-5.5s | ~-5.5s | ~s~n",
[Host ++ ":" ++ integer_to_list(Port),
"",
"",
io_lib:format("~p", [Lb_pid])]
);
[Tid | _] ->
catch (
begin
Size = ets:info(Tid, size),
io:format("~40.40s | ~-5.5s | ~-5.5s | ~s~n",
[Host ++ ":" ++ integer_to_list(Port),
integer_to_list(Tid),
integer_to_list(Size),
io_lib:format("~p", [Lb_pid])]
)
end
)
end
end, Dests).
%% @doc Shows some internal information about load balancing to a
%% specified Host:Port. Info about workers spawned using
%% spawn_worker_process/2 or spawn_link_worker_process/2 is not
%% included.
show_dest_status(Host, Port) ->
case ets:lookup(ibrowse_lb, {Host, Port}) of
[] ->
no_active_processes;
[#lb_pid{pid = Lb_pid}] ->
io:format("Load Balancer Pid : ~p~n", [Lb_pid]),
io:format("LB process msg q size : ~p~n", [(catch process_info(Lb_pid, message_queue_len))]),
case lists:dropwhile(
fun(Tid) ->
ets:info(Tid, owner) /= Lb_pid
end, ets:all()) of
[] ->
io:format("Couldn't locate ETS table for ~p~n", [Lb_pid]);
[Tid | _] ->
First = ets:first(Tid),
Last = ets:last(Tid),
Size = ets:info(Tid, size),
io:format("LB ETS table id : ~p~n", [Tid]),
io:format("Num Connections : ~p~n", [Size]),
case Size of
0 ->
ok;
_ ->
{First_p_sz, _} = First,
{Last_p_sz, _} = Last,
io:format("Smallest pipeline : ~1000.p~n", [First_p_sz]),
io:format("Largest pipeline : ~1000.p~n", [Last_p_sz])
end
end
[] ->
no_active_processes;
[#lb_pid{pid = Lb_pid}] ->
io:format("Load Balancer Pid : ~p~n", [Lb_pid]),
io:format("LB process msg q size : ~p~n", [(catch process_info(Lb_pid, message_queue_len))]),
case lists:dropwhile(
fun(Tid) ->
ets:info(Tid, owner) /= Lb_pid
end, ets:all()) of
[] ->
io:format("Couldn't locate ETS table for ~p~n", [Lb_pid]);
[Tid | _] ->
First = ets:first(Tid),
Last = ets:last(Tid),
Size = ets:info(Tid, size),
io:format("LB ETS table id : ~p~n", [Tid]),
io:format("Num Connections : ~p~n", [Size]),
case Size of
0 ->
ok;
_ ->
{First_p_sz, _} = First,
{Last_p_sz, _} = Last,
io:format("Smallest pipeline : ~1000.p~n", [First_p_sz]),
io:format("Largest pipeline : ~1000.p~n", [Last_p_sz])
end
end
end.
%% @doc Clear current configuration for ibrowse and load from the file
@ -604,40 +616,40 @@ init(_) ->
import_config() ->
case code:priv_dir(ibrowse) of
{error, _} = Err ->
Err;
PrivDir ->
Filename = filename:join(PrivDir, "ibrowse.conf"),
import_config(Filename)
{error, _} = Err ->
Err;
PrivDir ->
Filename = filename:join(PrivDir, "ibrowse.conf"),
import_config(Filename)
end.
import_config(Filename) ->
case file:consult(Filename) of
{ok, Terms} ->
ets:delete_all_objects(ibrowse_conf),
Fun = fun({dest, Host, Port, MaxSess, MaxPipe, Options})
when is_list(Host), is_integer(Port),
is_integer(MaxSess), MaxSess > 0,
is_integer(MaxPipe), MaxPipe > 0, is_list(Options) ->
I = [{{max_sessions, Host, Port}, MaxSess},
{{max_pipeline_size, Host, Port}, MaxPipe},
{{options, Host, Port}, Options}],
lists:foreach(
fun({X, Y}) ->
ets:insert(ibrowse_conf,
#ibrowse_conf{key = X,
value = Y})
end, I);
({K, V}) ->
ets:insert(ibrowse_conf,
#ibrowse_conf{key = K,
value = V});
(X) ->
io:format("Skipping unrecognised term: ~p~n", [X])
end,
lists:foreach(Fun, Terms);
Err ->
Err
{ok, Terms} ->
ets:delete_all_objects(ibrowse_conf),
Fun = fun({dest, Host, Port, MaxSess, MaxPipe, Options})
when is_list(Host), is_integer(Port),
is_integer(MaxSess), MaxSess > 0,
is_integer(MaxPipe), MaxPipe > 0, is_list(Options) ->
I = [{{max_sessions, Host, Port}, MaxSess},
{{max_pipeline_size, Host, Port}, MaxPipe},
{{options, Host, Port}, Options}],
lists:foreach(
fun({X, Y}) ->
ets:insert(ibrowse_conf,
#ibrowse_conf{key = X,
value = Y})
end, I);
({K, V}) ->
ets:insert(ibrowse_conf,
#ibrowse_conf{key = K,
value = V});
(X) ->
io:format("Skipping unrecognised term: ~p~n", [X])
end,
lists:foreach(Fun, Terms);
Err ->
Err
end.
%% @doc Internal export
@ -648,10 +660,10 @@ get_config_value(Key) ->
%% @doc Internal export
get_config_value(Key, DefVal) ->
case ets:lookup(ibrowse_conf, Key) of
[] ->
DefVal;
[#ibrowse_conf{value = V}] ->
V
[] ->
DefVal;
[#ibrowse_conf{value = V}] ->
V
end.
set_config_value(Key, Val) ->
@ -712,36 +724,36 @@ handle_info(all_trace_off, State) ->
Mspec = [{{ibrowse_conf,{trace,'$1','$2'},true},[],[{{'$1','$2'}}]}],
Trace_on_dests = ets:select(ibrowse_conf, Mspec),
Fun = fun(#lb_pid{host_port = {H, P}, pid = Pid}, _) ->
case lists:member({H, P}, Trace_on_dests) of
false ->
ok;
true ->
catch Pid ! {trace, false}
end;
(_, Acc) ->
Acc
end,
case lists:member({H, P}, Trace_on_dests) of
false ->
ok;
true ->
catch Pid ! {trace, false}
end;
(_, Acc) ->
Acc
end,
ets:foldl(Fun, undefined, ibrowse_lb),
ets:select_delete(ibrowse_conf, [{{ibrowse_conf,{trace,'$1','$2'},true},[],['true']}]),
{noreply, State};
handle_info({trace, Bool}, State) ->
put(my_trace_flag, Bool),
{noreply, State};
handle_info({trace, Bool, Host, Port}, State) ->
Fun = fun(#lb_pid{host_port = {H, P}, pid = Pid}, _)
when H == Host,
P == Port ->
catch Pid ! {trace, Bool};
(_, Acc) ->
Acc
end,
when H == Host,
P == Port ->
catch Pid ! {trace, Bool};
(_, Acc) ->
Acc
end,
ets:foldl(Fun, undefined, ibrowse_lb),
ets:insert(ibrowse_conf, #ibrowse_conf{key = {trace, Host, Port},
value = Bool}),
value = Bool}),
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.

+ 0
- 1
src/ibrowse_app.erl Ver arquivo

@ -6,7 +6,6 @@
%%% Created : 15 Oct 2003 by Chandrashekhar Mullaparthi <chandrashekhar.mullaparthi@t-mobile.co.uk>
%%%-------------------------------------------------------------------
-module(ibrowse_app).
-vsn('$Id: ibrowse_app.erl,v 1.1 2005/05/05 22:28:28 chandrusf Exp $ ').
-behaviour(application).
%%--------------------------------------------------------------------

+ 894
- 683
src/ibrowse_http_client.erl
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 0
- 2
src/ibrowse_lb.erl Ver arquivo

@ -6,8 +6,6 @@
%%% Created : 6 Mar 2008 by chandru <chandrashekhar.mullaparthi@t-mobile.co.uk>
%%%-------------------------------------------------------------------
-module(ibrowse_lb).
-vsn('$Id: ibrowse_lb.erl,v 1.2 2009/07/01 22:43:19 chandrusf Exp $ ').
-author(chandru).
-behaviour(gen_server).
%%--------------------------------------------------------------------

+ 120
- 93
src/ibrowse_lib.erl Ver arquivo

@ -5,7 +5,6 @@
%% @doc Module with a few useful functions
-module(ibrowse_lib).
-vsn('$Id: ibrowse_lib.erl,v 1.6 2008/03/27 01:35:50 chandrusf Exp $ ').
-author('chandru').
-ifdef(debug).
-compile(export_all).
@ -14,22 +13,22 @@
-include("ibrowse.hrl").
-export([
get_trace_status/2,
do_trace/2,
do_trace/3,
url_encode/1,
decode_rfc822_date/1,
status_code/1,
dec2hex/2,
drv_ue/1,
drv_ue/2,
encode_base64/1,
decode_base64/1,
get_value/2,
get_value/3,
parse_url/1,
printable_date/0
]).
get_trace_status/2,
do_trace/2,
do_trace/3,
url_encode/1,
decode_rfc822_date/1,
status_code/1,
dec2hex/2,
drv_ue/1,
drv_ue/2,
encode_base64/1,
decode_base64/1,
get_value/2,
get_value/3,
parse_url/1,
printable_date/0
]).
get_trace_status(Host, Port) ->
ibrowse:get_config_value({trace, Host, Port}, false).
@ -39,10 +38,10 @@ drv_ue(Str) ->
drv_ue(Str, Port).
drv_ue(Str, Port) ->
case erlang:port_control(Port, 1, Str) of
[] ->
Str;
Res ->
Res
[] ->
Str;
Res ->
Res
end.
%% @doc URL-encodes a string based on RFC 1738. Returns a flat list.
@ -72,10 +71,10 @@ d2h(N) -> N+$a-10.
decode_rfc822_date(String) when is_list(String) ->
case catch decode_rfc822_date_1(string:tokens(String, ", \t\r\n")) of
{'EXIT', _} ->
{error, invalid_date};
Res ->
Res
{'EXIT', _} ->
{error, invalid_date};
Res ->
Res
end.
% TODO: Have to handle the Zone
@ -86,15 +85,15 @@ decode_rfc822_date_1([Day,Month,Year, Time,_Zone]) ->
MonthI = month_int(Month),
YearI = list_to_integer(Year),
TimeTup = case string:tokens(Time, ":") of
[H,M] ->
{list_to_integer(H),
list_to_integer(M),
0};
[H,M,S] ->
{list_to_integer(H),
list_to_integer(M),
list_to_integer(S)}
end,
[H,M] ->
{list_to_integer(H),
list_to_integer(M),
0};
[H,M,S] ->
{list_to_integer(H),
list_to_integer(M),
list_to_integer(S)}
end,
{{YearI,MonthI,DayI}, TimeTup}.
month_int("Jan") -> 1;
@ -204,9 +203,9 @@ decode_base64(Bin) when is_binary(Bin) ->
list_to_binary(List).
decode_base64_1([H | T], Acc) when ((H == $\t) or
(H == 32) or
(H == $\r) or
(H == $\n)) ->
(H == 32) or
(H == $\r) or
(H == $\n)) ->
decode_base64_1(T, Acc);
decode_base64_1([$=, $=], Acc) ->
@ -254,10 +253,10 @@ b64_to_int($/) -> 63.
get_value(Tag, TVL, DefVal) ->
case lists:keysearch(Tag, 1, TVL) of
false ->
DefVal;
{value, {_, Val}} ->
Val
false ->
DefVal;
{value, {_, Val}} ->
Val
end.
get_value(Tag, TVL) ->
@ -272,13 +271,20 @@ parse_url([$:, $/, $/ | _], get_protocol, Url, []) ->
parse_url([$:, $/, $/ | T], get_protocol, Url, TmpAcc) ->
Prot = list_to_atom(lists:reverse(TmpAcc)),
parse_url(T, get_username,
Url#url{protocol = Prot},
[]);
parse_url([$/ | T], get_username, Url, TmpAcc) ->
Url#url{protocol = Prot},
[]);
parse_url([H | T], get_username, Url, TmpAcc) when H == $/;
H == $? ->
Path = case H of
$/ ->
[$/ | T];
$? ->
[$/, $? | T]
end,
%% No username/password. No port number
Url#url{host = lists:reverse(TmpAcc),
port = default_port(Url#url.protocol),
path = [$/ | T]};
port = default_port(Url#url.protocol),
path = Path};
parse_url([$: | T], get_username, Url, TmpAcc) ->
%% It is possible that no username/password has been
%% specified. But we'll continue with the assumption that there is
@ -286,77 +292,98 @@ parse_url([$: | T], get_username, Url, TmpAcc) ->
%% username/password indeed. If we encounter a '/', it was
%% actually the hostname
parse_url(T, get_password,
Url#url{username = lists:reverse(TmpAcc)},
[]);
Url#url{username = lists:reverse(TmpAcc)},
[]);
parse_url([$@ | T], get_username, Url, TmpAcc) ->
parse_url(T, get_host,
Url#url{username = lists:reverse(TmpAcc),
password = ""},
[]);
Url#url{username = lists:reverse(TmpAcc),
password = ""},
[]);
parse_url([$@ | T], get_password, Url, TmpAcc) ->
parse_url(T, get_host,
Url#url{password = lists:reverse(TmpAcc)},
[]);
parse_url([$/ | T], get_password, Url, TmpAcc) ->
Url#url{password = lists:reverse(TmpAcc)},
[]);
parse_url([H | T], get_password, Url, TmpAcc) when H == $/;
H == $? ->
%% Ok, what we thought was the username/password was the hostname
%% and portnumber
#url{username=User} = Url,
Port = list_to_integer(lists:reverse(TmpAcc)),
Path = case H of
$/ ->
[$/ | T];
$? ->
[$/, $? | T]
end,
Url#url{host = User,
port = Port,
username = undefined,
password = undefined,
path = [$/ | T]};
port = Port,
username = undefined,
password = undefined,
path = Path};
parse_url([$: | T], get_host, #url{} = Url, TmpAcc) ->
parse_url(T, get_port,
Url#url{host = lists:reverse(TmpAcc)},
[]);
parse_url([$/ | T], get_host, #url{protocol=Prot} = Url, TmpAcc) ->
Url#url{host = lists:reverse(TmpAcc)},
[]);
parse_url([H | T], get_host, #url{protocol=Prot} = Url, TmpAcc) when H == $/;
H == $? ->
Path = case H of
$/ ->
[$/ | T];
$? ->
[$/, $? | T]
end,
Url#url{host = lists:reverse(TmpAcc),
port = default_port(Prot),
path = [$/ | T]};
parse_url([$/ | T], get_port, #url{protocol=Prot} = Url, TmpAcc) ->
port = default_port(Prot),
path = Path};
parse_url([H | T], get_port, #url{protocol=Prot} = Url, TmpAcc) when H == $/;
H == $? ->
Path = case H of
$/ ->
[$/ | T];
$? ->
[$/, $? | T]
end,
Port = case TmpAcc of
[] ->
default_port(Prot);
_ ->
list_to_integer(lists:reverse(TmpAcc))
end,
Url#url{port = Port, path = [$/ | T]};
[] ->
default_port(Prot);
_ ->
list_to_integer(lists:reverse(TmpAcc))
end,
Url#url{port = Port, path = Path};
parse_url([H | T], State, Url, TmpAcc) ->
parse_url(T, State, Url, [H | TmpAcc]);
parse_url([], get_host, Url, TmpAcc) when TmpAcc /= [] ->
Url#url{host = lists:reverse(TmpAcc),
port = default_port(Url#url.protocol),
path = "/"};
port = default_port(Url#url.protocol),
path = "/"};
parse_url([], get_username, Url, TmpAcc) when TmpAcc /= [] ->
Url#url{host = lists:reverse(TmpAcc),
port = default_port(Url#url.protocol),
path = "/"};
port = default_port(Url#url.protocol),
path = "/"};
parse_url([], get_port, #url{protocol=Prot} = Url, TmpAcc) ->
Port = case TmpAcc of
[] ->
default_port(Prot);
_ ->
list_to_integer(lists:reverse(TmpAcc))
end,
[] ->
default_port(Prot);
_ ->
list_to_integer(lists:reverse(TmpAcc))
end,
Url#url{port = Port,
path = "/"};
path = "/"};
parse_url([], get_password, Url, TmpAcc) ->
%% Ok, what we thought was the username/password was the hostname
%% and portnumber
#url{username=User} = Url,
Port = case TmpAcc of
[] ->
default_port(Url#url.protocol);
_ ->
list_to_integer(lists:reverse(TmpAcc))
end,
[] ->
default_port(Url#url.protocol);
_ ->
list_to_integer(lists:reverse(TmpAcc))
end,
Url#url{host = User,
port = Port,
username = undefined,
password = undefined,
path = "/"};
port = Port,
username = undefined,
password = undefined,
path = "/"};
parse_url([], State, Url, TmpAcc) ->
{invalid_uri_2, State, Url, TmpAcc}.
@ -387,13 +414,13 @@ do_trace(Fmt, Args) ->
-ifdef(DEBUG).
do_trace(_, Fmt, Args) ->
io:format("~s -- (~s) - "++Fmt,
[printable_date(),
get(ibrowse_trace_token) | Args]).
[printable_date(),
get(ibrowse_trace_token) | Args]).
-else.
do_trace(true, Fmt, Args) ->
io:format("~s -- (~s) - "++Fmt,
[printable_date(),
get(ibrowse_trace_token) | Args]);
[printable_date(),
get(ibrowse_trace_token) | Args]);
do_trace(_, _, _) ->
ok.
-endif.

+ 0
- 2
src/ibrowse_sup.erl Ver arquivo

@ -6,8 +6,6 @@
%%% Created : 15 Oct 2003 by Chandrashekhar Mullaparthi <chandrashekhar.mullaparthi@t-mobile.co.uk>
%%%-------------------------------------------------------------------
-module(ibrowse_sup).
-vsn('$Id: ibrowse_sup.erl,v 1.1 2005/05/05 22:28:28 chandrusf Exp $ ').
-behaviour(supervisor).
%%--------------------------------------------------------------------
%% Include files

+ 0
- 1
src/ibrowse_test.erl Ver arquivo

@ -4,7 +4,6 @@
%%% Created : 14 Oct 2003 by Chandrashekhar Mullaparthi <chandrashekhar.mullaparthi@t-mobile.co.uk>
-module(ibrowse_test).
-vsn('$Id: ibrowse_test.erl,v 1.4 2009/07/01 22:43:19 chandrusf Exp $ ').
-export([
load_test/3,
send_reqs_1/3,

+ 1
- 1
vsn.mk Ver arquivo

@ -1,2 +1,2 @@
IBROWSE_VSN = 1.5.6
IBROWSE_VSN = 1.6.0

Carregando…
Cancelar
Salvar