├── .gitignore
├── .travis.yml
├── BSD_LICENSE
├── CHANGELOG
├── CONTRIBUTORS
├── LICENSE
├── Makefile
├── README.md
├── bootstrap_travis.sh
├── doc
├── edoc-info
├── erlang.png
├── ibrowse.html
├── ibrowse_app.html
├── ibrowse_http_client.html
├── ibrowse_lb.html
├── ibrowse_lib.html
├── ibrowse_socks5.html
├── ibrowse_sup.html
├── index.html
├── modules-frame.html
├── overview-summary.html
├── short-desc
└── stylesheet.css
├── erlang.mk
├── include
└── ibrowse.hrl
├── priv
└── ibrowse.conf
├── rebar.config
├── rebar.lock
├── src
├── Emakefile.src
├── ibrowse.app.src
├── ibrowse.erl
├── ibrowse_app.erl
├── ibrowse_http_client.erl
├── ibrowse_lb.erl
├── ibrowse_lib.erl
├── ibrowse_socks5.erl
└── ibrowse_sup.erl
└── test
├── Makefile
├── ibrowse_lib_tests.erl
├── ibrowse_load_test.erl
├── ibrowse_socks_server.erl
├── ibrowse_test.erl
├── ibrowse_test_server.erl
└── ibrowse_tests.erl
/.gitignore:
--------------------------------------------------------------------------------
1 | _build
2 | ebin
3 | *.beam
4 | *.sw*
5 | deps
6 | .DS_Store
7 | erl_crash.dump
8 | .eunit
9 | mime.types
10 | .rebar
11 | *.plt
12 | .rebar
13 | *~
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: erlang
2 | otp_release:
3 | - R16B
4 | - R16B03-1
5 | - 17.0
6 | - 17.1
7 | - 18.0
8 | - 18.1
9 | - 18.2.1
10 | - 19.3
11 | - 20.0
12 | before_script:
13 | - "./bootstrap_travis.sh"
14 | script: "./rebar3 eunit"
15 |
--------------------------------------------------------------------------------
/BSD_LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2005-2014, Chandrashekhar Mullaparthi
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 | * 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.
8 | * Neither the name of the T-Mobile nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9 |
10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 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.
11 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | CONTRIBUTIONS & CHANGE HISTORY
2 | ==============================
3 |
4 | 21-09-2018 - v4.4.2
5 | * Fix for #163 - default to using IPv4
6 |
7 | 23-08-2018 - v4.4.1
8 | * Fixes to TLS socket handling (PR#163)
9 | * Fix ipv6 address family handling (PR#155)
10 | * Don't send messages to closed/inactive connections (PR#152)
11 |
12 | 28-01-2017 - v4.4
13 | * Fixes to SOCKS over SSL processing
14 | * Added stream_full_chunks option
15 | * Merged pull requests 145, 151
16 |
17 | 07-06-2016 - v4.3
18 | * Adopted erlang.mk for compiling. I find it easier to understand
19 | how 'make' behaves compared to rebar. This repo can still be built
20 | using rebar for those who prefer it
21 | * Removed references to lager. Introduced configurable logging function
22 | * Fixed an issue where the calling process was getting an extra
23 | spurious timeout message when the request was timing out
24 |
25 | 19-04-2016 - v4.2.4
26 | * Fixed travis-ci build as it was failing in running tests.
27 | No code changes to ibrowse
28 |
29 | 19-04-2016 - v4.2.3
30 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/143
31 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/142
32 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/139
33 | * Fixed behaviour of option preserve_status_line
34 |
35 | 25-11-2015 - v4.2.2
36 | * Fix to ibrowse.app.src to enable publishing using Hex
37 |
38 | 25-11-2015 - v4.2.1
39 | * Merged pull request https://github.com/cmullaparthi/ibrowse/pull/132
40 | * Merged pull request https://github.com/cmullaparthi/ibrowse/pull/137
41 |
42 | 28-09-2015 - v4.2
43 | * Merged long pending improvements to pipelining
44 | https://github.com/cmullaparthi/ibrowse/pull/123
45 | * Merged pull request https://github.com/cmullaparthi/ibrowse/pull/131
46 |
47 | 03-08-2015 - v4.1.2
48 | * R18 compatibility fix
49 | https://github.com/cmullaparthi/ibrowse/issues/129
50 | * Add max_attempts option
51 | https://github.com/cmullaparthi/ibrowse/pull/125
52 | * Fix for https://github.com/cmullaparthi/ibrowse/pull/120
53 | * Enhanced SOCKS5 support
54 | https://github.com/cmullaparthi/ibrowse/pull/117
55 |
56 | 10-07-2014 - v4.1.1
57 | * Added support for accepting binaries as header names
58 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/110
59 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/111
60 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/112
61 |
62 | 18-04-2013 - v4.1.0
63 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/101
64 | * Support for https://github.com/cmullaparthi/ibrowse/issues/90
65 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/86
66 | * Merged various contributions. Please see commit history for details
67 | * Introduced the return_raw_request option
68 |
69 | 09-04-2013 - v4.0.2
70 | * Tagging master with new version to cover changes
71 | contributed over the past few months via various pull requests
72 |
73 | 07-08-2012 - v4.0.1
74 | * Fix issue 67 properly.
75 |
76 | 03-08-2012 - v4.0.0
77 | * Fixed a regression in handling HEAD.
78 | https://github.com/cmullaparthi/ibrowse/issues/67
79 |
80 | * Fixed a bug in handling SSL requests through a proxy
81 |
82 | 06-04-2012 - v3.0.4
83 | * Fix for the following issue
84 | https://github.com/cmullaparthi/ibrowse/issues/67
85 |
86 | 13-03-2012 - v3.0.3
87 | * Fixes the following issues
88 | https://github.com/cmullaparthi/ibrowse/issues/64
89 | https://github.com/cmullaparthi/ibrowse/issues/63
90 | https://github.com/cmullaparthi/ibrowse/issues/62
91 |
92 | 31-01-2012 - v3.0.2
93 | * Fixed bug when stopping ibrowse. Not service affecting.
94 |
95 | 23-01-2012 - v3.0.1
96 | * Fixed bug highlighted by Dialyzer
97 |
98 | 23-01-2012 - v3.0.0
99 | * Change to the way pipelining works.
100 | * Fixed various issues reported
101 |
102 | 13-04-2011 - v2.2.0
103 | * Filipe David Manana added IPv6 support. This is a major new
104 | feature, Thank you Filipe!
105 | * Joseph Wayne Norton contributed tweaks to .gitignore
106 |
107 | 09-02-2011 - v2.1.4
108 | * Fixed a bug reported by Ryan Zezeski with the
109 | save_response_to_file option.
110 | https://github.com/cmullaparthi/ibrowse/issues#issue/33
111 |
112 | 16-01-2011 - v2.1.3
113 | * Fixed issues with streaming and chunked responses when using
114 | the 'caller controls socket' feature. See following links for
115 | details. Contributed by Filipe David Manana.
116 | https://github.com/cmullaparthi/ibrowse/pull/24
117 | https://github.com/cmullaparthi/ibrowse/pull/25
118 | https://github.com/cmullaparthi/ibrowse/pull/27
119 | https://github.com/cmullaparthi/ibrowse/pull/28
120 | https://github.com/cmullaparthi/ibrowse/pull/29
121 |
122 | * Fix for issue 32 reported by fholzhauser
123 | https://github.com/cmullaparthi/ibrowse/issues#issue/32
124 |
125 | * Fixed some dialyzer warnings. Thanks to Kostis for reporting
126 | them.
127 |
128 | 20-12-2010 - v2.1.2
129 | * Pipelining wasn't working when used in conjunction with the
130 | {stream_to, {self(), once}} option. Bug report by
131 | Filipe David Manana.
132 |
133 | 10-12-2010 - v2.1.1
134 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/issue/20
135 | by Filipe David Manana
136 |
137 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/issue/21
138 | by Filipe David Manana
139 |
140 | * Fix for https://github.com/cmullaparthi/ibrowse/issues/issue/23
141 | by Filipe David Manana
142 |
143 | * Fix for bugs when using SSL by Jo?o Lopes
144 |
145 | 25-10-2010 - v2.1.0
146 | * Fixed build on OpenSolaris. Bug report and patch from
147 | tholschuh.
148 | http://github.com/cmullaparthi/ibrowse/issues/issue/10
149 |
150 | * Fixed behaviour of inactivity_timeout option. Reported by
151 | Jo?o Lopes.
152 | http://github.com/cmullaparthi/ibrowse/issues/issue/11
153 |
154 | * Prevent atom table pollution when bogus URLs are input to
155 | ibrowse. Bug report by Jo?o Lopes.
156 | http://github.com/cmullaparthi/ibrowse/issues/issue/13
157 |
158 | * Automatically do Chunked-Transfer encoding of request body
159 | when the body is generated by a fun. Patch provided by
160 | Filipe David Manana.
161 | http://github.com/cmullaparthi/ibrowse/issues/issue/14
162 |
163 | * Depending on input options, ibrowse sometimes included multiple
164 | Content-Length headers. Bug reported by Paul J. Davis
165 | http://github.com/cmullaparthi/ibrowse/issues/issue/15
166 |
167 | * Deal with webservers which do not provide a Reason-Phrase on the
168 | response Status-Line. Patch provided by Jeroen Koops.
169 | http://github.com/cmullaparthi/ibrowse/issues/issue/16
170 |
171 | * Fixed http://github.com/cmullaparthi/ibrowse/issues/issue/17
172 | This was reported by Filipe David Manana.
173 |
174 | * Fixed http://github.com/cmullaparthi/ibrowse/issues/issue/19
175 | This was reported by Dan Kelley and Filipe David Manana.
176 |
177 | * Added ibrowse:stream_close/1 to close the connection
178 | associated with a certain response stream. Patch provided by
179 | Jo?o Lopes.
180 |
181 | * Prevent port number being included in the Host header when port
182 | 443 is intended. Bug reported by Andrew Tunnell-Jones
183 |
184 | 24-09-2010 - v2.0.1
185 | * Removed a spurious io:format statement
186 |
187 | 22-09-2010 - v2.0.0.
188 |
189 | * Added option preserve_chunked_encoding. This allows the
190 | caller to get the raw HTTP response when the
191 | Transfer-Encoding is Chunked. This feature was requested
192 | by Benoit Chesneau who wanted to write a HTTP proxy using
193 | ibrowse.
194 |
195 | * Fixed bug with the {stream_to, {Pid, once}} option. Bug
196 | report and lot of help from Filipe David Manana. Thank
197 | you Filipe.
198 |
199 | * The {error, conn_failed} and {error, send_failed} return
200 | values are now of the form {error, {conn_failed, Err}}
201 | and {error, {send_failed, Err}}. This is so that the
202 | specific socket error can be returned to the caller. I
203 | think it looks a bit ugly, but that is the best
204 | compromise I could come up with.
205 |
206 | * Added application configuration parameters
207 | default_max_sessions and default_max_pipeline_size. These
208 | were previously hard coded to 10.
209 |
210 | * Versioning of ibrowse now follows the Semantic Versioning
211 | principles. See http://semver.org. Thanks to Anthony
212 | Molinaro for nudging me in this direction.
213 |
214 | * The connect_timeout option now only applies to the
215 | connection setup phase. In previous versions, the time
216 | taken to setup the connection was deducted from the
217 | specified timeout value for the request.
218 |
219 | 17-07-2010 - * Merged change made by Filipe David Manana to use the base64
220 | module for encoding/decoding.
221 |
222 | 11-06-2010 - * Removed use of deprecated concat_binary. Patch supplied by
223 | Steve Vinoski
224 |
225 | 10-06-2010 - * Fixed bug in https requests not going via the proxy
226 |
227 | 12-05-2010 - * Added support for the CONNECT method to tunnel HTTPS through
228 | a proxy. When a https URL is requested through a proxy,
229 | ibrowse will automatically use the CONNECT method to first
230 | setup a tunnel through the proxy. Once this succeeds, the
231 | actual request is dispatched. Successfully tested with the
232 | new SSL implementation in R13B-03
233 | * Added SSL support for direct connections.
234 | See ibrowse:spawn_worker_process/1 and
235 | ibrowse:spawn_link_worker_process/1
236 | * Added option to return raw status line and raw unparsed headers
237 |
238 | 23-04-2010 - * Fixes to URL parsing by Karol Skocik
239 |
240 | 08-11-2009 - * Added option headers_as_is
241 |
242 | 04-10-2009 - * Patch from Kostis Sagonas to cleanup some code and suppress
243 | dialyzer warnings
244 |
245 | 24-09-2009 - * When a filename was supplied with the 'save_response_to_file'
246 | option, the option was being ignored. Bug report from
247 | Adam Kocoloski
248 |
249 | 05-09-2009 - * Introduced option to allow caller to set socket options.
250 |
251 | 29-07-2009 - * The ETS table created for load balancing of requests was not
252 | being deleted which led to the node not being able to create
253 | any more ETS tables if queries were made to many number of
254 | webservers. ibrowse now deletes the ETS table it creates once the
255 | last connection to a webserver is dropped.
256 | Reported by Seth Falcon.
257 | * Spurious data being returned at end of body in certain cases of
258 | chunked encoded responses from the server.
259 | Reported by Chris Newcombe.
260 |
261 | 03-07-2009 - Added option {stream_to, {Pid, once}} which allows the caller
262 | to control when it wants to receive more data. If this option
263 | is used, the call ibrowse:stream_next(Req_id) should be used
264 | to get more data.
265 | * Patch submitted by Steve Vinoski to remove compiler warnings
266 | about the use of obsolete guards
267 |
268 | 29-06-2009 - * Fixed following issues reported by Oscar Hellstr?m
269 | * Use {active, once} instead of {active, true}
270 | * Fix 'dodgy' timeout handling
271 | * Use binaries internally instead of lists to reduce memory
272 | consumption on 64 bit platforms. The default response format
273 | is still 'list' to maintain backwards compatibility. Use the
274 | option {response_format, binary} to get responses as binaries.
275 | * Fixed chunking bug (reported by Adam Kocoloski)
276 | * Added new option {inactivity_timeout, Milliseconds} to timeout
277 | requests if no data is received on the link for the specified
278 | interval. Useful when responses are large and links are flaky.
279 | * Added ibrowse:all_trace_off/0 to turn off all tracing
280 | * Change to the way responses to asynchronous requests are
281 | returned. The following messages have been removed.
282 | * {ibrowse_async_response, Req_id, {chunk_start, Chunk_size}}
283 | * {ibrowse_async_response, Req_id, chunk_end}
284 | * Fixed Makefiles as part of Debian packaging
285 | (thanks to Thomas Lindgren)
286 | * Moved repository from Sourceforge to Github
287 |
288 | 11-06-2009 - * Added option to control size of streamed chunks. Also added
289 | option for the client to receive responses in binary format.
290 |
291 | 21-05-2008 - * Fixed bug in reading some options from the ibrowse.conf file.
292 | Reported by Erik Reitsma on the erlyaws mailing list
293 | * Fixed bug when cleaning up closing connections
294 |
295 | 27-03-2008 - * Major rewrite of the load balancing feature. Additional module,
296 | ibrowse_lb.erl, introduced to achieve this.
297 | * Can now get a handle to a connection process which is not part of
298 | the load balancing pool. Useful when an application is making
299 | requests to a webserver which are time consuming (such as
300 | uploading a large file). Such requests can be put on a separate
301 | connection, and all other smaller/quicker requests can use the
302 | load balancing pool. See ibrowse:spawn_worker_process/2 and
303 | ibrowse:spawn_link_worker_process/2
304 | * Ram Krishnan sent a patch to enable a client to send a lot of
305 | data in a request by providing a fun which is invoked by the
306 | connection handling process. This fun can fetch the data from
307 | any where. This is useful when trying to upload a large file
308 | to a webserver.
309 | * Use the TCP_NODELAY option on every socket by default
310 | * Rudimentary support for load testing of ibrowse. Undocumented,
311 | but see ibrowse_test:load_test/3. Use the source, Luke!
312 | * New function ibrowse:show_dest_status/2 to view state of
313 | connections/pipelines to a web server
314 |
315 | 20-02-2008 - Ram Krishnan sent another patch for another hidden bug in the
316 | save_response_to_file feature.
317 |
318 | 07-02-2008 - Ram Krishnan (kriyative _at_ gmail dot com) sent a simple patch to
319 | enable specifying the filename in the save_response_to_file option.
320 | When testing the patch, I realised that my original implementation
321 | of this feature was quite flaky and a lot of corner cases were
322 | not covered. Fixed all of them. Thanks Ram!
323 |
324 | 17-10-2007 - Matthew Reilly (matthew dot reilly _at_ sipphone dot com)
325 | sent a bug report and a fix. If the chunk trailer spans two TCP
326 | packets, then ibrowse fails to recognise that the chunked transfer
327 | has ended.
328 |
329 | 29-08-2007 - Bug report by Peter Kristensen(ptx _at_ daimi dot au dot dk).
330 | ibrowse crashes when the webserver returns just the Status line
331 | and nothing else.
332 |
333 | 28-06-2007 - Added host_header option to enable connection to secure sites
334 | via stunnel
335 |
336 | 20-04-2007 - Geoff Cant sent a patch to remove URL encoding for digits in
337 | ibrowse_lib:url_encode/1.
338 | ibrowse had a dependency on the inets application because the
339 | ibrowse_http_client.erl invoked httpd_util:encode_base64/1. This
340 | dependency is now removed and the encode_base64/1 has been
341 | implemented in ibrowse_lib.erl
342 |
343 | 06-03-2007 - Eric Merritt sent a patch to support WebDAV requests.
344 |
345 | 12-01-2007 - Derek Upham sent in a bug fix. The reset_state function was not
346 | behaving correctly when the transfer encoding was not chunked.
347 |
348 | 13-11-2006 - Youn?s Hafri reported a bug where ibrowse was not returning the
349 | temporary filename when the server was closing the connection
350 | after sending the data (as in HTTP/1.0).
351 | Released ibrowse under the BSD license
352 |
353 | 12-10-2006 - Chris Newcombe reported bug in dealing with requests where no
354 | body is expected in the response. The first request would succeed
355 | and the next request would hang.
356 |
357 | 24-May-2006 - Sean Hinde reported a bug. Async responses with pipelining was
358 | returning the wrong result.
359 |
360 | 08-Dec-2005 - Richard Cameron (camster@citeulike.org). Patch to ibrowse to
361 | prevent port number being included in the Host header when port
362 | 80 is intended.
363 |
364 | 22-Nov-2005 - Added ability to generate requests using the Chunked
365 | Transfer-Encoding.
366 |
367 | 08-May-2005 - Youn?s Hafri made a CRUX LINUX port of ibrowse.
368 | http://yhafri.club.fr/crux/index.html
369 |
--------------------------------------------------------------------------------
/CONTRIBUTORS:
--------------------------------------------------------------------------------
1 | CONTRIBUTORS
2 | ============
3 | The following people have helped maked ibrowse better by reporting bugs,
4 | supplying patches and also asking for new features. Please write to me if you
5 | have contributed and I've missed you out.
6 |
7 | In alphabetical order:
8 |
9 | Adam Kocoloski
10 | Andrew Tunnell-Jones
11 | Anthony Molinaro
12 | Benjamin P Lee (https://github.com/benjaminplee)
13 | Benoit Chesneau (https://github.com/benoitc)
14 | Brian Richards (http://github.com/richbria)
15 | Chris Newcombe
16 | Dan Kelley
17 | Dan Schwabe (https://github.com/dfschwabe)
18 | Dave Cottlehuber
19 | Derek Upham
20 | Eric Merritt
21 | Erik Reitsma
22 | Fabian MuscarielloG
23 | Filipe David Manana
24 | Geoff Cant
25 | Jeroen Koops
26 | Jo?o Lopes
27 | Joseph Wayne Norton
28 | Karol Skocik
29 | Konstantin Nikiforov
30 | Kostis Sagonas
31 | Marcelo Gornstein (https://github.com/marcelog)
32 | Matthew Reilly
33 | Michael Terry
34 | Oscar Hellstr?m
35 | Paul J. Davis
36 | Peter Kristensen
37 | Ram Krishnan
38 | Richard Cameron
39 | Robert Newson (https://github.com/rnewson)
40 | Ryan Flynn
41 | Ryan Zezeski
42 | Sean Hinde
43 | Serge Polkovnikov (https://github.com/serge2)
44 | Sergey Samokhi
45 | Seth Falcon
46 | Steve Vinoski
47 | Thomas Lindgren
48 | Vincent Ambo
49 | Youn?s Hafri
50 | Yury Gargay (https://github.com/surik)
51 | fholzhauser (https://github.com/fholzhauser/)
52 | getong (https://github.com/getong)
53 | lissana (https://github.com/lissana)
54 | hyperthunk (https://github.com/hyperthunk/)
55 | Mistagrooves (https://github.com/Mistagrooves/)
56 | tholschuh (https://github.com/tholschuh/)
57 | https://github.com/apauley
58 | https://github.com/AeroNotix
59 | https://github.com/dis
60 | https://github.com/f355
61 | https://github.com/flycodepl
62 | https://github.com/helllamer
63 | https://github.com/marutha
64 | https://github.com/nrdufour
65 | https://github.com/pib
66 | https://github.com/puzza007
67 | https://github.com/rflynn
68 | https://github.com/Vagabond
69 | https://github.com/divolgin
70 | https://github.com/vans163
71 | https://github.com/shakugan
72 | https://github.com/p2k
73 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ibrowse - a HTTP client written in erlang
2 | Copyright (C) 2005-2014 Chandrashekhar Mullaparthi
3 |
4 | This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.
5 |
6 | This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
7 |
8 | You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
9 |
10 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PROJECT=ibrowse
2 | PLT_APPS=erts kernel stdlib ssl crypto public_key
3 | TEST_ERLC_OPTS=-pa ../ibrowse/ebin
4 |
5 | include erlang.mk
6 |
7 | test: app eunit unit_tests old_tests
8 | @echo "====================================================="
9 |
10 | unit_tests:
11 | @echo "====================================================="
12 | @echo "Running tests..."
13 | @cd test && make test && cd ..
14 |
15 | old_tests:
16 | @echo "====================================================="
17 | @echo "Running old tests..."
18 | @cd test && make old_tests && cd ..
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ibrowse [](http://travis-ci.org/cmullaparthi/ibrowse)
2 |
3 | ibrowse is a HTTP client written in erlang.
4 |
5 | **License:** ibrowse is available under two different licenses.
6 | LGPL or the BSD license.
7 |
8 | **Comments to:** chandrashekhar.mullaparthi@gmail.com
9 |
10 | **Current Version:** 4.4.2
11 |
12 | **Latest Version:** git://github.com/cmullaparthi/ibrowse.git
13 |
14 |
15 |
16 | ## Features
17 |
18 | * [RFC2616](http://www.ietf.org/rfc/rfc2616.txt) compliant (AFAIK)
19 | * supports GET, POST, OPTIONS, HEAD, PUT, DELETE, TRACE,
20 | MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, MOVE and COPY
21 | * Understands HTTP/0.9, HTTP/1.0 and HTTP/1.1
22 | * Understands chunked encoding
23 | * Can generate requests using [Chunked Transfer-Encoding](http://en.wikipedia.org/wiki/Chunked_transfer_encoding)
24 | * Pools of connections to each webserver
25 | * Pipelining support
26 | * Download to file
27 | * Asynchronous requests. Responses are streamed to a process
28 | * Basic authentication
29 | * Supports proxy authentication
30 | * Supports SOCKS5
31 | * Authentication methods 0 (No authentication) and 2(Username/password) supported
32 | * Can talk to secure webservers using SSL
33 | * *Any other features in the code not listed here :)*
34 |
35 |
36 |
37 | ## Usage Examples
38 |
39 | Remember to start ibrowse first:
40 |
41 | ```erlang
42 | 5> ibrowse:start().
43 | {ok,<0.94.0>}
44 | ```
45 |
46 |
47 |
48 | ### Synchronous Requests
49 |
50 | A simple `GET` request:
51 |
52 | ```erlang
53 | 6> ibrowse:send_req("http://intranet/messenger/", [], get).
54 | {ok,"200",
55 | [{"Server","Microsoft-IIS/5.0"},
56 | {"Content-Location","http://intranet/messenger/index.html"},
57 | {"Date","Fri, 17 Dec 2004 15:16:19 GMT"},
58 | {"Content-Type","text/html"},
59 | {"Accept-Ranges","bytes"},
60 | {"Last-Modified","Fri, 17 Dec 2004 08:38:21 GMT"},
61 | {"Etag","\"aa7c9dc313e4c41:d77\""},
62 | {"Content-Length","953"}],
63 | "..."}
64 | ```
65 |
66 |
67 | A `GET` using a proxy:
68 |
69 | ```erlang
70 | 7> ibrowse:send_req("http://www.google.com/", [], get, [],
71 | [{proxy_user, "XXXXX"},
72 | {proxy_password, "XXXXX"},
73 | {proxy_host, "proxy"},
74 | {proxy_port, 8080}], 1000).
75 | {ok,"302",
76 | [{"Date","Fri, 17 Dec 2004 15:22:56 GMT"},
77 | {"Content-Length","217"},
78 | {"Content-Type","text/html"},
79 | {"Set-Cookie",
80 | "PREF=ID=f58155c797f9..."},
81 | {"Server","GWS/2.1"},
82 | {"Location",
83 | "http://www.google.co.uk/cxfer?c=PREF%3D:TM%3D110329..."},
84 | {"Via","1.1 netapp01 (NetCache NetApp/5.5R2)"}],
85 | "...\r\n"}
86 | ```
87 |
88 |
89 | A `GET` response saved to file. A temporary file is created and the
90 | filename returned. The response will only be saved to file if the
91 | status code is in the `200` range. The directory to download to can
92 | be set using the application env var `download_dir` - the default
93 | is the current working directory:
94 |
95 | ```erlang
96 | 8> ibrowse:send_req("http://www.erlang.se/", [], get, [],
97 | [{proxy_user, "XXXXX"},
98 | {proxy_password, "XXXXX"},
99 | {proxy_host, "proxy"},
100 | {proxy_port, 8080},
101 | {save_response_to_file, true}], 1000).
102 | {error,req_timedout}
103 |
104 | 9> ibrowse:send_req("http://www.erlang.se/", [], get, [],
105 | [{proxy_user, "XXXXX"},
106 | {proxy_password, "XXXXX"},
107 | {proxy_host, "proxy"},
108 | {proxy_port, 8080},
109 | {save_response_to_file, true}], 5000).
110 | {ok,"200",
111 | [{"Transfer-Encoding","chunked"},
112 | {"Date","Fri, 17 Dec 2004 15:24:36 GMT"},
113 | {"Content-Type","text/html"},
114 | {"Server","Apache/1.3.9 (Unix)"},
115 | {"Via","1.1 netapp01 (NetCache NetApp/5.5R2)"}],
116 | {file,"/Users/chandru/code/ibrowse/src/ibrowse_tmp_file_1103297041125854"}}
117 | ```
118 |
119 |
120 | Setting the size of the connection pool and pipeline. This sets the
121 | number of maximum connections to the specified server to `10` and the pipeline
122 | size to `1`. Connections are assumed to be already setup.
123 |
124 | ```erlang
125 | 11> ibrowse:set_dest("www.hotmail.com", 80, [{max_sessions, 10},
126 | {max_pipeline_size, 1}]).
127 | ok
128 | ```
129 |
130 |
131 | Example using the `HEAD` method:
132 |
133 | ```erlang
134 | 56> ibrowse:send_req("http://www.erlang.org", [], head).
135 | {ok,"200",
136 | [{"Date","Mon, 28 Feb 2005 04:40:53 GMT"},
137 | {"Server","Apache/1.3.9 (Unix)"},
138 | {"Last-Modified","Thu, 10 Feb 2005 09:31:23 GMT"},
139 | {"Etag","\"8d71d-1efa-420b29eb\""},
140 | {"Accept-ranges","bytes"},
141 | {"Content-Length","7930"},
142 | {"Content-Type","text/html"}],
143 | []}
144 | ```
145 |
146 |
147 | Example using the `OPTIONS` method:
148 |
149 | ```erlang
150 | 62> ibrowse:send_req("http://www.sun.com", [], options).
151 | {ok,"200",
152 | [{"Server","Sun Java System Web Server 6.1"},
153 | {"Date","Mon, 28 Feb 2005 04:44:39 GMT"},
154 | {"Content-Length","0"},
155 | {"P3p",
156 | "policyref=\"http://www.sun.com/p3p/Sun_P3P_Policy.xml\", CP=\"CAO DSP COR CUR ADMa DEVa TAIa PSAa PSDa CONi TELi OUR SAMi PUBi IND PHY ONL PUR COM NAV INT DEM CNT STA POL PRE GOV\""},
157 | {"Set-Cookie",
158 | "SUN_ID=X.X.X.X:169191109565879; EXPIRES=Wednesday, 31-Dec-2025 23:59:59 GMT; DOMAIN=.sun.com; PATH=/"},
159 | {"Allow",
160 | "HEAD, GET, PUT, POST, DELETE, TRACE, OPTIONS, MOVE, INDEX, MKDIR, RMDIR"}],
161 | []}
162 | ```
163 |
164 |
165 |
166 | ### Asynchronous Requests
167 |
168 | Example of an asynchronous `GET` request:
169 |
170 | ```erlang
171 | 18> ibrowse:send_req("http://www.google.com", [], get, [],
172 | [{proxy_user, "XXXXX"},
173 | {proxy_password, "XXXXX"},
174 | {proxy_host, "proxy"},
175 | {proxy_port, 8080},
176 | {stream_to, self()}]).
177 | {ibrowse_req_id,{1115,327256,389608}}
178 |
179 | 19> flush().
180 | Shell got {ibrowse_async_headers,{1115,327256,389608},
181 | "302",
182 | [{"Date","Thu, 05 May 2005 21:06:41 GMT"},
183 | {"Content-Length","217"},
184 | {"Content-Type","text/html"},
185 | {"Set-Cookie",
186 | "PREF=ID=b601f16bfa32f071:CR=1:TM=1115327201:LM=1115327201:S=OX5hSB525AMjUUu7; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com"},
187 | {"Server","GWS/2.1"},
188 | {"Location",
189 | "http://www.google.co.uk/cxfer?c=PREF%3D:TM%3D1115327201:S%3DDS9pDJ4IHcAuZ_AS&prev=/"},
190 | {"Via",
191 | "1.1 hatproxy01 (NetCache NetApp/5.6.2)"}]}
192 | Shell got {ibrowse_async_response,{1115,327256,389608},
193 | "...\r\n"}
194 | Shell got {ibrowse_async_response_end,{1115,327256,389608}}
195 | ok
196 | ```
197 |
198 |
199 | Another asynchronous `GET` request:
200 |
201 | ```erlang
202 | 24> ibrowse:send_req("http://yaws.hyber.org/simple_ex2.yaws", [], get, [],
203 | [{proxy_user, "XXXXX"},
204 | {proxy_password, "XXXXX"},
205 | {proxy_host, "proxy"},
206 | {proxy_port, 8080},
207 | {stream_to, self()}]).
208 | {ibrowse_req_id,{1115,327430,512314}}
209 |
210 | 25> flush().
211 | Shell got {ibrowse_async_headers,{1115,327430,512314},
212 | "200",
213 | [{"Date","Thu, 05 May 2005 20:58:08 GMT"},
214 | {"Content-Length","64"},
215 | {"Content-Type","text/html;charset="},
216 | {"Server",
217 | "Yaws/1.54 Yet Another Web Server"},
218 | {"Via",
219 | "1.1 hatproxy01 (NetCache NetApp/5.6.2)"}]}
220 | Shell got {ibrowse_async_response,{1115,327430,512314},
221 | "...\n"}
222 | Shell got {ibrowse_async_response_end,{1115,327430,512314}}
223 | ```
224 |
225 |
226 | Example of request which fails when using the async option. Here
227 | the `{ibrowse_req_id, ReqId}` is not returned. Instead the error code is
228 | returned.
229 |
230 | ```erlang
231 | 68> ibrowse:send_req("http://www.earlyriser.org", [], get, [], [{stream_to, self()}]).
232 | {error,conn_failed}
233 | ```
234 |
235 |
236 |
237 | ### Other Examples
238 |
239 | Example of request using both Proxy-Authorization and authorization
240 | by the final webserver:
241 |
242 | ```erlang
243 | 17> ibrowse:send_req("http://www.erlang.se/lic_area/protected/patches/erl_756_otp_beam.README",
244 | [], get, [],
245 | [{proxy_user, "XXXXX"},
246 | {proxy_password, "XXXXX"},
247 | {proxy_host, "proxy"},
248 | {proxy_port, 8080},
249 | {basic_auth, {"XXXXX", "XXXXXX"}}]).
250 | {ok,"200",
251 | [{"Accept-Ranges","bytes"},
252 | {"Date","Thu, 05 May 2005 21:02:09 GMT"},
253 | {"Content-Length","2088"},
254 | {"Content-Type","text/plain"},
255 | {"Server","Apache/1.3.9 (Unix)"},
256 | {"Last-Modified","Tue, 03 May 2005 15:08:18 GMT"},
257 | {"ETag","\"1384c8-828-427793e2\""},
258 | {"Via","1.1 hatproxy01 (NetCache NetApp/5.6.2)"}],
259 | "Patch Id:\t\terl_756_otp_beam\n..."}
260 | ```
261 |
262 |
263 | Example of a `TRACE` request. Very interesting! yaws.hyber.org didn't
264 | support this. Nor did www.google.com. But good old BBC supports this:
265 |
266 | ```erlang
267 | 37> ibrowse:send_req("http://www.bbc.co.uk/", [], trace, [],
268 | [{proxy_user, "XXXXX"},
269 | {proxy_password, "XXXXX"},
270 | {proxy_host, "proxy"},
271 | {proxy_port, 8080}]).
272 | {ok,"200",
273 | [{"Transfer-Encoding","chunked"},
274 | {"Date","Thu, 05 May 2005 21:40:27 GMT"},
275 | {"Content-Type","message/http"},
276 | {"Server","Apache/2.0.51 (Unix)"},
277 | {"Set-Cookie",
278 | "BBC-UID=7452e72a..."},
279 | {"Set-Cookie",
280 | "BBC-UID=7452e72a..."},
281 | {"Via","1.1 hatproxy01 (NetCache NetApp/5.6.2)"}],
282 | "TRACE / HTTP/1.1\r\nHost: www.bbc.co.uk\r\nConnection: keep-alive\r\nX-Forwarded-For: 172.24.28.29\r\nVia: 1.1 hatproxy01 (NetCache NetApp/5.6.2)\r\nCookie: BBC-UID=7452e...\r\n\r\n"}
283 | ```
284 |
285 | A `GET` using a socks5:
286 |
287 | ```erlang
288 | ibrowse:send_req("http://google.com", [], get, [],
289 | [{socks5_host, "127.0.0.1"},
290 | {socks5_port, 5335}]).
291 |
292 | ibrowse:send_req("http://google.com", [], get, [],
293 | [{socks5_host, "127.0.0.1"},
294 | {socks5_port, 5335},
295 | {socks5_user, "user4321"},
296 | {socks5_password, "pass7654"}]).
297 | ```
298 |
--------------------------------------------------------------------------------
/bootstrap_travis.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | curl -O -L https://s3.amazonaws.com/rebar3/rebar3
4 | chmod +x rebar3
5 | ./rebar3 update
--------------------------------------------------------------------------------
/doc/edoc-info:
--------------------------------------------------------------------------------
1 | %% encoding: UTF-8
2 | {application,ibrowse}.
3 | {modules,[ibrowse,ibrowse_app,ibrowse_http_client,ibrowse_lb,ibrowse_lib,
4 | ibrowse_socks5,ibrowse_sup]}.
5 |
--------------------------------------------------------------------------------
/doc/erlang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cmullaparthi/ibrowse/22d6fd6baa6e83633aa2923f41589945c1d2dc2f/doc/erlang.png
--------------------------------------------------------------------------------
/doc/ibrowse.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Module ibrowse
6 |
7 |
8 |
9 |
10 |
11 |
12 | Module ibrowse
13 | The ibrowse application implements an HTTP 1.1 client in erlang.
14 | Copyright © 2005-2014 Chandrashekhar Mullaparthi
15 |
16 | Behaviours: gen_server .
17 | Authors: Chandrashekhar Mullaparthi (chandrashekhar dot mullaparthi at gmail dot com ).
18 |
19 | The ibrowse application implements an HTTP 1.1 client in erlang. This
20 | module implements the API of the HTTP client. There is one named
21 | process called 'ibrowse' which assists in load balancing and maintaining configuration. There is one load balancing process per unique webserver. There is
22 | one process to handle one TCP connection to a webserver
23 | (implemented in the module ibrowse_http_client). Multiple connections to a
24 | webserver are setup based on the settings for each webserver. The
25 | ibrowse process also determines which connection to pipeline a
26 | certain request on. The functions to call are send_req/3,
27 | send_req/4, send_req/5, send_req/6.
28 |
29 | Here are a few sample invocations.
30 |
31 |
32 | ibrowse:send_req("http://intranet/messenger/", [], get).
33 |
34 |
35 | ibrowse:send_req("http://www.google.com/", [], get, [],
36 | [{proxy_user, "XXXXX"},
37 | {proxy_password, "XXXXX"},
38 | {proxy_host, "proxy"},
39 | {proxy_port, 8080}], 1000).
40 |
41 |
42 | ibrowse:send_req("http://www.erlang.org/download/otp_src_R10B-3.tar.gz", [], get, [],
43 | [{proxy_user, "XXXXX"},
44 | {proxy_password, "XXXXX"},
45 | {proxy_host, "proxy"},
46 | {proxy_port, 8080},
47 | {save_response_to_file, true}], 1000).
48 |
49 |
50 | ibrowse:send_req("http://www.erlang.org", [], head).
51 |
52 |
53 | ibrowse:send_req("http://www.sun.com", [], options).
54 |
55 |
56 | ibrowse:send_req("http://www.bbc.co.uk", [], trace).
57 |
58 |
59 | ibrowse:send_req("http://www.google.com", [], get, [],
60 | [{stream_to, self()}]).
61 |
62 |
63 |
64 |
121 |
122 |
123 |
124 |
125 |
126 |
add_config(Terms) -> any()
127 |
Add additional configuration elements at runtime.
128 |
129 |
130 |
131 |
all_trace_off() -> ok
132 |
Turn Off ALL tracing
133 |
134 |
135 |
136 |
code_change(OldVsn, State, Extra) -> any()
137 |
138 |
139 |
140 |
141 |
get_config_value(Key) -> any()
142 |
Internal export
143 |
144 |
145 |
146 |
get_config_value(Key, DefVal) -> any()
147 |
Internal export
148 |
149 |
150 |
151 |
get_metrics() -> any()
152 |
153 |
154 |
155 |
156 |
get_metrics(Host, Port) -> any()
157 |
158 |
159 |
160 |
161 |
handle_call(Request, From, State) -> any()
162 |
163 |
164 |
165 |
166 |
handle_cast(Msg, State) -> any()
167 |
168 |
169 |
170 |
171 |
handle_info(Info, State) -> any()
172 |
173 |
174 |
175 |
176 |
init(X1) -> any()
177 |
178 |
179 |
180 |
181 |
rescan_config() -> any()
182 |
Clear current configuration for ibrowse and load from the file
183 | ibrowse.conf in the IBROWSE_EBIN/../priv directory. Current
184 | configuration is cleared only if the ibrowse.conf file is readable
185 | using file:consult/1
186 |
187 |
188 |
189 |
rescan_config(Terms) -> any()
190 |
191 |
192 |
193 | This is the basic function to send a HTTP request.
197 | The Status return value indicates the HTTP status code returned by the webserver
198 |
199 |
200 | Same as send_req/3.
204 | If a list is specified for the body it has to be a flat list. The body can also be a fun/0 or a fun/1.
205 | If fun/0, the connection handling process will repeatdely call the fun until it returns an error or eof.
Fun() = {ok, Data} | eof
206 | If fun/1, the connection handling process will repeatedly call the fun with the supplied state until it returns an error or eof. Fun(State) = {ok, Data} | {ok, Data, NewState} | eof
207 |
208 |
209 |
210 |
send_req(Url::string(), Headers::headerList() , Method::method() , Body::body() , Options::optionList() ) -> response()
211 |
optionList() = [option() ]option() = {max_sessions, integer()} | {response_format, response_format() } | {stream_full_chunks, boolean()} | {stream_chunk_size, integer()} | {max_pipeline_size, integer()} | {trace, boolean()} | {is_ssl, boolean()} | {ssl_options, [SSLOpt]} | {pool_name, atom()} | {proxy_host, string()} | {proxy_port, integer()} | {proxy_user, string()} | {proxy_password, string()} | {use_absolute_uri, boolean()} | {basic_auth, {username() , password() }} | {cookie, string()} | {content_length, integer()} | {content_type, string()} | {save_response_to_file, srtf() } | {stream_to, stream_to() } | {http_vsn, {MajorVsn, MinorVsn}} | {host_header, string()} | {inactivity_timeout, integer()} | {connect_timeout, integer()} | {socket_options, Sock_opts} | {transfer_encoding, {chunked, ChunkSize}} | {headers_as_is, boolean()} | {give_raw_headers, boolean()} | {preserve_chunked_encoding, boolean()} | {workaround, head_response_with_body} | {worker_process_options, list()} | {return_raw_request, true} | {max_attempts, integer()}stream_to() = process() | {process() , once}process() = pid() | atom()username() = string()password() = string()SSLOpt = term() Sock_opts = [Sock_opt] Sock_opt = term() ChunkSize = integer() srtf() = boolean() | filename() | {append, filename() }filename() = string()response_format() = list | binary
212 |
Same as send_req/4.
213 |
214 |
215 | Same as send_req/5.
219 | All timeout values are in milliseconds.
220 |
221 |
222 |
223 |
send_req_direct(Conn_pid, Url, Headers, Method) -> any()
224 |
Same as send_req/3 except that the first argument is the PID
225 | returned by spawn_worker_process/2 or spawn_link_worker_process/2
226 |
227 |
228 |
229 |
send_req_direct(Conn_pid, Url, Headers, Method, Body) -> any()
230 |
Same as send_req/4 except that the first argument is the PID
231 | returned by spawn_worker_process/2 or spawn_link_worker_process/2
232 |
233 |
234 |
235 |
send_req_direct(Conn_pid, Url, Headers, Method, Body, Options) -> any()
236 |
Same as send_req/5 except that the first argument is the PID
237 | returned by spawn_worker_process/2 or spawn_link_worker_process/2
238 |
239 |
240 |
241 |
send_req_direct(Conn_pid, Url, Headers, Method, Body, Options, Timeout) -> any()
242 |
Same as send_req/6 except that the first argument is the PID
243 | returned by spawn_worker_process/2 or spawn_link_worker_process/2
244 |
245 |
246 |
247 |
set_dest(Host, Port, T) -> any()
248 |
Deprecated. Use set_max_sessions/3 and set_max_pipeline_size/3
249 | for achieving the same effect.
250 |
251 |
252 |
253 |
set_max_attempts(Host::string(), Port::integer(), Max::integer()) -> ok
254 |
Set the maximum attempts for each connection to a specific Host:Port.
255 |
256 |
257 |
258 |
set_max_pipeline_size(Host::string(), Port::integer(), Max::integer()) -> ok
259 |
Set the maximum pipeline size for each connection to a specific Host:Port.
260 |
261 |
262 |
263 |
set_max_sessions(Host::string(), Port::integer(), Max::integer()) -> ok
264 |
Set the maximum number of connections allowed to a specific Host:Port.
265 |
266 |
267 |
268 |
show_dest_status() -> any()
269 |
Shows some internal information about load balancing. Info
270 | about workers spawned using spawn_worker_process/2 or
271 | spawn_link_worker_process/2 is not included.
272 |
273 |
274 |
275 |
show_dest_status(Url) -> any()
276 |
277 |
278 |
279 |
280 |
show_dest_status(Host, Port) -> any()
281 |
Shows some internal information about load balancing to a
282 | specified Host:Port. Info about workers spawned using
283 | spawn_worker_process/2 or spawn_link_worker_process/2 is not
284 | included.
285 |
286 |
287 |
288 |
spawn_link_worker_process(Url::string() | {Host::string(), Port::integer()}) -> {ok, pid()}
289 |
Same as spawn_worker_process/1 except the the calling process
290 | is linked to the worker process which is spawned.
291 |
292 |
293 |
294 |
spawn_link_worker_process(Host::string(), Port::integer()) -> {ok, pid()}
295 |
Same as spawn_link_worker_process/1 except with Erlang process options.
296 |
297 |
298 |
299 |
spawn_worker_process(Url::string() | {Host::string(), Port::integer()}) -> {ok, pid()}
300 |
Creates a HTTP client process to the specified Host:Port which
301 | is not part of the load balancing pool. This is useful in cases
302 | where some requests to a webserver might take a long time whereas
303 | some might take a very short time. To avoid getting these quick
304 | requests stuck in the pipeline behind time consuming requests, use
305 | this function to get a handle to a connection process.
306 | Note: Calling this function only creates a worker process. No connection
307 | is setup. The connection attempt is made only when the first
308 | request is sent via any of the send_req_direct/4,5,6,7 functions.
309 | Note: It is the responsibility of the calling process to control
310 | pipeline size on such connections.
311 |
312 |
313 |
314 |
spawn_worker_process(Host::string(), Port::integer()) -> {ok, pid()}
315 |
Same as spawn_worker_process/1 except with Erlang process options.
316 |
317 |
318 |
319 |
start() -> any()
320 |
Starts the ibrowse process without linking. Useful when testing using the shell
321 |
322 |
323 |
324 |
start_link() -> {ok, pid()}
325 |
Starts the ibrowse process linked to the calling process. Usually invoked by the supervisor ibrowse_sup
326 |
327 |
328 |
329 |
stop() -> any()
330 |
Stop the ibrowse process. Useful when testing using the shell.
331 |
332 |
333 |
334 |
stop_worker_process(Conn_pid::pid()) -> ok
335 |
Terminate a worker process spawned using
336 | spawn_worker_process/2 or spawn_link_worker_process/2. Requests in
337 | progress will get the error response
{error, closing_on_request}
338 |
339 |
340 |
341 |
stream_close(Req_id::req_id() ) -> ok | {error, unknown_req_id}
342 |
Tell ibrowse to close the connection associated with the
343 | specified stream. Should be used in conjunction with the
344 | stream_to
option. Note that all requests in progress on
345 | the connection which is serving this Req_id will be aborted, and an
346 | error returned.
347 |
348 |
349 |
350 |
stream_next(Req_id::req_id() ) -> ok | {error, unknown_req_id}
351 |
Tell ibrowse to stream the next chunk of data to the
352 | caller. Should be used in conjunction with the
353 | stream_to
option
354 |
355 |
356 |
357 |
terminate(Reason, State) -> any()
358 |
359 |
360 |
361 |
362 |
trace_off() -> any()
363 |
Turn tracing off for the ibrowse process
364 |
365 |
366 |
367 |
trace_off(Host, Port) -> ok
368 |
Turn tracing OFF for all connections to the specified HTTP
369 | server.
370 |
371 |
372 |
373 |
trace_on() -> any()
374 |
Turn tracing on for the ibrowse process
375 |
376 |
377 |
378 |
trace_on(Host, Port) -> ok
379 |
Host = string() Port = integer()
380 |
Turn tracing on for all connections to the specified HTTP
381 | server. Host is whatever is specified as the domain name in the URL
382 |
383 |
384 |
385 | Generated by EDoc, Nov 6 2015, 11:40:24.
386 |
387 |
388 |
--------------------------------------------------------------------------------
/doc/ibrowse_app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Module ibrowse_app
6 |
7 |
8 |
9 |
10 |
11 |
12 | Module ibrowse_app
13 |
14 |
15 | Behaviours: application .
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
start(Type, StartArgs) -> any()
27 |
28 |
29 |
30 |
31 |
stop(State) -> any()
32 |
33 |
34 |
35 |
36 | Generated by EDoc, Nov 6 2015, 11:40:24.
37 |
38 |
39 |
--------------------------------------------------------------------------------
/doc/ibrowse_http_client.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Module ibrowse_http_client
6 |
7 |
8 |
9 |
10 |
11 |
12 | Module ibrowse_http_client
13 |
14 |
15 | Behaviours: gen_server .
16 |
17 |
18 |
31 |
32 |
33 |
34 |
35 |
36 |
code_change(OldVsn, State, Extra) -> any()
37 |
38 |
39 |
40 |
41 |
handle_call(Request, From, State) -> any()
42 |
43 |
44 |
45 |
46 |
handle_cast(Msg, State) -> any()
47 |
48 |
49 |
50 |
51 |
handle_info(Info, State) -> any()
52 |
53 |
54 |
55 |
56 |
init(Url) -> any()
57 |
58 |
59 |
60 |
61 |
send_req(Conn_Pid, Url, Headers, Method, Body, Options, Timeout) -> any()
62 |
63 |
64 |
65 |
66 |
start(Args) -> any()
67 |
68 |
69 |
70 |
71 |
start(Args, Options) -> any()
72 |
73 |
74 |
75 |
76 |
start_link(Args) -> any()
77 |
78 |
79 |
80 |
81 |
start_link(Args, Options) -> any()
82 |
83 |
84 |
85 |
86 |
stop(Conn_pid) -> any()
87 |
88 |
89 |
90 |
91 |
terminate(Reason, State) -> any()
92 |
93 |
94 |
95 |
96 | Generated by EDoc, Nov 6 2015, 11:40:24.
97 |
98 |
99 |
--------------------------------------------------------------------------------
/doc/ibrowse_lb.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Module ibrowse_lb
6 |
7 |
8 |
9 |
10 |
11 |
12 | Module ibrowse_lb
13 |
14 |
15 | Behaviours: gen_server .
16 |
17 |
18 |
28 |
29 |
30 |
31 |
32 |
33 |
code_change(OldVsn, State, Extra) -> any()
34 |
35 |
36 |
37 |
38 |
handle_call(Request, From, State) -> any()
39 |
40 |
41 |
42 |
43 |
handle_cast(Msg, State) -> any()
44 |
45 |
46 |
47 |
48 |
handle_info(Info, State) -> any()
49 |
50 |
51 |
52 |
53 |
init(X1) -> any()
54 |
55 |
56 |
57 |
58 |
spawn_connection(Lb_pid, Url, Max_sessions, Max_pipeline_size, SSL_options, Process_options) -> any()
59 |
60 |
61 |
62 |
63 |
start_link(Args) -> any()
64 |
65 |
66 |
67 |
68 |
stop(Lb_pid) -> any()
69 |
70 |
71 |
72 |
73 |
terminate(Reason, State) -> any()
74 |
75 |
76 |
77 |
78 | Generated by EDoc, Nov 6 2015, 11:40:24.
79 |
80 |
81 |
--------------------------------------------------------------------------------
/doc/ibrowse_lib.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Module ibrowse_lib
6 |
7 |
8 |
9 |
10 |
11 |
12 | Module ibrowse_lib
13 | Module with a few useful functions.
14 |
15 |
16 | Module with a few useful functions
17 |
18 |
33 |
34 |
35 |
36 |
37 |
38 |
decode_base64(List::In) -> Out | exit ({error, invalid_input})
39 |
In = string() | binary() Out = string() | binary()
40 |
Implements the base64 decoding algorithm. The output data type matches in the input data type.
41 |
42 |
43 |
44 |
decode_rfc822_date(String) -> any()
45 |
46 |
47 |
48 |
49 |
do_trace(Fmt, Args) -> any()
50 |
51 |
52 |
53 |
54 |
do_trace(X1, Fmt, Args) -> any()
55 |
56 |
57 |
58 |
59 |
do_trace(X1, Fmt, Args) -> any()
60 |
61 |
62 |
63 |
64 |
encode_base64(List::In) -> Out
65 |
In = string() | binary() Out = string() | binary()
66 |
Implements the base64 encoding algorithm. The output data type matches in the input data type.
67 |
68 |
69 |
70 |
get_trace_status(Host, Port) -> any()
71 |
72 |
73 |
74 |
75 |
get_value(Tag, TVL) -> any()
76 |
77 |
78 |
79 |
80 |
get_value(Tag, TVL, DefVal) -> any()
81 |
82 |
83 |
84 |
85 |
parse_url(Url) -> any()
86 |
87 |
88 |
89 |
90 |
printable_date() -> any()
91 |
92 |
93 |
94 |
95 |
printable_date(Now) -> any()
96 |
97 |
98 |
99 |
100 |
status_code(StatusCode::status_code() ) -> StatusDescription
101 |
102 |
Given a status code, returns an atom describing the status code.
103 |
104 |
105 |
106 |
url_encode(Str) -> UrlEncodedStr
107 |
Str = string() UrlEncodedStr = string()
108 |
URL-encodes a string based on RFC 1738. Returns a flat list.
109 |
110 |
111 |
112 | Generated by EDoc, Nov 6 2015, 11:40:24.
113 |
114 |
115 |
--------------------------------------------------------------------------------
/doc/ibrowse_socks5.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Module ibrowse_socks5
6 |
7 |
8 |
9 |
10 |
11 |
12 | Module ibrowse_socks5
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
connect(Host, Port, Options, SockOptions, Timeout) -> any()
25 |
26 |
27 |
28 |
29 | Generated by EDoc, Nov 6 2015, 11:40:24.
30 |
31 |
32 |
--------------------------------------------------------------------------------
/doc/ibrowse_sup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Module ibrowse_sup
6 |
7 |
8 |
9 |
10 |
11 |
12 | Module ibrowse_sup
13 |
14 |
15 | Behaviours: supervisor .
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
init(X1) -> any()
27 |
28 |
29 |
30 |
31 |
start_link() -> any()
32 |
33 |
34 |
35 |
36 | Generated by EDoc, Nov 6 2015, 11:40:24.
37 |
38 |
39 |
--------------------------------------------------------------------------------
/doc/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The ibrowse application
5 |
6 |
7 |
8 |
9 |
10 |
11 | This page uses frames
12 | Your browser does not accept frames.
13 | You should go to the non-frame version instead.
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/doc/modules-frame.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The ibrowse application
5 |
6 |
7 |
8 | Modules
9 |
17 |
18 |
--------------------------------------------------------------------------------
/doc/overview-summary.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | The ibrowse application
6 |
7 |
8 |
9 |
10 | The ibrowse application
11 |
12 |
13 |
14 | Generated by EDoc, Nov 6 2015, 11:40:24.
15 |
16 |
17 |
--------------------------------------------------------------------------------
/doc/short-desc:
--------------------------------------------------------------------------------
1 | A powerful HTTP/1.1 client written in erlang
2 |
--------------------------------------------------------------------------------
/doc/stylesheet.css:
--------------------------------------------------------------------------------
1 | /* standard EDoc style sheet */
2 | body {
3 | font-family: Verdana, Arial, Helvetica, sans-serif;
4 | margin-left: .25in;
5 | margin-right: .2in;
6 | margin-top: 0.2in;
7 | margin-bottom: 0.2in;
8 | color: #000000;
9 | background-color: #ffffff;
10 | }
11 | h1,h2 {
12 | margin-left: -0.2in;
13 | }
14 | div.navbar {
15 | background-color: #add8e6;
16 | padding: 0.2em;
17 | }
18 | h2.indextitle {
19 | padding: 0.4em;
20 | background-color: #add8e6;
21 | }
22 | h3.function,h3.typedecl {
23 | background-color: #add8e6;
24 | padding-left: 1em;
25 | }
26 | div.spec {
27 | margin-left: 2em;
28 | background-color: #eeeeee;
29 | }
30 | a.module {
31 | text-decoration:none
32 | }
33 | a.module:hover {
34 | background-color: #eeeeee;
35 | }
36 | ul.definitions {
37 | list-style-type: none;
38 | }
39 | ul.index {
40 | list-style-type: none;
41 | background-color: #eeeeee;
42 | }
43 |
44 | /*
45 | * Minor style tweaks
46 | */
47 | ul {
48 | list-style-type: square;
49 | }
50 | table {
51 | border-collapse: collapse;
52 | }
53 | td {
54 | padding: 3
55 | }
56 |
--------------------------------------------------------------------------------
/include/ibrowse.hrl:
--------------------------------------------------------------------------------
1 | -ifndef(IBROWSE_HRL).
2 | -define(IBROWSE_HRL, "ibrowse.hrl").
3 |
4 | -record(url, {
5 | abspath,
6 | host,
7 | port,
8 | username,
9 | password,
10 | path,
11 | protocol,
12 | host_type % 'hostname', 'ipv4_address' or 'ipv6_address'
13 | }).
14 |
15 | -record(lb_pid, {host_port, pid, ets_tid}).
16 |
17 | -record(client_conn, {key, cur_pipeline_size = 0, reqs_served = 0}).
18 |
19 | -record(ibrowse_conf, {key, value}).
20 |
21 | -define(CONNECTIONS_LOCAL_TABLE, ibrowse_lb).
22 | -define(LOAD_BALANCER_NAMED_TABLE, ibrowse_lb).
23 | -define(CONF_TABLE, ibrowse_conf).
24 | -define(STREAM_TABLE, ibrowse_stream).
25 |
26 | -endif.
27 |
--------------------------------------------------------------------------------
/priv/ibrowse.conf:
--------------------------------------------------------------------------------
1 | %% Configuration file for specifying settings for HTTP servers which this
2 | %% client will connect to.
3 | %% The format of each entry is (one per line)
4 | %% {dest, Hostname, Portnumber, MaxSessions, MaxPipelineSize, Options}.
5 | %%
6 | %% where Hostname = string()
7 | %% Portnumber = integer()
8 | %% MaxSessions = integer()
9 | %% MaxPipelineSize = integer()
10 | %% Options = [{Tag, Val} | ...]
11 | %% Tag = term()
12 | %% Value = term()
13 | %% e.g.
14 | %% {dest, "covig02", 8000, 10, 10, [{is_ssl, true}, {ssl_options, [option()]}]}.
15 | %% If SSL is to be used, both the options, is_ssl and ssl_options MUST be specified
16 | %% where option() is all options supported in the ssl module
17 |
18 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | {erl_opts, [
2 | debug_info,
3 | warnings_as_errors,
4 | warn_unused_vars,
5 | nowarn_shadow_vars,
6 | warn_unused_import,
7 | {platform_define, "18|19|^2", new_rand},
8 | {platform_define, "^2", ets_ref}
9 | ]}.
10 | {xref_checks, [undefined_function_calls, deprecated_function_calls]}.
11 | {eunit_opts, [verbose]}.
12 |
--------------------------------------------------------------------------------
/rebar.lock:
--------------------------------------------------------------------------------
1 | [].
2 |
--------------------------------------------------------------------------------
/src/Emakefile.src:
--------------------------------------------------------------------------------
1 | '../src/ibrowse'.
2 | '../src/ibrowse_http_client'.
3 | '../src/ibrowse_app'.
4 | '../src/ibrowse_sup'.
5 | '../src/ibrowse_lib'.
6 | '../src/ibrowse_lb'.
7 | '../src/ibrowse_test'.
8 |
--------------------------------------------------------------------------------
/src/ibrowse.app.src:
--------------------------------------------------------------------------------
1 | {application, ibrowse,
2 | [{description, "Erlang HTTP client application"},
3 | {vsn, "4.4.2"},
4 | {registered, [ibrowse_sup, ibrowse]},
5 | {applications, [kernel,stdlib]},
6 | {env, []},
7 | {mod, {ibrowse_app, []}},
8 | {maintainers, ["Chandrashekhar Mullaparthi"]},
9 | {licenses, ["GPLv2", "BSD"]},
10 | {modules, []},
11 | {links, [{"Github", "https://github.com/cmullaparthi/ibrowse"}]}
12 | ]
13 | }.
14 |
--------------------------------------------------------------------------------
/src/ibrowse_app.erl:
--------------------------------------------------------------------------------
1 | %%%-------------------------------------------------------------------
2 | %%% File : ibrowse_app.erl
3 | %%% Author : Chandrashekhar Mullaparthi
4 | %%% Description :
5 | %%%
6 | %%% Created : 15 Oct 2003 by Chandrashekhar Mullaparthi
7 | %%%-------------------------------------------------------------------
8 | -module(ibrowse_app).
9 |
10 | -behaviour(application).
11 | %%--------------------------------------------------------------------
12 | %% Include files
13 | %%--------------------------------------------------------------------
14 |
15 | %%--------------------------------------------------------------------
16 | %% External exports
17 | %%--------------------------------------------------------------------
18 | -export([
19 | start/2,
20 | stop/1
21 | ]).
22 |
23 | %%--------------------------------------------------------------------
24 | %% Internal exports
25 | %%--------------------------------------------------------------------
26 | -export([
27 | ]).
28 |
29 | %%--------------------------------------------------------------------
30 | %% Macros
31 | %%--------------------------------------------------------------------
32 |
33 | %%--------------------------------------------------------------------
34 | %% Records
35 | %%--------------------------------------------------------------------
36 |
37 | %%====================================================================
38 | %% External functions
39 | %%====================================================================
40 | %%--------------------------------------------------------------------
41 | %% Func: start/2
42 | %% Returns: {ok, Pid} |
43 | %% {ok, Pid, State} |
44 | %% {error, Reason}
45 | %%--------------------------------------------------------------------
46 | start(_Type, _StartArgs) ->
47 | case ibrowse_sup:start_link() of
48 | {ok, Pid} ->
49 | {ok, Pid};
50 | Error ->
51 | Error
52 | end.
53 |
54 | %%--------------------------------------------------------------------
55 | %% Func: stop/1
56 | %% Returns: any
57 | %%--------------------------------------------------------------------
58 | stop(_State) ->
59 | ok.
60 |
61 | %%====================================================================
62 | %% Internal functions
63 | %%====================================================================
64 |
--------------------------------------------------------------------------------
/src/ibrowse_lb.erl:
--------------------------------------------------------------------------------
1 | %%%-------------------------------------------------------------------
2 | %%% File : ibrowse_lb.erl
3 | %%% Author : chandru
4 | %%% Description :
5 | %%%
6 | %%% Created : 6 Mar 2008 by chandru
7 | %%%-------------------------------------------------------------------
8 | -module(ibrowse_lb).
9 | -author(chandru).
10 | -behaviour(gen_server).
11 | %%--------------------------------------------------------------------
12 | %% Include files
13 | %%--------------------------------------------------------------------
14 |
15 | %%--------------------------------------------------------------------
16 | %% External exports
17 | -export([
18 | start_link/1,
19 | spawn_connection/6,
20 | stop/1
21 | ]).
22 |
23 | %% gen_server callbacks
24 | -export([
25 | init/1,
26 | handle_call/3,
27 | handle_cast/2,
28 | handle_info/2,
29 | terminate/2,
30 | code_change/3
31 | ]).
32 |
33 | -record(state, {parent_pid,
34 | ets_tid,
35 | host,
36 | port,
37 | max_sessions,
38 | max_pipeline_size,
39 | proc_state
40 | }).
41 |
42 | -include("ibrowse.hrl").
43 |
44 | %%====================================================================
45 | %% External functions
46 | %%====================================================================
47 | %%--------------------------------------------------------------------
48 | %% Function: start_link/0
49 | %% Description: Starts the server
50 | %%--------------------------------------------------------------------
51 | start_link(Args) ->
52 | gen_server:start_link(?MODULE, Args, []).
53 |
54 | %%====================================================================
55 | %% Server functions
56 | %%====================================================================
57 |
58 | %%--------------------------------------------------------------------
59 | %% Function: init/1
60 | %% Description: Initiates the server
61 | %% Returns: {ok, State} |
62 | %% {ok, State, Timeout} |
63 | %% ignore |
64 | %% {stop, Reason}
65 | %%--------------------------------------------------------------------
66 | init([Host, Port]) ->
67 | process_flag(trap_exit, true),
68 | Max_sessions = ibrowse:get_config_value({max_sessions, Host, Port}, 10),
69 | Max_pipe_sz = ibrowse:get_config_value({max_pipeline_size, Host, Port}, 10),
70 | put(my_trace_flag, ibrowse_lib:get_trace_status(Host, Port)),
71 | put(ibrowse_trace_token, ["LB: ", Host, $:, integer_to_list(Port)]),
72 | State = #state{parent_pid = whereis(ibrowse),
73 | host = Host,
74 | port = Port,
75 | max_pipeline_size = Max_pipe_sz,
76 | max_sessions = Max_sessions},
77 | State_1 = maybe_create_ets(State),
78 | {ok, State_1}.
79 |
80 | spawn_connection(Lb_pid, Url,
81 | Max_sessions,
82 | Max_pipeline_size,
83 | SSL_options,
84 | Process_options)
85 | when is_pid(Lb_pid),
86 | is_record(Url, url),
87 | is_integer(Max_pipeline_size),
88 | is_integer(Max_sessions) ->
89 | gen_server:call(Lb_pid,
90 | {spawn_connection, Url, Max_sessions, Max_pipeline_size, SSL_options, Process_options}).
91 |
92 | stop(Lb_pid) ->
93 | case catch gen_server:call(Lb_pid, stop) of
94 | {'EXIT', {timeout, _}} ->
95 | exit(Lb_pid, kill);
96 | ok ->
97 | ok
98 | end.
99 | %%--------------------------------------------------------------------
100 | %% Function: handle_call/3
101 | %% Description: Handling call messages
102 | %% Returns: {reply, Reply, State} |
103 | %% {reply, Reply, State, Timeout} |
104 | %% {noreply, State} |
105 | %% {noreply, State, Timeout} |
106 | %% {stop, Reason, Reply, State} | (terminate/2 is called)
107 | %% {stop, Reason, State} (terminate/2 is called)
108 | %%--------------------------------------------------------------------
109 |
110 | handle_call(stop, _From, #state{ets_tid = undefined} = State) ->
111 | gen_server:reply(_From, ok),
112 | {stop, normal, State};
113 |
114 | handle_call(stop, _From, #state{ets_tid = Tid} = State) ->
115 | stop_all_conn_procs(Tid),
116 | gen_server:reply(_From, ok),
117 | {stop, normal, State};
118 |
119 | handle_call(_, _From, #state{proc_state = shutting_down} = State) ->
120 | {reply, {error, shutting_down}, State};
121 |
122 | handle_call({spawn_connection, Url, Max_sess, Max_pipe, SSL_options, Process_options}, _From,
123 | State) ->
124 | State_1 = maybe_create_ets(State),
125 | Tid = State_1#state.ets_tid,
126 | Tid_size = ets:info(Tid, size),
127 | case Tid_size >= Max_sess of
128 | true ->
129 | Reply = find_best_connection(Tid, Max_pipe),
130 | {reply, Reply, State_1#state{max_sessions = Max_sess,
131 | max_pipeline_size = Max_pipe}};
132 | false ->
133 | {ok, Pid} = ibrowse_http_client:start({Tid, Url, SSL_options}, Process_options),
134 | Ts = os:timestamp(),
135 | ets:insert(Tid, {{1, Ts, Pid}, []}),
136 | {reply, {ok, {1, Ts, Pid}}, State_1#state{max_sessions = Max_sess,
137 | max_pipeline_size = Max_pipe}}
138 | end;
139 |
140 | handle_call(Request, _From, State) ->
141 | Reply = {unknown_request, Request},
142 | {reply, Reply, State}.
143 |
144 | %%--------------------------------------------------------------------
145 | %% Function: handle_cast/2
146 | %% Description: Handling cast messages
147 | %% Returns: {noreply, State} |
148 | %% {noreply, State, Timeout} |
149 | %% {stop, Reason, State} (terminate/2 is called)
150 | %%--------------------------------------------------------------------
151 | handle_cast(_Msg, State) ->
152 | {noreply, State}.
153 |
154 | %%--------------------------------------------------------------------
155 | %% Function: handle_info/2
156 | %% Description: Handling all non call/cast messages
157 | %% Returns: {noreply, State} |
158 | %% {noreply, State, Timeout} |
159 | %% {stop, Reason, State} (terminate/2 is called)
160 | %%--------------------------------------------------------------------
161 |
162 | handle_info({trace, Bool}, #state{ets_tid = undefined} = State) ->
163 | put(my_trace_flag, Bool),
164 | {noreply, State};
165 |
166 | handle_info({trace, Bool}, #state{ets_tid = Tid} = State) ->
167 | ets:foldl(fun({{_, Pid}, _}, Acc) when is_pid(Pid) ->
168 | catch Pid ! {trace, Bool},
169 | Acc;
170 | (_, Acc) ->
171 | Acc
172 | end, undefined, Tid),
173 | put(my_trace_flag, Bool),
174 | {noreply, State};
175 |
176 | handle_info(timeout, State) ->
177 | %% We can't shutdown the process immediately because a request
178 | %% might be in flight. So we first remove the entry from the
179 | %% ibrowse_lb ets table, and then shutdown a couple of seconds
180 | %% later
181 | ets:delete(ibrowse_lb, {State#state.host, State#state.port}),
182 | erlang:send_after(2000, self(), shutdown),
183 | {noreply, State#state{proc_state = shutting_down}};
184 |
185 | handle_info(shutdown, State) ->
186 | {stop, normal, State};
187 |
188 | handle_info(_Info, State) ->
189 | {noreply, State}.
190 |
191 | %%--------------------------------------------------------------------
192 | %% Function: terminate/2
193 | %% Description: Shutdown the server
194 | %% Returns: any (ignored by gen_server)
195 | %%--------------------------------------------------------------------
196 | terminate(_Reason, #state{host = Host, port = Port, ets_tid = Tid} = _State) ->
197 | catch ets:delete(ibrowse_lb, {Host, Port}),
198 | stop_all_conn_procs(Tid),
199 | ok.
200 |
201 | stop_all_conn_procs(Tid) ->
202 | ets:foldl(fun({{_, _, Pid}, _}, Acc) ->
203 | ibrowse_http_client:stop(Pid),
204 | Acc
205 | end, [], Tid).
206 |
207 | %%--------------------------------------------------------------------
208 | %% Func: code_change/3
209 | %% Purpose: Convert process state when code is changed
210 | %% Returns: {ok, NewState}
211 | %%--------------------------------------------------------------------
212 | code_change(_OldVsn, State, _Extra) ->
213 | {ok, State}.
214 |
215 | %%--------------------------------------------------------------------
216 | %%% Internal functions
217 | %%--------------------------------------------------------------------
218 | find_best_connection(Tid, Max_pipe) ->
219 | case ets:first(Tid) of
220 | {Spec_size, Ts, Pid} = First when Spec_size < Max_pipe ->
221 | ets:delete(Tid, First),
222 | ets:insert(Tid, {{Spec_size + 1, Ts, Pid}, []}),
223 | {ok, First};
224 | _ ->
225 | {error, retry_later}
226 | end.
227 |
228 | maybe_create_ets(#state{ets_tid = undefined, host = Host, port = Port} = State) ->
229 | Tid = ets:new(ibrowse_lb, [public, ordered_set]),
230 | ets:insert(ibrowse_lb, #lb_pid{host_port = {Host, Port}, pid = self(), ets_tid = Tid}),
231 | State#state{ets_tid = Tid};
232 | maybe_create_ets(State) ->
233 | State.
234 |
--------------------------------------------------------------------------------
/src/ibrowse_lib.erl:
--------------------------------------------------------------------------------
1 | %%% File : ibrowse_lib.erl
2 | %%% Author : Chandrashekhar Mullaparthi
3 | %%% Description :
4 | %%% Created : 27 Feb 2004 by Chandrashekhar Mullaparthi
5 | %% @doc Module with a few useful functions
6 |
7 | -module(ibrowse_lib).
8 | -author('chandru').
9 | -ifdef(debug).
10 | -compile(export_all).
11 | -endif.
12 |
13 | -include("ibrowse.hrl").
14 |
15 | -ifdef(EUNIT).
16 | -include_lib("eunit/include/eunit.hrl").
17 | -endif.
18 |
19 | -export([
20 | get_trace_status/2,
21 | do_trace/2,
22 | do_trace/3,
23 | log_msg/2,
24 | url_encode/1,
25 | decode_rfc822_date/1,
26 | status_code/1,
27 | encode_base64/1,
28 | decode_base64/1,
29 | get_value/2,
30 | get_value/3,
31 | parse_url/1,
32 | printable_date/0,
33 | printable_date/1
34 | ]).
35 |
36 | get_trace_status(Host, Port) ->
37 | ibrowse:get_config_value({trace, Host, Port}, false).
38 |
39 | %% @doc URL-encodes a string based on RFC 1738. Returns a flat list.
40 | %% @spec url_encode(Str) -> UrlEncodedStr
41 | %% Str = string()
42 | %% UrlEncodedStr = string()
43 | url_encode(Str) when is_list(Str) ->
44 | url_encode_char(lists:reverse(Str), []).
45 |
46 | url_encode_char([X | T], Acc) when X >= $0, X =< $9 ->
47 | url_encode_char(T, [X | Acc]);
48 | url_encode_char([X | T], Acc) when X >= $a, X =< $z ->
49 | url_encode_char(T, [X | Acc]);
50 | url_encode_char([X | T], Acc) when X >= $A, X =< $Z ->
51 | url_encode_char(T, [X | Acc]);
52 | url_encode_char([X | T], Acc) when X == $-; X == $_; X == $. ->
53 | url_encode_char(T, [X | Acc]);
54 | url_encode_char([32 | T], Acc) ->
55 | url_encode_char(T, [$+ | Acc]);
56 | url_encode_char([X | T], Acc) ->
57 | url_encode_char(T, [$%, d2h(X bsr 4), d2h(X band 16#0f) | Acc]);
58 | url_encode_char([], Acc) ->
59 | Acc.
60 |
61 | d2h(N) when N<10 -> N+$0;
62 | d2h(N) -> N+$a-10.
63 |
64 | decode_rfc822_date(String) when is_list(String) ->
65 | case catch decode_rfc822_date_1(string:tokens(String, ", \t\r\n")) of
66 | {'EXIT', _} ->
67 | {error, invalid_date};
68 | Res ->
69 | Res
70 | end.
71 |
72 | % TODO: Have to handle the Zone
73 | decode_rfc822_date_1([_,DayInt,Month,Year, Time,Zone]) ->
74 | decode_rfc822_date_1([DayInt,Month,Year, Time,Zone]);
75 | decode_rfc822_date_1([Day,Month,Year, Time,_Zone]) ->
76 | DayI = list_to_integer(Day),
77 | MonthI = month_int(Month),
78 | YearI = list_to_integer(Year),
79 | TimeTup = case string:tokens(Time, ":") of
80 | [H,M] ->
81 | {list_to_integer(H),
82 | list_to_integer(M),
83 | 0};
84 | [H,M,S] ->
85 | {list_to_integer(H),
86 | list_to_integer(M),
87 | list_to_integer(S)}
88 | end,
89 | {{YearI,MonthI,DayI}, TimeTup}.
90 |
91 | month_int("Jan") -> 1;
92 | month_int("Feb") -> 2;
93 | month_int("Mar") -> 3;
94 | month_int("Apr") -> 4;
95 | month_int("May") -> 5;
96 | month_int("Jun") -> 6;
97 | month_int("Jul") -> 7;
98 | month_int("Aug") -> 8;
99 | month_int("Sep") -> 9;
100 | month_int("Oct") -> 10;
101 | month_int("Nov") -> 11;
102 | month_int("Dec") -> 12.
103 |
104 | %% @doc Given a status code, returns an atom describing the status code.
105 | %% @spec status_code(StatusCode::status_code()) -> StatusDescription
106 | %% status_code() = string() | integer()
107 | %% StatusDescription = atom()
108 | status_code(100) -> continue;
109 | status_code(101) -> switching_protocols;
110 | status_code(102) -> processing;
111 | status_code(200) -> ok;
112 | status_code(201) -> created;
113 | status_code(202) -> accepted;
114 | status_code(203) -> non_authoritative_information;
115 | status_code(204) -> no_content;
116 | status_code(205) -> reset_content;
117 | status_code(206) -> partial_content;
118 | status_code(207) -> multi_status;
119 | status_code(300) -> multiple_choices;
120 | status_code(301) -> moved_permanently;
121 | status_code(302) -> found;
122 | status_code(303) -> see_other;
123 | status_code(304) -> not_modified;
124 | status_code(305) -> use_proxy;
125 | status_code(306) -> unused;
126 | status_code(307) -> temporary_redirect;
127 | status_code(400) -> bad_request;
128 | status_code(401) -> unauthorized;
129 | status_code(402) -> payment_required;
130 | status_code(403) -> forbidden;
131 | status_code(404) -> not_found;
132 | status_code(405) -> method_not_allowed;
133 | status_code(406) -> not_acceptable;
134 | status_code(407) -> proxy_authentication_required;
135 | status_code(408) -> request_timeout;
136 | status_code(409) -> conflict;
137 | status_code(410) -> gone;
138 | status_code(411) -> length_required;
139 | status_code(412) -> precondition_failed;
140 | status_code(413) -> request_entity_too_large;
141 | status_code(414) -> request_uri_too_long;
142 | status_code(415) -> unsupported_media_type;
143 | status_code(416) -> requested_range_not_satisfiable;
144 | status_code(417) -> expectation_failed;
145 | status_code(422) -> unprocessable_entity;
146 | status_code(423) -> locked;
147 | status_code(424) -> failed_dependency;
148 | status_code(500) -> internal_server_error;
149 | status_code(501) -> not_implemented;
150 | status_code(502) -> bad_gateway;
151 | status_code(503) -> service_unavailable;
152 | status_code(504) -> gateway_timeout;
153 | status_code(505) -> http_version_not_supported;
154 | status_code(507) -> insufficient_storage;
155 | status_code(X) when is_list(X) -> status_code(list_to_integer(X));
156 | status_code(_) -> unknown_status_code.
157 |
158 | %% @doc Implements the base64 encoding algorithm. The output data type matches in the input data type.
159 | %% @spec encode_base64(In) -> Out
160 | %% In = string() | binary()
161 | %% Out = string() | binary()
162 | encode_base64(List) when is_list(List) ->
163 | binary_to_list(base64:encode(List));
164 | encode_base64(Bin) when is_binary(Bin) ->
165 | base64:encode(Bin).
166 |
167 | %% @doc Implements the base64 decoding algorithm. The output data type matches in the input data type.
168 | %% @spec decode_base64(In) -> Out | exit({error, invalid_input})
169 | %% In = string() | binary()
170 | %% Out = string() | binary()
171 | decode_base64(List) when is_list(List) ->
172 | binary_to_list(base64:decode(List));
173 | decode_base64(Bin) when is_binary(Bin) ->
174 | base64:decode(Bin).
175 |
176 | get_value(Tag, TVL, DefVal) ->
177 | case lists:keysearch(Tag, 1, TVL) of
178 | false ->
179 | DefVal;
180 | {value, {_, Val}} ->
181 | Val
182 | end.
183 |
184 | get_value(Tag, TVL) ->
185 | {value, {_, V}} = lists:keysearch(Tag,1,TVL),
186 | V.
187 |
188 | parse_url(Url) ->
189 | try
190 | case parse_url(Url, get_protocol, #url{abspath=Url}, []) of
191 | #url{host_type = undefined, host = Host} = UrlRec ->
192 | case inet_parse:address(Host) of
193 | {ok, {_, _, _, _, _, _, _, _}} ->
194 | UrlRec#url{host_type = ipv6_address};
195 | {ok, {_, _, _, _}} ->
196 | UrlRec#url{host_type = ipv4_address};
197 | _ ->
198 | UrlRec#url{host_type = hostname}
199 | end;
200 | #url{} = UrlRec ->
201 | UrlRec;
202 | _ ->
203 | {error, invalid_uri}
204 | end
205 | catch _:_ ->
206 | {error, invalid_uri}
207 | end.
208 |
209 | parse_url([$:, $/, $/ | _], get_protocol, Url, []) ->
210 | {invalid_uri_1, Url};
211 | parse_url([$:, $/, $/ | T], get_protocol, Url, TmpAcc) ->
212 | Prot = list_to_existing_atom(lists:reverse(TmpAcc)),
213 | parse_url(T, get_username,
214 | Url#url{protocol = Prot},
215 | []);
216 | parse_url([H | T], get_username, Url, TmpAcc) when H == $/;
217 | H == $? ->
218 | Path = case H of
219 | $/ ->
220 | [$/ | T];
221 | $? ->
222 | [$/, $? | T]
223 | end,
224 | %% No username/password. No port number
225 | Url#url{host = lists:reverse(TmpAcc),
226 | port = default_port(Url#url.protocol),
227 | path = Path};
228 | parse_url([$: | T], get_username, Url, TmpAcc) ->
229 | %% It is possible that no username/password has been
230 | %% specified. But we'll continue with the assumption that there is
231 | %% a username/password. If we encounter a '@' later on, there is a
232 | %% username/password indeed. If we encounter a '/', it was
233 | %% actually the hostname
234 | parse_url(T, get_password,
235 | Url#url{username = lists:reverse(TmpAcc)},
236 | []);
237 | parse_url([$@ | T], get_username, Url, TmpAcc) ->
238 | parse_url(T, get_host,
239 | Url#url{username = lists:reverse(TmpAcc),
240 | password = ""},
241 | []);
242 | parse_url([$[ | T], get_username, Url, []) ->
243 | % IPv6 address literals are enclosed by square brackets:
244 | % http://www.ietf.org/rfc/rfc2732.txt
245 | parse_url(T, get_ipv6_address, Url#url{host_type = ipv6_address}, []);
246 | parse_url([$[ | T], get_username, _Url, TmpAcc) ->
247 | {error, {invalid_username_or_host, lists:reverse(TmpAcc) ++ "[" ++ T}};
248 | parse_url([$[ | _], get_password, _Url, []) ->
249 | {error, missing_password};
250 | parse_url([$[ | T], get_password, Url, TmpAcc) ->
251 | % IPv6 address literals are enclosed by square brackets:
252 | % http://www.ietf.org/rfc/rfc2732.txt
253 | parse_url(T, get_ipv6_address,
254 | Url#url{host_type = ipv6_address,
255 | password = lists:reverse(TmpAcc)},
256 | []);
257 | parse_url([$@ | T], get_password, Url, TmpAcc) ->
258 | parse_url(T, get_host,
259 | Url#url{password = lists:reverse(TmpAcc)},
260 | []);
261 | parse_url([H | T], get_password, Url, TmpAcc) when H == $/;
262 | H == $? ->
263 | %% Ok, what we thought was the username/password was the hostname
264 | %% and portnumber
265 | #url{username=User} = Url,
266 | Port = list_to_integer(lists:reverse(TmpAcc)),
267 | Path = case H of
268 | $/ ->
269 | [$/ | T];
270 | $? ->
271 | [$/, $? | T]
272 | end,
273 | Url#url{host = User,
274 | port = Port,
275 | username = undefined,
276 | password = undefined,
277 | path = Path};
278 | parse_url([$] | T], get_ipv6_address, #url{protocol = Prot} = Url, TmpAcc) ->
279 | Addr = lists:reverse(TmpAcc),
280 | case inet_parse:address(Addr) of
281 | {ok, {_, _, _, _, _, _, _, _}} ->
282 | Url2 = Url#url{host = Addr, port = default_port(Prot)},
283 | case T of
284 | [$: | T2] ->
285 | parse_url(T2, get_port, Url2, []);
286 | [$/ | T2] ->
287 | Url2#url{path = [$/ | T2]};
288 | [$? | T2] ->
289 | Url2#url{path = [$/, $? | T2]};
290 | [] ->
291 | Url2#url{path = "/"};
292 | _ ->
293 | {error, {invalid_host, "[" ++ Addr ++ "]" ++ T}}
294 | end;
295 | _ ->
296 | {error, {invalid_ipv6_address, Addr}}
297 | end;
298 | parse_url([$[ | T], get_host, #url{} = Url, []) ->
299 | parse_url(T, get_ipv6_address, Url#url{host_type = ipv6_address}, []);
300 | parse_url([$: | T], get_host, #url{} = Url, TmpAcc) ->
301 | parse_url(T, get_port,
302 | Url#url{host = lists:reverse(TmpAcc)},
303 | []);
304 | parse_url([H | T], get_host, #url{protocol=Prot} = Url, TmpAcc) when H == $/;
305 | H == $? ->
306 | Path = case H of
307 | $/ ->
308 | [$/ | T];
309 | $? ->
310 | [$/, $? | T]
311 | end,
312 | Url#url{host = lists:reverse(TmpAcc),
313 | port = default_port(Prot),
314 | path = Path};
315 | parse_url([H | T], get_port, #url{protocol=Prot} = Url, TmpAcc) when H == $/;
316 | H == $? ->
317 | Path = case H of
318 | $/ ->
319 | [$/ | T];
320 | $? ->
321 | [$/, $? | T]
322 | end,
323 | Port = case TmpAcc of
324 | [] ->
325 | default_port(Prot);
326 | _ ->
327 | list_to_integer(lists:reverse(TmpAcc))
328 | end,
329 | Url#url{port = Port, path = Path};
330 | parse_url([H | T], State, Url, TmpAcc) ->
331 | parse_url(T, State, Url, [H | TmpAcc]);
332 | parse_url([], get_host, Url, TmpAcc) when TmpAcc /= [] ->
333 | Url#url{host = lists:reverse(TmpAcc),
334 | port = default_port(Url#url.protocol),
335 | path = "/"};
336 | parse_url([], get_username, Url, TmpAcc) when TmpAcc /= [] ->
337 | Url#url{host = lists:reverse(TmpAcc),
338 | port = default_port(Url#url.protocol),
339 | path = "/"};
340 | parse_url([], get_port, #url{protocol=Prot} = Url, TmpAcc) ->
341 | Port = case TmpAcc of
342 | [] ->
343 | default_port(Prot);
344 | _ ->
345 | list_to_integer(lists:reverse(TmpAcc))
346 | end,
347 | Url#url{port = Port,
348 | path = "/"};
349 | parse_url([], get_password, Url, TmpAcc) ->
350 | %% Ok, what we thought was the username/password was the hostname
351 | %% and portnumber
352 | #url{username=User} = Url,
353 | Port = case TmpAcc of
354 | [] ->
355 | default_port(Url#url.protocol);
356 | _ ->
357 | list_to_integer(lists:reverse(TmpAcc))
358 | end,
359 | Url#url{host = User,
360 | port = Port,
361 | username = undefined,
362 | password = undefined,
363 | path = "/"};
364 | parse_url([], State, Url, TmpAcc) ->
365 | {invalid_uri_2, State, Url, TmpAcc}.
366 |
367 | default_port(socks5) -> 1080;
368 | default_port(http) -> 80;
369 | default_port(https) -> 443;
370 | default_port(ftp) -> 21.
371 |
372 | printable_date() ->
373 | printable_date(os:timestamp()).
374 |
375 | printable_date(Now) ->
376 | {{Y,Mo,D},{H, M, S}} = calendar:now_to_local_time(Now),
377 | {_,_,MicroSecs} = Now,
378 | [integer_to_list(Y),
379 | $-,
380 | integer_to_list(Mo),
381 | $-,
382 | integer_to_list(D),
383 | $_,
384 | integer_to_list(H),
385 | $:,
386 | integer_to_list(M),
387 | $:,
388 | integer_to_list(S),
389 | $:,
390 | integer_to_list(MicroSecs div 1000)].
391 |
392 | do_trace(Fmt, Args) ->
393 | do_trace(get(my_trace_flag), Fmt, Args).
394 |
395 | -ifdef(DEBUG).
396 | do_trace(_, Fmt, Args) ->
397 | io:format("~s -- (~s) - "++Fmt,
398 | [printable_date(),
399 | get(ibrowse_trace_token) | Args]).
400 | -else.
401 | do_trace(true, Fmt, Args) ->
402 | Fmt_1 = "~s -- (~s) - "++Fmt,
403 | Args_1 = [printable_date(),
404 | get(ibrowse_trace_token) | Args],
405 | case application:get_env(ibrowse, logger_mf) of
406 | {ok, {M, F}} ->
407 | log_msg(M, F, Fmt_1, Args_1);
408 | _ ->
409 | log_msg(io, format, Fmt_1, Args_1)
410 | end;
411 | do_trace(_, _, _) ->
412 | ok.
413 | -endif.
414 |
415 | log_msg(Fmt, Args) ->
416 | case application:get_env(ibrowse, logger_mf) of
417 | {ok, {M, F}} ->
418 | log_msg(M, F, Fmt, Args),
419 | ok;
420 | _ ->
421 | ok
422 | end.
423 |
424 | log_msg(M, F, Fmt, Args) ->
425 | catch apply(M, F, [Fmt, Args]).
426 |
427 | -ifdef(EUNIT).
428 |
429 | parse_url_test() ->
430 | Urls = [{"http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html",
431 | #url{abspath = "http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html",
432 | host = "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210",
433 | port = 80, protocol = http, path = "/index.html",
434 | host_type = ipv6_address}},
435 | {"http://[1080:0:0:0:8:800:200C:417A]/index.html",
436 | #url{abspath = "http://[1080:0:0:0:8:800:200C:417A]/index.html",
437 | host_type = ipv6_address, port = 80, protocol = http,
438 | host = "1080:0:0:0:8:800:200C:417A", path = "/index.html"}},
439 | {"http://[3ffe:2a00:100:7031::1]",
440 | #url{abspath = "http://[3ffe:2a00:100:7031::1]",
441 | host_type = ipv6_address, port = 80, protocol = http,
442 | host = "3ffe:2a00:100:7031::1", path = "/"}},
443 | {"http://[1080::8:800:200C:417A]/foo",
444 | #url{abspath = "http://[1080::8:800:200C:417A]/foo",
445 | host_type = ipv6_address, port = 80, protocol = http,
446 | host = "1080::8:800:200C:417A", path = "/foo"}},
447 | {"http://[::192.9.5.5]/ipng",
448 | #url{abspath = "http://[::192.9.5.5]/ipng",
449 | host_type = ipv6_address, port = 80, protocol = http,
450 | host = "::192.9.5.5", path = "/ipng"}},
451 | {"http://[::FFFF:129.144.52.38]:80/index.html",
452 | #url{abspath = "http://[::FFFF:129.144.52.38]:80/index.html",
453 | host_type = ipv6_address, port = 80, protocol = http,
454 | host = "::FFFF:129.144.52.38", path = "/index.html"}},
455 | {"http://[2010:836B:4179::836B:4179]",
456 | #url{abspath = "http://[2010:836B:4179::836B:4179]",
457 | host_type = ipv6_address, port = 80, protocol = http,
458 | host = "2010:836B:4179::836B:4179", path = "/"}}
459 | ],
460 | lists:foreach(
461 | fun({Url, Expected_result}) ->
462 | ?assertMatch(Expected_result, parse_url(Url))
463 | end, Urls).
464 |
465 | -endif.
466 |
--------------------------------------------------------------------------------
/src/ibrowse_socks5.erl:
--------------------------------------------------------------------------------
1 | % Licensed under the Apache License, Version 2.0 (the "License"); you may not
2 | % use this file except in compliance with the License. You may obtain a copy of
3 | % the License at
4 | %
5 | % http://www.apache.org/licenses/LICENSE-2.0
6 | %
7 | % Unless required by applicable law or agreed to in writing, software
8 | % distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 | % WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 | % License for the specific language governing permissions and limitations under
11 | % the License.
12 |
13 | -module(ibrowse_socks5).
14 |
15 | -define(VERSION, 5).
16 | -define(CONNECT, 1).
17 |
18 | -define(NO_AUTH, 0).
19 | -define(USERPASS, 2).
20 | -define(UNACCEPTABLE, 16#FF).
21 | -define(RESERVED, 0).
22 |
23 | -define(ATYP_IPV4, 1).
24 | -define(ATYP_DOMAINNAME, 3).
25 | -define(ATYP_IPV6, 4).
26 |
27 | -define(SUCCEEDED, 0).
28 |
29 | -export([connect/5]).
30 |
31 | -import(ibrowse_lib, [get_value/2, get_value/3]).
32 |
33 | connect(Host, Port, Options, SockOptions, Timeout) ->
34 | Socks5Host = get_value(socks5_host, Options),
35 | Socks5Port = get_value(socks5_port, Options),
36 | case gen_tcp:connect(Socks5Host, Socks5Port, SockOptions, Timeout) of
37 | {ok, Socket} ->
38 | case handshake(Socket, Options) of
39 | ok ->
40 | case connect(Host, Port, Socket) of
41 | ok ->
42 | {ok, Socket};
43 | Else ->
44 | gen_tcp:close(Socket),
45 | Else
46 | end;
47 | Else ->
48 | gen_tcp:close(Socket),
49 | Else
50 | end;
51 | Else ->
52 | Else
53 | end.
54 |
55 | handshake(Socket, Options) when is_port(Socket) ->
56 | User = get_value(socks5_user, Options, <<>>),
57 | Handshake_msg = case User of
58 | <<>> ->
59 | <>;
60 | User ->
61 | <>
62 | end,
63 | ok = gen_tcp:send(Socket, Handshake_msg),
64 | case gen_tcp:recv(Socket, 2) of
65 | {ok, <>} ->
66 | ok;
67 | {ok, <>} ->
68 | Password = get_value(socks5_password, Options, <<>>),
69 | Auth_msg = list_to_binary([1,
70 | iolist_size(User), User,
71 | iolist_size(Password), Password]),
72 | ok = gen_tcp:send(Socket, Auth_msg),
73 | case gen_tcp:recv(Socket, 2) of
74 | {ok, <<1, ?SUCCEEDED>>} ->
75 | ok;
76 | _ ->
77 | {error, unacceptable}
78 | end;
79 | {ok, <>} ->
80 | {error, unacceptable};
81 | {error, Reason} ->
82 | {error, Reason}
83 | end.
84 |
85 | connect(Host, Port, Via) when is_list(Host) ->
86 | connect(list_to_binary(Host), Port, Via);
87 | connect(Host, Port, Via) when is_binary(Host), is_integer(Port),
88 | is_port(Via) ->
89 | {AddressType, Address} = case inet:parse_address(binary_to_list(Host)) of
90 | {ok, {IP1, IP2, IP3, IP4}} ->
91 | {?ATYP_IPV4, <>};
92 | {ok, {IP1, IP2, IP3, IP4, IP5, IP6, IP7, IP8}} ->
93 | {?ATYP_IPV6, <>};
94 | _ ->
95 | HostLength = byte_size(Host),
96 | {?ATYP_DOMAINNAME, <>}
97 | end,
98 | ok = gen_tcp:send(Via,
99 | <>),
102 | case gen_tcp:recv(Via, 0) of
103 | {ok, <>} ->
104 | ok;
105 | {ok, <>} ->
106 | {error, rep(Rep)};
107 | {error, Reason} ->
108 | {error, Reason}
109 | end.
110 |
111 | rep(0) -> succeeded;
112 | rep(1) -> server_fail;
113 | rep(2) -> disallowed_by_ruleset;
114 | rep(3) -> network_unreachable;
115 | rep(4) -> host_unreachable;
116 | rep(5) -> connection_refused;
117 | rep(6) -> ttl_expired;
118 | rep(7) -> command_not_supported;
119 | rep(8) -> address_type_not_supported.
120 |
--------------------------------------------------------------------------------
/src/ibrowse_sup.erl:
--------------------------------------------------------------------------------
1 | %%%-------------------------------------------------------------------
2 | %%% File : ibrowse_sup.erl
3 | %%% Author : Chandrashekhar Mullaparthi
4 | %%% Description :
5 | %%%
6 | %%% Created : 15 Oct 2003 by Chandrashekhar Mullaparthi
7 | %%%-------------------------------------------------------------------
8 | -module(ibrowse_sup).
9 | -behaviour(supervisor).
10 | %%--------------------------------------------------------------------
11 | %% Include files
12 | %%--------------------------------------------------------------------
13 |
14 | %%--------------------------------------------------------------------
15 | %% External exports
16 | %%--------------------------------------------------------------------
17 | -export([
18 | start_link/0
19 | ]).
20 |
21 | %%--------------------------------------------------------------------
22 | %% Internal exports
23 | %%--------------------------------------------------------------------
24 | -export([
25 | init/1
26 | ]).
27 |
28 | %%--------------------------------------------------------------------
29 | %% Macros
30 | %%--------------------------------------------------------------------
31 | -define(SERVER, ?MODULE).
32 |
33 | %%--------------------------------------------------------------------
34 | %% Records
35 | %%--------------------------------------------------------------------
36 |
37 | %%====================================================================
38 | %% External functions
39 | %%====================================================================
40 | %%--------------------------------------------------------------------
41 | %% Function: start_link/0
42 | %% Description: Starts the supervisor
43 | %%--------------------------------------------------------------------
44 | start_link() ->
45 | supervisor:start_link({local, ?SERVER}, ?MODULE, []).
46 |
47 | %%====================================================================
48 | %% Server functions
49 | %%====================================================================
50 | %%--------------------------------------------------------------------
51 | %% Func: init/1
52 | %% Returns: {ok, {SupFlags, [ChildSpec]}} |
53 | %% ignore |
54 | %% {error, Reason}
55 | %%--------------------------------------------------------------------
56 | init([]) ->
57 | AChild = {ibrowse,{ibrowse,start_link,[]},
58 | permanent,2000,worker,[ibrowse, ibrowse_http_client]},
59 | {ok,{{one_for_all,10,1}, [AChild]}}.
60 |
61 | %%====================================================================
62 | %% Internal functions
63 | %%====================================================================
64 |
--------------------------------------------------------------------------------
/test/Makefile:
--------------------------------------------------------------------------------
1 | IBROWSE_EBIN_PATH=../../ibrowse/ebin
2 |
3 | compile:
4 | @erl -pa $(IBROWSE_EBIN_PATH) -make
5 |
6 | test: compile
7 | @erl -noshell -boot start_clean -pa $(IBROWSE_EBIN_PATH) -s ibrowse_test local_unit_tests -s erlang halt
8 |
9 | old_tests: compile
10 | @erl -noshell -boot start_clean -pa $(IBROWSE_EBIN_PATH) -s ibrowse_test unit_tests -s erlang halt
11 |
12 | test_shell: compile
13 | erl -boot start_clean -pa $(IBROWSE_EBIN_PATH) -s ibrowse_test_server start_server
14 |
15 | clean:
16 | @rm -f *.beam
17 |
--------------------------------------------------------------------------------
/test/ibrowse_lib_tests.erl:
--------------------------------------------------------------------------------
1 | %%% File : ibrowse_lib.erl
2 | %%% Authors : Chandrashekhar Mullaparthi ,
3 | %%% Filipe David Manana
4 | %%% Description : Tests for the module ibrowse_lib.erl
5 | %%% Created : 12 April 2011 by Filipe David Manana
6 |
7 | -module(ibrowse_lib_tests).
8 | -include_lib("eunit/include/eunit.hrl").
9 | -include("../include/ibrowse.hrl").
10 |
11 |
12 | parse_urls_test_() ->
13 | {timeout, 60, [fun parse_urls/0]}.
14 |
15 |
16 | parse_urls() ->
17 | ?assertMatch(#url{
18 | abspath = "http://localhost",
19 | host = "localhost",
20 | host_type = hostname,
21 | port = 80,
22 | path = "/",
23 | username = undefined,
24 | password = undefined,
25 | protocol = http
26 | },
27 | ibrowse_lib:parse_url("http://localhost")),
28 | ?assertMatch(#url{
29 | abspath = "http://localhost:80/",
30 | host = "localhost",
31 | host_type = hostname,
32 | port = 80,
33 | path = "/",
34 | username = undefined,
35 | password = undefined,
36 | protocol = http
37 | },
38 | ibrowse_lib:parse_url("http://localhost:80/")),
39 | ?assertMatch(#url{
40 | abspath = "http://127.0.0.1:8000/",
41 | host = "127.0.0.1",
42 | host_type = ipv4_address,
43 | port = 8000,
44 | path = "/",
45 | username = undefined,
46 | password = undefined,
47 | protocol = http
48 | },
49 | ibrowse_lib:parse_url("http://127.0.0.1:8000/")),
50 | ?assertMatch(#url{
51 | abspath = "https://foo:bar@127.0.0.1:8000/test",
52 | host = "127.0.0.1",
53 | host_type = ipv4_address,
54 | port = 8000,
55 | path = "/test",
56 | username = "foo",
57 | password = "bar",
58 | protocol = https
59 | },
60 | ibrowse_lib:parse_url("https://foo:bar@127.0.0.1:8000/test")),
61 | ?assertMatch(#url{
62 | abspath = "https://[::1]",
63 | host = "::1",
64 | host_type = ipv6_address,
65 | port = 443,
66 | path = "/",
67 | username = undefined,
68 | password = undefined,
69 | protocol = https
70 | },
71 | ibrowse_lib:parse_url("https://[::1]")),
72 | ?assertMatch(#url{
73 | abspath = "http://[::1]:8080",
74 | host = "::1",
75 | host_type = ipv6_address,
76 | port = 8080,
77 | path = "/",
78 | username = undefined,
79 | password = undefined,
80 | protocol = http
81 | },
82 | ibrowse_lib:parse_url("http://[::1]:8080")),
83 | ?assertMatch(#url{
84 | abspath = "http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:8081/index.html",
85 | host = "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210",
86 | host_type = ipv6_address,
87 | port = 8081,
88 | path = "/index.html",
89 | username = undefined,
90 | password = undefined,
91 | protocol = http
92 | },
93 | ibrowse_lib:parse_url("http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:8081/index.html")),
94 | ?assertMatch(#url{
95 | abspath = "http://[1080:0:0:0:8:800:200C:417A]/foo/bar",
96 | host = "1080:0:0:0:8:800:200C:417A",
97 | host_type = ipv6_address,
98 | port = 80,
99 | path = "/foo/bar",
100 | username = undefined,
101 | password = undefined,
102 | protocol = http
103 | },
104 | ibrowse_lib:parse_url("http://[1080:0:0:0:8:800:200C:417A]/foo/bar")),
105 | ?assertMatch(#url{
106 | abspath = "http://[1080:0:0:0:8:800:200C:417A]:8080/foo/bar",
107 | host = "1080:0:0:0:8:800:200C:417A",
108 | host_type = ipv6_address,
109 | port = 8080,
110 | path = "/foo/bar",
111 | username = undefined,
112 | password = undefined,
113 | protocol = http
114 | },
115 | ibrowse_lib:parse_url("http://[1080:0:0:0:8:800:200C:417A]:8080/foo/bar")),
116 | ?assertMatch(#url{
117 | abspath = "http://[::192.9.5.5]:6000/foo?q=bar",
118 | host = "::192.9.5.5",
119 | host_type = ipv6_address,
120 | port = 6000,
121 | path = "/foo?q=bar",
122 | username = undefined,
123 | password = undefined,
124 | protocol = http
125 | },
126 | ibrowse_lib:parse_url("http://[::192.9.5.5]:6000/foo?q=bar")),
127 | ?assertMatch({error, invalid_uri},
128 | ibrowse_lib:parse_url("http://[:1080:0:0:0:8:800:200C:417A:]:6000/foo?q=bar")),
129 | ?assertMatch({error, invalid_uri},
130 | ibrowse_lib:parse_url("http://[12::z]")),
131 | ?assertMatch({error, invalid_uri},
132 | ibrowse_lib:parse_url("http://foo[1080:0:0:0:8:800:200C:417A]:6000")),
133 | ?assertMatch({error, invalid_uri},
134 | ibrowse_lib:parse_url("http://foo:[1080:0:0:0:8:800:200C:417A]:6000")),
135 | ok.
136 |
--------------------------------------------------------------------------------
/test/ibrowse_load_test.erl:
--------------------------------------------------------------------------------
1 | -module(ibrowse_load_test).
2 | %%-compile(export_all).
3 | -export([
4 | random_seed/0,
5 | start/3,
6 | query_state/0,
7 | shutdown/0,
8 | start_1/3,
9 | calculate_timings/0,
10 | get_mmv/2,
11 | spawn_workers/2,
12 | spawn_workers/4,
13 | wait_for_workers/1,
14 | worker_loop/2,
15 | update_unknown_counter/2
16 | ]).
17 |
18 | -ifdef(new_rand).
19 |
20 | -define(RAND, rand).
21 | random_seed() ->
22 | ok.
23 |
24 | -else.
25 |
26 | -define(RAND, random).
27 | random_seed() ->
28 | random:seed(os:timestamp()).
29 |
30 | -endif.
31 |
32 | -define(ibrowse_load_test_counters, ibrowse_load_test_counters).
33 |
34 | start(Num_workers, Num_requests, Max_sess) ->
35 | proc_lib:spawn(fun() ->
36 | start_1(Num_workers, Num_requests, Max_sess)
37 | end).
38 |
39 | query_state() ->
40 | ibrowse_load_test ! query_state.
41 |
42 | shutdown() ->
43 | ibrowse_load_test ! shutdown.
44 |
45 | start_1(Num_workers, Num_requests, Max_sess) ->
46 | register(ibrowse_load_test, self()),
47 | application:start(ibrowse),
48 | application:set_env(ibrowse, inactivity_timeout, 5000),
49 | Ulimit = os:cmd("ulimit -n"),
50 | case catch list_to_integer(string:strip(Ulimit, right, $\n)) of
51 | X when is_integer(X), X > 3000 ->
52 | ok;
53 | X ->
54 | io:format("Load test not starting. {insufficient_value_for_ulimit, ~p}~n", [X]),
55 | exit({insufficient_value_for_ulimit, X})
56 | end,
57 | ets:new(?ibrowse_load_test_counters, [named_table, public]),
58 | ets:new(ibrowse_load_timings, [named_table, public]),
59 | try
60 | ets:insert(?ibrowse_load_test_counters, [{success, 0},
61 | {failed, 0},
62 | {timeout, 0},
63 | {retry_later, 0},
64 | {one_request_only, 0}
65 | ]),
66 | ibrowse:set_max_sessions("localhost", 8081, Max_sess),
67 | Start_time = os:timestamp(),
68 | Workers = spawn_workers(Num_workers, Num_requests),
69 | erlang:send_after(1000, self(), print_diagnostics),
70 | ok = wait_for_workers(Workers),
71 | End_time = os:timestamp(),
72 | Time_in_secs = trunc(round(timer:now_diff(End_time, Start_time) / 1000000)),
73 | Req_count = Num_workers * Num_requests,
74 | [{_, Success_count}] = ets:lookup(?ibrowse_load_test_counters, success),
75 | case Success_count == Req_count of
76 | true ->
77 | io:format("Test success. All requests succeeded~n", []);
78 | false when Success_count > 0 ->
79 | io:format("Test failed. Some successes~n", []);
80 | false ->
81 | io:format("Test failed. ALL requests FAILED~n", [])
82 | end,
83 | case Time_in_secs > 0 of
84 | true ->
85 | io:format("Reqs/sec achieved : ~p~n", [trunc(round(Success_count / Time_in_secs))]);
86 | false ->
87 | ok
88 | end,
89 | io:format("Load test results:~n~p~n", [ets:tab2list(?ibrowse_load_test_counters)]),
90 | io:format("Timings: ~p~n", [calculate_timings()])
91 | catch Err ->
92 | io:format("Err: ~p~n", [Err])
93 | after
94 | ets:delete(?ibrowse_load_test_counters),
95 | ets:delete(ibrowse_load_timings),
96 | unregister(ibrowse_load_test)
97 | end.
98 |
99 | calculate_timings() ->
100 | {Max, Min, Mean} = get_mmv(ets:first(ibrowse_load_timings), {0, 9999999, 0}),
101 | Variance = trunc(round(ets:foldl(fun({_, X}, X_acc) ->
102 | (X - Mean)*(X-Mean) + X_acc
103 | end, 0, ibrowse_load_timings) / ets:info(ibrowse_load_timings, size))),
104 | Std_dev = trunc(round(math:sqrt(Variance))),
105 | {ok, [{max, Max},
106 | {min, Min},
107 | {mean, Mean},
108 | {variance, Variance},
109 | {standard_deviation, Std_dev}]}.
110 |
111 | get_mmv('$end_of_table', {Max, Min, Total}) ->
112 | Mean = trunc(round(Total / ets:info(ibrowse_load_timings, size))),
113 | {Max, Min, Mean};
114 | get_mmv(Key, {Max, Min, Total}) ->
115 | [{_, V}] = ets:lookup(ibrowse_load_timings, Key),
116 | get_mmv(ets:next(ibrowse_load_timings, Key), {max(Max, V), min(Min, V), Total + V}).
117 |
118 |
119 | spawn_workers(Num_w, Num_r) ->
120 | spawn_workers(Num_w, Num_r, self(), []).
121 |
122 | spawn_workers(0, _Num_requests, _Parent, Acc) ->
123 | lists:reverse(Acc);
124 | spawn_workers(Num_workers, Num_requests, Parent, Acc) ->
125 | Pid_ref = spawn_monitor(fun() ->
126 | random_seed(),
127 | case catch worker_loop(Parent, Num_requests) of
128 | {'EXIT', Rsn} ->
129 | io:format("Worker crashed with reason: ~p~n", [Rsn]);
130 | _ ->
131 | ok
132 | end
133 | end),
134 | spawn_workers(Num_workers - 1, Num_requests, Parent, [Pid_ref | Acc]).
135 |
136 | wait_for_workers([]) ->
137 | ok;
138 | wait_for_workers([{Pid, Pid_ref} | T] = Pids) ->
139 | receive
140 | {done, Pid} ->
141 | wait_for_workers(T);
142 | {done, Some_pid} ->
143 | wait_for_workers([{Pid, Pid_ref} | lists:keydelete(Some_pid, 1, T)]);
144 | print_diagnostics ->
145 | io:format("~1000.p~n", [ibrowse:get_metrics()]),
146 | erlang:send_after(1000, self(), print_diagnostics),
147 | wait_for_workers(Pids);
148 | query_state ->
149 | io:format("Waiting for ~p~n", [Pids]),
150 | wait_for_workers(Pids);
151 | shutdown ->
152 | io:format("Shutting down on command. Still waiting for ~p workers~n", [length(Pids)]);
153 | {'DOWN', _, process, _, normal} ->
154 | wait_for_workers(Pids);
155 | {'DOWN', _, process, Down_pid, Rsn} ->
156 | io:format("Worker ~p died. Reason: ~p~n", [Down_pid, Rsn]),
157 | wait_for_workers(lists:keydelete(Down_pid, 1, Pids));
158 | X ->
159 | io:format("Recvd unknown msg: ~p~n", [X]),
160 | wait_for_workers(Pids)
161 | end.
162 |
163 | worker_loop(Parent, 0) ->
164 | Parent ! {done, self()};
165 | worker_loop(Parent, N) ->
166 | Delay = ?RAND:uniform(100),
167 | Url = case Delay rem 10 of
168 | %% Change 10 to some number between 0-9 depending on how
169 | %% much chaos you want to introduce into the server
170 | %% side. The higher the number, the more often the
171 | %% server will close a connection after serving the
172 | %% first request, thereby forcing the client to
173 | %% retry. Any number of 10 or higher will disable this
174 | %% chaos mechanism
175 | 10 ->
176 | ets:update_counter(?ibrowse_load_test_counters, one_request_only, 1),
177 | "http://localhost:8081/ibrowse_handle_one_request_only";
178 | _ ->
179 | "http://localhost:8081/blah"
180 | end,
181 | Start_time = os:timestamp(),
182 | Res = ibrowse:send_req(Url, [], get),
183 | End_time = os:timestamp(),
184 | Time_taken = trunc(round(timer:now_diff(End_time, Start_time) / 1000)),
185 | ets:insert(ibrowse_load_timings, {os:timestamp(), Time_taken}),
186 | case Res of
187 | {ok, "200", _, _} ->
188 | ets:update_counter(?ibrowse_load_test_counters, success, 1);
189 | {error, req_timedout} ->
190 | ets:update_counter(?ibrowse_load_test_counters, timeout, 1);
191 | {error, retry_later} ->
192 | ets:update_counter(?ibrowse_load_test_counters, retry_later, 1);
193 | {error, Reason} ->
194 | update_unknown_counter(Reason, 1);
195 | _ ->
196 | io:format("~p -- Res: ~p~n", [self(), Res]),
197 | ets:update_counter(?ibrowse_load_test_counters, failed, 1)
198 | end,
199 | timer:sleep(Delay),
200 | worker_loop(Parent, N - 1).
201 |
202 | update_unknown_counter(Counter, Inc_val) ->
203 | case catch ets:update_counter(?ibrowse_load_test_counters, Counter, Inc_val) of
204 | {'EXIT', _} ->
205 | ets:insert_new(?ibrowse_load_test_counters, {Counter, 0}),
206 | update_unknown_counter(Counter, Inc_val);
207 | _ ->
208 | ok
209 | end.
210 |
--------------------------------------------------------------------------------
/test/ibrowse_socks_server.erl:
--------------------------------------------------------------------------------
1 | %%%-------------------------------------------------------------------
2 | %%% @author Chandru Mullaparthi <>
3 | %%% @copyright (C) 2016, Chandru Mullaparthi
4 | %%% @doc
5 | %%%
6 | %%% @end
7 | %%% Created : 19 Apr 2016 by Chandru Mullaparthi <>
8 | %%%-------------------------------------------------------------------
9 | -module(ibrowse_socks_server).
10 |
11 | -behaviour(gen_server).
12 |
13 | %% API
14 | -export([start/2, stop/1]).
15 |
16 | %% gen_server callbacks
17 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
18 | terminate/2, code_change/3]).
19 |
20 | -define(SERVER, ?MODULE).
21 |
22 | -record(state, {listen_port, listen_socket, auth_method}).
23 |
24 | -define(NO_AUTH, 0).
25 | -define(AUTH_USER_PW, 2).
26 |
27 | %%%===================================================================
28 | %%% API
29 | %%%===================================================================
30 |
31 | start(Port, Auth_method) ->
32 | Name = make_proc_name(Port),
33 | gen_server:start({local, Name}, ?MODULE, [Port, Auth_method], []).
34 |
35 | stop(Port) ->
36 | make_proc_name(Port) ! stop.
37 |
38 | make_proc_name(Port) ->
39 | list_to_atom("ibrowse_socks_server_" ++ integer_to_list(Port)).
40 |
41 | %%%===================================================================
42 | %%% gen_server callbacks
43 | %%%===================================================================
44 |
45 | init([Port, Auth_method]) ->
46 | State = #state{listen_port = Port, auth_method = Auth_method},
47 | {ok, Sock} = gen_tcp:listen(State#state.listen_port, [{active, false}, binary, {reuseaddr, true}]),
48 | self() ! accept_connection,
49 | process_flag(trap_exit, true),
50 | {ok, State#state{listen_socket = Sock}}.
51 |
52 | handle_call(_Request, _From, State) ->
53 | Reply = ok,
54 | {reply, Reply, State}.
55 |
56 | handle_cast(_Msg, State) ->
57 | {noreply, State}.
58 |
59 | handle_info(accept_connection, State) ->
60 | case gen_tcp:accept(State#state.listen_socket, 1000) of
61 | {error, timeout} ->
62 | self() ! accept_connection,
63 | {noreply, State};
64 | {ok, Socket} ->
65 | Pid = proc_lib:spawn_link(fun() ->
66 | socks_server_loop(Socket, State#state.auth_method)
67 | end),
68 | gen_tcp:controlling_process(Socket, Pid),
69 | Pid ! ready,
70 | self() ! accept_connection,
71 | {noreply, State};
72 | _Err ->
73 | {stop, normal, State}
74 | end;
75 |
76 | handle_info(stop, State) ->
77 | {stop, normal, State};
78 |
79 | handle_info(_Info, State) ->
80 | {noreply, State}.
81 |
82 | terminate(_Reason, _State) ->
83 | ok.
84 |
85 | code_change(_OldVsn, State, _Extra) ->
86 | {ok, State}.
87 |
88 | %%%===================================================================
89 | %%% Internal functions
90 | %%%===================================================================
91 | socks_server_loop(In_socket, Auth_method) ->
92 | receive
93 | ready ->
94 | socks_server_loop(In_socket, Auth_method, <<>>, unauth)
95 | end.
96 |
97 | socks_server_loop(In_socket, Auth_method, Acc, unauth) ->
98 | inet:setopts(In_socket, [{active, once}]),
99 | receive
100 | {tcp, In_socket, Data} ->
101 | Acc_1 = list_to_binary([Acc, Data]),
102 | case Acc_1 of
103 | <<5, ?NO_AUTH>> when Auth_method == ?NO_AUTH ->
104 | ok = gen_tcp:send(In_socket, <<5, ?NO_AUTH>>),
105 | socks_server_loop(In_socket, Auth_method, <<>>, auth_done);
106 | <<5, Num_auth_methods, Auth_methods:Num_auth_methods/binary>> ->
107 | case lists:member(Auth_method, binary_to_list(Auth_methods)) of
108 | true ->
109 | ok = gen_tcp:send(In_socket, <<5, Auth_method>>),
110 | Conn_state = case Auth_method of
111 | ?NO_AUTH -> auth_done;
112 | _ -> auth_pending
113 | end,
114 | socks_server_loop(In_socket, Auth_method, <<>>, Conn_state);
115 | false ->
116 | ok = gen_tcp:send(In_socket, <<5, 16#ff>>),
117 | gen_tcp:close(In_socket)
118 | end;
119 | _ ->
120 | ok = gen_tcp:send(In_socket, <<5, 0>>),
121 | gen_tcp:close(In_socket)
122 | end;
123 | {tcp_closed, In_socket} ->
124 | ok;
125 | {tcp_error, In_socket, _Rsn} ->
126 | ok
127 | end;
128 | socks_server_loop(In_socket, Auth_method, Acc, auth_pending) ->
129 | inet:setopts(In_socket, [{active, once}]),
130 | receive
131 | {tcp, In_socket, Data} ->
132 | Acc_1 = list_to_binary([Acc, Data]),
133 | case Acc_1 of
134 | <<1, U_len, Username:U_len/binary, P_len, Password:P_len/binary>> ->
135 | case check_user_pw(Username, Password) of
136 | ok ->
137 | ok = gen_tcp:send(In_socket, <<1, 0>>),
138 | socks_server_loop(In_socket, Auth_method, <<>>, auth_done);
139 | notok ->
140 | ok = gen_tcp:send(In_socket, <<1, 1>>),
141 | gen_tcp:close(In_socket)
142 | end;
143 | _ ->
144 | socks_server_loop(In_socket, Auth_method, Acc_1, auth_pending)
145 | end;
146 | {tcp_closed, In_socket} ->
147 | ok;
148 | {tcp_error, In_socket, _Rsn} ->
149 | ok
150 | end;
151 | socks_server_loop(In_socket, Auth_method, Acc, auth_done) ->
152 | inet:setopts(In_socket, [{active, once}]),
153 | receive
154 | {tcp, In_socket, Data} ->
155 | Acc_1 = list_to_binary([Acc, Data]),
156 | case Acc_1 of
157 | <<5, 1, 0, Addr_type, Dest_ip:4/binary, Dest_port:16>> when Addr_type == 1->
158 | handle_connect(In_socket, Addr_type, Dest_ip, Dest_port);
159 | <<5, 1, 0, Addr_type, Dest_len, Dest_hostname:Dest_len/binary, Dest_port:16>> when Addr_type == 3 ->
160 | handle_connect(In_socket, Addr_type, Dest_hostname, Dest_port);
161 | <<5, 1, 0, Addr_type, Dest_ip:16/binary, Dest_port:16>> when Addr_type == 4->
162 | handle_connect(In_socket, Addr_type, Dest_ip, Dest_port);
163 | _ ->
164 | socks_server_loop(In_socket, Auth_method, Acc_1, auth_done)
165 | end;
166 | {tcp_closed, In_socket} ->
167 | ok;
168 | {tcp_error, In_socket, _Rsn} ->
169 | ok
170 | end.
171 |
172 | handle_connect(In_socket, Addr_type, Dest_host, Dest_port) ->
173 | Dest_host_1 = case Addr_type of
174 | 1 ->
175 | list_to_tuple(binary_to_list(Dest_host));
176 | 3 ->
177 | binary_to_list(Dest_host);
178 | 4 ->
179 | list_to_tuple(binary_to_list(Dest_host))
180 | end,
181 | case gen_tcp:connect(Dest_host_1, Dest_port, [binary, {active, once}]) of
182 | {ok, Out_socket} ->
183 | Addr = case Addr_type of
184 | 1 ->
185 | <>;
186 | 3 ->
187 | Len = size(Dest_host),
188 | <>;
189 | 4 ->
190 | <>
191 | end,
192 | ok = gen_tcp:send(In_socket, <<5, 0, 0, Addr_type, Addr/binary>>),
193 | inet:setopts(In_socket, [{active, once}]),
194 | inet:setopts(Out_socket, [{active, once}]),
195 | connected_loop(In_socket, Out_socket);
196 | _Err ->
197 | ok = gen_tcp:send(<<5, 1>>),
198 | gen_tcp:close(In_socket)
199 | end.
200 |
201 | check_user_pw(<<"user">>, <<"password">>) ->
202 | ok;
203 | check_user_pw(_, _) ->
204 | notok.
205 |
206 | connected_loop(In_socket, Out_socket) ->
207 | receive
208 | {tcp, In_socket, Data} ->
209 | inet:setopts(In_socket, [{active, once}]),
210 | ok = gen_tcp:send(Out_socket, Data),
211 | connected_loop(In_socket, Out_socket);
212 | {tcp, Out_socket, Data} ->
213 | inet:setopts(Out_socket, [{active, once}]),
214 | ok = gen_tcp:send(In_socket, Data),
215 | connected_loop(In_socket, Out_socket);
216 | _ ->
217 | ok
218 | end.
219 |
--------------------------------------------------------------------------------
/test/ibrowse_test.erl:
--------------------------------------------------------------------------------
1 | %%% File : ibrowse_test.erl
2 | %%% Author : Chandrashekhar Mullaparthi
3 | %%% Description : Test ibrowse
4 | %%% Created : 14 Oct 2003 by Chandrashekhar Mullaparthi
5 |
6 | -module(ibrowse_test).
7 | -export([
8 | load_test_/3,
9 | send_reqs_1/3,
10 | do_send_req/2,
11 | local_unit_tests/0,
12 | unit_tests/0,
13 | unit_tests/2,
14 | unit_tests_1/3,
15 | verify_chunked_streaming/0,
16 | verify_chunked_streaming/1,
17 | test_chunked_streaming_once/0,
18 | i_do_async_req_list/4,
19 | test_stream_once/3,
20 | test_stream_once/4,
21 | test_20122010/0,
22 | test_20122010/1,
23 | test_pipeline_head_timeout/0,
24 | test_pipeline_head_timeout/1,
25 | do_test_pipeline_head_timeout/4,
26 | test_head_transfer_encoding/0,
27 | test_head_transfer_encoding/1,
28 | test_head_response_with_body/0,
29 | test_head_response_with_body/1,
30 | test_303_response_with_no_body/0,
31 | test_303_response_with_no_body/1,
32 | test_303_response_with_a_body/0,
33 | test_303_response_with_a_body/1,
34 | test_preserve_status_line/0,
35 | test_binary_headers/0,
36 | test_binary_headers/1,
37 | test_dead_lb_pid/0,
38 | test_generate_body_0/0,
39 | test_retry_of_requests/0,
40 | test_retry_of_requests/1,
41 | test_save_to_file_no_content_length/0,
42 | socks5_noauth/0,
43 | socks5_auth_succ/0,
44 | socks5_auth_fail/0
45 | ]).
46 |
47 | -include_lib("ibrowse/include/ibrowse.hrl").
48 |
49 | %%------------------------------------------------------------------------------
50 | %% Unit Tests
51 | %%------------------------------------------------------------------------------
52 | -define(LOCAL_TESTS, [
53 | {local_test_fun, socks5_noauth, []},
54 | {local_test_fun, socks5_auth_succ, []},
55 | {local_test_fun, socks5_auth_fail, []},
56 | {local_test_fun, test_preserve_status_line, []},
57 | {local_test_fun, test_save_to_file_no_content_length, []},
58 | {local_test_fun, test_20122010, []},
59 | {local_test_fun, test_pipeline_head_timeout, []},
60 | {local_test_fun, test_head_transfer_encoding, []},
61 | {local_test_fun, test_head_response_with_body, []},
62 | {local_test_fun, test_303_response_with_a_body, []},
63 | {local_test_fun, test_303_response_with_no_body, []},
64 | {local_test_fun, test_binary_headers, []},
65 | {local_test_fun, test_dead_lb_pid, []},
66 | {local_test_fun, test_retry_of_requests, []},
67 | {local_test_fun, verify_chunked_streaming, []},
68 | {local_test_fun, test_chunked_streaming_once, []},
69 | {local_test_fun, test_generate_body_0, []}
70 | ]).
71 |
72 | -define(TEST_LIST, [{"http://intranet/messenger", get},
73 | {"http://www.google.co.uk", get},
74 | {"http://www.google.com", get},
75 | {"http://www.google.com", options},
76 | {"https://mail.google.com", get},
77 | {"http://www.sun.com", get},
78 | {"http://www.oracle.com", get},
79 | {"http://www.bbc.co.uk", get},
80 | {"http://www.bbc.co.uk", trace},
81 | {"http://www.bbc.co.uk", options},
82 | {"http://yaws.hyber.org", get},
83 | {"http://jigsaw.w3.org/HTTP/ChunkedScript", get},
84 | {"http://jigsaw.w3.org/HTTP/TE/foo.txt", get},
85 | {"http://jigsaw.w3.org/HTTP/TE/bar.txt", get},
86 | {"http://jigsaw.w3.org/HTTP/connection.html", get},
87 | {"http://jigsaw.w3.org/HTTP/cc.html", get},
88 | {"http://jigsaw.w3.org/HTTP/cc-private.html", get},
89 | {"http://jigsaw.w3.org/HTTP/cc-proxy-revalidate.html", get},
90 | {"http://jigsaw.w3.org/HTTP/cc-nocache.html", get},
91 | {"http://jigsaw.w3.org/HTTP/h-content-md5.html", get},
92 | {"http://jigsaw.w3.org/HTTP/h-retry-after.html", get},
93 | {"http://jigsaw.w3.org/HTTP/h-retry-after-date.html", get},
94 | {"http://jigsaw.w3.org/HTTP/neg", get},
95 | {"http://jigsaw.w3.org/HTTP/negbad", get},
96 | {"http://jigsaw.w3.org/HTTP/400/toolong/", get},
97 | {"http://jigsaw.w3.org/HTTP/300/", get},
98 | {"http://jigsaw.w3.org/HTTP/Basic/", get, [{basic_auth, {"guest", "guest"}}]},
99 | {"http://jigsaw.w3.org/HTTP/CL/", get},
100 | {"http://www.httpwatch.com/httpgallery/chunked/", get},
101 | {"https://github.com", get, [{ssl_options, [{depth, 2}]}]}
102 | ]).
103 |
104 | socks5_noauth() ->
105 | case ibrowse:send_req("http://localhost:8181/success", [], get, [],
106 | [{socks5_host, "localhost"}, {socks5_port, 8282}], 2000) of
107 | {ok, "200", _, _} ->
108 | success;
109 | Err ->
110 | Err
111 | end.
112 |
113 | socks5_auth_succ() ->
114 | case ibrowse:send_req("http://localhost:8181/success", [], get, [],
115 | [{socks5_host, "localhost"}, {socks5_port, 8383},
116 | {socks5_user, <<"user">>}, {socks5_password, <<"password">>}], 2000) of
117 | {ok, "200", _, _} ->
118 | success;
119 | Err ->
120 | Err
121 | end.
122 |
123 | socks5_auth_fail() ->
124 | case ibrowse:send_req("http://localhost:8181/success", [], get, [],
125 | [{socks5_host, "localhost"}, {socks5_port, 8282},
126 | {socks5_user, <<"user">>}, {socks5_password, <<"wrong_password">>}], 2000) of
127 | {error,{conn_failed,{error,unacceptable}}} ->
128 | success;
129 | Err ->
130 | Err
131 | end.
132 |
133 | test_stream_once(Url, Method, Options) ->
134 | test_stream_once(Url, Method, Options, 5000).
135 |
136 | test_stream_once(Url, Method, Options, Timeout) ->
137 | case ibrowse:send_req(Url, [], Method, [], [{stream_to, {self(), once}} | Options], Timeout) of
138 | {ibrowse_req_id, Req_id} ->
139 | case ibrowse:stream_next(Req_id) of
140 | ok ->
141 | test_stream_once(Req_id);
142 | Err ->
143 | Err
144 | end;
145 | Err ->
146 | Err
147 | end.
148 |
149 | test_stream_once(Req_id) ->
150 | receive
151 | {ibrowse_async_headers, Req_id, StatCode, Headers} ->
152 | io:format("Recvd headers~n~p~n", [{ibrowse_async_headers, Req_id, StatCode, Headers}]),
153 | case ibrowse:stream_next(Req_id) of
154 | ok ->
155 | test_stream_once(Req_id);
156 | Err ->
157 | Err
158 | end;
159 | {ibrowse_async_response, Req_id, {error, Err}} ->
160 | io:format("Recvd error: ~p~n", [Err]);
161 | {ibrowse_async_response, Req_id, Body_1} ->
162 | io:format("Recvd body part: ~n~p~n", [{ibrowse_async_response, Req_id, Body_1}]),
163 | case ibrowse:stream_next(Req_id) of
164 | ok ->
165 | test_stream_once(Req_id);
166 | Err ->
167 | Err
168 | end;
169 | {ibrowse_async_response_end, Req_id} ->
170 | ok
171 | end.
172 |
173 | %% Use ibrowse:set_max_sessions/3 and ibrowse:set_max_pipeline_size/3 to
174 | %% tweak settings before running the load test. The defaults are 10 and 10.
175 | load_test_(Url, NumWorkers, NumReqsPerWorker) when is_list(Url),
176 | is_integer(NumWorkers),
177 | is_integer(NumReqsPerWorker),
178 | NumWorkers > 0,
179 | NumReqsPerWorker > 0 ->
180 | proc_lib:spawn(?MODULE, send_reqs_1, [Url, NumWorkers, NumReqsPerWorker]).
181 |
182 | send_reqs_1(Url, NumWorkers, NumReqsPerWorker) ->
183 | Start_time = os:timestamp(),
184 | ets:new(pid_table, [named_table, public]),
185 | ets:new(ibrowse_test_results, [named_table, public]),
186 | ets:new(ibrowse_errors, [named_table, public, ordered_set]),
187 | ets:new(ibrowse_counter, [named_table, public, ordered_set]),
188 | ets:insert(ibrowse_counter, {req_id, 1}),
189 | init_results(),
190 | process_flag(trap_exit, true),
191 | log_msg("Starting spawning of workers...~n", []),
192 | spawn_workers(Url, NumWorkers, NumReqsPerWorker),
193 | log_msg("Finished spawning workers...~n", []),
194 | do_wait(Url),
195 | End_time = os:timestamp(),
196 | log_msg("All workers are done...~n", []),
197 | log_msg("ibrowse_test_results table: ~n~p~n", [ets:tab2list(ibrowse_test_results)]),
198 | log_msg("Start time: ~1000.p~n", [calendar:now_to_local_time(Start_time)]),
199 | log_msg("End time : ~1000.p~n", [calendar:now_to_local_time(End_time)]),
200 | Elapsed_time_secs = trunc(timer:now_diff(End_time, Start_time) / 1000000),
201 | log_msg("Elapsed : ~p~n", [Elapsed_time_secs]),
202 | log_msg("Reqs/sec : ~p~n", [round(trunc((NumWorkers*NumReqsPerWorker) / Elapsed_time_secs))]),
203 | dump_errors().
204 |
205 | init_results() ->
206 | ets:insert(ibrowse_test_results, {crash, 0}),
207 | ets:insert(ibrowse_test_results, {send_failed, 0}),
208 | ets:insert(ibrowse_test_results, {other_error, 0}),
209 | ets:insert(ibrowse_test_results, {success, 0}),
210 | ets:insert(ibrowse_test_results, {retry_later, 0}),
211 | ets:insert(ibrowse_test_results, {trid_mismatch, 0}),
212 | ets:insert(ibrowse_test_results, {success_no_trid, 0}),
213 | ets:insert(ibrowse_test_results, {failed, 0}),
214 | ets:insert(ibrowse_test_results, {timeout, 0}),
215 | ets:insert(ibrowse_test_results, {req_id, 0}).
216 |
217 | spawn_workers(_Url, 0, _) ->
218 | ok;
219 | spawn_workers(Url, NumWorkers, NumReqsPerWorker) ->
220 | Pid = proc_lib:spawn_link(?MODULE, do_send_req, [Url, NumReqsPerWorker]),
221 | ets:insert(pid_table, {Pid, []}),
222 | spawn_workers(Url, NumWorkers - 1, NumReqsPerWorker).
223 |
224 | do_wait(Url) ->
225 | receive
226 | {'EXIT', _, normal} ->
227 | catch ibrowse:show_dest_status(Url),
228 | catch ibrowse:show_dest_status(),
229 | do_wait(Url);
230 | {'EXIT', Pid, Reason} ->
231 | ets:delete(pid_table, Pid),
232 | ets:insert(ibrowse_errors, {Pid, Reason}),
233 | ets:update_counter(ibrowse_test_results, crash, 1),
234 | do_wait(Url);
235 | Msg ->
236 | io:format("Recvd unknown message...~p~n", [Msg]),
237 | do_wait(Url)
238 | after 1000 ->
239 | case ets:info(pid_table, size) of
240 | 0 ->
241 | done;
242 | _ ->
243 | catch ibrowse:show_dest_status(Url),
244 | catch ibrowse:show_dest_status(),
245 | do_wait(Url)
246 | end
247 | end.
248 |
249 | do_send_req(Url, NumReqs) ->
250 | do_send_req_1(Url, NumReqs).
251 |
252 | do_send_req_1(_Url, 0) ->
253 | ets:delete(pid_table, self());
254 | do_send_req_1(Url, NumReqs) ->
255 | Counter = integer_to_list(ets:update_counter(ibrowse_test_results, req_id, 1)),
256 | case ibrowse:send_req(Url, [{"ib_req_id", Counter}], get, [], [], 10000) of
257 | {ok, _Status, Headers, _Body} ->
258 | case lists:keysearch("ib_req_id", 1, Headers) of
259 | {value, {_, Counter}} ->
260 | ets:update_counter(ibrowse_test_results, success, 1);
261 | {value, _} ->
262 | ets:update_counter(ibrowse_test_results, trid_mismatch, 1);
263 | false ->
264 | ets:update_counter(ibrowse_test_results, success_no_trid, 1)
265 | end;
266 | {error, req_timedout} ->
267 | ets:update_counter(ibrowse_test_results, timeout, 1);
268 | {error, send_failed} ->
269 | ets:update_counter(ibrowse_test_results, send_failed, 1);
270 | {error, retry_later} ->
271 | ets:update_counter(ibrowse_test_results, retry_later, 1);
272 | Err ->
273 | ets:insert(ibrowse_errors, {os:timestamp(), Err}),
274 | ets:update_counter(ibrowse_test_results, other_error, 1),
275 | ok
276 | end,
277 | do_send_req_1(Url, NumReqs-1).
278 |
279 | dump_errors() ->
280 | case ets:info(ibrowse_errors, size) of
281 | 0 ->
282 | ok;
283 | _ ->
284 | {A, B, C} = os:timestamp(),
285 | Filename = lists:flatten(
286 | io_lib:format("ibrowse_errors_~p_~p_~p.txt" , [A, B, C])),
287 | case file:open(Filename, [write, delayed_write, raw]) of
288 | {ok, Iod} ->
289 | dump_errors(ets:first(ibrowse_errors), Iod);
290 | Err ->
291 | io:format("failed to create file ~s. Reason: ~p~n", [Filename, Err]),
292 | ok
293 | end
294 | end.
295 |
296 | dump_errors('$end_of_table', Iod) ->
297 | file:close(Iod);
298 | dump_errors(Key, Iod) ->
299 | [{_, Term}] = ets:lookup(ibrowse_errors, Key),
300 | file:write(Iod, io_lib:format("~p~n", [Term])),
301 | dump_errors(ets:next(ibrowse_errors, Key), Iod).
302 |
303 | local_unit_tests() ->
304 | unit_tests([], ?LOCAL_TESTS).
305 |
306 | unit_tests() ->
307 | unit_tests([], ?TEST_LIST).
308 |
309 | unit_tests(Options, Test_list) ->
310 | error_logger:tty(false),
311 | application:start(crypto),
312 | application:start(asn1),
313 | application:start(public_key),
314 | application:start(ssl),
315 | (catch ibrowse_test_server:start_server(8181, tcp)),
316 | application:start(ibrowse),
317 | Options_1 = Options ++ [{connect_timeout, 5000}],
318 | Test_timeout = proplists:get_value(test_timeout, Options, 60000),
319 | {Pid, Ref} = erlang:spawn_monitor(?MODULE, unit_tests_1, [self(), Options_1, Test_list]),
320 | receive
321 | {done, Pid} ->
322 | ok;
323 | {'DOWN', Ref, _, _, Info} ->
324 | io:format("Test process crashed: ~p~n", [Info])
325 | after Test_timeout ->
326 | exit(Pid, kill),
327 | io:format("Timed out waiting for tests to complete~n", [])
328 | end,
329 | catch ibrowse_test_server:stop_server(8181),
330 | error_logger:tty(true),
331 | ok.
332 |
333 | unit_tests_1(Parent, Options, Test_list) ->
334 | lists:foreach(fun({local_test_fun, Fun_name, Args}) ->
335 | execute_req(local_test_fun, Fun_name, Args);
336 | ({Url, Method}) ->
337 | execute_req(Url, Method, Options);
338 | ({Url, Method, X_Opts}) ->
339 | execute_req(Url, Method, X_Opts ++ Options)
340 | end, Test_list),
341 | Parent ! {done, self()}.
342 |
343 | verify_chunked_streaming() ->
344 | verify_chunked_streaming([]).
345 |
346 | verify_chunked_streaming(Options) ->
347 | io:format("~nVerifying that chunked streaming is working...~n", []),
348 | Url = "http://www.httpwatch.com/httpgallery/chunked/",
349 | io:format(" URL: ~s~n", [Url]),
350 | io:format(" Fetching data without streaming...~n", []),
351 | Result_without_streaming = ibrowse:send_req(
352 | Url, [], get, [],
353 | [{response_format, binary} | Options]),
354 | io:format(" Fetching data with streaming as list...~n", []),
355 | Async_response_list = do_async_req_list(
356 | Url, get, [{response_format, list} | Options]),
357 | io:format(" Fetching data with streaming as binary...~n", []),
358 | Async_response_bin = do_async_req_list(
359 | Url, get, [{response_format, binary} | Options]),
360 | io:format(" Fetching data with streaming as binary, {active, once}...~n", []),
361 | Async_response_bin_once = do_async_req_list(
362 | Url, get, [once, {response_format, binary} | Options]),
363 | Res1 = compare_responses(Result_without_streaming, Async_response_list, Async_response_bin),
364 | Res2 = compare_responses(Result_without_streaming, Async_response_list, Async_response_bin_once),
365 | case {Res1, Res2} of
366 | {success, success} ->
367 | io:format(" Chunked streaming working~n", []),
368 | success;
369 | _ ->
370 | ok
371 | end.
372 |
373 | test_chunked_streaming_once() ->
374 | test_chunked_streaming_once([]).
375 |
376 | test_chunked_streaming_once(Options) ->
377 | io:format("~nTesting chunked streaming with the {stream_to, {Pid, once}} option...~n", []),
378 | Url = "http://www.httpwatch.com/httpgallery/chunked/",
379 | io:format(" URL: ~s~n", [Url]),
380 | io:format(" Fetching data with streaming as binary, {active, once}...~n", []),
381 | case do_async_req_list(Url, get, [once, {response_format, binary} | Options]) of
382 | {ok, _, _, _} ->
383 | success;
384 | Err ->
385 | io:format(" Fail: ~p~n", [Err])
386 | end.
387 |
388 | compare_responses({ok, St_code, _, Body}, {ok, St_code, _, Body}, {ok, St_code, _, Body}) ->
389 | success;
390 | compare_responses({ok, St_code, _, Body_1}, {ok, St_code, _, Body_2}, {ok, St_code, _, Body_3}) ->
391 | case Body_1 of
392 | Body_2 ->
393 | io:format("Body_1 and Body_2 match~n", []);
394 | Body_3 ->
395 | io:format("Body_1 and Body_3 match~n", []);
396 | _ when Body_2 == Body_3 ->
397 | io:format("Body_2 and Body_3 match~n", []);
398 | _ ->
399 | io:format("All three bodies are different!~n", [])
400 | end,
401 | io:format("Body_1 -> ~p~n", [Body_1]),
402 | io:format("Body_2 -> ~p~n", [Body_2]),
403 | io:format("Body_3 -> ~p~n", [Body_3]),
404 | fail_bodies_mismatch;
405 | compare_responses(R1, R2, R3) ->
406 | io:format("R1 -> ~p~n", [R1]),
407 | io:format("R2 -> ~p~n", [R2]),
408 | io:format("R3 -> ~p~n", [R3]),
409 | fail.
410 |
411 | %% do_async_req_list(Url) ->
412 | %% do_async_req_list(Url, get).
413 |
414 | %% do_async_req_list(Url, Method) ->
415 | %% do_async_req_list(Url, Method, [{stream_to, self()},
416 | %% {stream_chunk_size, 1000}]).
417 |
418 | do_async_req_list(Url, Method, Options) ->
419 | {Pid,_} = erlang:spawn_monitor(?MODULE, i_do_async_req_list,
420 | [self(), Url, Method,
421 | Options ++ [{stream_chunk_size, 1000}]]),
422 | %% io:format("Spawned process ~p~n", [Pid]),
423 | wait_for_resp(Pid).
424 |
425 | wait_for_resp(Pid) ->
426 | receive
427 | {async_result, Pid, Res} ->
428 | Res;
429 | {async_result, Other_pid, _} ->
430 | io:format("~p: Waiting for result from ~p: got from ~p~n", [self(), Pid, Other_pid]),
431 | wait_for_resp(Pid);
432 | {'DOWN', _, _, Pid, Reason} ->
433 | {'EXIT', Reason};
434 | {'DOWN', _, _, _, _} ->
435 | wait_for_resp(Pid);
436 | {'EXIT', _, normal} ->
437 | wait_for_resp(Pid);
438 | Msg ->
439 | io:format("Recvd unknown message: ~p~n", [Msg]),
440 | wait_for_resp(Pid)
441 | after 100000 ->
442 | {error, timeout}
443 | end.
444 |
445 | i_do_async_req_list(Parent, Url, Method, Options) ->
446 | Options_1 = case lists:member(once, Options) of
447 | true ->
448 | [{stream_to, {self(), once}} | (Options -- [once])];
449 | false ->
450 | [{stream_to, self()} | Options]
451 | end,
452 | Res = ibrowse:send_req(Url, [], Method, [], Options_1),
453 | case Res of
454 | {ibrowse_req_id, Req_id} ->
455 | Result = wait_for_async_resp(Req_id, Options, undefined, undefined, []),
456 | Parent ! {async_result, self(), Result};
457 | Err ->
458 | Parent ! {async_result, self(), Err}
459 | end.
460 |
461 | wait_for_async_resp(Req_id, Options, Acc_Stat_code, Acc_Headers, Body) ->
462 | receive
463 | {ibrowse_async_headers, Req_id, StatCode, Headers} ->
464 | %% io:format("Recvd headers...~n", []),
465 | maybe_stream_next(Req_id, Options),
466 | wait_for_async_resp(Req_id, Options, StatCode, Headers, Body);
467 | {ibrowse_async_response_end, Req_id} ->
468 | %% io:format("Recvd end of response.~n", []),
469 | Body_1 = list_to_binary(lists:reverse(Body)),
470 | {ok, Acc_Stat_code, Acc_Headers, Body_1};
471 | {ibrowse_async_response, Req_id, Data} ->
472 | maybe_stream_next(Req_id, Options),
473 | %% io:format("Recvd data...~n", []),
474 | wait_for_async_resp(Req_id, Options, Acc_Stat_code, Acc_Headers, [Data | Body]);
475 | {ibrowse_async_response, Req_id, {error, _} = Err} ->
476 | {ok, Acc_Stat_code, Acc_Headers, Err};
477 | Err ->
478 | {ok, Acc_Stat_code, Acc_Headers, Err}
479 | after 10000 ->
480 | {timeout, Acc_Stat_code, Acc_Headers, Body}
481 | end.
482 |
483 | maybe_stream_next(Req_id, Options) ->
484 | case lists:member(once, Options) of
485 | true ->
486 | ibrowse:stream_next(Req_id);
487 | false ->
488 | ok
489 | end.
490 |
491 | execute_req(local_test_fun, Method, Args) ->
492 | reset_ibrowse(),
493 | Result = (catch apply(?MODULE, Method, Args)),
494 | io:format(" ~-54.54w: ", [Method]),
495 | io:format("~p~n", [Result]);
496 | execute_req(Url, Method, Options) ->
497 | io:format("~7.7w, ~50.50s: ", [Method, Url]),
498 | Result = (catch ibrowse:send_req(Url, [], Method, [], Options)),
499 | case Result of
500 | {ok, SCode, _H, _B} ->
501 | io:format("Status code: ~p~n", [SCode]);
502 | Err ->
503 | io:format("~p~n", [Err])
504 | end.
505 |
506 | log_msg(Fmt, Args) ->
507 | io:format("~s -- " ++ Fmt,
508 | [ibrowse_lib:printable_date() | Args]).
509 |
510 | %%------------------------------------------------------------------------------
511 | %% Test what happens when the response to a HEAD request is a
512 | %% Chunked-Encoding response with a non-empty body. Issue #67 on
513 | %% Github
514 | %% ------------------------------------------------------------------------------
515 | test_head_transfer_encoding() ->
516 | clear_msg_q(),
517 | test_head_transfer_encoding("http://localhost:8181/ibrowse_head_test").
518 |
519 | test_head_transfer_encoding(Url) ->
520 | case ibrowse:send_req(Url, [], head) of
521 | {ok, "200", _, _} ->
522 | success;
523 | Res ->
524 | {test_failed, Res}
525 | end.
526 |
527 | %%------------------------------------------------------------------------------
528 | %% Test what happens when the response to a HEAD request is a
529 | %% Chunked-Encoding response with a non-empty body. Issue #67 on
530 | %% Github
531 | %% ------------------------------------------------------------------------------
532 | test_binary_headers() ->
533 | clear_msg_q(),
534 | test_binary_headers("http://localhost:8181/ibrowse_echo_header").
535 |
536 | test_binary_headers(Url) ->
537 | case ibrowse:send_req(Url, [{<<"x-binary">>, <<"x-header">>}], get) of
538 | {ok, "200", Headers, _} ->
539 | case proplists:get_value("x-binary", Headers) of
540 | "x-header" ->
541 | success;
542 | V ->
543 | {fail, V}
544 | end;
545 | Res ->
546 | {test_failed, Res}
547 | end.
548 |
549 | %%------------------------------------------------------------------------------
550 | %% Test what happens when the response to a HEAD request is a
551 | %% Chunked-Encoding response with a non-empty body. Issue #67 on
552 | %% Github
553 | %% ------------------------------------------------------------------------------
554 | test_head_response_with_body() ->
555 | clear_msg_q(),
556 | test_head_response_with_body("http://localhost:8181/ibrowse_head_transfer_enc").
557 |
558 | test_head_response_with_body(Url) ->
559 | case ibrowse:send_req(Url, [], head, [], [{workaround, head_response_with_body}]) of
560 | {ok, "400", _, _} ->
561 | success;
562 | Res ->
563 | {test_failed, Res}
564 | end.
565 |
566 | %%------------------------------------------------------------------------------
567 | %% Test what happens when a 303 response has no body
568 | %% Github issue #97
569 | %% ------------------------------------------------------------------------------
570 | test_303_response_with_no_body() ->
571 | clear_msg_q(),
572 | test_303_response_with_no_body("http://localhost:8181/ibrowse_303_no_body_test").
573 |
574 | test_303_response_with_no_body(Url) ->
575 | ibrowse:add_config([{allow_303_with_no_body, true}]),
576 | case ibrowse:send_req(Url, [], post) of
577 | {ok, "303", _, _} ->
578 | success;
579 | Res ->
580 | {test_failed, Res}
581 | end.
582 |
583 | %% Make sure we don't break requests that do have a body.
584 | test_303_response_with_a_body() ->
585 | clear_msg_q(),
586 | test_303_response_with_no_body("http://localhost:8181/ibrowse_303_with_body_test").
587 |
588 | test_303_response_with_a_body(Url) ->
589 | ibrowse:add_config([{allow_303_with_no_body, true}]),
590 | case ibrowse:send_req(Url, [], post) of
591 | {ok, "303", _, "abcde"} ->
592 | success;
593 | Res ->
594 | {test_failed, Res}
595 | end.
596 |
597 | %% Test that the 'preserve_status_line' option works as expected
598 | test_preserve_status_line() ->
599 | case ibrowse:send_req("http://localhost:8181/ibrowse_preserve_status_line", [], get, [],
600 | [{preserve_status_line, true}]) of
601 | {ok, "200", [{ibrowse_status_line,<<"HTTP/1.1 200 OKBlah">>} | _], _} ->
602 | success;
603 | Res ->
604 | {test_failed, Res}
605 | end.
606 |
607 | %%------------------------------------------------------------------------------
608 | %% Test that when the save_response_to_file option is used with a server which
609 | %% does not send the Content-Length header, the response is saved correctly to
610 | %% a file
611 | %%------------------------------------------------------------------------------
612 | test_save_to_file_no_content_length() ->
613 | clear_msg_q(),
614 | {{Y, M, D}, {H, Mi, S}} = calendar:local_time(),
615 | Test_file = filename:join
616 | ([".",
617 | lists:flatten(
618 | io_lib:format("test_save_to_file_no_content_length_~p~p~p_~p~p~p.txt", [Y, M, D, H, Mi, S]))]),
619 | try
620 | case ibrowse:send_req("http://localhost:8181/ibrowse_send_file_conn_close", [], get, [],
621 | [{save_response_to_file, Test_file}]) of
622 | {ok, "200", _, {file, Test_file}} ->
623 | success;
624 | Res ->
625 | {test_failed, Res}
626 | end
627 | after
628 | file:delete(Test_file)
629 | end.
630 |
631 | %%------------------------------------------------------------------------------
632 | %% Test that retry of requests happens correctly, and that ibrowse doesn't retry
633 | %% if there is not enough time left
634 | %%------------------------------------------------------------------------------
635 | test_retry_of_requests() ->
636 | clear_msg_q(),
637 | test_retry_of_requests("http://localhost:8181/ibrowse_handle_one_request_only_with_delay").
638 |
639 | test_retry_of_requests(Url) ->
640 | reset_ibrowse(),
641 | Timeout_1 = 2050,
642 | Res_1 = test_retry_of_requests(Url, Timeout_1),
643 | case lists:filter(fun({_Pid, {ok, "200", _, _}}) ->
644 | true;
645 | (_) -> false
646 | end, Res_1) of
647 | [_|_] = X ->
648 | Res_1_1 = Res_1 -- X,
649 | case lists:all(
650 | fun({_Pid, {error, retry_later}}) ->
651 | true;
652 | (_) ->
653 | false
654 | end, Res_1_1) of
655 | true ->
656 | ok;
657 | false ->
658 | exit({failed, Timeout_1, Res_1})
659 | end;
660 | _ ->
661 | exit({failed, Timeout_1, Res_1})
662 | end,
663 | Timeout_2 = 2200,
664 | Res_2 = test_retry_of_requests(Url, Timeout_2),
665 | case lists:filter(fun({_Pid, {ok, "200", _, _}}) ->
666 | true;
667 | (_) -> false
668 | end, Res_2) of
669 | [_|_] = Res_2_X ->
670 | Res_2_1 = Res_2 -- Res_2_X,
671 | case lists:all(
672 | fun({_Pid, {error, X_err_2}}) ->
673 | (X_err_2 == retry_later) orelse (X_err_2 == req_timedout);
674 | (_) ->
675 | false
676 | end, Res_2_1) of
677 | true ->
678 | ok;
679 | false ->
680 | exit({failed, {?MODULE, ?LINE}, Timeout_2, Res_2})
681 | end;
682 | _ ->
683 | exit({failed, {?MODULE, ?LINE}, Timeout_2, Res_2})
684 | end,
685 | success.
686 |
687 | test_retry_of_requests(Url, Timeout) ->
688 | #url{host = Host, port = Port} = ibrowse_lib:parse_url(Url),
689 | ibrowse:set_max_sessions(Host, Port, 1),
690 | Parent = self(),
691 | Pids = lists:map(fun(_) ->
692 | spawn(fun() ->
693 | Res = (catch ibrowse:send_req(Url, [], get, [], [], Timeout)),
694 | Parent ! {self(), Res}
695 | end)
696 | end, lists:seq(1,10)),
697 | accumulate_worker_resp(Pids).
698 |
699 | %%------------------------------------------------------------------------------
700 | %% Test what happens when the request at the head of a pipeline times out
701 | %%------------------------------------------------------------------------------
702 | test_pipeline_head_timeout() ->
703 | clear_msg_q(),
704 | test_pipeline_head_timeout("http://localhost:8181/ibrowse_inac_timeout_test").
705 |
706 | test_pipeline_head_timeout(Url) ->
707 | {ok, Pid} = ibrowse:spawn_worker_process(Url),
708 | Fixed_timeout = 2000,
709 | Test_parent = self(),
710 | Fun = fun({fixed, Timeout}) ->
711 | X_pid = spawn(fun() ->
712 | do_test_pipeline_head_timeout(Url, Pid, Test_parent, Timeout)
713 | end),
714 | %% io:format("Pid ~p with a fixed timeout~n", [X_pid]),
715 | X_pid;
716 | (Timeout_mult) ->
717 | Timeout = Fixed_timeout + Timeout_mult*1000,
718 | X_pid = spawn(fun() ->
719 | do_test_pipeline_head_timeout(Url, Pid, Test_parent, Timeout)
720 | end),
721 | %% io:format("Pid ~p with a timeout of ~p~n", [X_pid, Timeout]),
722 | X_pid
723 | end,
724 | Pids = [Fun(X) || X <- [{fixed, Fixed_timeout} | lists:seq(1,10)]],
725 | Result = accumulate_worker_resp(Pids),
726 | case lists:all(fun({_, X_res}) ->
727 | (X_res == {error,req_timedout}) orelse (X_res == {error, connection_closed})
728 | end, Result) of
729 | true ->
730 | success;
731 | false ->
732 | {test_failed, Result}
733 | end.
734 |
735 | do_test_pipeline_head_timeout(Url, Pid, Test_parent, Req_timeout) ->
736 | Resp = ibrowse:send_req_direct(
737 | Pid,
738 | Url,
739 | [], get, [],
740 | [{socket_options,[{keepalive,true}]},
741 | {inactivity_timeout,180000},
742 | {connect_timeout,180000}], Req_timeout),
743 | Test_parent ! {self(), Resp}.
744 |
745 | accumulate_worker_resp(Pids) ->
746 | accumulate_worker_resp(Pids, []).
747 |
748 | accumulate_worker_resp([_ | _] = Pids, Acc) ->
749 | receive
750 | {Pid, Res} when is_pid(Pid) ->
751 | accumulate_worker_resp(Pids -- [Pid], [{Pid, Res} | Acc]);
752 | Err ->
753 | io:format("Received unexpected: ~p~n", [Err])
754 | end;
755 | accumulate_worker_resp([], Acc) ->
756 | lists:reverse(Acc).
757 |
758 | clear_msg_q() ->
759 | receive
760 | _ ->
761 | clear_msg_q()
762 | after 0 ->
763 | ok
764 | end.
765 | %%------------------------------------------------------------------------------
766 | %%
767 | %%------------------------------------------------------------------------------
768 |
769 | test_20122010() ->
770 | test_20122010("http://localhost:8181").
771 |
772 | test_20122010(Url) ->
773 | {ok, Pid} = ibrowse:spawn_worker_process(Url),
774 | Expected_resp = <<"1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16-17-18-19-20-21-22-23-24-25-26-27-28-29-30-31-32-33-34-35-36-37-38-39-40-41-42-43-44-45-46-47-48-49-50-51-52-53-54-55-56-57-58-59-60-61-62-63-64-65-66-67-68-69-70-71-72-73-74-75-76-77-78-79-80-81-82-83-84-85-86-87-88-89-90-91-92-93-94-95-96-97-98-99-100">>,
775 | Test_parent = self(),
776 | Fun = fun() ->
777 | do_test_20122010(Url, Pid, Expected_resp, Test_parent)
778 | end,
779 | Pids = [erlang:spawn_monitor(Fun) || _ <- lists:seq(1,10)],
780 | wait_for_workers(Pids).
781 |
782 | wait_for_workers([{Pid, _Ref} | Pids]) ->
783 | receive
784 | {Pid, success} ->
785 | wait_for_workers(Pids)
786 | after 60000 ->
787 | test_failed
788 | end;
789 | wait_for_workers([]) ->
790 | success.
791 |
792 | do_test_20122010(Url, Pid, Expected_resp, Test_parent) ->
793 | do_test_20122010(10, Url, Pid, Expected_resp, Test_parent).
794 |
795 | do_test_20122010(0, _Url, _Pid, _Expected_resp, Test_parent) ->
796 | Test_parent ! {self(), success};
797 | do_test_20122010(Rem_count, Url, Pid, Expected_resp, Test_parent) ->
798 | {ibrowse_req_id, Req_id} = ibrowse:send_req_direct(
799 | Pid,
800 | Url ++ "/ibrowse_stream_once_chunk_pipeline_test",
801 | [], get, [],
802 | [{stream_to, {self(), once}},
803 | {inactivity_timeout, 10000},
804 | {include_ibrowse_req_id, true}]),
805 | do_trace("~p -- sent request ~1000.p~n", [self(), Req_id]),
806 | Req_id_str = lists:flatten(io_lib:format("~1000.p",[Req_id])),
807 | receive
808 | {ibrowse_async_headers, Req_id, "200", Headers} ->
809 | case lists:keysearch("x-ibrowse-request-id", 1, Headers) of
810 | {value, {_, Req_id_str}} ->
811 | ok;
812 | {value, {_, Req_id_1}} ->
813 | do_trace("~p -- Sent req-id: ~1000.p. Recvd: ~1000.p~n",
814 | [self(), Req_id, Req_id_1]),
815 | exit(req_id_mismatch)
816 | end
817 | after 5000 ->
818 | do_trace("~p -- response headers not received~n", [self()]),
819 | exit({timeout, test_failed})
820 | end,
821 | do_trace("~p -- response headers received~n", [self()]),
822 | ok = ibrowse:stream_next(Req_id),
823 | case do_test_20122010_1(Expected_resp, Req_id, []) of
824 | true ->
825 | do_test_20122010(Rem_count - 1, Url, Pid, Expected_resp, Test_parent);
826 | false ->
827 | Test_parent ! {self(), failed}
828 | end.
829 |
830 | do_test_20122010_1(Expected_resp, Req_id, Acc) ->
831 | receive
832 | {ibrowse_async_response, Req_id, Body_part} ->
833 | ok = ibrowse:stream_next(Req_id),
834 | do_test_20122010_1(Expected_resp, Req_id, [Body_part | Acc]);
835 | {ibrowse_async_response_end, Req_id} ->
836 | Acc_1 = list_to_binary(lists:reverse(Acc)),
837 | Result = Acc_1 == Expected_resp,
838 | do_trace("~p -- End of response. Result: ~p~n", [self(), Result]),
839 | Result
840 | after 1000 ->
841 | exit({timeout, test_failed})
842 | end.
843 |
844 | %%------------------------------------------------------------------------------
845 | %% Test requests where body is generated using a Fun
846 | %%------------------------------------------------------------------------------
847 | test_generate_body_0() ->
848 | Tid = ets:new(ibrowse_test_state, [public]),
849 | try
850 | Body_1 = <<"Part 1 of the body">>,
851 | Body_2 = <<"Part 2 of the body\r\n">>,
852 | Size = size(Body_1) + size(Body_2),
853 | Body = list_to_binary([Body_1, Body_2]),
854 | Fun = fun() ->
855 | case ets:lookup(Tid, body_gen_state) of
856 | [] ->
857 | ets:insert(Tid, {body_gen_state, 1}),
858 | {ok, Body_1};
859 | [{_, 1}]->
860 | ets:insert(Tid, {body_gen_state, 2}),
861 | {ok, Body_2};
862 | [{_, 2}] ->
863 | eof
864 | end
865 | end,
866 | case ibrowse:send_req("http://localhost:8181/echo_body",
867 | [{"Content-Length", Size}],
868 | post,
869 | Fun,
870 | [{response_format, binary},
871 | {http_vsn, {1,1}}]) of
872 | {ok, "200", _, Body} ->
873 | success;
874 | Err ->
875 | io:format("Test failed : ~p~n", [Err]),
876 | {test_failed, Err}
877 | end
878 | after
879 | ets:delete(Tid)
880 | end.
881 |
882 | %%------------------------------------------------------------------------------
883 | %% Test that when an lb process dies, its entry is removed from the ibrowse_lb
884 | %% table by the next requestor and replaced with a new process
885 | %%------------------------------------------------------------------------------
886 | test_dead_lb_pid() ->
887 | {Host, Port} = {"localhost", 8181},
888 | Url = "http://" ++ Host ++ ":" ++ integer_to_list(Port),
889 | {ok, "200", _, _} = ibrowse:send_req(Url, [], get),
890 | [{lb_pid, {Host, Port}, Pid, _}] = ets:lookup(ibrowse_lb, {Host, Port}),
891 | true = exit(Pid, kill),
892 | false = is_process_alive(Pid),
893 | {ok, "200", _, _} = ibrowse:send_req(Url, [], get),
894 | [{lb_pid, {Host, Port}, NewPid, _}] = ets:lookup(ibrowse_lb, {Host, Port}),
895 | true = NewPid /= Pid,
896 | true = is_process_alive(NewPid),
897 | success.
898 |
899 | do_trace(Fmt, Args) ->
900 | do_trace(get(my_trace_flag), Fmt, Args).
901 |
902 | do_trace(true, Fmt, Args) ->
903 | io:format("~s -- " ++ Fmt, [ibrowse_lib:printable_date() | Args]);
904 | do_trace(_, _, _) ->
905 | ok.
906 |
907 | reset_ibrowse() ->
908 | application:stop(ibrowse),
909 | application:start(ibrowse).
910 |
--------------------------------------------------------------------------------
/test/ibrowse_test_server.erl:
--------------------------------------------------------------------------------
1 | %%% File : ibrowse_test_server.erl
2 | %%% Author : Chandrashekhar Mullaparthi
3 | %%% Description : A server to simulate various test scenarios
4 | %%% Created : 17 Oct 2010 by Chandrashekhar Mullaparthi
5 |
6 | -module(ibrowse_test_server).
7 | -export([
8 | start_server/0,
9 | start_server/2,
10 | stop_server/1,
11 | get_conn_pipeline_depth/0
12 | ]).
13 |
14 | -ifdef(new_rand).
15 | -define(RAND, rand).
16 | -else.
17 | -define(RAND, random).
18 | -endif.
19 |
20 | -record(request, {method, uri, version, headers = [], body = [], state}).
21 |
22 | -define(dec2hex(X), erlang:integer_to_list(X, 16)).
23 | -define(ACCEPT_TIMEOUT_MS, 10000).
24 | -define(CONN_PIPELINE_DEPTH, conn_pipeline_depth).
25 |
26 | start_server() ->
27 | start_server(8181, tcp).
28 |
29 | start_server(Port, Sock_type) ->
30 | Fun = fun() ->
31 | Proc_name = server_proc_name(Port),
32 | case whereis(Proc_name) of
33 | undefined ->
34 | register(Proc_name, self()),
35 | ets:new(?CONN_PIPELINE_DEPTH, [named_table, public, set]),
36 | case do_listen(Sock_type, Port, [{active, false},
37 | {reuseaddr, true},
38 | {nodelay, true},
39 | {packet, http}]) of
40 | {ok, Sock} ->
41 | do_trace("Server listening on port: ~p~n", [Port]),
42 | accept_loop(Sock, Sock_type);
43 | Err ->
44 | erlang:error(
45 | lists:flatten(
46 | io_lib:format(
47 | "Failed to start server on port ~p. ~p~n",
48 | [Port, Err]))),
49 | exit({listen_error, Err})
50 | end;
51 | _X ->
52 | ok
53 | end
54 | end,
55 | spawn_link(Fun),
56 | ibrowse_socks_server:start(8282, 0), %% No auth
57 | ibrowse_socks_server:start(8383, 2). %% Username/Password auth
58 |
59 | stop_server(Port) ->
60 | catch server_proc_name(Port) ! stop,
61 | ibrowse_socks_server:stop(8282),
62 | ibrowse_socks_server:stop(8383),
63 | timer:sleep(2000), % wait for server to receive msg and unregister
64 | ok.
65 |
66 | get_conn_pipeline_depth() ->
67 | ets:tab2list(?CONN_PIPELINE_DEPTH).
68 |
69 | server_proc_name(Port) ->
70 | list_to_atom("ibrowse_test_server_"++integer_to_list(Port)).
71 |
72 | do_listen(tcp, Port, Opts) ->
73 | gen_tcp:listen(Port, Opts);
74 | do_listen(ssl, Port, Opts) ->
75 | application:start(crypto),
76 | application:start(ssl),
77 | ssl:listen(Port, Opts).
78 |
79 | -ifdef(OTP_RELEASE).
80 |
81 | do_accept(tcp, Listen_sock) ->
82 | gen_tcp:accept(Listen_sock, ?ACCEPT_TIMEOUT_MS);
83 | do_accept(ssl, Listen_sock) ->
84 | ssl:handshake(Listen_sock, ?ACCEPT_TIMEOUT_MS).
85 |
86 | -else.
87 |
88 | do_accept(tcp, Listen_sock) ->
89 | gen_tcp:accept(Listen_sock, ?ACCEPT_TIMEOUT_MS);
90 | do_accept(ssl, Listen_sock) ->
91 | ssl:ssl_accept(Listen_sock, ?ACCEPT_TIMEOUT_MS).
92 |
93 | -endif.
94 |
95 | accept_loop(Sock, Sock_type) ->
96 | case do_accept(Sock_type, Sock) of
97 | {ok, Conn} ->
98 | Pid = spawn_link(fun() -> connection(Conn, Sock_type) end),
99 | set_controlling_process(Conn, Sock_type, Pid),
100 | accept_loop(Sock, Sock_type);
101 | {error, timeout} ->
102 | receive
103 | stop ->
104 | ok
105 | after 10 ->
106 | accept_loop(Sock, Sock_type)
107 | end;
108 | Err ->
109 | Err
110 | end.
111 |
112 | connection(Conn, Sock_type) ->
113 | catch ets:insert(?CONN_PIPELINE_DEPTH, {self(), 0}),
114 | try
115 | inet:setopts(Conn, [{packet, http}, {active, true}]),
116 | server_loop(Conn, Sock_type, #request{})
117 | after
118 | catch ets:delete(?CONN_PIPELINE_DEPTH, self())
119 | end.
120 |
121 | set_controlling_process(Sock, tcp, Pid) ->
122 | gen_tcp:controlling_process(Sock, Pid);
123 | set_controlling_process(Sock, ssl, Pid) ->
124 | ssl:controlling_process(Sock, Pid).
125 |
126 | setopts(Sock, tcp, Opts) ->
127 | inet:setopts(Sock, Opts);
128 | setopts(Sock, ssl, Opts) ->
129 | ssl:setopts(Sock, Opts).
130 |
131 | server_loop(Sock, Sock_type, #request{headers = Headers} = Req) ->
132 | receive
133 | {http, Sock, {http_request, HttpMethod, HttpUri, HttpVersion}} ->
134 | catch ets:update_counter(?CONN_PIPELINE_DEPTH, self(), 1),
135 | server_loop(Sock, Sock_type, Req#request{method = HttpMethod,
136 | uri = HttpUri,
137 | version = HttpVersion});
138 | {http, Sock, {http_header, _, _, _, _} = H} ->
139 | server_loop(Sock, Sock_type, Req#request{headers = [H | Headers]});
140 | {http, Sock, http_eoh} ->
141 | case process_request(Sock, Sock_type, Req) of
142 | close_connection ->
143 | gen_tcp:shutdown(Sock, read_write);
144 | not_done ->
145 | ok;
146 | collect_body ->
147 | server_loop(Sock, Sock_type, Req#request{state = collect_body});
148 | _ ->
149 | catch ets:update_counter(?CONN_PIPELINE_DEPTH, self(), -1)
150 | end,
151 | server_loop(Sock, Sock_type, #request{});
152 | {http, Sock, {http_error, Packet}} when Req#request.state == collect_body ->
153 | Req_1 = Req#request{body = list_to_binary([Packet, Req#request.body])},
154 | case process_request(Sock, Sock_type, Req_1) of
155 | close_connection ->
156 | gen_tcp:shutdown(Sock, read_write);
157 | ok ->
158 | server_loop(Sock, Sock_type, #request{});
159 | collect_body ->
160 | server_loop(Sock, Sock_type, Req_1)
161 | end;
162 | {http, Sock, {http_error, Err}} ->
163 | io:format("Error parsing HTTP request:~n"
164 | "Req so far : ~p~n"
165 | "Err : ~p", [Req, Err]),
166 | exit({http_error, Err});
167 | {setopts, Opts} ->
168 | setopts(Sock, Sock_type, Opts),
169 | server_loop(Sock, Sock_type, Req);
170 | {tcp_closed, Sock} ->
171 | do_trace("Client closed connection~n", []),
172 | ok;
173 | Other ->
174 | io:format("Recvd unknown msg: ~p~n", [Other]),
175 | exit({unknown_msg, Other})
176 | after 120000 ->
177 | do_trace("Timing out client connection~n", []),
178 | ok
179 | end.
180 |
181 | do_trace(Fmt, Args) ->
182 | do_trace(get(my_trace_flag), Fmt, Args).
183 |
184 | do_trace(true, Fmt, Args) ->
185 | io:format("~s -- " ++ Fmt, [ibrowse_lib:printable_date() | Args]);
186 | do_trace(_, _, _) ->
187 | ok.
188 |
189 | process_request(Sock, Sock_type,
190 | #request{method='GET',
191 | headers = Headers,
192 | uri = {abs_path, "/ibrowse_stream_once_chunk_pipeline_test"}} = Req) ->
193 | Req_id = case lists:keysearch("X-Ibrowse-Request-Id", 3, Headers) of
194 | false ->
195 | "";
196 | {value, {http_header, _, _, _, Req_id_1}} ->
197 | Req_id_1
198 | end,
199 | Req_id_header = ["x-ibrowse-request-id: ", Req_id, "\r\n"],
200 | do_trace("Recvd req: ~p~n", [Req]),
201 | Body = string:join([integer_to_list(X) || X <- lists:seq(1,100)], "-"),
202 | Chunked_body = chunk_request_body(Body, 50),
203 | Resp_1 = [<<"HTTP/1.1 200 OK\r\n">>,
204 | Req_id_header,
205 | <<"Transfer-Encoding: chunked\r\n\r\n">>],
206 | Resp_2 = Chunked_body,
207 | do_send(Sock, Sock_type, Resp_1),
208 | timer:sleep(100),
209 | do_send(Sock, Sock_type, Resp_2);
210 | process_request(Sock, Sock_type,
211 | #request{method='GET',
212 | headers = _Headers,
213 | uri = {abs_path, "/ibrowse_inac_timeout_test"}} = Req) ->
214 | do_trace("Recvd req: ~p. Sleeping for 30 secs...~n", [Req]),
215 | timer:sleep(3000),
216 | do_trace("...Sending response now.~n", []),
217 | Resp = <<"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n">>,
218 | do_send(Sock, Sock_type, Resp);
219 | process_request(Sock, Sock_type,
220 | #request{method='HEAD',
221 | headers = _Headers,
222 | uri = {abs_path, "/ibrowse_head_transfer_enc"}}) ->
223 | Resp = <<"HTTP/1.1 400 Bad Request\r\nServer: Apache-Coyote/1.1\r\nContent-Length:5\r\nDate: Wed, 04 Apr 2012 16:53:49 GMT\r\n\r\nabcde">>,
224 | do_send(Sock, Sock_type, Resp);
225 | process_request(Sock, Sock_type,
226 | #request{method='POST',
227 | headers = Headers,
228 | uri = {abs_path, "/echo_body"},
229 | body = Body}) ->
230 | Content_len = get_content_length(Headers),
231 | case iolist_size(Body) == Content_len of
232 | true ->
233 | Resp = [<<"HTTP/1.1 200 OK\r\nContent-Length: ">>, integer_to_list(Content_len), <<"\r\nServer: ibrowse_test_server\r\n\r\n">>, Body],
234 | do_send(Sock, Sock_type, list_to_binary(Resp));
235 | false ->
236 | collect_body
237 | end;
238 | process_request(Sock, Sock_type,
239 | #request{method='GET',
240 | headers = Headers,
241 | uri = {abs_path, "/ibrowse_echo_header"}}) ->
242 | Tag = "x-binary",
243 | Headers_1 = [{to_lower(X), to_lower(Y)} || {http_header, _, X, _, Y} <- Headers],
244 | X_binary_header_val = case lists:keysearch(Tag, 1, Headers_1) of
245 | false ->
246 | "not_found";
247 | {value, {_, V}} ->
248 | V
249 | end,
250 | Resp = [<<"HTTP/1.1 200 OK\r\n">>,
251 | <<"Server: ibrowse_test\r\n">>,
252 | Tag, ": ", X_binary_header_val, "\r\n",
253 | <<"Content-Length: 0\r\n\r\n">>],
254 | do_send(Sock, Sock_type, Resp);
255 |
256 | process_request(Sock, Sock_type,
257 | #request{method='HEAD',
258 | headers = _Headers,
259 | uri = {abs_path, "/ibrowse_head_test"}}) ->
260 | Resp = <<"HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\Date: Wed, 04 Apr 2012 16:53:49 GMT\r\nConnection: close\r\n\r\n">>,
261 | do_send(Sock, Sock_type, Resp);
262 | process_request(Sock, Sock_type,
263 | #request{method='POST',
264 | headers = _Headers,
265 | uri = {abs_path, "/ibrowse_303_no_body_test"}}) ->
266 | Resp = <<"HTTP/1.1 303 See Other\r\nLocation: http://example.org\r\n\r\n">>,
267 | do_send(Sock, Sock_type, Resp);
268 | process_request(Sock, Sock_type,
269 | #request{method='POST',
270 | headers = _Headers,
271 | uri = {abs_path, "/ibrowse_303_with_body_test"}}) ->
272 | Resp = <<"HTTP/1.1 303 See Other\r\nLocation: http://example.org\r\nContent-Length: 5\r\n\r\nabcde">>,
273 | do_send(Sock, Sock_type, Resp);
274 | process_request(Sock, Sock_type,
275 | #request{method='GET',
276 | headers = _Headers,
277 | uri = {abs_path, "/ibrowse_preserve_status_line"}}) ->
278 | Resp = <<"HTTP/1.1 200 OKBlah\r\nContent-Length: 5\r\n\r\nabcde">>,
279 | do_send(Sock, Sock_type, Resp);
280 | process_request(Sock, Sock_type,
281 | #request{method='GET',
282 | headers = _Headers,
283 | uri = {abs_path, "/ibrowse_handle_one_request_only_with_delay"}}) ->
284 | timer:sleep(2000),
285 | Resp = <<"HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nDate: Wed, 04 Apr 2012 16:53:49 GMT\r\nConnection: close\r\n\r\n">>,
286 | do_send(Sock, Sock_type, Resp),
287 | close_connection;
288 | process_request(Sock, Sock_type,
289 | #request{method='GET',
290 | headers = _Headers,
291 | uri = {abs_path, "/ibrowse_handle_one_request_only"}}) ->
292 | Resp = <<"HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nDate: Wed, 04 Apr 2012 16:53:49 GMT\r\nConnection: close\r\n\r\n">>,
293 | do_send(Sock, Sock_type, Resp),
294 | close_connection;
295 | process_request(Sock, Sock_type,
296 | #request{method='GET',
297 | headers = _Headers,
298 | uri = {abs_path, "/ibrowse_send_file_conn_close"}}) ->
299 | Resp = <<"HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nDate: Wed, 04 Apr 2012 16:53:49 GMT\r\nConnection: close\r\n\r\nblahblah-">>,
300 | do_send(Sock, Sock_type, Resp),
301 | timer:sleep(1000),
302 | do_send(Sock, Sock_type, <<"blahblah">>),
303 | close_connection;
304 | process_request(_Sock, _Sock_type, #request{uri = {abs_path, "/never_respond"} } ) ->
305 | not_done;
306 | process_request(Sock, Sock_type, Req) ->
307 | do_trace("Recvd req: ~p~n", [Req]),
308 | Resp = <<"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n">>,
309 | do_send(Sock, Sock_type, Resp),
310 | timer:sleep(?RAND:uniform(100)).
311 |
312 | do_send(Sock, tcp, Resp) ->
313 | gen_tcp:send(Sock, Resp);
314 | do_send(Sock, ssl, Resp) ->
315 | ssl:send(Sock, Resp).
316 |
317 | %%------------------------------------------------------------------------------
318 | %% Utility functions
319 | %%------------------------------------------------------------------------------
320 |
321 | chunk_request_body(Body, _ChunkSize) when is_tuple(Body) orelse
322 | is_function(Body) ->
323 | Body;
324 | chunk_request_body(Body, ChunkSize) ->
325 | chunk_request_body(Body, ChunkSize, []).
326 |
327 | chunk_request_body(Body, _ChunkSize, Acc) when Body == <<>>; Body == [] ->
328 | LastChunk = "0\r\n",
329 | lists:reverse(["\r\n", LastChunk | Acc]);
330 | chunk_request_body(Body, ChunkSize, Acc) when is_binary(Body),
331 | size(Body) >= ChunkSize ->
332 | <> = Body,
333 | Chunk = [?dec2hex(ChunkSize),"\r\n",
334 | ChunkBody, "\r\n"],
335 | chunk_request_body(Rest, ChunkSize, [Chunk | Acc]);
336 | chunk_request_body(Body, _ChunkSize, Acc) when is_binary(Body) ->
337 | BodySize = size(Body),
338 | Chunk = [?dec2hex(BodySize),"\r\n",
339 | Body, "\r\n"],
340 | LastChunk = "0\r\n",
341 | lists:reverse(["\r\n", LastChunk, Chunk | Acc]);
342 | chunk_request_body(Body, ChunkSize, Acc) when length(Body) >= ChunkSize ->
343 | {ChunkBody, Rest} = split_list_at(Body, ChunkSize),
344 | Chunk = [?dec2hex(ChunkSize),"\r\n",
345 | ChunkBody, "\r\n"],
346 | chunk_request_body(Rest, ChunkSize, [Chunk | Acc]);
347 | chunk_request_body(Body, _ChunkSize, Acc) when is_list(Body) ->
348 | BodySize = length(Body),
349 | Chunk = [?dec2hex(BodySize),"\r\n",
350 | Body, "\r\n"],
351 | LastChunk = "0\r\n",
352 | lists:reverse(["\r\n", LastChunk, Chunk | Acc]).
353 |
354 | split_list_at(List, N) ->
355 | split_list_at(List, N, []).
356 |
357 | split_list_at([], _, Acc) ->
358 | {lists:reverse(Acc), []};
359 | split_list_at(List2, 0, List1) ->
360 | {lists:reverse(List1), List2};
361 | split_list_at([H | List2], N, List1) ->
362 | split_list_at(List2, N-1, [H | List1]).
363 |
364 | to_lower(X) when is_atom(X) ->
365 | list_to_atom(to_lower(atom_to_list(X)));
366 | to_lower(X) when is_list(X) ->
367 | string:to_lower(X).
368 |
369 | get_content_length([{http_header, _, 'Content-Length', _, V} | _]) ->
370 | list_to_integer(V);
371 | get_content_length([{http_header, _, _X, _, _Y} | T]) ->
372 | get_content_length(T);
373 | get_content_length([]) ->
374 | undefined.
375 |
--------------------------------------------------------------------------------
/test/ibrowse_tests.erl:
--------------------------------------------------------------------------------
1 | %%% File : ibrowse_tests.erl
2 | %%% Authors : Benjamin Lee
3 | %%% Dan Schwabe
4 | %%% Brian Richards
5 | %%% Description : Functional tests of the ibrowse library using a live test HTTP server
6 | %%% Created : 18 November 2014 by Benjamin Lee
7 |
8 | -module(ibrowse_tests).
9 |
10 | -include_lib("eunit/include/eunit.hrl").
11 | -define(PER_TEST_TIMEOUT_SEC, 60).
12 | -define(TIMEDTEST(Desc, Fun), {Desc, {timeout, ?PER_TEST_TIMEOUT_SEC, fun Fun/0}}).
13 |
14 | -define(SERVER_PORT, 8181).
15 | -define(BASE_URL, "http://localhost:" ++ integer_to_list(?SERVER_PORT)).
16 | -define(SHORT_TIMEOUT_MS, 5000).
17 | -define(LONG_TIMEOUT_MS, 30000).
18 | -define(PAUSE_FOR_CONNECTIONS_MS, 4000).
19 |
20 | %%-compile(export_all).
21 |
22 | setup() ->
23 | application:start(crypto),
24 | application:start(public_key),
25 | application:start(ssl),
26 | ibrowse_test_server:start_server(?SERVER_PORT, tcp),
27 | ibrowse:start(),
28 | ok.
29 |
30 | teardown(_) ->
31 | ibrowse:stop(),
32 | ibrowse_test_server:stop_server(?SERVER_PORT),
33 | ok.
34 |
35 | running_server_fixture_test_() ->
36 | {foreach,
37 | fun setup/0,
38 | fun teardown/1,
39 | [
40 | ?TIMEDTEST("Simple request can be honored", simple_request),
41 | ?TIMEDTEST("Slow server causes timeout", slow_server_timeout),
42 | ?TIMEDTEST("Pipeline depth goes down with responses", pipeline_depth),
43 | ?TIMEDTEST("Pipelines refill", pipeline_refill),
44 | ?TIMEDTEST("Timeout closes pipe", closing_pipes),
45 | ?TIMEDTEST("Requests are balanced over connections", balanced_connections),
46 | ?TIMEDTEST("Pipeline too small signals retries", small_pipeline),
47 | ?TIMEDTEST("Dest status can be gathered", status)
48 | ]
49 | }.
50 |
51 | simple_request() ->
52 | ?assertMatch({ok, "200", _, _}, ibrowse:send_req(?BASE_URL, [], get, [], [])).
53 |
54 | slow_server_timeout() ->
55 | ?assertMatch({error, req_timedout}, ibrowse:send_req(?BASE_URL ++ "/never_respond", [], get, [], [], 5000)).
56 |
57 | pipeline_depth() ->
58 | MaxSessions = 2,
59 | MaxPipeline = 2,
60 | RequestsSent = 2,
61 | EmptyPipelineDepth = 0,
62 |
63 | ?assertEqual([], ibrowse_test_server:get_conn_pipeline_depth()),
64 |
65 | Fun = fun() -> ibrowse:send_req(?BASE_URL, [], get, [], [{max_sessions, MaxSessions}, {max_pipeline_size, MaxPipeline}], ?SHORT_TIMEOUT_MS) end,
66 | times(RequestsSent, fun() -> spawn_link(Fun) end),
67 |
68 | timer:sleep(?PAUSE_FOR_CONNECTIONS_MS),
69 |
70 | Counts = [Count || {_Pid, Count} <- ibrowse_test_server:get_conn_pipeline_depth()],
71 | ?assertEqual(MaxSessions, length(Counts)),
72 | ?assertEqual(lists:duplicate(MaxSessions, EmptyPipelineDepth), Counts).
73 |
74 | pipeline_refill() ->
75 | MaxSessions = 2,
76 | MaxPipeline = 2,
77 | RequestsToFill = MaxSessions * MaxPipeline,
78 |
79 | %% Send off enough requests to fill sessions and pipelines in rappid succession
80 | Fun = fun() -> ibrowse:send_req(?BASE_URL, [], get, [], [{max_sessions, MaxSessions}, {max_pipeline_size, MaxPipeline}], ?SHORT_TIMEOUT_MS) end,
81 | times(RequestsToFill, fun() -> spawn_link(Fun) end),
82 | timer:sleep(?PAUSE_FOR_CONNECTIONS_MS),
83 |
84 | % Verify that connections properly reported their completed responses and can still accept more
85 | ?assertMatch({ok, "200", _, _}, ibrowse:send_req(?BASE_URL, [], get, [], [{max_sessions, MaxSessions}, {max_pipeline_size, MaxPipeline}], ?SHORT_TIMEOUT_MS)),
86 |
87 | % and do it again to make sure we really are clear
88 | times(RequestsToFill, fun() -> spawn_link(Fun) end),
89 | timer:sleep(?PAUSE_FOR_CONNECTIONS_MS),
90 |
91 | % Verify that connections properly reported their completed responses and can still accept more
92 | ?assertMatch({ok, "200", _, _}, ibrowse:send_req(?BASE_URL, [], get, [], [{max_sessions, MaxSessions}, {max_pipeline_size, MaxPipeline}], ?SHORT_TIMEOUT_MS)).
93 |
94 | closing_pipes() ->
95 | MaxSessions = 2,
96 | MaxPipeline = 2,
97 | RequestsSent = 2,
98 | BalancedNumberOfRequestsPerConnection = 1,
99 |
100 | ?assertEqual([], ibrowse_test_server:get_conn_pipeline_depth()),
101 |
102 | Fun = fun() -> ibrowse:send_req(?BASE_URL ++ "/never_respond", [], get, [], [{max_sessions, MaxSessions}, {max_pipeline_size, MaxPipeline}], ?SHORT_TIMEOUT_MS) end,
103 | times(RequestsSent, fun() -> spawn_link(Fun) end),
104 |
105 | timer:sleep(?PAUSE_FOR_CONNECTIONS_MS),
106 |
107 | Counts = [Count || {_Pid, Count} <- ibrowse_test_server:get_conn_pipeline_depth()],
108 | ?assertEqual(MaxSessions, length(Counts)),
109 | ?assertEqual(lists:duplicate(MaxSessions, BalancedNumberOfRequestsPerConnection), Counts),
110 |
111 | timer:sleep(?SHORT_TIMEOUT_MS),
112 |
113 | ?assertEqual([], ibrowse_test_server:get_conn_pipeline_depth()).
114 |
115 | balanced_connections() ->
116 | MaxSessions = 4,
117 | MaxPipeline = 100,
118 | RequestsSent = 80,
119 | BalancedNumberOfRequestsPerConnection = 20,
120 |
121 | ?assertEqual([], ibrowse_test_server:get_conn_pipeline_depth()),
122 |
123 | Fun = fun() -> ibrowse:send_req(?BASE_URL ++ "/never_respond", [], get, [], [{max_sessions, MaxSessions}, {max_pipeline_size, MaxPipeline}], ?LONG_TIMEOUT_MS) end,
124 | times(RequestsSent, fun() -> spawn_link(Fun) end),
125 |
126 | timer:sleep(?PAUSE_FOR_CONNECTIONS_MS),
127 |
128 | Counts = [Count || {_Pid, Count} <- ibrowse_test_server:get_conn_pipeline_depth()],
129 | ?assertEqual(MaxSessions, length(Counts)),
130 |
131 | ?assertEqual(lists:duplicate(MaxSessions, BalancedNumberOfRequestsPerConnection), Counts).
132 |
133 | small_pipeline() ->
134 | MaxSessions = 10,
135 | MaxPipeline = 10,
136 | RequestsSent = 100,
137 | FullRequestsPerConnection = 10,
138 |
139 | ?assertEqual([], ibrowse_test_server:get_conn_pipeline_depth()),
140 |
141 | Fun = fun() -> ibrowse:send_req(?BASE_URL ++ "/never_respond", [], get, [], [{max_sessions, MaxSessions}, {max_pipeline_size, MaxPipeline}], ?SHORT_TIMEOUT_MS) end,
142 | times(RequestsSent, fun() -> spawn(Fun) end),
143 |
144 | timer:sleep(?PAUSE_FOR_CONNECTIONS_MS), %% Wait for everyone to get in line
145 |
146 | ibrowse:show_dest_status("localhost", 8181),
147 | Counts = [Count || {_Pid, Count} <- ibrowse_test_server:get_conn_pipeline_depth()],
148 | ?assertEqual(MaxSessions, length(Counts)),
149 |
150 | ?assertEqual(lists:duplicate(MaxSessions, FullRequestsPerConnection), Counts),
151 |
152 | Response = ibrowse:send_req(?BASE_URL ++ "/never_respond", [], get, [], [{max_sessions, MaxSessions}, {max_pipeline_size, MaxPipeline}], ?SHORT_TIMEOUT_MS),
153 |
154 | ?assertEqual({error, retry_later}, Response).
155 |
156 | status() ->
157 | MaxSessions = 10,
158 | MaxPipeline = 10,
159 | RequestsSent = 100,
160 |
161 | Fun = fun() -> ibrowse:send_req(?BASE_URL ++ "/never_respond", [], get, [], [{max_sessions, MaxSessions}, {max_pipeline_size, MaxPipeline}], ?SHORT_TIMEOUT_MS) end,
162 | times(RequestsSent, fun() -> spawn(Fun) end),
163 |
164 | timer:sleep(?PAUSE_FOR_CONNECTIONS_MS), %% Wait for everyone to get in line
165 |
166 | ibrowse:show_dest_status(),
167 | ibrowse:show_dest_status("http://localhost:8181").
168 |
169 |
170 | times(0, _) ->
171 | ok;
172 | times(X, Fun) ->
173 | Fun(),
174 | times(X - 1, Fun).
175 |
--------------------------------------------------------------------------------