This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
||||
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
||||
|
||||
name: Ruby
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu, macos]
|
||||
ruby-version: [2.5, 2.6, 2.7, 3.0, head, debug, truffleruby]
|
||||
continue-on-error: ${{ endsWith(matrix.ruby, 'head') || matrix.ruby == 'debug' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install libcurl header
|
||||
run: |
|
||||
if ${{ matrix.os == 'macos' }}
|
||||
then
|
||||
brew install curl
|
||||
else
|
||||
sudo apt update && sudo apt install -y --no-install-recommends libcurl4-openssl-dev
|
||||
fi
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby-version }}
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
- name: Run tests
|
||||
run: bundle exec rake
|
||||
@@ -0,0 +1,8 @@
|
||||
*.gem
|
||||
.bundle
|
||||
Gemfile.lock
|
||||
.DS_Store
|
||||
.yardoc
|
||||
doc
|
||||
coverage
|
||||
.idea
|
||||
@@ -0,0 +1,3 @@
|
||||
--tty
|
||||
--color
|
||||
--format documentation
|
||||
@@ -0,0 +1,375 @@
|
||||
# Changelog
|
||||
|
||||
## Master
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.15.0...master)
|
||||
|
||||
* Added `redirect_url` value to available informations and `Easy::Mirror`.
|
||||
([Adrien Rey-Jarthon](https://github.com/jarthod)
|
||||
|
||||
## 0.15.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.14.0...v0.15.0)
|
||||
|
||||
## 0.12.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.11.0...v0.12.0)
|
||||
|
||||
- Removed deprecated `CURLE_SSL_CACERT` pinned in curl v7.62.0 ([@arku](https://github.com/arku) in [#158](https://github.com/typhoeus/ethon/pull/158))
|
||||
|
||||
|
||||
## 0.11.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.10.1...v0.11.0)
|
||||
|
||||
## 0.10.1
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.10.0...v0.10.1)
|
||||
|
||||
## 0.10.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.9.1...v0.10.0)
|
||||
|
||||
## 0.9.1
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.9.0...v0.9.1)
|
||||
|
||||
## 0.9.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.8.1...v0.9.0)
|
||||
|
||||
## 0.8.1
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.8.0...v0.8.1)
|
||||
|
||||
* Support optional escaping of params.
|
||||
([Tasos Laskos](https://github.com/zapotek)
|
||||
* `Easy::Mirror`: Reduced object allocations and method calls during info handling.
|
||||
([Tasos Laskos](https://github.com/zapotek)
|
||||
|
||||
## 0.8.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.7.3...v0.7.4)
|
||||
|
||||
* `Easy::Mirror`: Reduced object allocations and method calls during info handling.
|
||||
([Tasos Laskos](https://github.com/zapotek)
|
||||
|
||||
## 0.7.4
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.7.3...v0.7.4)
|
||||
|
||||
* Support different array encodings for params.
|
||||
([Marcello Barnaba](https://github.com/ifad), [\#104](https://github.com/typhoeus/ethon/pull/104))
|
||||
* Programtic access to version infos.
|
||||
([Jonas Wagner](https://github.com/jwagner), [\#90](https://github.com/typhoeus/ethon/pull/90))
|
||||
|
||||
|
||||
## 0.7.3
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.7.2...v0.7.3)
|
||||
|
||||
* `Ethon::Curl::FDSet`
|
||||
* Set `:fd_array` size to the current MS Windows `FD_SETSIZE` (2048).
|
||||
([Tasos Laskos](https://github.com/zapotek)
|
||||
|
||||
* Added `redirect_time` value to available informations and `Easy::Mirror`.
|
||||
([Adrien Jarthon](https://github.com/jarthod)
|
||||
|
||||
## 0.7.2
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.7.1...v0.7.2)
|
||||
|
||||
* FFI data-types updated to be more correct.
|
||||
|
||||
## 0.7.1
|
||||
|
||||
* MS Windows determination delegated to `Gem.windows?` for better accuracy.
|
||||
* FFI data-types updated to work on MS Windows.
|
||||
|
||||
## 0.7.0
|
||||
|
||||
Not backwards compatible changes:
|
||||
|
||||
* `mime-types` are no longer a dependency. The gem will be still used if available to determine the mime type of a file which is uploaded. That means you have to have take care of the gem installation yourself.
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.6.3...v0.7.0)
|
||||
|
||||
## 0.6.3
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.6.2...v0.6.3)
|
||||
|
||||
## 0.6.2
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.6.1...v0.6.2)
|
||||
|
||||
## 0.6.1
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.6.0...v0.6.1)
|
||||
|
||||
The changelog entries are coming soon!
|
||||
|
||||
## 0.6.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.12...v0.6.0)
|
||||
|
||||
The changelog entries are coming soon!
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* URL-encode nullbytes in parameters instead of escaping them to `\\0`.
|
||||
([Tasos Laskos](https://github.com/zapotek)
|
||||
|
||||
## 0.5.12
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.11...v0.5.12)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Performance optimizations.
|
||||
([Kyle Oppenheim](https://github.com/koppenheim) and [Richie Vos](https://github.com/richievos), [\#48](https://github.com/typhoeus/ethon/pull/48))
|
||||
* Reuse memory pointer.
|
||||
([Richie Vos](https://github.com/richievos), [\#49](https://github.com/typhoeus/ethon/pull/49))
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Fix windows install.
|
||||
([Derik Olsson](https://github.com/derikolsson), [\#47](https://github.com/typhoeus/ethon/pull/47))
|
||||
* Handle urls that already contain query params.
|
||||
([Turner King](https://github.com/turnerking ), [\#45](https://github.com/typhoeus/ethon/pull/45))
|
||||
|
||||
## 0.5.11
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.10...v0.5.11)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Add support for postredirs, unrestricted_auth.
|
||||
* Add support for cookie, cookiejar, cookiefile.
|
||||
([erwanlr](https://github.com/erwanlr), [\#46](https://github.com/typhoeus/ethon/pull/46))
|
||||
* Relax ffi requirements.
|
||||
([voxik](https://github.com/voxik), [\#40](https://github.com/typhoeus/ethon/pull/40))
|
||||
* Various documentation improvements.
|
||||
([Craig Little](https://github.com/craiglittle))
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Fix the memory leaks.
|
||||
([Richie Vos](https://github.com/richievos), [\#45](https://github.com/typhoeus/ethon/pull/45))
|
||||
|
||||
## 0.5.10
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.9...v0.5.10)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Allow custom requests.
|
||||
([Nathan Sutton](https://github.com/nate), [\#36](https://github.com/typhoeus/ethon/pull/36))
|
||||
* Use updated version of FFI.
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Fix windows install issue.
|
||||
([brainsucker](https://github.com/brainsucker), [\#38](https://github.com/typhoeus/ethon/pull/38))
|
||||
|
||||
## 0.5.9
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.8...v0.5.9)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Allow to set multiple protocols.
|
||||
|
||||
## 0.5.8
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.7...v0.5.8)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Add support for protocols and redir_protocols(
|
||||
[libcurl SASL buffer overflow vulnerability](http://curl.haxx.se/docs/adv_20130206.html)).
|
||||
* Add max_send_speed_large and max_recv_speed_large([Paul Schuegraf](https://github.com/pschuegr), [\#33](https://github.com/typhoeus/ethon/pull/33))
|
||||
|
||||
## 0.5.7
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.6...v0.5.7)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Use new version of ffi.
|
||||
|
||||
## 0.5.6
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.4...v0.5.6)
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Easy#reset resets on_complete callbacks.
|
||||
|
||||
## 0.5.4
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.3...v0.5.4)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Use Libc#getdtablesize to get the FDSet size.
|
||||
* New libcurl option accept_encoding.
|
||||
* Documentation updates.
|
||||
|
||||
## 0.5.3
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.2...v0.5.3)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Deprecate Easy#prepare. It is no longer necessary.
|
||||
* Unroll metaprogramming for easy and multi options.
|
||||
* More specs.
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Correct size for FDSets
|
||||
* Add proxytypes to enums.
|
||||
|
||||
## 0.5.2
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.1...v0.5.2)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* New libcurl option keypasswd.
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Correct request logging when using multi interface.
|
||||
* Remove invalid libcurl option sslcertpasswd.
|
||||
|
||||
## 0.5.1
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.5.0...v0.5.1)
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Mark Curl.select and Curl.easy_perform as blocking so that the GIL is
|
||||
released by ffi.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.4.4...v0.5.0)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* New libcurl option proxyuserpwd
|
||||
* Rename response_header to response_headers
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Mark Curl.select and Curl.easy_perform as blocking so that the GIL is
|
||||
released by ffi.
|
||||
|
||||
## 0.4.4
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.4.3...v0.4.4)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Prepare multi explicit like easy
|
||||
|
||||
## 0.4.3
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.4.2...v0.4.3)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Remove deprecated libcurl option put
|
||||
* More documentation
|
||||
* New libcurl option connecttimeout_ms and timeout_ms
|
||||
* Support multi options
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Handle nil values in query params
|
||||
|
||||
## 0.4.2
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.4.1...v0.4.2)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* New libcurl option forbid_reuse
|
||||
* Use libcurls escape instead of CGI::escape
|
||||
|
||||
## 0.4.1
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.4.0...v0.4.1)
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Handle nested hash in an array in params correct
|
||||
( [\#201](https://github.com/typhoeus/typhoeus/issues/201) )
|
||||
|
||||
## 0.4.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.3.0...v0.4.0)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* ruby 1.8.7 compatible
|
||||
* Ethon.logger
|
||||
* Deal with string param/body
|
||||
* More documentation
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Add multi_cleanup to curl
|
||||
|
||||
## 0.3.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.2.0...v0.3.0)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* New libcurl option proxyport
|
||||
* Raise invalid value error when providing a wrong key for sslversion or httpauth
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Libcurl option sslversion is handled correct
|
||||
|
||||
## 0.2.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.1.0...v0.2.0)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* GET requests are using custom requests only when there is a request body
|
||||
* Easy#on_complete takes multiple callbacks
|
||||
* raise Errors::GlobalInit when libcurls global_init failed instead of
|
||||
runtime error
|
||||
* raise Errors::InvalidOption if option is invalid
|
||||
|
||||
## 0.1.0
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.0.2...v0.1.0)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Documentation
|
||||
( [Alex P](https://github.com/ifesdjeen), [\#13](https://github.com/typhoeus/ethon/issues/13) )
|
||||
* New libcurl option dns_cache_timeout
|
||||
( [Chris Heald](https://github.com/cheald), [\#192](https://github.com/typhoeus/typhoeus/pull/192) )
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Libcurl option ssl_verifyhost takes an integer.
|
||||
* Add space between header key and value.
|
||||
|
||||
## 0.0.2
|
||||
|
||||
[Full Changelog](https://github.com/typhoeus/ethon/compare/v0.0.1...v0.0.2)
|
||||
|
||||
Bugfixes:
|
||||
|
||||
* Add libcurl.so.4 to ffi_lib in order to load correct lib on Debian.
|
||||
* Escape zero bytes.
|
||||
|
||||
## 0.0.1 Initial version
|
||||
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
source "https://rubygems.org"
|
||||
gemspec
|
||||
|
||||
if Gem.ruby_version < Gem::Version.new("1.9.3")
|
||||
gem "rake", "< 11"
|
||||
else
|
||||
gem "rake"
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
gem "rspec", "~> 3.4"
|
||||
|
||||
gem "sinatra"
|
||||
|
||||
if Gem.ruby_version < Gem::Version.new("2.0.0")
|
||||
gem "json", "< 2"
|
||||
else
|
||||
gem "json"
|
||||
end
|
||||
|
||||
if Gem.ruby_version >= Gem::Version.new("2.0.0")
|
||||
gem "mime-types", "~> 1.18"
|
||||
end
|
||||
|
||||
if Gem.ruby_version >= Gem::Version.new("2.2.0")
|
||||
gem "mustermann"
|
||||
elsif Gem.ruby_version >= Gem::Version.new("2.1.0")
|
||||
gem "mustermann", "0.4.0"
|
||||
elsif Gem.ruby_version >= Gem::Version.new("2.0.0")
|
||||
gem "mustermann", "0.3.1"
|
||||
end
|
||||
|
||||
if Gem.ruby_version >= Gem::Version.new("3.0.0")
|
||||
gem "webrick"
|
||||
end
|
||||
end
|
||||
|
||||
group :perf do
|
||||
gem "benchmark-ips"
|
||||
gem "patron"
|
||||
gem "curb"
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
# vim:set filetype=ruby:
|
||||
guard(
|
||||
"rspec",
|
||||
:all_after_pass => false,
|
||||
:cli => "--fail-fast --tty --format documentation --colour") do
|
||||
|
||||
watch(%r{^spec/.+_spec\.rb$})
|
||||
watch(%r{^lib/(.+)\.rb$}) { |match| "spec/#{match[1]}_spec.rb" }
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2012-2016 Hans Hasselberg
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,118 @@
|
||||
[](https://badge.fury.io/rb/ethon)
|
||||
[](https://github.com/typhoeus/ethon/actions/workflows/ruby.yml)
|
||||
|
||||
# Ethon
|
||||
|
||||
In Greek mythology, Ethon, the son of Typhoeus and Echidna, is a gigantic eagle. So much for the history.
|
||||
In the modern world, Ethon is a very basic libcurl wrapper using ffi.
|
||||
|
||||
* [Documentation](http://rubydoc.info/github/typhoeus/ethon/frames/Ethon)
|
||||
* [Website](http://typhoeus.github.com/)
|
||||
* [Mailing list](http://groups.google.com/group/typhoeus)
|
||||
|
||||
## Installation
|
||||
|
||||
With bundler:
|
||||
|
||||
gem "ethon"
|
||||
|
||||
With rubygems:
|
||||
|
||||
gem install ethon
|
||||
|
||||
## Usage
|
||||
|
||||
Making the first request is simple:
|
||||
|
||||
```ruby
|
||||
easy = Ethon::Easy.new(url: "www.example.com")
|
||||
easy.perform
|
||||
#=> :ok
|
||||
```
|
||||
|
||||
You have access to various options, such as following redirects:
|
||||
|
||||
```ruby
|
||||
easy = Ethon::Easy.new(url: "www.example.com", followlocation: true)
|
||||
easy.perform
|
||||
#=> :ok
|
||||
```
|
||||
|
||||
Once you're done you can inspect the response code and body:
|
||||
|
||||
```ruby
|
||||
easy = Ethon::Easy.new(url: "www.example.com", followlocation: true)
|
||||
easy.perform
|
||||
easy.response_code
|
||||
#=> 200
|
||||
easy.response_body
|
||||
#=> "<!doctype html><html ..."
|
||||
```
|
||||
|
||||
## Http
|
||||
|
||||
In order to make life easier, there are some helpers for making HTTP requests:
|
||||
|
||||
```ruby
|
||||
easy = Ethon::Easy.new
|
||||
easy.http_request("www.example.com", :get, { params: {a: 1} })
|
||||
easy.perform
|
||||
#=> :ok
|
||||
```
|
||||
|
||||
```ruby
|
||||
easy = Ethon::Easy.new
|
||||
easy.http_request("www.example.com", :post, { params: { a: 1 }, body: { b: 2 } })
|
||||
easy.perform
|
||||
#=> :ok
|
||||
```
|
||||
|
||||
This is really handy when making requests since you don't have to care about setting
|
||||
everything up correctly.
|
||||
|
||||
## Http2
|
||||
Standard http2 servers require the client to connect once and create a session (multi) and then add simple requests to the multi handler.
|
||||
The `perform` method then takes all the requests in the multi handler and sends them to the server.
|
||||
|
||||
See the following example
|
||||
```ruby
|
||||
multi = Ethon::Multi.new
|
||||
easy = Ethon::Easy.new
|
||||
|
||||
easy.http_request("www.example.com/get", :get, { http_version: :httpv2_0 })
|
||||
|
||||
# Sending a request with http version 2 will send an Upgrade header to the server, which many older servers will not support
|
||||
# See below for more info: https://everything.curl.dev/http/http2
|
||||
# If this is a problem, send the below:
|
||||
easy.http_request("www.example.com/get", :get, { http_version: :httpv2_prior_knowledge })
|
||||
|
||||
# To set the server to use http2 with https and http1 with http, send the following:
|
||||
easy.http_request("www.example.com/get", :get, { http_version: :httpv2_tls }
|
||||
|
||||
multi.add(easy)
|
||||
multi.perform
|
||||
```
|
||||
|
||||
## LICENSE
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright © 2012-2016 [Hans Hasselberg](http://www.hans.io)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons
|
||||
to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
require "bundler"
|
||||
Bundler.setup
|
||||
|
||||
require "rake"
|
||||
require "rspec/core/rake_task"
|
||||
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
||||
require "ethon/version"
|
||||
|
||||
task :gem => :build
|
||||
task :build do
|
||||
system "gem build ethon.gemspec"
|
||||
end
|
||||
|
||||
task :install => :build do
|
||||
system "gem install ethon-#{Ethon::VERSION}.gem"
|
||||
end
|
||||
|
||||
task :release => :build do
|
||||
system "git tag -a v#{Ethon::VERSION} -m 'Tagging #{Ethon::VERSION}'"
|
||||
system "git push --tags"
|
||||
system "gem push ethon-#{Ethon::VERSION}.gem"
|
||||
end
|
||||
|
||||
RSpec::Core::RakeTask.new(:spec) do |t|
|
||||
t.verbose = false
|
||||
t.ruby_opts = "-W -I./spec -rspec_helper"
|
||||
end
|
||||
|
||||
desc "Start up the test servers"
|
||||
task :start do
|
||||
require_relative 'spec/support/boot'
|
||||
begin
|
||||
Boot.start_servers(:rake)
|
||||
rescue Exception
|
||||
end
|
||||
end
|
||||
|
||||
task :default => :spec
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
lib = File.expand_path('../lib/', __FILE__)
|
||||
$:.unshift lib unless $:.include?(lib)
|
||||
|
||||
require 'ethon/version'
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "ethon"
|
||||
s.version = Ethon::VERSION
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.authors = ["Hans Hasselberg"]
|
||||
s.email = ["me@hans.io"]
|
||||
s.homepage = "https://github.com/typhoeus/ethon"
|
||||
s.summary = "Libcurl wrapper."
|
||||
s.description = "Very lightweight libcurl wrapper."
|
||||
|
||||
s.required_rubygems_version = ">= 1.3.6"
|
||||
s.license = 'MIT'
|
||||
|
||||
s.add_dependency('ffi', ['>= 1.15.0'])
|
||||
|
||||
s.files = `git ls-files`.split("\n")
|
||||
s.test_files = `git ls-files -- spec/*`.split("\n")
|
||||
s.require_path = 'lib'
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
require 'logger'
|
||||
require 'ffi'
|
||||
require 'thread'
|
||||
begin
|
||||
require 'mime/types/columnar'
|
||||
rescue LoadError
|
||||
begin
|
||||
require 'mime/types'
|
||||
rescue LoadError
|
||||
end
|
||||
end
|
||||
require 'tempfile'
|
||||
|
||||
require 'ethon/libc'
|
||||
require 'ethon/curl'
|
||||
require 'ethon/easy'
|
||||
require 'ethon/errors'
|
||||
require 'ethon/loggable'
|
||||
require 'ethon/multi'
|
||||
require 'ethon/version'
|
||||
|
||||
# Ethon is a very simple libcurl.
|
||||
# It provides direct access to libcurl functionality
|
||||
# as well as some helpers for doing http requests.
|
||||
#
|
||||
# Ethon was extracted from Typhoeus. If you want to
|
||||
# see how others use Ethon look at the Typhoeus code.
|
||||
#
|
||||
# @see https://www.github.com/typhoeus/typhoeus Typhoeus
|
||||
#
|
||||
# @note Please update to the latest libcurl version in order
|
||||
# to benefit from all features and bugfixes.
|
||||
# http://curl.haxx.se/download.html
|
||||
module Ethon
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon/curls/codes'
|
||||
require 'ethon/curls/options'
|
||||
require 'ethon/curls/infos'
|
||||
require 'ethon/curls/form_options'
|
||||
require 'ethon/curls/messages'
|
||||
require 'ethon/curls/functions'
|
||||
|
||||
module Ethon
|
||||
|
||||
# FFI Wrapper module for Curl. Holds constants and required initializers.
|
||||
#
|
||||
# @api private
|
||||
module Curl
|
||||
extend ::FFI::Library
|
||||
extend Ethon::Curls::Codes
|
||||
extend Ethon::Curls::Options
|
||||
extend Ethon::Curls::Infos
|
||||
extend Ethon::Curls::FormOptions
|
||||
extend Ethon::Curls::Messages
|
||||
|
||||
# :nodoc:
|
||||
def self.windows?
|
||||
Libc.windows?
|
||||
end
|
||||
|
||||
require 'ethon/curls/constants'
|
||||
require 'ethon/curls/settings'
|
||||
require 'ethon/curls/classes'
|
||||
extend Ethon::Curls::Functions
|
||||
|
||||
@blocking = true
|
||||
|
||||
@@initialized = false
|
||||
@@curl_mutex = Mutex.new
|
||||
|
||||
class << self
|
||||
# This function sets up the program environment that libcurl needs.
|
||||
# Think of it as an extension of the library loader.
|
||||
#
|
||||
# This function must be called at least once within a program (a program is all the
|
||||
# code that shares a memory space) before the program calls any other function in libcurl.
|
||||
# The environment it sets up is constant for the life of the program and is the same for
|
||||
# every program, so multiple calls have the same effect as one call.
|
||||
#
|
||||
# The flags option is a bit pattern that tells libcurl exactly what features to init,
|
||||
# as described below. Set the desired bits by ORing the values together. In normal
|
||||
# operation, you must specify CURL_GLOBAL_ALL. Don't use any other value unless
|
||||
# you are familiar with it and mean to control internal operations of libcurl.
|
||||
#
|
||||
# This function is not thread safe. You must not call it when any other thread in
|
||||
# the program (i.e. a thread sharing the same memory) is running. This doesn't just
|
||||
# mean no other thread that is using libcurl. Because curl_global_init() calls
|
||||
# functions of other libraries that are similarly thread unsafe, it could conflict with
|
||||
# any other thread that uses these other libraries.
|
||||
#
|
||||
# @raise [ Ethon::Errors::GlobalInit ] If Curl.global_init fails.
|
||||
def init
|
||||
@@curl_mutex.synchronize {
|
||||
if not @@initialized
|
||||
raise Errors::GlobalInit.new if Curl.global_init(GLOBAL_ALL) != 0
|
||||
@@initialized = true
|
||||
Ethon.logger.debug("ETHON: Libcurl initialized") if Ethon.logger
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# This function releases resources acquired by curl_global_init.
|
||||
# You should call curl_global_cleanup once for each call you make to
|
||||
# curl_global_init, after you are done using libcurl.
|
||||
# This function is not thread safe. You must not call it when any other thread in the
|
||||
# program (i.e. a thread sharing the same memory) is running. This doesn't just
|
||||
# mean no other thread that is using libcurl. Because curl_global_cleanup calls functions of other
|
||||
# libraries that are similarly thread unsafe, it could conflict with
|
||||
# any other thread that uses these other libraries.
|
||||
# See the description in libcurl of global environment requirements
|
||||
# for details of how to use this function.
|
||||
def cleanup
|
||||
@@curl_mutex.synchronize {
|
||||
if @@initialized
|
||||
Curl.global_cleanup()
|
||||
@@initialized = false
|
||||
Ethon.logger.debug("ETHON: Libcurl cleanup") if Ethon.logger
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Curl
|
||||
# :nodoc:
|
||||
class MsgData < ::FFI::Union
|
||||
layout :whatever, :pointer, :code, :easy_code
|
||||
end
|
||||
|
||||
# :nodoc:
|
||||
class Msg < ::FFI::Struct
|
||||
layout :code, :msg_code, :easy_handle, :pointer, :data, MsgData
|
||||
end
|
||||
|
||||
class VersionInfoData < ::FFI::Struct
|
||||
layout :curl_version, :uint8,
|
||||
:version, :string,
|
||||
:version_num, :int,
|
||||
:host, :string,
|
||||
:features, :int,
|
||||
:ssl_version, :string,
|
||||
:ssl_version_num, :long,
|
||||
:libz_version, :string,
|
||||
:protocols, :pointer
|
||||
end
|
||||
|
||||
# :nodoc:
|
||||
class FDSet < ::FFI::Struct
|
||||
if Curl.windows?
|
||||
layout :fd_count, :uint,
|
||||
# TODO: Make it future proof by dynamically grabbing FD_SETSIZE.
|
||||
:fd_array, [:uint, 2048]
|
||||
|
||||
def clear; self[:fd_count] = 0; end
|
||||
else
|
||||
# https://github.com/typhoeus/ethon/issues/182
|
||||
FD_SETSIZE = begin
|
||||
# Allow to override the (new) default cap
|
||||
if ENV['ETHON_FD_SIZE']
|
||||
ENV['ETHON_FD_SIZE']
|
||||
|
||||
# auto-detect ulimit, but cap at 2^16
|
||||
else
|
||||
[::Ethon::Libc.getdtablesize, 65_536].min
|
||||
end
|
||||
end
|
||||
|
||||
layout :fds_bits, [:long, FD_SETSIZE / ::FFI::Type::LONG.size]
|
||||
|
||||
# :nodoc:
|
||||
def clear; super; end
|
||||
end
|
||||
end
|
||||
|
||||
# :nodoc:
|
||||
class Timeval < ::FFI::Struct
|
||||
if Curl.windows?
|
||||
layout :sec, :long,
|
||||
:usec, :long
|
||||
else
|
||||
layout :sec, :time_t,
|
||||
:usec, :suseconds_t
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,122 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Curls # :nodoc:
|
||||
|
||||
# This module contains all easy and
|
||||
# multi return codes.
|
||||
module Codes
|
||||
|
||||
# Libcurl error codes, refer
|
||||
# https://github.com/bagder/curl/blob/master/include/curl/curl.h for details
|
||||
def easy_codes
|
||||
[
|
||||
:ok,
|
||||
:unsupported_protocol,
|
||||
:failed_init,
|
||||
:url_malformat,
|
||||
:not_built_in,
|
||||
:couldnt_resolve_proxy,
|
||||
:couldnt_resolve_host,
|
||||
:couldnt_connect,
|
||||
:ftp_weird_server_reply,
|
||||
:remote_access_denied,
|
||||
:ftp_accept_failed,
|
||||
:ftp_weird_pass_reply,
|
||||
:ftp_accept_timeout,
|
||||
:ftp_weird_pasv_reply,
|
||||
:ftp_weird_227_format,
|
||||
:ftp_cant_get_host,
|
||||
:obsolete16,
|
||||
:ftp_couldnt_set_type,
|
||||
:partial_file,
|
||||
:ftp_couldnt_retr_file,
|
||||
:obsolete20,
|
||||
:quote_error,
|
||||
:http_returned_error,
|
||||
:write_error,
|
||||
:obsolete24,
|
||||
:upload_failed,
|
||||
:read_error,
|
||||
:out_of_memory,
|
||||
:operation_timedout,
|
||||
:obsolete29,
|
||||
:ftp_port_failed,
|
||||
:ftp_couldnt_use_rest,
|
||||
:obsolete32,
|
||||
:range_error,
|
||||
:http_post_error,
|
||||
:ssl_connect_error,
|
||||
:bad_download_resume,
|
||||
:file_couldnt_read_file,
|
||||
:ldap_cannot_bind,
|
||||
:ldap_search_failed,
|
||||
:obsolete40,
|
||||
:function_not_found,
|
||||
:aborted_by_callback,
|
||||
:bad_function_argument,
|
||||
:obsolete44,
|
||||
:interface_failed,
|
||||
:obsolete46,
|
||||
:too_many_redirects ,
|
||||
:unknown_option,
|
||||
:telnet_option_syntax ,
|
||||
:obsolete50,
|
||||
:peer_failed_verification,
|
||||
:got_nothing,
|
||||
:ssl_engine_notfound,
|
||||
:ssl_engine_setfailed,
|
||||
:send_error,
|
||||
:recv_error,
|
||||
:obsolete57,
|
||||
:ssl_certproblem,
|
||||
:ssl_cipher,
|
||||
:bad_content_encoding,
|
||||
:ldap_invalid_url,
|
||||
:filesize_exceeded,
|
||||
:use_ssl_failed,
|
||||
:send_fail_rewind,
|
||||
:ssl_engine_initfailed,
|
||||
:login_denied,
|
||||
:tftp_notfound,
|
||||
:tftp_perm,
|
||||
:remote_disk_full,
|
||||
:tftp_illegal,
|
||||
:tftp_unknownid,
|
||||
:remote_file_exists,
|
||||
:tftp_nosuchuser,
|
||||
:conv_failed,
|
||||
:conv_reqd,
|
||||
:ssl_cacert_badfile,
|
||||
:remote_file_not_found,
|
||||
:ssh,
|
||||
:ssl_shutdown_failed,
|
||||
:again,
|
||||
:ssl_crl_badfile,
|
||||
:ssl_issuer_error,
|
||||
:ftp_pret_failed,
|
||||
:rtsp_cseq_error,
|
||||
:rtsp_session_error,
|
||||
:ftp_bad_file_list,
|
||||
:chunk_failed,
|
||||
:last
|
||||
]
|
||||
end
|
||||
|
||||
# Curl-Multi socket error codes, refer
|
||||
# https://github.com/bagder/curl/blob/master/include/curl/multi.h for details
|
||||
def multi_codes
|
||||
[
|
||||
:call_multi_perform, -1,
|
||||
:ok,
|
||||
:bad_handle,
|
||||
:bad_easy_handle,
|
||||
:out_of_memory,
|
||||
:internal_error,
|
||||
:bad_socket,
|
||||
:unknown_option,
|
||||
:last
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Curl
|
||||
# :nodoc:
|
||||
VERSION_NOW = 3
|
||||
|
||||
# Flag. Initialize SSL.
|
||||
GLOBAL_SSL = 0x01
|
||||
# Flag. Initialize win32 socket libraries.
|
||||
GLOBAL_WIN32 = 0x02
|
||||
# Flag. Initialize everything possible.
|
||||
GLOBAL_ALL = (GLOBAL_SSL | GLOBAL_WIN32)
|
||||
# Flag. Initialize everything by default.
|
||||
GLOBAL_DEFAULT = GLOBAL_ALL
|
||||
|
||||
# :nodoc:
|
||||
EasyCode = enum(:easy_code, easy_codes)
|
||||
# :nodoc:
|
||||
MultiCode = enum(:multi_code, multi_codes)
|
||||
|
||||
# :nodoc:
|
||||
EasyOption = enum(:easy_option, easy_options(:enum).to_a.flatten)
|
||||
# :nodoc:
|
||||
MultiOption = enum(:multi_option, multi_options(:enum).to_a.flatten)
|
||||
|
||||
# Used by curl_debug_callback when setting CURLOPT_DEBUGFUNCTION
|
||||
# https://github.com/bagder/curl/blob/master/include/curl/curl.h#L378 for details
|
||||
DebugInfoType = enum(:debug_info_type, debug_info_types)
|
||||
|
||||
# :nodoc:
|
||||
InfoType = enum(info_types.to_a.flatten)
|
||||
|
||||
# Info details, refer
|
||||
# https://github.com/bagder/curl/blob/master/src/tool_writeout.c#L66 for details
|
||||
Info = enum(:info, infos.to_a.flatten)
|
||||
|
||||
# Form options, used by FormAdd for temporary storage, refer
|
||||
# https://github.com/bagder/curl/blob/master/lib/formdata.h#L51 for details
|
||||
FormOption = enum(:form_option, form_options)
|
||||
|
||||
# :nodoc:
|
||||
MsgCode = enum(:msg_code, msg_codes)
|
||||
|
||||
VERSION_IPV6 = (1<<0) # IPv6-enabled
|
||||
VERSION_KERBEROS4 = (1<<1) # kerberos auth is supported
|
||||
VERSION_SSL = (1<<2) # SSL options are present
|
||||
VERSION_LIBZ = (1<<3) # libz features are present
|
||||
VERSION_NTLM = (1<<4) # NTLM auth is supported
|
||||
VERSION_GSSNEGOTIATE = (1<<5) # Negotiate auth supp
|
||||
VERSION_DEBUG = (1<<6) # built with debug capabilities
|
||||
VERSION_ASYNCHDNS = (1<<7) # asynchronous dns resolves
|
||||
VERSION_SPNEGO = (1<<8) # SPNEGO auth is supported
|
||||
VERSION_LARGEFILE = (1<<9) # supports files bigger than 2GB
|
||||
VERSION_IDN = (1<<10) # International Domain Names support
|
||||
VERSION_SSPI = (1<<11) # SSPI is supported
|
||||
VERSION_CONV = (1<<12) # character conversions supported
|
||||
VERSION_CURLDEBUG = (1<<13) # debug memory tracking supported
|
||||
VERSION_TLSAUTH_SRP = (1<<14) # TLS-SRP auth is supported
|
||||
VERSION_NTLM_WB = (1<<15) # NTLM delegating to winbind helper
|
||||
VERSION_HTTP2 = (1<<16) # HTTP2 support built
|
||||
VERSION_GSSAPI = (1<<17) # GSS-API is supported
|
||||
|
||||
SOCKET_BAD = -1
|
||||
SOCKET_TIMEOUT = SOCKET_BAD
|
||||
|
||||
PollAction = enum(:poll_action, [
|
||||
:none,
|
||||
:in,
|
||||
:out,
|
||||
:inout,
|
||||
:remove
|
||||
])
|
||||
|
||||
SocketReadiness = bitmask(:socket_readiness, [
|
||||
:in, # CURL_CSELECT_IN - 0x01 (bit 0)
|
||||
:out, # CURL_CSELECT_OUT - 0x02 (bit 1)
|
||||
:err, # CURL_CSELECT_ERR - 0x04 (bit 2)
|
||||
])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Curls
|
||||
|
||||
# This module contains the available options for forms.
|
||||
module FormOptions
|
||||
|
||||
# Form options, used by FormAdd for temporary storage, refer
|
||||
# https://github.com/bagder/curl/blob/master/lib/formdata.h#L51 for details
|
||||
def form_options
|
||||
[
|
||||
:none,
|
||||
:copyname,
|
||||
:ptrname,
|
||||
:namelength,
|
||||
:copycontents,
|
||||
:ptrcontents,
|
||||
:contentslength,
|
||||
:filecontent,
|
||||
:array,
|
||||
:obsolete,
|
||||
:file,
|
||||
:buffer,
|
||||
:bufferptr,
|
||||
:bufferlength,
|
||||
:contenttype,
|
||||
:contentheader,
|
||||
:filename,
|
||||
:end,
|
||||
:obsolete2,
|
||||
:stream,
|
||||
:last
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Curls
|
||||
|
||||
# This module contains the functions to be attached in order to work with
|
||||
# libcurl.
|
||||
module Functions
|
||||
|
||||
# :nodoc:
|
||||
def self.extended(base)
|
||||
base.attach_function :global_init, :curl_global_init, [:long], :int
|
||||
base.attach_function :global_cleanup, :curl_global_cleanup, [], :void
|
||||
base.attach_function :free, :curl_free, [:pointer], :void
|
||||
|
||||
base.attach_function :easy_init, :curl_easy_init, [], :pointer
|
||||
base.attach_function :easy_cleanup, :curl_easy_cleanup, [:pointer], :void
|
||||
base.attach_function :easy_getinfo, :curl_easy_getinfo, [:pointer, :info, :varargs], :easy_code
|
||||
base.attach_function :easy_setopt, :curl_easy_setopt, [:pointer, :easy_option, :varargs], :easy_code
|
||||
base.instance_variable_set(:@blocking, true)
|
||||
base.attach_function :easy_perform, :curl_easy_perform, [:pointer], :easy_code
|
||||
base.attach_function :easy_strerror, :curl_easy_strerror, [:easy_code], :string
|
||||
base.attach_function :easy_escape, :curl_easy_escape, [:pointer, :pointer, :int], :pointer
|
||||
base.attach_function :easy_reset, :curl_easy_reset, [:pointer], :void
|
||||
base.attach_function :easy_duphandle, :curl_easy_duphandle, [:pointer], :pointer
|
||||
|
||||
base.attach_function :formadd, :curl_formadd, [:pointer, :pointer, :varargs], :int
|
||||
base.attach_function :formfree, :curl_formfree, [:pointer], :void
|
||||
|
||||
base.attach_function :multi_init, :curl_multi_init, [], :pointer
|
||||
base.attach_function :multi_cleanup, :curl_multi_cleanup, [:pointer], :void
|
||||
base.attach_function :multi_add_handle, :curl_multi_add_handle, [:pointer, :pointer], :multi_code
|
||||
base.attach_function :multi_remove_handle, :curl_multi_remove_handle, [:pointer, :pointer], :multi_code
|
||||
base.attach_function :multi_info_read, :curl_multi_info_read, [:pointer, :pointer], Curl::Msg.ptr
|
||||
base.attach_function :multi_perform, :curl_multi_perform, [:pointer, :pointer], :multi_code
|
||||
base.attach_function :multi_timeout, :curl_multi_timeout, [:pointer, :pointer], :multi_code
|
||||
base.attach_function :multi_fdset, :curl_multi_fdset, [:pointer, Curl::FDSet.ptr, Curl::FDSet.ptr, Curl::FDSet.ptr, :pointer], :multi_code
|
||||
base.attach_function :multi_strerror, :curl_multi_strerror, [:int], :string
|
||||
base.attach_function :multi_setopt, :curl_multi_setopt, [:pointer, :multi_option, :varargs], :multi_code
|
||||
base.attach_function :multi_socket_action, :curl_multi_socket_action, [:pointer, :int, :socket_readiness, :pointer], :multi_code
|
||||
|
||||
base.attach_function :version, :curl_version, [], :string
|
||||
base.attach_function :version_info, :curl_version_info, [], Curl::VersionInfoData.ptr
|
||||
|
||||
base.attach_function :slist_append, :curl_slist_append, [:pointer, :string], :pointer
|
||||
base.attach_function :slist_free_all, :curl_slist_free_all, [:pointer], :void
|
||||
base.instance_variable_set(:@blocking, true)
|
||||
|
||||
if Curl.windows?
|
||||
base.ffi_lib 'ws2_32'
|
||||
else
|
||||
base.ffi_lib ::FFI::Library::LIBC
|
||||
end
|
||||
|
||||
base.attach_function :select, [:int, Curl::FDSet.ptr, Curl::FDSet.ptr, Curl::FDSet.ptr, Curl::Timeval.ptr], :int
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,151 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Curls
|
||||
|
||||
# This module contains logic for the available informations
|
||||
# on an easy, eg.: connect_time.
|
||||
module Infos
|
||||
|
||||
# Return info types.
|
||||
#
|
||||
# @example Return info types.
|
||||
# Ethon::Curl.info_types
|
||||
#
|
||||
# @return [ Hash ] The info types.
|
||||
def info_types
|
||||
{
|
||||
:string =>0x100000,
|
||||
:long => 0x200000,
|
||||
:double =>0x300000,
|
||||
:slist => 0x400000
|
||||
}
|
||||
end
|
||||
|
||||
# http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTDEBUGFUNCTION
|
||||
# https://github.com/bagder/curl/blob/master/include/curl/curl.h#L378
|
||||
#
|
||||
# @example Return debug info types.
|
||||
# Ethon::Curl.debug_info_types
|
||||
#
|
||||
# @return [ Hash ] The info types available to curl_debug_callback.
|
||||
def debug_info_types
|
||||
[
|
||||
:text, 0,
|
||||
:header_in,
|
||||
:header_out,
|
||||
:data_in,
|
||||
:data_out,
|
||||
:ssl_data_in,
|
||||
:ssl_data_out
|
||||
]
|
||||
end
|
||||
|
||||
# Return Info details, refer
|
||||
# https://github.com/bagder/curl/blob/master/src/tool_writeout.c#L66 for details
|
||||
#
|
||||
# @example Return infos.
|
||||
# Ethon::Curl.infos
|
||||
#
|
||||
# @return [ Hash ] The infos.
|
||||
def infos
|
||||
{
|
||||
:effective_url => info_types[:string] + 1,
|
||||
:response_code => info_types[:long] + 2,
|
||||
:total_time => info_types[:double] + 3,
|
||||
:namelookup_time => info_types[:double] + 4,
|
||||
:connect_time => info_types[:double] + 5,
|
||||
:pretransfer_time => info_types[:double] + 6,
|
||||
:size_upload => info_types[:double] + 7,
|
||||
:size_download => info_types[:double] + 8,
|
||||
:speed_download => info_types[:double] + 9,
|
||||
:speed_upload => info_types[:double] + 10,
|
||||
:header_size => info_types[:long] + 11,
|
||||
:request_size => info_types[:long] + 12,
|
||||
:ssl_verifyresult => info_types[:long] + 13,
|
||||
:filetime => info_types[:long] + 14,
|
||||
:content_length_download =>info_types[:double] + 15,
|
||||
:content_length_upload => info_types[:double] + 16,
|
||||
:starttransfer_time => info_types[:double] + 17,
|
||||
:content_type => info_types[:string] + 18,
|
||||
:redirect_time => info_types[:double] + 19,
|
||||
:redirect_count => info_types[:long] + 20,
|
||||
:private => info_types[:string] + 21,
|
||||
:http_connectcode => info_types[:long] + 22,
|
||||
:httpauth_avail => info_types[:long] + 23,
|
||||
:proxyauth_avail => info_types[:long] + 24,
|
||||
:os_errno => info_types[:long] + 25,
|
||||
:num_connects => info_types[:long] + 26,
|
||||
:ssl_engines => info_types[:slist] + 27,
|
||||
:cookielist => info_types[:slist] + 28,
|
||||
:lastsocket => info_types[:long] + 29,
|
||||
:ftp_entry_path => info_types[:string] + 30,
|
||||
:redirect_url => info_types[:string] + 31,
|
||||
:primary_ip => info_types[:string] + 32,
|
||||
:appconnect_time => info_types[:double] + 33,
|
||||
:certinfo => info_types[:slist] + 34,
|
||||
:condition_unmet => info_types[:long] + 35,
|
||||
:rtsp_session_id => info_types[:string] + 36,
|
||||
:rtsp_client_cseq => info_types[:long] + 37,
|
||||
:rtsp_server_cseq => info_types[:long] + 38,
|
||||
:rtsp_cseq_recv => info_types[:long] + 39,
|
||||
:primary_port => info_types[:long] + 40,
|
||||
:local_ip => info_types[:string] + 41,
|
||||
:local_port => info_types[:long] + 42,
|
||||
:last =>42
|
||||
}
|
||||
end
|
||||
|
||||
# Return info as string.
|
||||
#
|
||||
# @example Return info.
|
||||
# Curl.get_info_string(:primary_ip, easy)
|
||||
#
|
||||
# @param [ Symbol ] option The option name.
|
||||
# @param [ ::FFI::Pointer ] handle The easy handle.
|
||||
#
|
||||
# @return [ String ] The info.
|
||||
def get_info_string(option, handle)
|
||||
string_ptr = ::FFI::MemoryPointer.new(:pointer)
|
||||
|
||||
if easy_getinfo(handle, option, :pointer, string_ptr) == :ok
|
||||
ptr=string_ptr.read_pointer
|
||||
ptr.null? ? nil : ptr.read_string
|
||||
end
|
||||
end
|
||||
|
||||
# Return info as integer.
|
||||
#
|
||||
# @example Return info.
|
||||
# Curl.get_info_long(:response_code, easy)
|
||||
#
|
||||
# @param [ Symbol ] option The option name.
|
||||
# @param [ ::FFI::Pointer ] handle The easy handle.
|
||||
#
|
||||
# @return [ Integer ] The info.
|
||||
def get_info_long(option, handle)
|
||||
long_ptr = ::FFI::MemoryPointer.new(:long)
|
||||
|
||||
if easy_getinfo(handle, option, :pointer, long_ptr) == :ok
|
||||
long_ptr.read_long
|
||||
end
|
||||
end
|
||||
|
||||
# Return info as float
|
||||
#
|
||||
# @example Return info.
|
||||
# Curl.get_info_double(:response_code, easy)
|
||||
#
|
||||
# @param [ Symbol ] option The option name.
|
||||
# @param [ ::FFI::Pointer ] handle The easy handle.
|
||||
#
|
||||
# @return [ Float ] The info.
|
||||
def get_info_double(option, handle)
|
||||
double_ptr = ::FFI::MemoryPointer.new(:double)
|
||||
|
||||
if easy_getinfo(handle, option, :pointer, double_ptr) == :ok
|
||||
double_ptr.read_double
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Curls
|
||||
|
||||
# This module contains available message codes.
|
||||
module Messages
|
||||
|
||||
# Return message codes.
|
||||
#
|
||||
# @example Return message codes.
|
||||
# Ethon::Curl.msg_codes
|
||||
#
|
||||
# @return [ Array ] The messages codes.
|
||||
def msg_codes
|
||||
[:none, :done, :last]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,503 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Curls
|
||||
|
||||
# This module contains logic for setting options on
|
||||
# easy or multi interface.
|
||||
module Options
|
||||
|
||||
OPTION_STRINGS = { :easy => 'easy_options', :multi => 'multi_options' }.freeze
|
||||
FOPTION_STRINGS = { :easy => 'EASY_OPTIONS', :multi => 'MULTI_OPTIONS' }.freeze
|
||||
FUNCS = { :easy => 'easy_setopt', :multi => 'multi_setopt' }.freeze
|
||||
# Sets appropriate option for easy, depending on value type.
|
||||
def set_option(option, value, handle, type = :easy)
|
||||
type = type.to_sym unless type.is_a?(Symbol)
|
||||
raise NameError, "Ethon::Curls::Options unknown type #{type}." unless respond_to?(OPTION_STRINGS[type])
|
||||
opthash=send(OPTION_STRINGS[type], nil)
|
||||
raise Errors::InvalidOption.new(option) unless opthash.include?(option)
|
||||
|
||||
case opthash[option][:type]
|
||||
when :none
|
||||
return if value.nil?
|
||||
value=1
|
||||
va_type=:long
|
||||
when :int
|
||||
return if value.nil?
|
||||
va_type=:long
|
||||
value=value.to_i
|
||||
when :bool
|
||||
return if value.nil?
|
||||
va_type=:long
|
||||
value=(value&&value!=0) ? 1 : 0
|
||||
when :time
|
||||
return if value.nil?
|
||||
va_type=:long
|
||||
value=value.to_i
|
||||
when :enum
|
||||
return if value.nil?
|
||||
va_type=:long
|
||||
value = case value
|
||||
when Symbol
|
||||
opthash[option][:opts][value]
|
||||
when String
|
||||
opthash[option][:opts][value.to_sym]
|
||||
else
|
||||
value
|
||||
end.to_i
|
||||
when :bitmask
|
||||
return if value.nil?
|
||||
va_type=:long
|
||||
value = case value
|
||||
when Symbol
|
||||
opthash[option][:opts][value]
|
||||
when Array
|
||||
value.inject(0) { |res,v| res|opthash[option][:opts][v] }
|
||||
else
|
||||
value
|
||||
end.to_i
|
||||
when :string
|
||||
va_type=:string
|
||||
value=value.to_s unless value.nil?
|
||||
when :string_as_pointer
|
||||
va_type = :pointer
|
||||
s = ''
|
||||
s = value.to_s unless value.nil?
|
||||
value = FFI::MemoryPointer.new(:char, s.bytesize)
|
||||
value.put_bytes(0, s)
|
||||
when :string_escape_null
|
||||
va_type=:string
|
||||
value=Util.escape_zero_byte(value) unless value.nil?
|
||||
when :ffipointer
|
||||
va_type=:pointer
|
||||
raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? FFI::Pointer
|
||||
when :curl_slist
|
||||
va_type=:pointer
|
||||
raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? FFI::Pointer
|
||||
when :buffer
|
||||
raise NotImplementedError, "Ethon::Curls::Options option #{option} buffer type not implemented."
|
||||
when :dontuse_object
|
||||
raise NotImplementedError, "Ethon::Curls::Options option #{option} type not implemented."
|
||||
when :cbdata
|
||||
raise NotImplementedError, "Ethon::Curls::Options option #{option} callback data type not implemented. Use Ruby closures."
|
||||
when :callback
|
||||
va_type=:callback
|
||||
raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
|
||||
when :socket_callback
|
||||
va_type=:socket_callback
|
||||
raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
|
||||
when :timer_callback
|
||||
va_type=:timer_callback
|
||||
raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
|
||||
when :debug_callback
|
||||
va_type=:debug_callback
|
||||
raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
|
||||
when :progress_callback
|
||||
va_type=:progress_callback
|
||||
raise Errors::InvalidValue.new(option,value) unless value.nil? or value.is_a? Proc
|
||||
when :off_t
|
||||
return if value.nil?
|
||||
va_type=:int64
|
||||
value=value.to_i
|
||||
end
|
||||
|
||||
if va_type==:long or va_type==:int64 then
|
||||
bits=FFI.type_size(va_type)*8
|
||||
tv=((value<0) ? value.abs-1 : value)
|
||||
raise Errors::InvalidValue.new(option,value) unless tv<(1<<bits)
|
||||
end
|
||||
send(FUNCS[type], handle, opthash[option][:opt], va_type, value)
|
||||
end
|
||||
|
||||
OPTION_TYPE_BASE = {
|
||||
:long => 0,
|
||||
:objectpoint => 10000,
|
||||
:functionpoint => 20000,
|
||||
:off_t => 30000
|
||||
}
|
||||
OPTION_TYPE_MAP = {
|
||||
:none => :long,
|
||||
:int => :long,
|
||||
:bool => :long,
|
||||
:time => :long,
|
||||
:enum => :long, # Two ways to specify values (as opts parameter):
|
||||
# * Array of symbols, these will number sequentially
|
||||
# starting at 0. Skip elements with nil. (see :netrc)
|
||||
# * Hash of :symbol => enum_value (See :proxytype)
|
||||
:bitmask => :long, # Three ways to specify values (as opts parameter):
|
||||
# * Hash of :symbol => bitmask_value or Array.
|
||||
# An Array can be an array of already defined
|
||||
# Symbols, which represents a bitwise or of those
|
||||
# symbols. (See :httpauth)
|
||||
# * Array of symbols, these will number the bits
|
||||
# sequentially (i.e. 0, 1, 2, 4, etc.). Skip
|
||||
# elements with nil. The last element can be a
|
||||
# Hash, which will be interpreted as above.
|
||||
# (See :protocols)
|
||||
# :all defaults to all bits set
|
||||
:string => :objectpoint,
|
||||
:string_escape_null => :objectpoint,
|
||||
:string_as_pointer => :objectpoint,
|
||||
:ffipointer => :objectpoint, # FFI::Pointer
|
||||
:curl_slist => :objectpoint,
|
||||
:buffer => :objectpoint, # A memory buffer of size defined in the options
|
||||
:dontuse_object => :objectpoint, # An object we don't support (e.g. FILE*)
|
||||
:cbdata => :objectpoint,
|
||||
:callback => :functionpoint,
|
||||
:socket_callback => :functionpoint,
|
||||
:timer_callback => :functionpoint,
|
||||
:debug_callback => :functionpoint,
|
||||
:progress_callback => :functionpoint,
|
||||
:off_t => :off_t,
|
||||
}
|
||||
|
||||
def self.option(ftype,name,type,num,opts=nil)
|
||||
case type
|
||||
when :enum
|
||||
if opts.is_a? Array then
|
||||
opts=Hash[opts.each_with_index.to_a]
|
||||
elsif not opts.is_a? Hash then
|
||||
raise TypeError, "Ethon::Curls::Options #{ftype} #{name} Expected opts to be an Array or a Hash."
|
||||
end
|
||||
|
||||
when :bitmask
|
||||
if opts.is_a? Array then
|
||||
if opts.last.is_a? Hash then
|
||||
hopts=opts.pop
|
||||
else
|
||||
hopts={}
|
||||
end
|
||||
opts.each_with_index do |v,i|
|
||||
next if v.nil?
|
||||
if i==0 then
|
||||
hopts[v]=0
|
||||
else
|
||||
hopts[v]=1<<(i-1)
|
||||
end
|
||||
end
|
||||
opts=hopts
|
||||
elsif not opts.is_a? Hash then
|
||||
raise TypeError, "Ethon::Curls::Options #{ftype} #{name} Expected opts to be an Array or a Hash."
|
||||
end
|
||||
opts[:all]=-1 unless opts.include? :all
|
||||
opts.each do |k,v|
|
||||
if v.is_a? Array then
|
||||
opts[k]=v.map { |b| opts[b] }.inject :|
|
||||
end
|
||||
end
|
||||
|
||||
when :buffer
|
||||
raise TypeError, "Ethon::Curls::Options #{ftype} #{name} Expected opts to be an Array or a Hash." unless opts.is_a? Integer
|
||||
|
||||
else
|
||||
raise ArgumentError, "Ethon::Curls::Options #{ftype} #{name} Expected no opts." unless opts.nil?
|
||||
end
|
||||
opthash=const_get(FOPTION_STRINGS[ftype])
|
||||
opthash[name] = { :type => type,
|
||||
:opt => OPTION_TYPE_BASE[OPTION_TYPE_MAP[type]] + num,
|
||||
:opts => opts }
|
||||
end
|
||||
|
||||
def self.option_alias(ftype,name,*aliases)
|
||||
opthash=const_get(FOPTION_STRINGS[ftype])
|
||||
aliases.each { |a| opthash[a]=opthash[name] }
|
||||
end
|
||||
|
||||
def self.option_type(type)
|
||||
cname = FOPTION_STRINGS[type]
|
||||
const_set(cname, {})
|
||||
define_method(OPTION_STRINGS[type]) do |rt|
|
||||
return Ethon::Curls::Options.const_get(cname).map { |k, v| [k, v[:opt]] } if rt == :enum
|
||||
Ethon::Curls::Options.const_get(cname)
|
||||
end
|
||||
end
|
||||
|
||||
# Curl multi options, refer
|
||||
# Defined @ https://github.com/bagder/curl/blob/master/include/curl/multi.h
|
||||
# Documentation @ http://curl.haxx.se/libcurl/c/curl_multi_setopt.html
|
||||
option_type :multi
|
||||
|
||||
option :multi, :socketfunction, :socket_callback, 1
|
||||
option :multi, :socketdata, :cbdata, 2
|
||||
option :multi, :pipelining, :int, 3
|
||||
option :multi, :timerfunction, :timer_callback, 4
|
||||
option :multi, :timerdata, :cbdata, 5
|
||||
option :multi, :maxconnects, :int, 6
|
||||
option :multi, :max_host_connections, :int, 7
|
||||
option :multi, :max_pipeline_length, :int, 8
|
||||
option :multi, :content_length_penalty_size, :off_t, 9
|
||||
option :multi, :chunk_length_penalty_size, :off_t, 10
|
||||
option :multi, :pipelining_site_bl, :dontuse_object, 11
|
||||
option :multi, :pipelining_server_bl, :dontuse_object, 12
|
||||
option :multi, :max_total_connections, :int, 3
|
||||
|
||||
# Curl easy options
|
||||
# Defined @ https://github.com/bagder/curl/blob/master/include/curl/curl.h
|
||||
# Documentation @ http://curl.haxx.se/libcurl/c/curl_easy_setopt.html
|
||||
## BEHAVIOR OPTIONS
|
||||
option_type :easy
|
||||
|
||||
option :easy, :verbose, :bool, 41
|
||||
option :easy, :header, :bool, 42
|
||||
option :easy, :noprogress, :bool, 43
|
||||
option :easy, :nosignal, :bool, 99
|
||||
option :easy, :wildcardmatch, :bool, 197
|
||||
## CALLBACK OPTIONS
|
||||
option :easy, :writefunction, :callback, 11
|
||||
option :easy, :file, :cbdata, 1
|
||||
option_alias :easy, :file, :writedata
|
||||
option :easy, :readfunction, :callback, 12
|
||||
option :easy, :infile, :cbdata, 9
|
||||
option_alias :easy, :infile, :readdata
|
||||
option :easy, :ioctlfunction, :callback, 130
|
||||
option :easy, :ioctldata, :cbdata, 131
|
||||
option :easy, :seekfunction, :callback, 167
|
||||
option :easy, :seekdata, :cbdata, 168
|
||||
option :easy, :sockoptfunction, :callback, 148
|
||||
option :easy, :sockoptdata, :cbdata, 149
|
||||
option :easy, :opensocketfunction, :callback, 163
|
||||
option :easy, :opensocketdata, :cbdata, 164
|
||||
option :easy, :closesocketfunction, :callback, 208
|
||||
option :easy, :closesocketdata, :cbdata, 209
|
||||
option :easy, :path_as_is, :bool, 234
|
||||
option :easy, :progressfunction, :progress_callback, 56
|
||||
option :easy, :progressdata, :cbdata, 57
|
||||
option :easy, :headerfunction, :callback, 79
|
||||
option :easy, :writeheader, :cbdata, 29
|
||||
option_alias :easy, :writeheader, :headerdata
|
||||
option :easy, :debugfunction, :debug_callback, 94
|
||||
option :easy, :debugdata, :cbdata, 95
|
||||
option :easy, :ssl_ctx_function, :callback, 108
|
||||
option :easy, :ssl_ctx_data, :cbdata, 109
|
||||
option :easy, :conv_to_network_function, :callback, 143
|
||||
option :easy, :conv_from_network_function, :callback, 142
|
||||
option :easy, :conv_from_utf8_function, :callback, 144
|
||||
option :easy, :interleavefunction, :callback, 196
|
||||
option :easy, :interleavedata, :cbdata, 195
|
||||
option :easy, :chunk_bgn_function, :callback, 198
|
||||
option :easy, :chunk_end_function, :callback, 199
|
||||
option :easy, :chunk_data, :cbdata, 201
|
||||
option :easy, :fnmatch_function, :callback, 200
|
||||
option :easy, :fnmatch_data, :cbdata, 202
|
||||
option :easy, :xferinfofunction, :progress_callback, 219
|
||||
option :easy, :xferinfodata, :cbdata, 57
|
||||
## ERROR OPTIONS
|
||||
option :easy, :errorbuffer, :buffer, 10, 256
|
||||
option :easy, :stderr, :dontuse_object, 37
|
||||
option :easy, :failonerror, :bool, 45
|
||||
## NETWORK OPTIONS
|
||||
option :easy, :url, :string, 2
|
||||
option :easy, :protocols, :bitmask, 181, [nil, :http, :https, :ftp, :ftps, :scp, :sftp, :telnet, :ldap, :ldaps, :dict, :file, :tftp, :imap, :imaps, :pop3, :pop3s, :smtp, :smtps, :rtsp, :rtmp, :rtmpt, :rtmpe, :rtmpte, :rtmps, :rtmpts, :gopher]
|
||||
option :easy, :redir_protocols, :bitmask, 182, [nil, :http, :https, :ftp, :ftps, :scp, :sftp, :telnet, :ldap, :ldaps, :dict, :file, :tftp, :imap, :imaps, :pop3, :pop3s, :smtp, :smtps, :rtsp, :rtmp, :rtmpt, :rtmpe, :rtmpte, :rtmps, :rtmpts, :gopher]
|
||||
option :easy, :proxy, :string, 4
|
||||
option :easy, :proxyport, :int, 59
|
||||
option :easy, :proxytype, :enum, 101, [:http, :http_1_0, :https, nil, :socks4, :socks5, :socks4a, :socks5_hostname]
|
||||
option :easy, :noproxy, :string, 177
|
||||
option :easy, :httpproxytunnel, :bool, 61
|
||||
option :easy, :socks5_gssapi_service, :string, 179
|
||||
option :easy, :socks5_gssapi_nec, :bool, 180
|
||||
option :easy, :interface, :string, 62
|
||||
option :easy, :localport, :int, 139
|
||||
option :easy, :localportrange, :int, 140
|
||||
option :easy, :dns_cache_timeout, :int, 92
|
||||
option :easy, :dns_use_global_cache, :bool, 91 # Obsolete
|
||||
option :easy, :dns_interface, :string, 221
|
||||
option :easy, :dns_local_ip4, :string, 222
|
||||
option :easy, :dns_shuffle_addresses, :bool, 275
|
||||
option :easy, :buffersize, :int, 98
|
||||
option :easy, :port, :int, 3
|
||||
option :easy, :tcp_nodelay, :bool, 121
|
||||
option :easy, :address_scope, :int, 171
|
||||
option :easy, :tcp_fastopen, :bool, 212
|
||||
option :easy, :tcp_keepalive, :bool, 213
|
||||
option :easy, :tcp_keepidle, :int, 214
|
||||
option :easy, :tcp_keepintvl, :int, 215
|
||||
## NAMES and PASSWORDS OPTIONS (Authentication)
|
||||
option :easy, :netrc, :enum, 51, [:ignored, :optional, :required]
|
||||
option :easy, :netrc_file, :string, 118
|
||||
option :easy, :userpwd, :string, 5
|
||||
option :easy, :proxyuserpwd, :string, 6
|
||||
option :easy, :username, :string, 173
|
||||
option :easy, :password, :string, 174
|
||||
option :easy, :proxyusername, :string, 175
|
||||
option :easy, :proxypassword, :string, 176
|
||||
option :easy, :httpauth, :bitmask, 107, [:none, :basic, :digest, :gssnegotiate, :ntlm, :digest_ie, :ntlm_wb, {:only => 1<<31, :any => ~0x10, :anysafe => ~0x11, :auto => 0x1f}]
|
||||
option :easy, :tlsauth_type, :enum, 206, [:none, :srp]
|
||||
option :easy, :tlsauth_username, :string, 204
|
||||
option :easy, :tlsauth_password, :string, 205
|
||||
option :easy, :proxyauth, :bitmask, 111, [:none, :basic, :digest, :gssnegotiate, :ntlm, :digest_ie, :ntlm_wb, {:only => 1<<31, :any => ~0x10, :anysafe => ~0x11, :auto => 0x1f}]
|
||||
option :easy, :sasl_ir, :bool, 218
|
||||
## HTTP OPTIONS
|
||||
option :easy, :autoreferer, :bool, 58
|
||||
option :easy, :accept_encoding, :string, 102
|
||||
option_alias :easy, :accept_encoding, :encoding
|
||||
option :easy, :transfer_encoding, :bool, 207
|
||||
option :easy, :followlocation, :bool, 52
|
||||
option :easy, :unrestricted_auth, :bool, 105
|
||||
option :easy, :maxredirs, :int, 68
|
||||
option :easy, :postredir, :bitmask, 161, [:get_all, :post_301, :post_302, :post_303, {:post_all => [:post_301, :post_302, :post_303]}]
|
||||
option_alias :easy, :postredir, :post301
|
||||
option :easy, :put, :bool, 54
|
||||
option :easy, :post, :bool, 47
|
||||
option :easy, :postfields, :string, 15
|
||||
option :easy, :postfieldsize, :int, 60
|
||||
option :easy, :postfieldsize_large, :off_t, 120
|
||||
option :easy, :copypostfields, :string_as_pointer, 165
|
||||
option :easy, :httppost, :ffipointer, 24
|
||||
option :easy, :referer, :string, 16
|
||||
option :easy, :useragent, :string, 18
|
||||
option :easy, :httpheader, :curl_slist, 23
|
||||
option :easy, :http200aliases, :curl_slist, 104
|
||||
option :easy, :cookie, :string, 22
|
||||
option :easy, :cookiefile, :string, 31
|
||||
option :easy, :cookiejar, :string, 82
|
||||
option :easy, :cookiesession, :bool, 96
|
||||
option :easy, :cookielist, :string, 135
|
||||
option :easy, :httpget, :bool, 80
|
||||
option :easy, :http_version, :enum, 84, [:none, :httpv1_0, :httpv1_1, :httpv2_0, :httpv2_tls, :httpv2_prior_knowledge]
|
||||
option :easy, :ignore_content_length, :bool, 136
|
||||
option :easy, :http_content_decoding, :bool, 158
|
||||
option :easy, :http_transfer_decoding, :bool, 157
|
||||
## SMTP OPTIONS
|
||||
option :easy, :mail_from, :string, 186
|
||||
option :easy, :mail_rcpt, :curl_slist, 187
|
||||
option :easy, :mail_auth, :string, 217
|
||||
## TFTP OPTIONS
|
||||
option :easy, :tftp_blksize, :int, 178
|
||||
## FTP OPTIONS
|
||||
option :easy, :ftpport, :string, 17
|
||||
option :easy, :quote, :curl_slist, 28
|
||||
option :easy, :postquote, :curl_slist, 39
|
||||
option :easy, :prequote, :curl_slist, 93
|
||||
option :easy, :dirlistonly, :bool, 48
|
||||
option_alias :easy, :dirlistonly, :ftplistonly
|
||||
option :easy, :append, :bool, 50
|
||||
option_alias :easy, :append, :ftpappend
|
||||
option :easy, :ftp_use_eprt, :bool, 106
|
||||
option :easy, :ftp_use_epsv, :bool, 85
|
||||
option :easy, :ftp_use_pret, :bool, 188
|
||||
option :easy, :ftp_create_missing_dirs, :bool, 110
|
||||
option :easy, :ftp_response_timeout, :int, 112
|
||||
option_alias :easy, :ftp_response_timeout, :server_response_timeout
|
||||
option :easy, :ftp_alternative_to_user, :string, 147
|
||||
option :easy, :ftp_skip_pasv_ip, :bool, 137
|
||||
option :easy, :ftpsslauth, :enum, 129, [:default, :ssl, :tls]
|
||||
option :easy, :ftp_ssl_ccc, :enum, 154, [:none, :passive, :active]
|
||||
option :easy, :ftp_account, :string, 134
|
||||
option :easy, :ftp_filemethod, :enum, 138, [:default, :multicwd, :nocwd, :singlecwd]
|
||||
## RTSP OPTIONS
|
||||
option :easy, :rtsp_request, :enum, 189, [:none, :options, :describe, :announce, :setup, :play, :pause, :teardown, :get_parameter, :set_parameter, :record, :receive]
|
||||
option :easy, :rtsp_session_id, :string, 190
|
||||
option :easy, :rtsp_stream_uri, :string, 191
|
||||
option :easy, :rtsp_transport, :string, 192
|
||||
option_alias :easy, :httpheader, :rtspheader
|
||||
option :easy, :rtsp_client_cseq, :int, 193
|
||||
option :easy, :rtsp_server_cseq, :int, 194
|
||||
## PROTOCOL OPTIONS
|
||||
option :easy, :transfertext, :bool, 53
|
||||
option :easy, :proxy_transfer_mode, :bool, 166
|
||||
option :easy, :crlf, :bool, 27
|
||||
option :easy, :range, :string, 7
|
||||
option :easy, :resume_from, :int, 21
|
||||
option :easy, :resume_from_large, :off_t, 116
|
||||
option :easy, :customrequest, :string, 36
|
||||
option :easy, :filetime, :bool, 69
|
||||
option :easy, :nobody, :bool, 44
|
||||
option :easy, :infilesize, :int, 14
|
||||
option :easy, :infilesize_large, :off_t, 115
|
||||
option :easy, :upload, :bool, 46
|
||||
option :easy, :maxfilesize, :int, 114
|
||||
option :easy, :maxfilesize_large, :off_t, 117
|
||||
option :easy, :timecondition, :enum, 33, [:none, :ifmodsince, :ifunmodsince, :lastmod]
|
||||
option :easy, :timevalue, :time, 34
|
||||
## CONNECTION OPTIONS
|
||||
option :easy, :timeout, :int, 13
|
||||
option :easy, :timeout_ms, :int, 155
|
||||
option :easy, :low_speed_limit, :int, 19
|
||||
option :easy, :low_speed_time, :int, 20
|
||||
option :easy, :max_send_speed_large, :off_t, 145
|
||||
option :easy, :max_recv_speed_large, :off_t, 146
|
||||
option :easy, :maxconnects, :int, 71
|
||||
option :easy, :fresh_connect, :bool, 74
|
||||
option :easy, :forbid_reuse, :bool, 75
|
||||
option :easy, :connecttimeout, :int, 78
|
||||
option :easy, :connecttimeout_ms, :int, 156
|
||||
option :easy, :ipresolve, :enum, 113, [:whatever, :v4, :v6]
|
||||
option :easy, :connect_only, :bool, 141
|
||||
option :easy, :use_ssl, :enum, 119, [:none, :try, :control, :all]
|
||||
option_alias :easy, :use_ssl, :ftp_ssl
|
||||
option :easy, :resolve, :curl_slist, 203
|
||||
option :easy, :dns_servers, :string, 211
|
||||
option :easy, :accepttimeout_ms, :int, 212
|
||||
option :easy, :unix_socket_path, :string, 231
|
||||
option :easy, :pipewait, :bool, 237
|
||||
option_alias :easy, :unix_socket_path, :unix_socket
|
||||
## SSL and SECURITY OPTIONS
|
||||
option :easy, :sslcert, :string, 25
|
||||
option :easy, :sslcerttype, :string, 86
|
||||
option :easy, :sslkey, :string, 87
|
||||
option :easy, :sslkeytype, :string, 88
|
||||
option :easy, :keypasswd, :string, 26
|
||||
option_alias :easy, :keypasswd, :sslcertpasswd
|
||||
option_alias :easy, :keypasswd, :sslkeypasswd
|
||||
option :easy, :sslengine, :string, 89
|
||||
option :easy, :sslengine_default, :none, 90
|
||||
option :easy, :sslversion, :enum, 32, [:default, :tlsv1, :sslv2, :sslv3, :tlsv1_0, :tlsv1_1, :tlsv1_2, :tlsv1_3]
|
||||
option :easy, :ssl_verifypeer, :bool, 64
|
||||
option :easy, :cainfo, :string, 65
|
||||
option :easy, :issuercert, :string, 170
|
||||
option :easy, :capath, :string, 97
|
||||
option :easy, :crlfile, :string, 169
|
||||
option :easy, :ssl_verifyhost, :int, 81
|
||||
option :easy, :certinfo, :bool, 172
|
||||
option :easy, :random_file, :string, 76
|
||||
option :easy, :egdsocket, :string, 77
|
||||
option :easy, :ssl_cipher_list, :string, 83
|
||||
option :easy, :ssl_sessionid_cache, :bool, 150
|
||||
option :easy, :ssl_options, :bitmask, 216, [nil, :allow_beast]
|
||||
option :easy, :krblevel, :string, 63
|
||||
option_alias :easy, :krblevel, :krb4level
|
||||
option :easy, :gssapi_delegation, :bitmask, 210, [:none, :policy_flag, :flag]
|
||||
option :easy, :pinnedpublickey, :string, 230
|
||||
option_alias :easy, :pinnedpublickey, :pinned_public_key
|
||||
## PROXY SSL OPTIONS
|
||||
option :easy, :proxy_cainfo, :string, 246
|
||||
option :easy, :proxy_capath, :string, 247
|
||||
option :easy, :proxy_ssl_verifypeer, :bool, 248
|
||||
option :easy, :proxy_ssl_verifyhost, :int, 249
|
||||
option :easy, :proxy_sslversion, :enum, 250, [:default, :tlsv1, :sslv2, :sslv3, :tlsv1_0, :tlsv1_1, :tlsv1_2, :tlsv1_3]
|
||||
option :easy, :proxy_tlsauth_username, :string, 251
|
||||
option :easy, :proxy_tlsauth_password, :string, 252
|
||||
option :easy, :proxy_tlsauth_type, :enum, 253, [:none, :srp]
|
||||
option :easy, :proxy_sslcert, :string, 254
|
||||
option :easy, :proxy_sslcerttype, :string, 255
|
||||
option :easy, :proxy_sslkey, :string, 256
|
||||
option :easy, :proxy_sslkeytype, :string, 257
|
||||
option :easy, :proxy_keypasswd, :string, 258
|
||||
option_alias :easy, :proxy_keypasswd, :proxy_sslcertpasswd
|
||||
option_alias :easy, :proxy_keypasswd, :proxy_sslkeypasswd
|
||||
option :easy, :proxy_ssl_cipher_list, :string, 259
|
||||
option :easy, :proxy_crlfile, :string, 260
|
||||
option :easy, :proxy_ssl_options, :bitmask, 261, [nil, :allow_beast]
|
||||
option :easy, :pre_proxy, :string, 262
|
||||
option :easy, :proxy_pinnedpublickey, :string, 263
|
||||
option_alias :easy, :proxy_pinnedpublickey, :proxy_pinned_public_key
|
||||
option :easy, :proxy_issuercert, :string, 296
|
||||
## SSH OPTIONS
|
||||
option :easy, :ssh_auth_types, :bitmask, 151, [:none, :publickey, :password, :host, :keyboard, :agent, {:any => [:all], :default => [:any]}]
|
||||
option :easy, :ssh_host_public_key_md5, :string, 162
|
||||
option :easy, :ssh_public_keyfile, :string, 152
|
||||
option :easy, :ssh_private_keyfile, :string, 153
|
||||
option :easy, :ssh_knownhosts, :string, 183
|
||||
option :easy, :ssh_keyfunction, :callback, 184
|
||||
option :easy, :khstat, :enum, -1, [:fine_add_to_file, :fine, :reject, :defer] # Kludge to make this enum available... Access via CurL::EASY_OPTIONS[:khstat][:opt]
|
||||
option :easy, :ssh_keydata, :cbdata, 185
|
||||
## OTHER OPTIONS
|
||||
option :easy, :private, :cbdata, 103
|
||||
option :easy, :share, :dontuse_object, 100
|
||||
option :easy, :new_file_perms, :int, 159
|
||||
option :easy, :new_directory_perms, :int, 160
|
||||
## TELNET OPTIONS
|
||||
option :easy, :telnetoptions, :curl_slist, 70
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Curl
|
||||
callback :callback, [:pointer, :size_t, :size_t, :pointer], :size_t
|
||||
callback :socket_callback, [:pointer, :int, :poll_action, :pointer, :pointer], :multi_code
|
||||
callback :timer_callback, [:pointer, :long, :pointer], :multi_code
|
||||
callback :debug_callback, [:pointer, :debug_info_type, :pointer, :size_t, :pointer], :int
|
||||
callback :progress_callback, [:pointer, :long_long, :long_long, :long_long, :long_long], :int
|
||||
ffi_lib_flags :now, :global
|
||||
ffi_lib ['libcurl', 'libcurl.so.4']
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,315 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon/easy/informations'
|
||||
require 'ethon/easy/features'
|
||||
require 'ethon/easy/callbacks'
|
||||
require 'ethon/easy/options'
|
||||
require 'ethon/easy/header'
|
||||
require 'ethon/easy/util'
|
||||
require 'ethon/easy/params'
|
||||
require 'ethon/easy/form'
|
||||
require 'ethon/easy/http'
|
||||
require 'ethon/easy/operations'
|
||||
require 'ethon/easy/response_callbacks'
|
||||
require 'ethon/easy/debug_info'
|
||||
require 'ethon/easy/mirror'
|
||||
|
||||
module Ethon
|
||||
|
||||
# This is the class representing the libcurl easy interface
|
||||
# See http://curl.haxx.se/libcurl/c/libcurl-easy.html for more informations.
|
||||
#
|
||||
# @example You can access the libcurl easy interface through this class, every request is based on it. The simplest setup looks like that:
|
||||
#
|
||||
# e = Ethon::Easy.new(url: "www.example.com")
|
||||
# e.perform
|
||||
# #=> :ok
|
||||
#
|
||||
# @example You can the reuse this Easy for the next request:
|
||||
#
|
||||
# e.reset # reset easy handle
|
||||
# e.url = "www.google.com"
|
||||
# e.followlocation = true
|
||||
# e.perform
|
||||
# #=> :ok
|
||||
#
|
||||
# @see initialize
|
||||
class Easy
|
||||
include Ethon::Easy::Informations
|
||||
include Ethon::Easy::Callbacks
|
||||
include Ethon::Easy::Options
|
||||
include Ethon::Easy::Header
|
||||
include Ethon::Easy::Http
|
||||
include Ethon::Easy::Operations
|
||||
include Ethon::Easy::ResponseCallbacks
|
||||
extend Ethon::Easy::Features
|
||||
|
||||
# Returns the curl return code.
|
||||
#
|
||||
# @return [ Symbol ] The return code.
|
||||
# * :ok: All fine. Proceed as usual.
|
||||
# * :unsupported_protocol: The URL you passed to libcurl used a
|
||||
# protocol that this libcurl does not support. The support
|
||||
# might be a compile-time option that you didn't use, it can
|
||||
# be a misspelled protocol string or just a protocol
|
||||
# libcurl has no code for.
|
||||
# * :failed_init: Very early initialization code failed. This
|
||||
# is likely to be an internal error or problem, or a
|
||||
# resource problem where something fundamental couldn't
|
||||
# get done at init time.
|
||||
# * :url_malformat: The URL was not properly formatted.
|
||||
# * :not_built_in: A requested feature, protocol or option
|
||||
# was not found built-in in this libcurl due to a build-time
|
||||
# decision. This means that a feature or option was not enabled
|
||||
# or explicitly disabled when libcurl was built and in
|
||||
# order to get it to function you have to get a rebuilt libcurl.
|
||||
# * :couldnt_resolve_proxy: Couldn't resolve proxy. The given
|
||||
# proxy host could not be resolved.
|
||||
# * :couldnt_resolve_host: Couldn't resolve host. The given remote
|
||||
# host was not resolved.
|
||||
# * :couldnt_connect: Failed to connect() to host or proxy.
|
||||
# * :ftp_weird_server_reply: After connecting to a FTP server,
|
||||
# libcurl expects to get a certain reply back. This error
|
||||
# code implies that it got a strange or bad reply. The given
|
||||
# remote server is probably not an OK FTP server.
|
||||
# * :remote_access_denied: We were denied access to the resource
|
||||
# given in the URL. For FTP, this occurs while trying to
|
||||
# change to the remote directory.
|
||||
# * :ftp_accept_failed: While waiting for the server to connect
|
||||
# back when an active FTP session is used, an error code was
|
||||
# sent over the control connection or similar.
|
||||
# * :ftp_weird_pass_reply: After having sent the FTP password to
|
||||
# the server, libcurl expects a proper reply. This error code
|
||||
# indicates that an unexpected code was returned.
|
||||
# * :ftp_accept_timeout: During an active FTP session while
|
||||
# waiting for the server to connect, the CURLOPT_ACCEPTTIMOUT_MS
|
||||
# (or the internal default) timeout expired.
|
||||
# * :ftp_weird_pasv_reply: libcurl failed to get a sensible result
|
||||
# back from the server as a response to either a PASV or a
|
||||
# EPSV command. The server is flawed.
|
||||
# * :ftp_weird_227_format: FTP servers return a 227-line as a response
|
||||
# to a PASV command. If libcurl fails to parse that line,
|
||||
# this return code is passed back.
|
||||
# * :ftp_cant_get_host: An internal failure to lookup the host used
|
||||
# for the new connection.
|
||||
# * :ftp_couldnt_set_type: Received an error when trying to set
|
||||
# the transfer mode to binary or ASCII.
|
||||
# * :partial_file: A file transfer was shorter or larger than
|
||||
# expected. This happens when the server first reports an expected
|
||||
# transfer size, and then delivers data that doesn't match the
|
||||
# previously given size.
|
||||
# * :ftp_couldnt_retr_file: This was either a weird reply to a
|
||||
# 'RETR' command or a zero byte transfer complete.
|
||||
# * :quote_error: When sending custom "QUOTE" commands to the
|
||||
# remote server, one of the commands returned an error code that
|
||||
# was 400 or higher (for FTP) or otherwise indicated unsuccessful
|
||||
# completion of the command.
|
||||
# * :http_returned_error: This is returned if CURLOPT_FAILONERROR is
|
||||
# set TRUE and the HTTP server returns an error code that is >= 400.
|
||||
# * :write_error: An error occurred when writing received data to a
|
||||
# local file, or an error was returned to libcurl from a write callback.
|
||||
# * :upload_failed: Failed starting the upload. For FTP, the server
|
||||
# typically denied the STOR command. The error buffer usually
|
||||
# contains the server's explanation for this.
|
||||
# * :read_error: There was a problem reading a local file or an error
|
||||
# returned by the read callback.
|
||||
# * :out_of_memory: A memory allocation request failed. This is serious
|
||||
# badness and things are severely screwed up if this ever occurs.
|
||||
# * :operation_timedout: Operation timeout. The specified time-out
|
||||
# period was reached according to the conditions.
|
||||
# * :ftp_port_failed: The FTP PORT command returned error. This mostly
|
||||
# happens when you haven't specified a good enough address for
|
||||
# libcurl to use. See CURLOPT_FTPPORT.
|
||||
# * :ftp_couldnt_use_rest: The FTP REST command returned error. This
|
||||
# should never happen if the server is sane.
|
||||
# * :range_error: The server does not support or accept range requests.
|
||||
# * :http_post_error: This is an odd error that mainly occurs due to
|
||||
# internal confusion.
|
||||
# * :ssl_connect_error: A problem occurred somewhere in the SSL/TLS
|
||||
# handshake. You really want the error buffer and read the message
|
||||
# there as it pinpoints the problem slightly more. Could be
|
||||
# certificates (file formats, paths, permissions), passwords, and others.
|
||||
# * :bad_download_resume: The download could not be resumed because
|
||||
# the specified offset was out of the file boundary.
|
||||
# * :file_couldnt_read_file: A file given with FILE:// couldn't be
|
||||
# opened. Most likely because the file path doesn't identify an
|
||||
# existing file. Did you check file permissions?
|
||||
# * :ldap_cannot_bind: LDAP cannot bind. LDAP bind operation failed.
|
||||
# * :ldap_search_failed: LDAP search failed.
|
||||
# * :function_not_found: Function not found. A required zlib function was not found.
|
||||
# * :aborted_by_callback: Aborted by callback. A callback returned
|
||||
# "abort" to libcurl.
|
||||
# * :bad_function_argument: Internal error. A function was called with
|
||||
# a bad parameter.
|
||||
# * :interface_failed: Interface error. A specified outgoing interface
|
||||
# could not be used. Set which interface to use for outgoing
|
||||
# connections' source IP address with CURLOPT_INTERFACE.
|
||||
# * :too_many_redirects: Too many redirects. When following redirects,
|
||||
# libcurl hit the maximum amount. Set your limit with CURLOPT_MAXREDIRS.
|
||||
# * :unknown_option: An option passed to libcurl is not recognized/known.
|
||||
# Refer to the appropriate documentation. This is most likely a
|
||||
# problem in the program that uses libcurl. The error buffer might
|
||||
# contain more specific information about which exact option it concerns.
|
||||
# * :telnet_option_syntax: A telnet option string was Illegally formatted.
|
||||
# * :peer_failed_verification: The remote server's SSL certificate or
|
||||
# SSH md5 fingerprint was deemed not OK.
|
||||
# * :got_nothing: Nothing was returned from the server, and under the
|
||||
# circumstances, getting nothing is considered an error.
|
||||
# * :ssl_engine_notfound: The specified crypto engine wasn't found.
|
||||
# * :ssl_engine_setfailed: Failed setting the selected SSL crypto engine as default!
|
||||
# * :send_error: Failed sending network data.
|
||||
# * :recv_error: Failure with receiving network data.
|
||||
# * :ssl_certproblem: problem with the local client certificate.
|
||||
# * :ssl_cipher: Couldn't use specified cipher.
|
||||
# * :bad_content_encoding: Unrecognized transfer encoding.
|
||||
# * :ldap_invalid_url: Invalid LDAP URL.
|
||||
# * :filesize_exceeded: Maximum file size exceeded.
|
||||
# * :use_ssl_failed: Requested FTP SSL level failed.
|
||||
# * :send_fail_rewind: When doing a send operation curl had to rewind the data to
|
||||
# retransmit, but the rewinding operation failed.
|
||||
# * :ssl_engine_initfailed: Initiating the SSL Engine failed.
|
||||
# * :login_denied: The remote server denied curl to login
|
||||
# * :tftp_notfound: File not found on TFTP server.
|
||||
# * :tftp_perm: Permission problem on TFTP server.
|
||||
# * :remote_disk_full: Out of disk space on the server.
|
||||
# * :tftp_illegal: Illegal TFTP operation.
|
||||
# * :tftp_unknownid: Unknown TFTP transfer ID.
|
||||
# * :remote_file_exists: File already exists and will not be overwritten.
|
||||
# * :tftp_nosuchuser: This error should never be returned by a properly
|
||||
# functioning TFTP server.
|
||||
# * :conv_failed: Character conversion failed.
|
||||
# * :conv_reqd: Caller must register conversion callbacks.
|
||||
# * :ssl_cacert_badfile: Problem with reading the SSL CA cert (path? access rights?):
|
||||
# * :remote_file_not_found: The resource referenced in the URL does not exist.
|
||||
# * :ssh: An unspecified error occurred during the SSH session.
|
||||
# * :ssl_shutdown_failed: Failed to shut down the SSL connection.
|
||||
# * :again: Socket is not ready for send/recv wait till it's ready and try again.
|
||||
# This return code is only returned from curl_easy_recv(3) and curl_easy_send(3)
|
||||
# * :ssl_crl_badfile: Failed to load CRL file
|
||||
# * :ssl_issuer_error: Issuer check failed
|
||||
# * :ftp_pret_failed: The FTP server does not understand the PRET command at
|
||||
# all or does not support the given argument. Be careful when
|
||||
# using CURLOPT_CUSTOMREQUEST, a custom LIST command will be sent with PRET CMD
|
||||
# before PASV as well.
|
||||
# * :rtsp_cseq_error: Mismatch of RTSP CSeq numbers.
|
||||
# * :rtsp_session_error: Mismatch of RTSP Session Identifiers.
|
||||
# * :ftp_bad_file_list: Unable to parse FTP file list (during FTP wildcard downloading).
|
||||
# * :chunk_failed: Chunk callback reported error.
|
||||
# * :obsolete: These error codes will never be returned. They were used in an old
|
||||
# libcurl version and are currently unused.
|
||||
#
|
||||
# @see http://curl.haxx.se/libcurl/c/libcurl-errors.html
|
||||
attr_accessor :return_code
|
||||
|
||||
# Initialize a new Easy.
|
||||
# It initializes curl, if not already done and applies the provided options.
|
||||
# Look into {Ethon::Easy::Options Options} to see what you can provide in the
|
||||
# options hash.
|
||||
#
|
||||
# @example Create a new Easy.
|
||||
# Easy.new(url: "www.google.de")
|
||||
#
|
||||
# @param [ Hash ] options The options to set.
|
||||
# @option options :headers [ Hash ] Request headers.
|
||||
#
|
||||
# @return [ Easy ] A new Easy.
|
||||
#
|
||||
# @see Ethon::Easy::Options
|
||||
# @see http://curl.haxx.se/libcurl/c/curl_easy_setopt.html
|
||||
def initialize(options = {})
|
||||
Curl.init
|
||||
set_attributes(options)
|
||||
set_callbacks
|
||||
end
|
||||
|
||||
# Set given options.
|
||||
#
|
||||
# @example Set options.
|
||||
# easy.set_attributes(options)
|
||||
#
|
||||
# @param [ Hash ] options The options.
|
||||
#
|
||||
# @raise InvalidOption
|
||||
#
|
||||
# @see initialize
|
||||
def set_attributes(options)
|
||||
options.each_pair do |key, value|
|
||||
method = "#{key}="
|
||||
unless respond_to?(method)
|
||||
raise Errors::InvalidOption.new(key)
|
||||
end
|
||||
send(method, value)
|
||||
end
|
||||
end
|
||||
|
||||
# Reset easy. This means resetting all options and instance variables.
|
||||
# Also the easy handle is resetted.
|
||||
#
|
||||
# @example Reset.
|
||||
# easy.reset
|
||||
def reset
|
||||
@url = nil
|
||||
@escape = nil
|
||||
@hash = nil
|
||||
@on_complete = nil
|
||||
@on_headers = nil
|
||||
@on_body = nil
|
||||
@on_progress = nil
|
||||
@procs = nil
|
||||
@mirror = nil
|
||||
Curl.easy_reset(handle)
|
||||
set_callbacks
|
||||
end
|
||||
|
||||
# Clones libcurl session handle. This means that all options that is set in
|
||||
# the current handle will be set on duplicated handle.
|
||||
def dup
|
||||
e = super
|
||||
e.handle = Curl.easy_duphandle(handle)
|
||||
e.instance_variable_set(:@body_write_callback, nil)
|
||||
e.instance_variable_set(:@header_write_callback, nil)
|
||||
e.instance_variable_set(:@debug_callback, nil)
|
||||
e.instance_variable_set(:@progress_callback, nil)
|
||||
e.set_callbacks
|
||||
e
|
||||
end
|
||||
# Url escapes the value.
|
||||
#
|
||||
# @example Url escape.
|
||||
# easy.escape(value)
|
||||
#
|
||||
# @param [ String ] value The value to escape.
|
||||
#
|
||||
# @return [ String ] The escaped value.
|
||||
#
|
||||
# @api private
|
||||
def escape(value)
|
||||
string_pointer = Curl.easy_escape(handle, value, value.bytesize)
|
||||
returned_string = string_pointer.read_string
|
||||
Curl.free(string_pointer)
|
||||
returned_string
|
||||
end
|
||||
|
||||
# Returns the informations available through libcurl as
|
||||
# a hash.
|
||||
#
|
||||
# @return [ Hash ] The informations hash.
|
||||
def to_hash
|
||||
Kernel.warn("Ethon: Easy#to_hash is deprecated and will be removed, please use #mirror.")
|
||||
mirror.to_hash
|
||||
end
|
||||
|
||||
def mirror
|
||||
@mirror ||= Mirror.from_easy(self)
|
||||
end
|
||||
|
||||
# Return pretty log out.
|
||||
#
|
||||
# @example Return log out.
|
||||
# easy.log_inspect
|
||||
#
|
||||
# @return [ String ] The log out.
|
||||
def log_inspect
|
||||
"EASY #{mirror.log_informations.map{|k, v| "#{k}=#{v}"}.flatten.join(' ')}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,149 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This module contains all the logic around the callbacks,
|
||||
# which are needed to interact with libcurl.
|
||||
#
|
||||
# @api private
|
||||
module Callbacks
|
||||
|
||||
# :nodoc:
|
||||
def self.included(base)
|
||||
base.send(:attr_accessor, *[:response_body, :response_headers, :debug_info])
|
||||
end
|
||||
|
||||
# Set writefunction and headerfunction callback.
|
||||
# They are called by libcurl in order to provide the header and
|
||||
# the body from the request.
|
||||
#
|
||||
# @example Set callbacks.
|
||||
# easy.set_callbacks
|
||||
def set_callbacks
|
||||
Curl.set_option(:writefunction, body_write_callback, handle)
|
||||
Curl.set_option(:headerfunction, header_write_callback, handle)
|
||||
Curl.set_option(:debugfunction, debug_callback, handle)
|
||||
@response_body = String.new
|
||||
@response_headers = String.new
|
||||
@headers_called = false
|
||||
@debug_info = Ethon::Easy::DebugInfo.new
|
||||
end
|
||||
|
||||
# Returns the body write callback.
|
||||
#
|
||||
# @example Return the callback.
|
||||
# easy.body_write_callback
|
||||
#
|
||||
# @return [ Proc ] The callback.
|
||||
def body_write_callback
|
||||
@body_write_callback ||= proc do |stream, size, num, object|
|
||||
headers
|
||||
result = body(chunk = stream.read_string(size * num))
|
||||
@response_body << chunk if result == :unyielded
|
||||
result != :abort ? size * num : -1
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the header write callback.
|
||||
#
|
||||
# @example Return the callback.
|
||||
# easy.header_write_callback
|
||||
#
|
||||
# @return [ Proc ] The callback.
|
||||
def header_write_callback
|
||||
@header_write_callback ||= proc {|stream, size, num, object|
|
||||
result = headers
|
||||
@response_headers << stream.read_string(size * num)
|
||||
result != :abort ? size * num : -1
|
||||
}
|
||||
end
|
||||
|
||||
# Returns the debug callback. This callback is currently used
|
||||
# write the raw http request headers.
|
||||
#
|
||||
# @example Return the callback.
|
||||
# easy.debug_callback
|
||||
#
|
||||
# @return [ Proc ] The callback.
|
||||
def debug_callback
|
||||
@debug_callback ||= proc {|handle, type, data, size, udata|
|
||||
message = data.read_string(size)
|
||||
@debug_info.add type, message
|
||||
print message unless [:data_in, :data_out].include?(type)
|
||||
0
|
||||
}
|
||||
end
|
||||
|
||||
def set_progress_callback
|
||||
if Curl.version_info[:version] >= "7.32.0"
|
||||
Curl.set_option(:xferinfofunction, progress_callback, handle)
|
||||
else
|
||||
Curl.set_option(:progressfunction, progress_callback, handle)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the progress callback.
|
||||
#
|
||||
# @example Return the callback.
|
||||
# easy.progress_callback
|
||||
#
|
||||
# @return [ Proc ] The callback.
|
||||
def progress_callback
|
||||
@progress_callback ||= proc { |_, dltotal, dlnow, ultotal, ulnow|
|
||||
progress(dltotal, dlnow, ultotal, ulnow)
|
||||
0
|
||||
}
|
||||
end
|
||||
|
||||
# Set the read callback. This callback is used by libcurl to
|
||||
# read data when performing a PUT request.
|
||||
#
|
||||
# @example Set the callback.
|
||||
# easy.set_read_callback("a=1")
|
||||
#
|
||||
# @param [ String ] body The body.
|
||||
def set_read_callback(body)
|
||||
@request_body_read = 0
|
||||
readfunction do |stream, size, num, object|
|
||||
size = size * num
|
||||
body_size = if body.respond_to?(:bytesize)
|
||||
body.bytesize
|
||||
elsif body.respond_to?(:size)
|
||||
body.size
|
||||
elsif body.is_a?(File)
|
||||
File.size(body.path)
|
||||
end
|
||||
|
||||
left = body_size - @request_body_read
|
||||
size = left if size > left
|
||||
|
||||
if size > 0
|
||||
chunk = if body.respond_to?(:byteslice)
|
||||
body.byteslice(@request_body_read, size)
|
||||
elsif body.respond_to?(:read)
|
||||
body.read(size)
|
||||
else
|
||||
body[@request_body_read, size]
|
||||
end
|
||||
|
||||
stream.write_string(
|
||||
chunk, size
|
||||
)
|
||||
@request_body_read += size
|
||||
end
|
||||
size
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the body read callback.
|
||||
#
|
||||
# @example Return the callback.
|
||||
# easy.read_callback
|
||||
#
|
||||
# @return [ Proc ] The callback.
|
||||
def read_callback
|
||||
@read_callback
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This class is used to store and retreive debug information,
|
||||
# which is only saved when verbose is set to true.
|
||||
#
|
||||
# @api private
|
||||
class DebugInfo
|
||||
|
||||
MESSAGE_TYPES = Ethon::Curl::DebugInfoType.to_h.keys
|
||||
|
||||
class Message
|
||||
attr_reader :type, :message
|
||||
|
||||
def initialize(type, message)
|
||||
@type = type
|
||||
@message = message
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@messages = []
|
||||
end
|
||||
|
||||
def add(type, message)
|
||||
@messages << Message.new(type, message)
|
||||
end
|
||||
|
||||
def messages_for(type)
|
||||
@messages.select {|m| m.type == type }.map(&:message)
|
||||
end
|
||||
|
||||
MESSAGE_TYPES.each do |type|
|
||||
eval %Q|def #{type}; messages_for(:#{type}); end|
|
||||
end
|
||||
|
||||
def to_a
|
||||
@messages.map(&:message)
|
||||
end
|
||||
|
||||
def to_h
|
||||
Hash[MESSAGE_TYPES.map {|k| [k, send(k)] }]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This module contains class methods for feature checks
|
||||
module Features
|
||||
# Returns true if this curl version supports zlib.
|
||||
#
|
||||
# @example Return wether zlib is supported.
|
||||
# Ethon::Easy.supports_zlib?
|
||||
#
|
||||
# @return [ Boolean ] True if supported, else false.
|
||||
def supports_zlib?
|
||||
!!(Curl.version_info[:features] & Curl::VERSION_LIBZ)
|
||||
end
|
||||
|
||||
# Returns true if this curl version supports AsynchDNS.
|
||||
#
|
||||
# @example
|
||||
# Ethon::Easy.supports_asynch_dns?
|
||||
#
|
||||
# @return [ Boolean ] True if supported, else false.
|
||||
def supports_asynch_dns?
|
||||
!!(Curl.version_info[:features] & Curl::VERSION_ASYNCHDNS)
|
||||
end
|
||||
|
||||
alias :supports_timeout_ms? :supports_asynch_dns?
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon/easy/util'
|
||||
require 'ethon/easy/queryable'
|
||||
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This class represents a form and is used to send a payload in the
|
||||
# request body via POST/PUT.
|
||||
# It handles multipart forms, too.
|
||||
#
|
||||
# @api private
|
||||
class Form
|
||||
include Ethon::Easy::Util
|
||||
include Ethon::Easy::Queryable
|
||||
|
||||
# Return a new Form.
|
||||
#
|
||||
# @example Return a new Form.
|
||||
# Form.new({})
|
||||
#
|
||||
# @param [ Hash ] params The parameter with which to initialize the form.
|
||||
#
|
||||
# @return [ Form ] A new Form.
|
||||
def initialize(easy, params, multipart = nil)
|
||||
@easy = easy
|
||||
@params = params || {}
|
||||
@multipart = multipart
|
||||
end
|
||||
|
||||
# Return a pointer to the first form element in libcurl.
|
||||
#
|
||||
# @example Return the first form element.
|
||||
# form.first
|
||||
#
|
||||
# @return [ FFI::Pointer ] The first element.
|
||||
def first
|
||||
@first ||= FFI::MemoryPointer.new(:pointer)
|
||||
end
|
||||
|
||||
# Return a pointer to the last form element in libcurl.
|
||||
#
|
||||
# @example Return the last form element.
|
||||
# form.last
|
||||
#
|
||||
# @return [ FFI::Pointer ] The last element.
|
||||
def last
|
||||
@last ||= FFI::MemoryPointer.new(:pointer)
|
||||
end
|
||||
|
||||
# Return if form is multipart. The form is multipart
|
||||
# when it contains a file or multipart option is set on the form during creation.
|
||||
#
|
||||
# @example Return if form is multipart.
|
||||
# form.multipart?
|
||||
#
|
||||
# @return [ Boolean ] True if form is multipart, else false.
|
||||
def multipart?
|
||||
return true if @multipart
|
||||
query_pairs.any?{|pair| pair.respond_to?(:last) && pair.last.is_a?(Array)}
|
||||
end
|
||||
|
||||
# Add form elements to libcurl.
|
||||
#
|
||||
# @example Add form to libcurl.
|
||||
# form.materialize
|
||||
def materialize
|
||||
query_pairs.each { |pair| form_add(pair.first.to_s, pair.last) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def form_add(name, content)
|
||||
case content
|
||||
when Array
|
||||
Curl.formadd(first, last,
|
||||
:form_option, :copyname, :pointer, name,
|
||||
:form_option, :namelength, :long, name.bytesize,
|
||||
:form_option, :file, :string, content[2],
|
||||
:form_option, :filename, :string, content[0],
|
||||
:form_option, :contenttype, :string, content[1],
|
||||
:form_option, :end
|
||||
)
|
||||
else
|
||||
Curl.formadd(first, last,
|
||||
:form_option, :copyname, :pointer, name,
|
||||
:form_option, :namelength, :long, name.bytesize,
|
||||
:form_option, :copycontents, :pointer, content.to_s,
|
||||
:form_option, :contentslength, :long, content ? content.to_s.bytesize : 0,
|
||||
:form_option, :end
|
||||
)
|
||||
end
|
||||
|
||||
setup_garbage_collection
|
||||
end
|
||||
|
||||
def setup_garbage_collection
|
||||
# first is a pointer to a pointer. Since it's a MemoryPointer it will
|
||||
# auto clean itself up, but we need to clean up the object it points
|
||||
# to. So this results in (pseudo-c):
|
||||
# form_data_cleanup_handler = *first
|
||||
# curl_form_free(form_data_cleanup_handler)
|
||||
@form_data_cleanup_handler ||= FFI::AutoPointer.new(@first.get_pointer(0), Curl.method(:formfree))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,61 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
# This module contains the logic around adding headers to libcurl.
|
||||
#
|
||||
# @api private
|
||||
module Header
|
||||
# Return headers, return empty hash if none.
|
||||
#
|
||||
# @example Return the headers.
|
||||
# easy.headers
|
||||
#
|
||||
# @return [ Hash ] The headers.
|
||||
def headers
|
||||
@headers ||= {}
|
||||
end
|
||||
|
||||
# Set the headers.
|
||||
#
|
||||
# @example Set the headers.
|
||||
# easy.headers = {'User-Agent' => 'ethon'}
|
||||
#
|
||||
# @param [ Hash ] headers The headers.
|
||||
def headers=(headers)
|
||||
headers ||= {}
|
||||
header_list = nil
|
||||
headers.each do |k, v|
|
||||
header_list = Curl.slist_append(header_list, compose_header(k,v))
|
||||
end
|
||||
Curl.set_option(:httpheader, header_list, handle)
|
||||
|
||||
@header_list = header_list && FFI::AutoPointer.new(header_list, Curl.method(:slist_free_all))
|
||||
end
|
||||
|
||||
# Return header_list.
|
||||
#
|
||||
# @example Return header_list.
|
||||
# easy.header_list
|
||||
#
|
||||
# @return [ FFI::Pointer ] The header list.
|
||||
def header_list
|
||||
@header_list
|
||||
end
|
||||
|
||||
# Compose libcurl header string from key and value.
|
||||
# Also replaces null bytes, because libcurl will complain
|
||||
# otherwise.
|
||||
#
|
||||
# @example Compose header.
|
||||
# easy.compose_header('User-Agent', 'Ethon')
|
||||
#
|
||||
# @param [ String ] key The header name.
|
||||
# @param [ String ] value The header value.
|
||||
#
|
||||
# @return [ String ] The composed header.
|
||||
def compose_header(key, value)
|
||||
Util.escape_zero_byte("#{key}: #{value}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon/easy/http/actionable'
|
||||
require 'ethon/easy/http/post'
|
||||
require 'ethon/easy/http/get'
|
||||
require 'ethon/easy/http/head'
|
||||
require 'ethon/easy/http/put'
|
||||
require 'ethon/easy/http/delete'
|
||||
require 'ethon/easy/http/patch'
|
||||
require 'ethon/easy/http/options'
|
||||
require 'ethon/easy/http/custom'
|
||||
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This module contains logic about making valid HTTP requests.
|
||||
module Http
|
||||
|
||||
# Set specified options in order to make a HTTP request.
|
||||
# Look at {Ethon::Easy::Options Options} to see what you can
|
||||
# provide in the options hash.
|
||||
#
|
||||
# @example Set options for HTTP request.
|
||||
# easy.http_request("www.google.com", :get, {})
|
||||
#
|
||||
# @param [ String ] url The url.
|
||||
# @param [ String ] action_name The HTTP action name.
|
||||
# @param [ Hash ] options The options hash.
|
||||
#
|
||||
# @option options :params [ Hash ] Params hash which
|
||||
# is attached to the url.
|
||||
# @option options :body [ Hash ] Body hash which
|
||||
# becomes the request body. It is a PUT body for
|
||||
# PUT requests and a POST for everything else.
|
||||
# @option options :headers [ Hash ] Request headers.
|
||||
#
|
||||
# @return [ void ]
|
||||
#
|
||||
# @see Ethon::Easy::Options
|
||||
def http_request(url, action_name, options = {})
|
||||
fabricate(url, action_name, options).setup(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return the corresponding action class.
|
||||
#
|
||||
# @example Return the action.
|
||||
# Action.fabricate(:get)
|
||||
# Action.fabricate(:smash)
|
||||
#
|
||||
# @param [ String ] url The url.
|
||||
# @param [ String ] action_name The HTTP action name.
|
||||
# @param [ Hash ] options The option hash.
|
||||
#
|
||||
# @return [ Easy::Ethon::Actionable ] The request instance.
|
||||
def fabricate(url, action_name, options)
|
||||
constant_name = action_name.to_s.capitalize
|
||||
|
||||
if Ethon::Easy::Http.const_defined?(constant_name)
|
||||
Ethon::Easy::Http.const_get(constant_name).new(url, options)
|
||||
else
|
||||
Ethon::Easy::Http::Custom.new(constant_name.upcase, url, options)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,157 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon/easy/http/putable'
|
||||
require 'ethon/easy/http/postable'
|
||||
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
# This module represents a Http Action and is a factory
|
||||
# for more real actions like GET, HEAD, POST and PUT.
|
||||
module Actionable
|
||||
|
||||
QUERY_OPTIONS = [ :params, :body, :params_encoding ]
|
||||
|
||||
# Create a new action.
|
||||
#
|
||||
# @example Create a new action.
|
||||
# Action.new("www.example.com", {})
|
||||
#
|
||||
# @param [ String ] url The url.
|
||||
# @param [ Hash ] options The options.
|
||||
#
|
||||
# @return [ Action ] A new action.
|
||||
def initialize(url, options)
|
||||
@url = url
|
||||
@options, @query_options = parse_options(options)
|
||||
end
|
||||
|
||||
# Return the url.
|
||||
#
|
||||
# @example Return url.
|
||||
# action.url
|
||||
#
|
||||
# @return [ String ] The url.
|
||||
def url
|
||||
@url
|
||||
end
|
||||
|
||||
# Return the options hash.
|
||||
#
|
||||
# @example Return options.
|
||||
# action.options
|
||||
#
|
||||
# @return [ Hash ] The options.
|
||||
def options
|
||||
@options
|
||||
end
|
||||
|
||||
# Returns the query options hash.
|
||||
#
|
||||
# @example Return query options.
|
||||
# action.query_options
|
||||
#
|
||||
# @return [ Hash ] The query options.
|
||||
def query_options
|
||||
@query_options
|
||||
end
|
||||
|
||||
# Return the params.
|
||||
#
|
||||
# @example Return params.
|
||||
# action.params
|
||||
#
|
||||
# @return [ Params ] The params.
|
||||
def params
|
||||
@params ||= Params.new(@easy, query_options.fetch(:params, nil))
|
||||
end
|
||||
|
||||
# Return the form.
|
||||
#
|
||||
# @example Return form.
|
||||
# action.form
|
||||
#
|
||||
# @return [ Form ] The form.
|
||||
def form
|
||||
@form ||= Form.new(@easy, query_options.fetch(:body, nil), options.fetch(:multipart, nil))
|
||||
end
|
||||
|
||||
# Get the requested array encoding. By default it's
|
||||
# :typhoeus, but it can also be set to :rack.
|
||||
#
|
||||
# @example Get encoding from options
|
||||
# action.params_encoding
|
||||
#
|
||||
def params_encoding
|
||||
@params_encoding ||= query_options.fetch(:params_encoding, :typhoeus)
|
||||
end
|
||||
|
||||
# Setup everything necessary for a proper request.
|
||||
#
|
||||
# @example setup.
|
||||
# action.setup(easy)
|
||||
#
|
||||
# @param [ easy ] easy the easy to setup.
|
||||
def setup(easy)
|
||||
@easy = easy
|
||||
|
||||
# Order is important, @easy will be used to provide access to options
|
||||
# relevant to the following operations (like whether or not to escape
|
||||
# values).
|
||||
easy.set_attributes(options)
|
||||
|
||||
set_form(easy) unless form.empty?
|
||||
|
||||
if params.empty?
|
||||
easy.url = url
|
||||
else
|
||||
set_params(easy)
|
||||
end
|
||||
end
|
||||
|
||||
# Setup request with params.
|
||||
#
|
||||
# @example Setup nothing.
|
||||
# action.set_params(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def set_params(easy)
|
||||
params.escape = easy.escape?
|
||||
params.params_encoding = params_encoding
|
||||
|
||||
base_url, base_params = url.split('?')
|
||||
base_url << '?'
|
||||
base_url << base_params.to_s
|
||||
base_url << '&' if base_params
|
||||
base_url << params.to_s
|
||||
|
||||
easy.url = base_url
|
||||
end
|
||||
|
||||
# Setup request with form.
|
||||
#
|
||||
# @example Setup nothing.
|
||||
# action.set_form(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def set_form(easy)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_options(options)
|
||||
query_options = {}
|
||||
options = options.dup
|
||||
|
||||
QUERY_OPTIONS.each do |query_option|
|
||||
if options.key?(query_option)
|
||||
query_options[query_option] = options.delete(query_option)
|
||||
end
|
||||
end
|
||||
|
||||
return options, query_options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
|
||||
# This class knows everything about making requests for custom HTTP verbs.
|
||||
class Custom
|
||||
include Ethon::Easy::Http::Actionable
|
||||
include Ethon::Easy::Http::Postable
|
||||
|
||||
def initialize(verb, url, options)
|
||||
@verb = verb
|
||||
super(url, options)
|
||||
end
|
||||
|
||||
# Setup easy to make a request.
|
||||
#
|
||||
# @example Setup.
|
||||
# custom.set_params(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def setup(easy)
|
||||
super
|
||||
easy.customrequest = @verb
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
|
||||
# This class knows everything about making DELETE requests.
|
||||
class Delete
|
||||
include Ethon::Easy::Http::Actionable
|
||||
include Ethon::Easy::Http::Postable
|
||||
|
||||
# Setup easy to make a DELETE request.
|
||||
#
|
||||
# @example Setup customrequest.
|
||||
# delete.setup(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def setup(easy)
|
||||
super
|
||||
easy.customrequest = "DELETE"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
|
||||
# This class knows everything about making GET requests.
|
||||
class Get
|
||||
include Ethon::Easy::Http::Actionable
|
||||
include Ethon::Easy::Http::Postable
|
||||
|
||||
# Setup easy to make a GET request.
|
||||
#
|
||||
# @example Setup.
|
||||
# get.set_params(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def setup(easy)
|
||||
super
|
||||
easy.customrequest = "GET" unless form.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
|
||||
# This class knows everything about making HEAD requests.
|
||||
class Head
|
||||
include Ethon::Easy::Http::Actionable
|
||||
include Ethon::Easy::Http::Postable
|
||||
|
||||
# Setup easy to make a HEAD request.
|
||||
#
|
||||
# @example Setup.
|
||||
# get.set_params(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def setup(easy)
|
||||
super
|
||||
easy.nobody = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
|
||||
# This class knows everything about making OPTIONS requests.
|
||||
class Options
|
||||
include Ethon::Easy::Http::Actionable
|
||||
include Ethon::Easy::Http::Postable
|
||||
|
||||
# Setup easy to make a OPTIONS request.
|
||||
#
|
||||
# @example Setup.
|
||||
# options.setup(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def setup(easy)
|
||||
super
|
||||
easy.customrequest = "OPTIONS"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
|
||||
# This class knows everything about making PATCH requests.
|
||||
class Patch
|
||||
include Ethon::Easy::Http::Actionable
|
||||
include Ethon::Easy::Http::Postable
|
||||
|
||||
# Setup easy to make a PATCH request.
|
||||
#
|
||||
# @example Setup.
|
||||
# patch.setup(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def setup(easy)
|
||||
super
|
||||
easy.customrequest = "PATCH"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
# This class knows everything about making POST requests.
|
||||
class Post
|
||||
include Ethon::Easy::Http::Actionable
|
||||
include Ethon::Easy::Http::Postable
|
||||
|
||||
# Setup easy to make a POST request.
|
||||
#
|
||||
# @example Setup.
|
||||
# post.setup(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def setup(easy)
|
||||
super
|
||||
if form.empty?
|
||||
easy.postfieldsize = 0
|
||||
easy.copypostfields = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
|
||||
# This module contains logic for setting up a [multipart] POST body.
|
||||
module Postable
|
||||
|
||||
# Set things up when form is provided.
|
||||
# Deals with multipart forms.
|
||||
#
|
||||
# @example Setup.
|
||||
# post.set_form(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def set_form(easy)
|
||||
easy.url ||= url
|
||||
form.params_encoding = params_encoding
|
||||
if form.multipart?
|
||||
form.escape = false
|
||||
form.materialize
|
||||
easy.httppost = form.first.read_pointer
|
||||
else
|
||||
form.escape = easy.escape?
|
||||
easy.postfieldsize = form.to_s.bytesize
|
||||
easy.copypostfields = form.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
|
||||
# This class knows everything about making PUT requests.
|
||||
class Put
|
||||
include Ethon::Easy::Http::Actionable
|
||||
include Ethon::Easy::Http::Putable
|
||||
|
||||
# Setup easy to make a PUT request.
|
||||
#
|
||||
# @example Setup.
|
||||
# put.setup(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def setup(easy)
|
||||
super
|
||||
if form.empty?
|
||||
easy.upload = true
|
||||
easy.infilesize = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
module Http
|
||||
|
||||
# This module contains logic about setting up a PUT body.
|
||||
module Putable
|
||||
# Set things up when form is provided.
|
||||
# Deals with multipart forms.
|
||||
#
|
||||
# @example Setup.
|
||||
# put.set_form(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to setup.
|
||||
def set_form(easy)
|
||||
easy.upload = true
|
||||
form.escape = true
|
||||
form.params_encoding = params_encoding
|
||||
easy.infilesize = form.to_s.bytesize
|
||||
easy.set_read_callback(form.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,116 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This module contains the methods to return informations
|
||||
# from the easy handle. See http://curl.haxx.se/libcurl/c/curl_easy_getinfo.html
|
||||
# for more information.
|
||||
module Informations
|
||||
|
||||
# Holds available informations and their type, which is needed to
|
||||
# request the informations from libcurl.
|
||||
AVAILABLE_INFORMATIONS = {
|
||||
# Return the available HTTP auth methods.
|
||||
:httpauth_avail => :long,
|
||||
|
||||
# Return the total time in seconds for the previous
|
||||
# transfer, including name resolution, TCP connection, etc.
|
||||
:total_time => :double,
|
||||
|
||||
# Return the time, in seconds, it took from the start
|
||||
# until the first byte was received by libcurl. This
|
||||
# includes pre-transfer time and also the time the
|
||||
# server needs to calculate the result.
|
||||
:starttransfer_time => :double,
|
||||
|
||||
# Return the time, in seconds, it took from the start
|
||||
# until the SSL/SSH connect/handshake to the remote
|
||||
# host was completed. This time is most often very near
|
||||
# to the pre-transfer time, except for cases such as HTTP
|
||||
# pipelining where the pre-transfer time can be delayed
|
||||
# due to waits in line for the pipeline and more.
|
||||
:appconnect_time => :double,
|
||||
|
||||
# Return the time, in seconds, it took from the start
|
||||
# until the file transfer was just about to begin. This
|
||||
# includes all pre-transfer commands and negotiations
|
||||
# that are specific to the particular protocol(s) involved.
|
||||
# It does not involve the sending of the protocol-
|
||||
# specific request that triggers a transfer.
|
||||
:pretransfer_time => :double,
|
||||
|
||||
# Return the time, in seconds, it took from the start
|
||||
# until the connect to the remote host (or proxy) was completed.
|
||||
:connect_time => :double,
|
||||
|
||||
# Return the time, in seconds, it took from the
|
||||
# start until the name resolution was completed.
|
||||
:namelookup_time => :double,
|
||||
|
||||
# Return the time, in seconds, it took for all redirection steps
|
||||
# include name lookup, connect, pretransfer and transfer before the
|
||||
# final transaction was started. time_redirect shows the complete
|
||||
# execution time for multiple redirections. (Added in 7.12.3)
|
||||
:redirect_time => :double,
|
||||
|
||||
# Return the last used effective url.
|
||||
:effective_url => :string,
|
||||
|
||||
# Return the string holding the IP address of the most recent
|
||||
# connection done with this curl handle. This string
|
||||
# may be IPv6 if that's enabled.
|
||||
:primary_ip => :string,
|
||||
|
||||
# Return the last received HTTP, FTP or SMTP response code.
|
||||
# The value will be zero if no server response code has
|
||||
# been received. Note that a proxy's CONNECT response should
|
||||
# be read with http_connect_code and not this.
|
||||
:response_code => :long,
|
||||
|
||||
:request_size => :long,
|
||||
|
||||
# Return the total number of redirections that were
|
||||
# actually followed.
|
||||
:redirect_count => :long,
|
||||
|
||||
# URL a redirect would take you to, had you enabled redirects (Added in 7.18.2)
|
||||
:redirect_url => :string,
|
||||
|
||||
# Return the bytes, the total amount of bytes that were uploaded
|
||||
:size_upload => :double,
|
||||
|
||||
# Return the bytes, the total amount of bytes that were downloaded.
|
||||
# The amount is only for the latest transfer and will be reset again
|
||||
# for each new transfer. This counts actual payload data, what's
|
||||
# also commonly called body. All meta and header data are excluded
|
||||
# and will not be counted in this number.
|
||||
:size_download => :double,
|
||||
|
||||
# Return the bytes/second, the average upload speed that curl
|
||||
# measured for the complete upload
|
||||
:speed_upload => :double,
|
||||
|
||||
# Return the bytes/second, the average download speed that curl
|
||||
# measured for the complete download
|
||||
:speed_download => :double
|
||||
}
|
||||
|
||||
AVAILABLE_INFORMATIONS.each do |name, type|
|
||||
eval %Q|def #{name}; Curl.send(:get_info_#{type}, :#{name}, handle); end|
|
||||
end
|
||||
|
||||
# Returns true if this curl version supports zlib.
|
||||
#
|
||||
# @example Return wether zlib is supported.
|
||||
# easy.supports_zlib?
|
||||
#
|
||||
# @return [ Boolean ] True if supported, else false.
|
||||
# @deprecated Please use the static version instead
|
||||
def supports_zlib?
|
||||
Kernel.warn("Ethon: Easy#supports_zlib? is deprecated and will be removed, please use Easy#.")
|
||||
Easy.supports_zlib?
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
class Mirror
|
||||
attr_reader :options
|
||||
alias_method :to_hash, :options
|
||||
|
||||
INFORMATIONS_TO_MIRROR = Informations::AVAILABLE_INFORMATIONS.keys +
|
||||
[:return_code, :response_headers, :response_body, :debug_info]
|
||||
|
||||
INFORMATIONS_TO_LOG = [:effective_url, :response_code, :return_code, :total_time]
|
||||
|
||||
def self.from_easy(easy)
|
||||
options = {}
|
||||
INFORMATIONS_TO_MIRROR.each do |info|
|
||||
options[info] = easy.send(info)
|
||||
end
|
||||
new(options)
|
||||
end
|
||||
|
||||
def initialize(options = {})
|
||||
@options = options
|
||||
end
|
||||
|
||||
def log_informations
|
||||
Hash[*INFORMATIONS_TO_LOG.map do |info|
|
||||
[info, options[info]]
|
||||
end.flatten]
|
||||
end
|
||||
|
||||
INFORMATIONS_TO_MIRROR.each do |info|
|
||||
eval %Q|def #{info}; options[#{info}]; end|
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
# This module contains the logic to prepare and perform
|
||||
# an easy.
|
||||
module Operations
|
||||
# Returns a pointer to the curl easy handle.
|
||||
#
|
||||
# @example Return the handle.
|
||||
# easy.handle
|
||||
#
|
||||
# @return [ FFI::Pointer ] A pointer to the curl easy handle.
|
||||
def handle
|
||||
@handle ||= FFI::AutoPointer.new(Curl.easy_init, Curl.method(:easy_cleanup))
|
||||
end
|
||||
|
||||
# Sets a pointer to the curl easy handle.
|
||||
# @param [ ::FFI::Pointer ] Easy handle that will be assigned.
|
||||
def handle=(h)
|
||||
@handle = h
|
||||
end
|
||||
|
||||
# Perform the easy request.
|
||||
#
|
||||
# @example Perform the request.
|
||||
# easy.perform
|
||||
#
|
||||
# @return [ Integer ] The return code.
|
||||
def perform
|
||||
@return_code = Curl.easy_perform(handle)
|
||||
if Ethon.logger.debug?
|
||||
Ethon.logger.debug { "ETHON: performed #{log_inspect}" }
|
||||
end
|
||||
complete
|
||||
@return_code
|
||||
end
|
||||
|
||||
# Clean up the easy.
|
||||
#
|
||||
# @example Perform clean up.
|
||||
# easy.cleanup
|
||||
#
|
||||
# @return the result of the free which is nil
|
||||
def cleanup
|
||||
handle.free
|
||||
end
|
||||
|
||||
# Prepare the easy. Options, headers and callbacks
|
||||
# were set.
|
||||
#
|
||||
# @example Prepare easy.
|
||||
# easy.prepare
|
||||
#
|
||||
# @deprecated It is no longer necessary to call prepare.
|
||||
def prepare
|
||||
Ethon.logger.warn(
|
||||
"ETHON: It is no longer necessary to call "+
|
||||
"Easy#prepare. It's going to be removed "+
|
||||
"in future versions."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This module contains the logic and knowledge about the
|
||||
# available options on easy.
|
||||
module Options
|
||||
attr_reader :url
|
||||
|
||||
def url=(value)
|
||||
@url = value
|
||||
Curl.set_option(:url, value, handle)
|
||||
end
|
||||
|
||||
def escape=( b )
|
||||
@escape = b
|
||||
end
|
||||
|
||||
def escape?
|
||||
return true if !defined?(@escape) || @escape.nil?
|
||||
@escape
|
||||
end
|
||||
|
||||
def multipart=(b)
|
||||
@multipart = b
|
||||
end
|
||||
|
||||
def multipart?
|
||||
!!@multipart
|
||||
end
|
||||
|
||||
Curl.easy_options(nil).each do |opt, props|
|
||||
method_name = "#{opt}=".freeze
|
||||
unless method_defined? method_name
|
||||
define_method(method_name) do |value|
|
||||
Curl.set_option(opt, value, handle)
|
||||
value
|
||||
end
|
||||
end
|
||||
next if props[:type] != :callback || method_defined?(opt)
|
||||
define_method(opt) do |&block|
|
||||
@procs ||= {}
|
||||
@procs[opt.to_sym] = block
|
||||
Curl.set_option(opt, block, handle)
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon/easy/util'
|
||||
require 'ethon/easy/queryable'
|
||||
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This class represents HTTP request parameters.
|
||||
#
|
||||
# @api private
|
||||
class Params
|
||||
include Ethon::Easy::Util
|
||||
include Ethon::Easy::Queryable
|
||||
|
||||
# Create a new Params.
|
||||
#
|
||||
# @example Create a new Params.
|
||||
# Params.new({})
|
||||
#
|
||||
# @param [ Hash ] params The params to use.
|
||||
#
|
||||
# @return [ Params ] A new Params.
|
||||
def initialize(easy, params)
|
||||
@easy = easy
|
||||
@params = params || {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,154 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This module contains logic about building
|
||||
# query parameters for url or form.
|
||||
module Queryable
|
||||
|
||||
# :nodoc:
|
||||
def self.included(base)
|
||||
base.send(:attr_accessor, :escape)
|
||||
base.send(:attr_accessor, :params_encoding)
|
||||
end
|
||||
|
||||
# Return wether there are elements in params or not.
|
||||
#
|
||||
# @example Return if params is empty.
|
||||
# form.empty?
|
||||
#
|
||||
# @return [ Boolean ] True if params is empty, else false.
|
||||
def empty?
|
||||
@params.empty?
|
||||
end
|
||||
|
||||
# Return the string representation of params.
|
||||
#
|
||||
# @example Return string representation.
|
||||
# params.to_s
|
||||
#
|
||||
# @return [ String ] The string representation.
|
||||
def to_s
|
||||
@to_s ||= query_pairs.map{ |pair|
|
||||
return pair if pair.is_a?(String)
|
||||
|
||||
if escape && @easy
|
||||
pair.map{ |e| @easy.escape(e.to_s) }.join("=")
|
||||
else
|
||||
pair.join("=")
|
||||
end
|
||||
}.join('&')
|
||||
end
|
||||
|
||||
# Return the query pairs.
|
||||
#
|
||||
# @example Return the query pairs.
|
||||
# params.query_pairs
|
||||
#
|
||||
# @return [ Array ] The query pairs.
|
||||
def query_pairs
|
||||
@query_pairs ||= build_query_pairs(@params)
|
||||
end
|
||||
|
||||
# Return query pairs build from a hash.
|
||||
#
|
||||
# @example Build query pairs.
|
||||
# action.build_query_pairs({a: 1, b: 2})
|
||||
# #=> [[:a, 1], [:b, 2]]
|
||||
#
|
||||
# @param [ Hash ] hash The hash to go through.
|
||||
#
|
||||
# @return [ Array ] The array of query pairs.
|
||||
def build_query_pairs(hash)
|
||||
return [hash] if hash.is_a?(String)
|
||||
|
||||
pairs = []
|
||||
recursively_generate_pairs(hash, nil, pairs)
|
||||
pairs
|
||||
end
|
||||
|
||||
# Return file info for a file.
|
||||
#
|
||||
# @example Return file info.
|
||||
# action.file_info(File.open('fubar', 'r'))
|
||||
#
|
||||
# @param [ File ] file The file to handle.
|
||||
#
|
||||
# @return [ Array ] Array of informations.
|
||||
def file_info(file)
|
||||
filename = File.basename(file.path)
|
||||
[
|
||||
filename,
|
||||
mime_type(filename),
|
||||
File.expand_path(file.path)
|
||||
]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mime_type(filename)
|
||||
if defined?(MIME) && t = MIME::Types.type_for(filename).first
|
||||
t.to_s
|
||||
else
|
||||
'application/octet-stream'
|
||||
end
|
||||
end
|
||||
|
||||
def recursively_generate_pairs(h, prefix, pairs)
|
||||
case h
|
||||
when Hash
|
||||
encode_hash_pairs(h, prefix, pairs)
|
||||
when Array
|
||||
if params_encoding == :rack
|
||||
encode_rack_array_pairs(h, prefix, pairs)
|
||||
elsif params_encoding == :multi
|
||||
encode_multi_array_pairs(h, prefix, pairs)
|
||||
elsif params_encoding == :none
|
||||
pairs << [prefix, h]
|
||||
else
|
||||
encode_indexed_array_pairs(h, prefix, pairs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def encode_hash_pairs(h, prefix, pairs)
|
||||
h.each_pair do |k,v|
|
||||
key = prefix.nil? ? k : "#{prefix}[#{k}]"
|
||||
pairs_for(v, key, pairs)
|
||||
end
|
||||
end
|
||||
|
||||
def encode_indexed_array_pairs(h, prefix, pairs)
|
||||
h.each_with_index do |v, i|
|
||||
key = "#{prefix}[#{i}]"
|
||||
pairs_for(v, key, pairs)
|
||||
end
|
||||
end
|
||||
|
||||
def encode_rack_array_pairs(h, prefix, pairs)
|
||||
h.each do |v|
|
||||
key = "#{prefix}[]"
|
||||
pairs_for(v, key, pairs)
|
||||
end
|
||||
end
|
||||
|
||||
def encode_multi_array_pairs(h, prefix, pairs)
|
||||
h.each_with_index do |v, i|
|
||||
key = prefix
|
||||
pairs_for(v, key, pairs)
|
||||
end
|
||||
end
|
||||
|
||||
def pairs_for(v, key, pairs)
|
||||
case v
|
||||
when Hash, Array
|
||||
recursively_generate_pairs(v, key, pairs)
|
||||
when File, Tempfile
|
||||
pairs << [key, file_info(v)]
|
||||
else
|
||||
pairs << [key, v]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,136 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy
|
||||
|
||||
# This module contains the logic for the response callbacks.
|
||||
# The on_complete callback is the only one at the moment.
|
||||
#
|
||||
# You can set multiple callbacks, which are then executed
|
||||
# in the same order.
|
||||
#
|
||||
# easy.on_complete { p 1 }
|
||||
# easy.on_complete { p 2 }
|
||||
# easy.complete
|
||||
# #=> 1
|
||||
# #=> 2
|
||||
#
|
||||
# You can clear the callbacks:
|
||||
#
|
||||
# easy.on_complete { p 1 }
|
||||
# easy.on_complete { p 2 }
|
||||
# easy.on_complete.clear
|
||||
# easy.on_complete
|
||||
# #=> []
|
||||
module ResponseCallbacks
|
||||
|
||||
# Set on_headers callback.
|
||||
#
|
||||
# @example Set on_headers.
|
||||
# request.on_headers { p "yay" }
|
||||
#
|
||||
# @param [ Block ] block The block to execute.
|
||||
def on_headers(&block)
|
||||
@on_headers ||= []
|
||||
@on_headers << block if block_given?
|
||||
@on_headers
|
||||
end
|
||||
|
||||
# Execute on_headers callbacks.
|
||||
#
|
||||
# @example Execute on_headers.
|
||||
# request.headers
|
||||
def headers
|
||||
return if @headers_called
|
||||
@headers_called = true
|
||||
if defined?(@on_headers) and not @on_headers.nil?
|
||||
result = nil
|
||||
@on_headers.each do |callback|
|
||||
result = callback.call(self)
|
||||
break if result == :abort
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
# Set on_complete callback.
|
||||
#
|
||||
# @example Set on_complete.
|
||||
# request.on_complete { p "yay" }
|
||||
#
|
||||
# @param [ Block ] block The block to execute.
|
||||
def on_complete(&block)
|
||||
@on_complete ||= []
|
||||
@on_complete << block if block_given?
|
||||
@on_complete
|
||||
end
|
||||
|
||||
# Execute on_complete callbacks.
|
||||
#
|
||||
# @example Execute on_completes.
|
||||
# request.complete
|
||||
def complete
|
||||
headers unless @response_headers.empty?
|
||||
if defined?(@on_complete) and not @on_complete.nil?
|
||||
@on_complete.each{ |callback| callback.call(self) }
|
||||
end
|
||||
end
|
||||
|
||||
# Set on_progress callback.
|
||||
#
|
||||
# @example Set on_progress.
|
||||
# request.on_progress {|dltotal, dlnow, ultotal, ulnow| p "#{dltotal} #{dlnow} #{ultotal} #{ulnow}" }
|
||||
#
|
||||
# @param [ Block ] block The block to execute.
|
||||
def on_progress(&block)
|
||||
@on_progress ||= []
|
||||
if block_given?
|
||||
@on_progress << block
|
||||
set_progress_callback
|
||||
self.noprogress = 0
|
||||
end
|
||||
@on_progress
|
||||
end
|
||||
|
||||
# Execute on_progress callbacks.
|
||||
#
|
||||
# @example Execute on_progress.
|
||||
# request.body(1, 1, 1, 1)
|
||||
def progress(dltotal, dlnow, ultotal, ulnow)
|
||||
if defined?(@on_progress) and not @on_progress.nil?
|
||||
@on_progress.each{ |callback| callback.call(dltotal, dlnow, ultotal, ulnow) }
|
||||
end
|
||||
end
|
||||
|
||||
# Set on_body callback.
|
||||
#
|
||||
# @example Set on_body.
|
||||
# request.on_body { |chunk| p "yay" }
|
||||
#
|
||||
# @param [ Block ] block The block to execute.
|
||||
def on_body(&block)
|
||||
@on_body ||= []
|
||||
@on_body << block if block_given?
|
||||
@on_body
|
||||
end
|
||||
|
||||
# Execute on_body callbacks.
|
||||
#
|
||||
# @example Execute on_body.
|
||||
# request.body("This data came from HTTP.")
|
||||
#
|
||||
# @return [ Object ] If there are no on_body callbacks, returns the symbol :unyielded.
|
||||
def body(chunk)
|
||||
if defined?(@on_body) and not @on_body.nil?
|
||||
result = nil
|
||||
@on_body.each do |callback|
|
||||
result = callback.call(chunk, self)
|
||||
break if result == :abort
|
||||
end
|
||||
result
|
||||
else
|
||||
:unyielded
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Easy # :nodoc:
|
||||
|
||||
# This module contains small helpers.
|
||||
#
|
||||
# @api private
|
||||
module Util
|
||||
|
||||
# Escapes zero bytes in strings.
|
||||
#
|
||||
# @example Escape zero bytes.
|
||||
# Util.escape_zero_byte("1\0")
|
||||
# #=> "1\\0"
|
||||
#
|
||||
# @param [ Object ] value The value to escape.
|
||||
#
|
||||
# @return [ String, Object ] Escaped String if
|
||||
# zero byte found, original object if not.
|
||||
def escape_zero_byte(value)
|
||||
return value unless value.to_s.include?(0.chr)
|
||||
value.to_s.gsub(0.chr, '\\\0')
|
||||
end
|
||||
|
||||
extend self
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon/errors/ethon_error'
|
||||
require 'ethon/errors/global_init'
|
||||
require 'ethon/errors/multi_timeout'
|
||||
require 'ethon/errors/multi_fdset'
|
||||
require 'ethon/errors/multi_add'
|
||||
require 'ethon/errors/multi_remove'
|
||||
require 'ethon/errors/select'
|
||||
require 'ethon/errors/invalid_option'
|
||||
require 'ethon/errors/invalid_value'
|
||||
|
||||
module Ethon
|
||||
|
||||
# This namespace contains all errors raised by ethon.
|
||||
module Errors
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Errors
|
||||
|
||||
# Default Ethon error class for all custom errors.
|
||||
class EthonError < StandardError
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Errors
|
||||
|
||||
# Raises when global_init failed.
|
||||
class GlobalInit < EthonError
|
||||
def initialize
|
||||
super("An error occured initializing curl.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Errors
|
||||
|
||||
# Raises when option is invalid.
|
||||
class InvalidOption < EthonError
|
||||
def initialize(option)
|
||||
super("The option: #{option} is invalid.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Errors
|
||||
|
||||
# Raises when option is invalid.
|
||||
class InvalidValue < EthonError
|
||||
def initialize(option, value)
|
||||
super("The value: #{value} is invalid for option: #{option}.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Errors
|
||||
|
||||
# Raises when multi_add_handle failed.
|
||||
class MultiAdd < EthonError
|
||||
def initialize(code, easy)
|
||||
super("An error occured adding the easy handle: #{easy} to the multi: #{code}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Errors
|
||||
|
||||
# Raises when multi_fdset failed.
|
||||
class MultiFdset < EthonError
|
||||
def initialize(code)
|
||||
super("An error occured getting the fdset: #{code}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Errors
|
||||
|
||||
# Raises when multi_remove_handle failed.
|
||||
class MultiRemove < EthonError
|
||||
def initialize(code, easy)
|
||||
super("An error occured removing the easy handle: #{easy} from the multi: #{code}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Errors
|
||||
|
||||
# Raised when multi_timeout failed.
|
||||
class MultiTimeout < EthonError
|
||||
def initialize(code)
|
||||
super("An error occured getting the timeout: #{code}")
|
||||
# "An error occured getting the timeout: #{code}: #{Curl.multi_strerror(code)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
module Errors
|
||||
|
||||
# Raised when select failed.
|
||||
class Select < EthonError
|
||||
def initialize(errno)
|
||||
super("An error occured on select: #{errno}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
|
||||
# FFI Wrapper module for Libc.
|
||||
#
|
||||
# @api private
|
||||
module Libc
|
||||
extend FFI::Library
|
||||
ffi_lib 'c'
|
||||
|
||||
# :nodoc:
|
||||
def self.windows?
|
||||
Gem.win_platform?
|
||||
end
|
||||
|
||||
unless windows?
|
||||
attach_function :getdtablesize, [], :int
|
||||
attach_function :free, [:pointer], :void
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
|
||||
# Contains logging behaviour.
|
||||
module Loggable
|
||||
|
||||
# Get the logger.
|
||||
#
|
||||
# @note Will try to grab Rails' logger first before creating a new logger
|
||||
# with stdout.
|
||||
#
|
||||
# @example Get the logger.
|
||||
# Loggable.logger
|
||||
#
|
||||
# @return [ Logger ] The logger.
|
||||
def logger
|
||||
return @logger if defined?(@logger)
|
||||
@logger = rails_logger || default_logger
|
||||
end
|
||||
|
||||
# Set the logger.
|
||||
#
|
||||
# @example Set the logger.
|
||||
# Loggable.logger = Logger.new($stdout)
|
||||
#
|
||||
# @param [ Logger ] logger The logger to set.
|
||||
#
|
||||
# @return [ Logger ] The new logger.
|
||||
def logger=(logger)
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Gets the default Ethon logger - stdout.
|
||||
#
|
||||
# @example Get the default logger.
|
||||
# Loggable.default_logger
|
||||
#
|
||||
# @return [ Logger ] The default logger.
|
||||
def default_logger
|
||||
logger = Logger.new($stdout)
|
||||
logger.level = Logger::INFO
|
||||
logger
|
||||
end
|
||||
|
||||
# Get the Rails logger if it's defined.
|
||||
#
|
||||
# @example Get Rails' logger.
|
||||
# Loggable.rails_logger
|
||||
#
|
||||
# @return [ Logger ] The Rails logger.
|
||||
def rails_logger
|
||||
defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
||||
end
|
||||
end
|
||||
extend Loggable
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon/easy/util'
|
||||
require 'ethon/multi/stack'
|
||||
require 'ethon/multi/operations'
|
||||
require 'ethon/multi/options'
|
||||
|
||||
module Ethon
|
||||
|
||||
# This class represents libcurl multi.
|
||||
class Multi
|
||||
include Ethon::Multi::Stack
|
||||
include Ethon::Multi::Operations
|
||||
include Ethon::Multi::Options
|
||||
|
||||
# Create a new multi. Initialize curl in case
|
||||
# it didn't happen before.
|
||||
#
|
||||
# @example Create a new Multi.
|
||||
# Multi.new
|
||||
#
|
||||
# @param [ Hash ] options The options.
|
||||
#
|
||||
# @option options :socketdata [String]
|
||||
# Pass a pointer to whatever you want passed to the
|
||||
# curl_socket_callback's forth argument, the userp pointer. This is not
|
||||
# used by libcurl but only passed-thru as-is. Set the callback pointer
|
||||
# with CURLMOPT_SOCKETFUNCTION.
|
||||
# @option options :pipelining [Boolean]
|
||||
# Pass a long set to 1 to enable or 0 to disable. Enabling pipelining
|
||||
# on a multi handle will make it attempt to perform HTTP Pipelining as
|
||||
# far as possible for transfers using this handle. This means that if
|
||||
# you add a second request that can use an already existing connection,
|
||||
# the second request will be "piped" on the same connection rather than
|
||||
# being executed in parallel. (Added in 7.16.0)
|
||||
# @option options :timerfunction [Proc]
|
||||
# Pass a pointer to a function matching the curl_multi_timer_callback
|
||||
# prototype. This function will then be called when the timeout value
|
||||
# changes. The timeout value is at what latest time the application
|
||||
# should call one of the "performing" functions of the multi interface
|
||||
# (curl_multi_socket_action(3) and curl_multi_perform(3)) - to allow
|
||||
# libcurl to keep timeouts and retries etc to work. A timeout value of
|
||||
# -1 means that there is no timeout at all, and 0 means that the
|
||||
# timeout is already reached. Libcurl attempts to limit calling this
|
||||
# only when the fixed future timeout time actually changes. See also
|
||||
# CURLMOPT_TIMERDATA. This callback can be used instead of, or in
|
||||
# addition to, curl_multi_timeout(3). (Added in 7.16.0)
|
||||
# @option options :timerdata [String]
|
||||
# Pass a pointer to whatever you want passed to the
|
||||
# curl_multi_timer_callback's third argument, the userp pointer. This
|
||||
# is not used by libcurl but only passed-thru as-is. Set the callback
|
||||
# pointer with CURLMOPT_TIMERFUNCTION. (Added in 7.16.0)
|
||||
# @option options :maxconnects [Integer]
|
||||
# Pass a long. The set number will be used as the maximum amount of
|
||||
# simultaneously open connections that libcurl may cache. Default is
|
||||
# 10, and libcurl will enlarge the size for each added easy handle to
|
||||
# make it fit 4 times the number of added easy handles.
|
||||
# By setting this option, you can prevent the cache size from growing
|
||||
# beyond the limit set by you.
|
||||
# When the cache is full, curl closes the oldest one in the cache to
|
||||
# prevent the number of open connections from increasing.
|
||||
# This option is for the multi handle's use only, when using the easy
|
||||
# interface you should instead use the CURLOPT_MAXCONNECTS option.
|
||||
# (Added in 7.16.3)
|
||||
# @option options :max_total_connections [Integer]
|
||||
# Pass a long. The set number will be used as the maximum amount of
|
||||
# simultaneously open connections in total. For each new session,
|
||||
# libcurl will open a new connection up to the limit set by
|
||||
# CURLMOPT_MAX_TOTAL_CONNECTIONS. When the limit is reached, the
|
||||
# sessions will be pending until there are available connections.
|
||||
# If CURLMOPT_PIPELINING is 1, libcurl will try to pipeline if the host
|
||||
# is capable of it.
|
||||
# The default value is 0, which means that there is no limit. However,
|
||||
# for backwards compatibility, setting it to 0 when CURLMOPT_PIPELINING
|
||||
# is 1 will not be treated as unlimited. Instead it will open only 1
|
||||
# connection and try to pipeline on it.
|
||||
# (Added in 7.30.0)
|
||||
# @option options :execution_mode [Boolean]
|
||||
# Either :perform (default) or :socket_action, specifies the usage
|
||||
# method that will be used on this multi object. The default :perform
|
||||
# mode provides a #perform function that uses curl_multi_perform
|
||||
# behind the scenes to automatically continue execution until all
|
||||
# requests have completed. The :socket_action mode provides an API
|
||||
# that allows the {Multi} object to be integrated into an external
|
||||
# IO loop, by calling #socket_action and responding to the
|
||||
# socketfunction and timerfunction callbacks, using the underlying
|
||||
# curl_multi_socket_action semantics.
|
||||
#
|
||||
# @return [ Multi ] The new multi.
|
||||
def initialize(options = {})
|
||||
Curl.init
|
||||
@execution_mode = options.delete(:execution_mode) || :perform
|
||||
set_attributes(options)
|
||||
init_vars
|
||||
end
|
||||
|
||||
# Set given options.
|
||||
#
|
||||
# @example Set options.
|
||||
# multi.set_attributes(options)
|
||||
#
|
||||
# @raise InvalidOption
|
||||
#
|
||||
# @see initialize
|
||||
#
|
||||
# @api private
|
||||
def set_attributes(options)
|
||||
options.each_pair do |key, value|
|
||||
unless respond_to?("#{key}=")
|
||||
raise Errors::InvalidOption.new(key)
|
||||
end
|
||||
method("#{key}=").call(value)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Internal function to gate functions to a specific execution mode
|
||||
#
|
||||
# @raise ArgumentError
|
||||
#
|
||||
# @api private
|
||||
def ensure_execution_mode(expected_mode)
|
||||
raise ArgumentError, "Expected the Multi to be in #{expected_mode} but it was in #{@execution_mode}" if expected_mode != @execution_mode
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,228 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Multi # :nodoc
|
||||
# This module contains logic to run a multi.
|
||||
module Operations
|
||||
STARTED_MULTI = "ETHON: started MULTI"
|
||||
PERFORMED_MULTI = "ETHON: performed MULTI"
|
||||
|
||||
# Return the multi handle. Inititialize multi handle,
|
||||
# in case it didn't happened already.
|
||||
#
|
||||
# @example Return multi handle.
|
||||
# multi.handle
|
||||
#
|
||||
# @return [ FFI::Pointer ] The multi handle.
|
||||
def handle
|
||||
@handle ||= FFI::AutoPointer.new(Curl.multi_init, Curl.method(:multi_cleanup))
|
||||
end
|
||||
|
||||
# Initialize variables.
|
||||
#
|
||||
# @example Initialize variables.
|
||||
# multi.init_vars
|
||||
#
|
||||
# @return [ void ]
|
||||
def init_vars
|
||||
if @execution_mode == :perform
|
||||
@timeout = ::FFI::MemoryPointer.new(:long)
|
||||
@timeval = Curl::Timeval.new
|
||||
@fd_read = Curl::FDSet.new
|
||||
@fd_write = Curl::FDSet.new
|
||||
@fd_excep = Curl::FDSet.new
|
||||
@max_fd = ::FFI::MemoryPointer.new(:int)
|
||||
elsif @execution_mode == :socket_action
|
||||
@running_count_pointer = FFI::MemoryPointer.new(:int)
|
||||
end
|
||||
end
|
||||
|
||||
# Perform multi.
|
||||
#
|
||||
# @return [ nil ]
|
||||
#
|
||||
# @example Perform multi.
|
||||
# multi.perform
|
||||
def perform
|
||||
ensure_execution_mode(:perform)
|
||||
|
||||
Ethon.logger.debug(STARTED_MULTI)
|
||||
while ongoing?
|
||||
run
|
||||
timeout = get_timeout
|
||||
next if timeout == 0
|
||||
reset_fds
|
||||
set_fds(timeout)
|
||||
end
|
||||
Ethon.logger.debug(PERFORMED_MULTI)
|
||||
nil
|
||||
end
|
||||
|
||||
# Prepare multi.
|
||||
#
|
||||
# @return [ nil ]
|
||||
#
|
||||
# @example Prepare multi.
|
||||
# multi.prepare
|
||||
#
|
||||
# @deprecated It is no longer necessary to call prepare.
|
||||
def prepare
|
||||
Ethon.logger.warn(
|
||||
"ETHON: It is no longer necessay to call "+
|
||||
"Multi#prepare. Its going to be removed "+
|
||||
"in future versions."
|
||||
)
|
||||
end
|
||||
|
||||
# Continue execution with an external IO loop.
|
||||
#
|
||||
# @example When no sockets are ready yet, or to begin.
|
||||
# multi.socket_action
|
||||
#
|
||||
# @example When a socket is readable
|
||||
# multi.socket_action(io_object, [:in])
|
||||
#
|
||||
# @example When a socket is readable and writable
|
||||
# multi.socket_action(io_object, [:in, :out])
|
||||
#
|
||||
# @return [ Symbol ] The Curl.multi_socket_action return code.
|
||||
def socket_action(io = nil, readiness = 0)
|
||||
ensure_execution_mode(:socket_action)
|
||||
|
||||
fd = if io.nil?
|
||||
::Ethon::Curl::SOCKET_TIMEOUT
|
||||
elsif io.is_a?(Integer)
|
||||
io
|
||||
else
|
||||
io.fileno
|
||||
end
|
||||
|
||||
code = Curl.multi_socket_action(handle, fd, readiness, @running_count_pointer)
|
||||
@running_count = @running_count_pointer.read_int
|
||||
|
||||
check
|
||||
|
||||
code
|
||||
end
|
||||
|
||||
# Return whether the multi still contains requests or not.
|
||||
#
|
||||
# @example Return if ongoing.
|
||||
# multi.ongoing?
|
||||
#
|
||||
# @return [ Boolean ] True if ongoing, else false.
|
||||
def ongoing?
|
||||
easy_handles.size > 0 || (!defined?(@running_count) || running_count > 0)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Get timeout.
|
||||
#
|
||||
# @example Get timeout.
|
||||
# multi.get_timeout
|
||||
#
|
||||
# @return [ Integer ] The timeout.
|
||||
#
|
||||
# @raise [ Ethon::Errors::MultiTimeout ] If getting the timeout fails.
|
||||
def get_timeout
|
||||
code = Curl.multi_timeout(handle, @timeout)
|
||||
raise Errors::MultiTimeout.new(code) unless code == :ok
|
||||
timeout = @timeout.read_long
|
||||
timeout = 1 if timeout < 0
|
||||
timeout
|
||||
end
|
||||
|
||||
# Reset file describtors.
|
||||
#
|
||||
# @example Reset fds.
|
||||
# multi.reset_fds
|
||||
#
|
||||
# @return [ void ]
|
||||
def reset_fds
|
||||
@fd_read.clear
|
||||
@fd_write.clear
|
||||
@fd_excep.clear
|
||||
end
|
||||
|
||||
# Set fds.
|
||||
#
|
||||
# @example Set fds.
|
||||
# multi.set_fds
|
||||
#
|
||||
# @return [ void ]
|
||||
#
|
||||
# @raise [ Ethon::Errors::MultiFdset ] If setting the file descriptors fails.
|
||||
# @raise [ Ethon::Errors::Select ] If select fails.
|
||||
def set_fds(timeout)
|
||||
code = Curl.multi_fdset(handle, @fd_read, @fd_write, @fd_excep, @max_fd)
|
||||
raise Errors::MultiFdset.new(code) unless code == :ok
|
||||
max_fd = @max_fd.read_int
|
||||
if max_fd == -1
|
||||
sleep(0.001)
|
||||
else
|
||||
@timeval[:sec] = timeout / 1000
|
||||
@timeval[:usec] = (timeout * 1000) % 1000000
|
||||
loop do
|
||||
code = Curl.select(max_fd + 1, @fd_read, @fd_write, @fd_excep, @timeval)
|
||||
break unless code < 0 && ::FFI.errno == Errno::EINTR::Errno
|
||||
end
|
||||
raise Errors::Select.new(::FFI.errno) if code < 0
|
||||
end
|
||||
end
|
||||
|
||||
# Check.
|
||||
#
|
||||
# @example Check.
|
||||
# multi.check
|
||||
#
|
||||
# @return [ void ]
|
||||
def check
|
||||
msgs_left = ::FFI::MemoryPointer.new(:int)
|
||||
while true
|
||||
msg = Curl.multi_info_read(handle, msgs_left)
|
||||
break if msg.null?
|
||||
next if msg[:code] != :done
|
||||
easy = easy_handles.find{ |e| e.handle == msg[:easy_handle] }
|
||||
easy.return_code = msg[:data][:code]
|
||||
Ethon.logger.debug { "ETHON: performed #{easy.log_inspect}" }
|
||||
delete(easy)
|
||||
easy.complete
|
||||
end
|
||||
end
|
||||
|
||||
# Run.
|
||||
#
|
||||
# @example Run
|
||||
# multi.run
|
||||
#
|
||||
# @return [ void ]
|
||||
def run
|
||||
running_count_pointer = FFI::MemoryPointer.new(:int)
|
||||
begin code = trigger(running_count_pointer) end while code == :call_multi_perform
|
||||
check
|
||||
end
|
||||
|
||||
# Trigger.
|
||||
#
|
||||
# @example Trigger.
|
||||
# multi.trigger
|
||||
#
|
||||
# @return [ Symbol ] The Curl.multi_perform return code.
|
||||
def trigger(running_count_pointer)
|
||||
code = Curl.multi_perform(handle, running_count_pointer)
|
||||
@running_count = running_count_pointer.read_int
|
||||
code
|
||||
end
|
||||
|
||||
# Return number of running requests.
|
||||
#
|
||||
# @example Return count.
|
||||
# multi.running_count
|
||||
#
|
||||
# @return [ Integer ] Number running requests.
|
||||
def running_count
|
||||
@running_count ||= nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,117 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Multi
|
||||
|
||||
# This module contains the logic and knowledge about the
|
||||
# available options on multi.
|
||||
module Options
|
||||
|
||||
# Sets max_total_connections option.
|
||||
#
|
||||
# @example Set max_total_connections option.
|
||||
# multi.max_total_conections = $value
|
||||
#
|
||||
# @param [ String ] value The value to set.
|
||||
#
|
||||
# @return [ void ]
|
||||
def max_total_connections=(value)
|
||||
Curl.set_option(:max_total_connections, value_for(value, :int), handle, :multi)
|
||||
end
|
||||
|
||||
# Sets maxconnects option.
|
||||
#
|
||||
# @example Set maxconnects option.
|
||||
# multi.maxconnects = $value
|
||||
#
|
||||
# @param [ String ] value The value to set.
|
||||
#
|
||||
# @return [ void ]
|
||||
def maxconnects=(value)
|
||||
Curl.set_option(:maxconnects, value_for(value, :int), handle, :multi)
|
||||
end
|
||||
|
||||
# Sets pipelining option.
|
||||
#
|
||||
# @example Set pipelining option.
|
||||
# multi.pipelining = $value
|
||||
#
|
||||
# @param [ String ] value The value to set.
|
||||
#
|
||||
# @return [ void ]
|
||||
def pipelining=(value)
|
||||
Curl.set_option(:pipelining, value_for(value, :int), handle, :multi)
|
||||
end
|
||||
|
||||
# Sets socketdata option.
|
||||
#
|
||||
# @example Set socketdata option.
|
||||
# multi.socketdata = $value
|
||||
#
|
||||
# @param [ String ] value The value to set.
|
||||
#
|
||||
# @return [ void ]
|
||||
def socketdata=(value)
|
||||
Curl.set_option(:socketdata, value_for(value, :string), handle, :multi)
|
||||
end
|
||||
|
||||
# Sets socketfunction option.
|
||||
#
|
||||
# @example Set socketfunction option.
|
||||
# multi.socketfunction = $value
|
||||
#
|
||||
# @param [ String ] value The value to set.
|
||||
#
|
||||
# @return [ void ]
|
||||
def socketfunction=(value)
|
||||
Curl.set_option(:socketfunction, value_for(value, :string), handle, :multi)
|
||||
end
|
||||
|
||||
# Sets timerdata option.
|
||||
#
|
||||
# @example Set timerdata option.
|
||||
# multi.timerdata = $value
|
||||
#
|
||||
# @param [ String ] value The value to set.
|
||||
#
|
||||
# @return [ void ]
|
||||
def timerdata=(value)
|
||||
Curl.set_option(:timerdata, value_for(value, :string), handle, :multi)
|
||||
end
|
||||
|
||||
# Sets timerfunction option.
|
||||
#
|
||||
# @example Set timerfunction option.
|
||||
# multi.timerfunction = $value
|
||||
#
|
||||
# @param [ String ] value The value to set.
|
||||
#
|
||||
# @return [ void ]
|
||||
def timerfunction=(value)
|
||||
Curl.set_option(:timerfunction, value_for(value, :string), handle, :multi)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Return the value to set to multi handle. It is converted with the help
|
||||
# of bool_options, enum_options and int_options.
|
||||
#
|
||||
# @example Return casted the value.
|
||||
# multi.value_for(:verbose)
|
||||
#
|
||||
# @return [ Object ] The casted value.
|
||||
def value_for(value, type, option = nil)
|
||||
return nil if value.nil?
|
||||
|
||||
if type == :bool
|
||||
value ? 1 : 0
|
||||
elsif type == :int
|
||||
value.to_i
|
||||
elsif value.is_a?(String)
|
||||
Ethon::Easy::Util.escape_zero_byte(value)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
class Multi
|
||||
|
||||
# This module provides the multi stack behaviour.
|
||||
module Stack
|
||||
|
||||
# Return easy handles.
|
||||
#
|
||||
# @example Return easy handles.
|
||||
# multi.easy_handles
|
||||
#
|
||||
# @return [ Array ] The easy handles.
|
||||
def easy_handles
|
||||
@easy_handles ||= []
|
||||
end
|
||||
|
||||
# Add an easy to the stack.
|
||||
#
|
||||
# @example Add easy.
|
||||
# multi.add(easy)
|
||||
#
|
||||
# @param [ Easy ] easy The easy to add.
|
||||
#
|
||||
# @raise [ Ethon::Errors::MultiAdd ] If adding an easy failed.
|
||||
def add(easy)
|
||||
return nil if easy_handles.include?(easy)
|
||||
|
||||
code = Curl.multi_add_handle(handle, easy.handle)
|
||||
raise Errors::MultiAdd.new(code, easy) unless code == :ok
|
||||
easy_handles << easy
|
||||
end
|
||||
|
||||
# Delete an easy from stack.
|
||||
#
|
||||
# @example Delete easy from stack.
|
||||
#
|
||||
# @param [ Easy ] easy The easy to delete.
|
||||
#
|
||||
# @raise [ Ethon::Errors::MultiRemove ] If removing an easy failed.
|
||||
def delete(easy)
|
||||
if easy_handles.delete(easy)
|
||||
code = Curl.multi_remove_handle(handle, easy.handle)
|
||||
raise Errors::MultiRemove.new(code, handle) unless code == :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
module Ethon
|
||||
|
||||
# Ethon version.
|
||||
VERSION = '0.16.0'
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
require 'ethon'
|
||||
require 'open-uri'
|
||||
require 'patron'
|
||||
require 'curb'
|
||||
require 'net/http'
|
||||
require 'cgi'
|
||||
require 'benchmark/ips'
|
||||
|
||||
require_relative '../spec/support/server'
|
||||
require_relative '../spec/support/localhost_server'
|
||||
|
||||
LocalhostServer.new(TESTSERVER.new, 3000)
|
||||
LocalhostServer.new(TESTSERVER.new, 3001)
|
||||
LocalhostServer.new(TESTSERVER.new, 3002)
|
||||
|
||||
url = 'http://localhost:3000/'.freeze
|
||||
uri = URI.parse('http://localhost:3000/').freeze
|
||||
ethon = Ethon::Easy.new(url: url)
|
||||
patron = Patron::Session.new
|
||||
patron_url = Patron::Session.new(base_url: url)
|
||||
curb = Curl::Easy.new(url)
|
||||
|
||||
puts '[Creation]'
|
||||
Benchmark.ips do |x|
|
||||
x.report('String.new') { '' }
|
||||
x.report('Easy.new') { Ethon::Easy.new }
|
||||
end
|
||||
|
||||
puts '[Escape]'
|
||||
Benchmark.ips do |x|
|
||||
x.report('CGI.escape') { CGI.escape("まつもと") }
|
||||
x.report('Easy.escape') { ethon.escape("まつもと") }
|
||||
end
|
||||
|
||||
puts '[Requests]'
|
||||
Benchmark.ips do |x|
|
||||
x.report('net/http') { Net::HTTP.get_response(uri) }
|
||||
x.report('open-uri') { open url }
|
||||
|
||||
x.report('patron') do
|
||||
patron.base_url = url
|
||||
patron.get('/')
|
||||
end
|
||||
|
||||
x.report('patron reuse') { patron_url.get('/') }
|
||||
|
||||
x.report('curb') do
|
||||
curb.url = url
|
||||
curb.perform
|
||||
end
|
||||
|
||||
x.report('curb reuse') { curb.perform }
|
||||
|
||||
x.report('Easy.perform') do
|
||||
ethon.url = url
|
||||
ethon.perform
|
||||
end
|
||||
|
||||
x.report('Easy.perform reuse') { ethon.perform }
|
||||
end
|
||||
|
||||
puts "[ 4 delayed Requests ]"
|
||||
Benchmark.ips do |x|
|
||||
x.report('net/http') do
|
||||
3.times do |i|
|
||||
uri = URI.parse("http://localhost:300#{i}/?delay=1")
|
||||
Net::HTTP.get_response(uri)
|
||||
end
|
||||
end
|
||||
|
||||
x.report("open-uri") do
|
||||
3.times do |i|
|
||||
open("http://localhost:300#{i}/?delay=1")
|
||||
end
|
||||
end
|
||||
|
||||
x.report("patron") do
|
||||
sess = Patron::Session.new
|
||||
3.times do |i|
|
||||
sess.base_url = "http://localhost:300#{i}/?delay=1"
|
||||
sess.get("/")
|
||||
end
|
||||
end
|
||||
|
||||
x.report("Easy.perform") do
|
||||
easy = Ethon::Easy.new
|
||||
3.times do |i|
|
||||
easy.url = "http://localhost:300#{i}/?delay=1"
|
||||
easy.perform
|
||||
end
|
||||
end
|
||||
|
||||
x.report("Multi.perform") do
|
||||
multi = Ethon::Multi.new
|
||||
3.times do |i|
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:300#{i}/?delay=1"
|
||||
multi.add(easy)
|
||||
end
|
||||
multi.perform
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,114 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ethon'
|
||||
require 'ethon/easy'
|
||||
|
||||
require_relative 'perf_spec_helper'
|
||||
require 'rspec/autorun'
|
||||
|
||||
describe "low-level interactions with libcurl" do
|
||||
describe Ethon::Multi do
|
||||
memory_leak_test("init") do
|
||||
Ethon::Multi.new
|
||||
end
|
||||
|
||||
memory_leak_test("handle") do
|
||||
Ethon::Multi.new.handle
|
||||
end
|
||||
end
|
||||
|
||||
describe Ethon::Easy do
|
||||
memory_leak_test("init") do
|
||||
Ethon::Easy.new
|
||||
end
|
||||
|
||||
memory_leak_test("handle") do
|
||||
Ethon::Easy.new.handle
|
||||
end
|
||||
|
||||
memory_leak_test("headers") do
|
||||
Ethon::Easy.new.headers = { "a" => 1, "b" => 2, "c" => 3, "d" => 4}
|
||||
end
|
||||
|
||||
memory_leak_test("escape") do
|
||||
Ethon::Easy.new.escape("the_sky&is_blue")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe Ethon::Easy::Form do
|
||||
memory_leak_test("init") do
|
||||
Ethon::Easy::Form.new(nil, {})
|
||||
end
|
||||
|
||||
memory_leak_test("first") do
|
||||
Ethon::Easy::Form.new(nil, {}).first
|
||||
end
|
||||
|
||||
memory_leak_test("last") do
|
||||
Ethon::Easy::Form.new(nil, {}).last
|
||||
end
|
||||
|
||||
memory_leak_test("materialized with some params") do
|
||||
form = Ethon::Easy::Form.new(nil, { "a" => "1" })
|
||||
form.materialize
|
||||
end
|
||||
|
||||
memory_leak_test("materialized with a file") do
|
||||
File.open(__FILE__, "r") do |file|
|
||||
form = Ethon::Easy::Form.new(nil, { "a" => file })
|
||||
form.materialize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "higher level operations" do
|
||||
memory_leak_test("a simple request") do
|
||||
Ethon::Easy.new(:url => "http://localhost:3001/",
|
||||
:forbid_reuse => true).perform
|
||||
end
|
||||
|
||||
memory_leak_test("a request with headers") do
|
||||
Ethon::Easy.new(:url => "http://localhost:3001/",
|
||||
:headers => { "Content-Type" => "application/json",
|
||||
"Something" => "1",
|
||||
"Else" => "qwerty",
|
||||
"Long-String" => "aassddffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz"},
|
||||
:forbid_reuse => true).perform
|
||||
end
|
||||
|
||||
memory_leak_test("a request with headers and params") do
|
||||
easy = Ethon::Easy.new(:url => "http://localhost:3001/",
|
||||
:headers => { "Content-Type" => "application/json",
|
||||
"Something" => "1",
|
||||
"Else" => "qwerty",
|
||||
"Long-String" => "aassddffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz"},
|
||||
:forbid_reuse => true)
|
||||
easy.http_request("http://localhost:3001/",
|
||||
:get,
|
||||
:params => { "param1" => "value1",
|
||||
"param2" => "value2",
|
||||
"param3" => "value3",
|
||||
"param4" => "value4"})
|
||||
end
|
||||
|
||||
memory_leak_test("a request with headers, params, and body") do
|
||||
easy = Ethon::Easy.new(:url => "http://localhost:3001/",
|
||||
:headers => { "Content-Type" => "application/json",
|
||||
"Something" => "1",
|
||||
"Else" => "qwerty",
|
||||
"Long-String" => "aassddffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz"},
|
||||
:forbid_reuse => true)
|
||||
easy.http_request("http://localhost:3001/",
|
||||
:get,
|
||||
:params => { "param1" => "value1",
|
||||
"param2" => "value2",
|
||||
"param3" => "value3",
|
||||
"param4" => "value4"},
|
||||
:body => {
|
||||
"body1" => "value1",
|
||||
"body2" => "value2",
|
||||
"body3" => "value3"
|
||||
})
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
#### SETUP
|
||||
require 'bundler'
|
||||
Bundler.setup
|
||||
require 'rspec'
|
||||
|
||||
require 'support/localhost_server'
|
||||
require 'support/server'
|
||||
require_relative 'support/memory_test_helpers'
|
||||
|
||||
require 'logger'
|
||||
|
||||
if ENV['VERBOSE']
|
||||
Ethon.logger = Logger.new($stdout)
|
||||
Ethon.logger.level = Logger::DEBUG
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.before(:suite) do
|
||||
LocalhostServer.new(TESTSERVER.new, 3001)
|
||||
end
|
||||
config.include(MemoryTestHelpers)
|
||||
config.extend(MemoryTestHelpers::TestMethods)
|
||||
end
|
||||
|
||||
MemoryTestHelpers.setup
|
||||
MemoryTestHelpers.logger = Logger.new($stdout)
|
||||
MemoryTestHelpers.logger.level = Logger::INFO
|
||||
MemoryTestHelpers.logger.formatter = proc do |severity, datetime, progname, msg|
|
||||
"\t\t#{msg}\n"
|
||||
end
|
||||
|
||||
if ENV['VERBOSE']
|
||||
MemoryTestHelpers.logger.level = Logger::DEBUG
|
||||
end
|
||||
|
||||
MemoryTestHelpers.iterations = ENV.fetch("ITERATIONS", 10_000).to_i
|
||||
@@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
require_relative 'ruby_object_leak_tracker'
|
||||
require_relative 'os_memory_leak_tracker'
|
||||
|
||||
module MemoryTestHelpers
|
||||
class << self
|
||||
attr_accessor :gc_proc, :iterations, :logger
|
||||
|
||||
def setup
|
||||
if RUBY_PLATFORM == "java"
|
||||
# for leak detection
|
||||
JRuby.objectspace = true if defined?(JRuby)
|
||||
# for gc
|
||||
require 'java'
|
||||
java_import 'java.lang.System'
|
||||
self.gc_proc = proc { System.gc }
|
||||
else
|
||||
self.gc_proc = proc { GC.start }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module TestMethods
|
||||
def memory_leak_test(description, &block)
|
||||
context(description) do
|
||||
it "doesn't leak ruby objects" do
|
||||
object_leak_tracker = RubyObjectLeakTracker.new
|
||||
track_memory_usage(object_leak_tracker, &block)
|
||||
object_leak_tracker.total_difference_between_runs.should be <= 10
|
||||
end
|
||||
|
||||
it "doesn't leak OS memory (C interop check)" do
|
||||
os_memory_leak_tracker = OSMemoryLeakTracker.new
|
||||
track_memory_usage(os_memory_leak_tracker, &block)
|
||||
os_memory_leak_tracker.total_difference_between_runs.should be <= 10
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def track_memory_usage(tracker)
|
||||
# Intentionally do all this setup before we do any testing
|
||||
logger = MemoryTestHelpers.logger
|
||||
iterations = MemoryTestHelpers.iterations
|
||||
|
||||
checkpoint_frequency = (iterations / 10.0).to_i
|
||||
gc_frequency = 20
|
||||
|
||||
warmup_iterations = [(iterations / 3.0).to_i, 500].min
|
||||
logger.info "Performing #{warmup_iterations} warmup iterations"
|
||||
warmup_iterations.times do
|
||||
yield
|
||||
MemoryTestHelpers.gc_proc.call
|
||||
end
|
||||
tracker.capture_initial_memory_usage
|
||||
|
||||
logger.info "Performing #{iterations} iterations (checkpoint every #{checkpoint_frequency})"
|
||||
|
||||
iterations.times do |i|
|
||||
yield
|
||||
|
||||
last_iteration = (i == iterations - 1)
|
||||
checkpoint = last_iteration || (i % checkpoint_frequency == 0)
|
||||
|
||||
if checkpoint || (i % gc_frequency == 0)
|
||||
MemoryTestHelpers.gc_proc.call
|
||||
end
|
||||
|
||||
if checkpoint
|
||||
logger.info "Iteration #{i} checkpoint"
|
||||
tracker.capture_memory_usage
|
||||
tracker.dump_status(logger)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
class OSMemoryLeakTracker
|
||||
attr_reader :current_run
|
||||
|
||||
def initialize
|
||||
@previous_run = @current_run = 0
|
||||
end
|
||||
|
||||
def difference_between_runs(basis=@previous_run)
|
||||
@current_run - basis
|
||||
end
|
||||
|
||||
def total_difference_between_runs
|
||||
difference_between_runs(@initial_count_run)
|
||||
end
|
||||
|
||||
def capture_initial_memory_usage
|
||||
capture_memory_usage
|
||||
@initial_count_run = @current_run
|
||||
end
|
||||
|
||||
def capture_memory_usage
|
||||
@previous_run = @current_run
|
||||
@current_run = rss_bytes
|
||||
end
|
||||
|
||||
def dump_status(logger)
|
||||
delta = difference_between_runs
|
||||
logger.add(log_level(delta), sprintf("\tTotal memory usage (kb): %d (%+d)", current_run, delta))
|
||||
end
|
||||
|
||||
private
|
||||
# amount of memory the current process "is using", in RAM
|
||||
# (doesn't include any swap memory that it may be using, just that in actual RAM)
|
||||
# Code loosely based on https://github.com/rdp/os/blob/master/lib/os.rb
|
||||
# returns 0 on windows
|
||||
def rss_bytes
|
||||
if ENV['OS'] == 'Windows_NT'
|
||||
0
|
||||
else
|
||||
`ps -o rss= -p #{Process.pid}`.to_i # in kilobytes
|
||||
end
|
||||
end
|
||||
|
||||
def log_level(delta)
|
||||
delta > 0 ? Logger::WARN : Logger::DEBUG
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
class RubyObjectLeakTracker
|
||||
attr_reader :previous_count_hash, :current_count_hash
|
||||
|
||||
def initialize
|
||||
@previous_count_hash = @current_count_hash = {}
|
||||
end
|
||||
|
||||
def difference_between_runs(basis=@previous_count_hash)
|
||||
@difference_between_runs ||= Hash[@current_count_hash.map do |object_class, count|
|
||||
[object_class, count - (basis[object_class] || 0)]
|
||||
end]
|
||||
end
|
||||
|
||||
def total_difference_between_runs
|
||||
difference_between_runs(@initial_count_hash).values.inject(0) { |sum, count| sum + count }
|
||||
end
|
||||
|
||||
def capture_initial_memory_usage
|
||||
capture_memory_usage
|
||||
@initial_count_hash = @current_count_hash
|
||||
end
|
||||
|
||||
def capture_memory_usage
|
||||
@difference_between_runs = nil
|
||||
@previous_count_hash = @current_count_hash
|
||||
|
||||
class_to_count = Hash.new { |hash, key| hash[key] = 0 }
|
||||
ObjectSpace.each_object { |obj| class_to_count[obj.class] += 1 }
|
||||
|
||||
sorted_class_to_count = class_to_count.sort_by { |k, v| -v }
|
||||
@current_count_hash = Hash[sorted_class_to_count]
|
||||
end
|
||||
|
||||
def dump_status(logger)
|
||||
diff = difference_between_runs
|
||||
most_used_objects = current_count_hash.to_a.sort_by(&:last).reverse[0, 20]
|
||||
|
||||
most_used_objects.each do |object_class, count|
|
||||
delta = diff[object_class]
|
||||
logger.add(log_level(delta), sprintf("\t%s: %d (%+d)", object_class, count, delta))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def log_level(delta)
|
||||
delta > 0 ? Logger::WARN : Logger::DEBUG
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Curl do
|
||||
describe ".init" do
|
||||
before { Ethon::Curl.send(:class_variable_set, :@@initialized, false) }
|
||||
|
||||
context "when global_init fails" do
|
||||
it "raises global init error" do
|
||||
expect(Ethon::Curl).to receive(:global_init).and_return(1)
|
||||
expect{ Ethon::Curl.init }.to raise_error(Ethon::Errors::GlobalInit)
|
||||
end
|
||||
end
|
||||
|
||||
context "when global_init works" do
|
||||
before { expect(Ethon::Curl).to receive(:global_init).and_return(0) }
|
||||
|
||||
it "doesn't raises global init error" do
|
||||
expect{ Ethon::Curl.init }.to_not raise_error
|
||||
end
|
||||
|
||||
it "logs" do
|
||||
expect(Ethon.logger).to receive(:debug)
|
||||
Ethon::Curl.init
|
||||
end
|
||||
end
|
||||
|
||||
context "when global_cleanup is called" do
|
||||
before { expect(Ethon::Curl).to receive(:global_cleanup) }
|
||||
|
||||
it "logs" do
|
||||
expect(Ethon.logger).to receive(:debug).twice
|
||||
Ethon::Curl.init
|
||||
Ethon::Curl.cleanup
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,81 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Callbacks do
|
||||
let!(:easy) { Ethon::Easy.new }
|
||||
|
||||
describe "#set_callbacks" do
|
||||
before do
|
||||
expect(Ethon::Curl).to receive(:set_option).exactly(3).times
|
||||
end
|
||||
|
||||
it "sets write-, debug-, and headerfunction" do
|
||||
easy.set_callbacks
|
||||
end
|
||||
|
||||
it "resets @response_body" do
|
||||
easy.set_callbacks
|
||||
expect(easy.instance_variable_get(:@response_body)).to eq("")
|
||||
end
|
||||
|
||||
it "resets @response_headers" do
|
||||
easy.set_callbacks
|
||||
expect(easy.instance_variable_get(:@response_headers)).to eq("")
|
||||
end
|
||||
|
||||
it "resets @debug_info" do
|
||||
easy.set_callbacks
|
||||
expect(easy.instance_variable_get(:@debug_info).to_a).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#progress_callback" do
|
||||
it "returns 0" do
|
||||
expect(easy.progress_callback.call(0,1,1,1,1)).to be(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#body_write_callback" do
|
||||
let(:body_write_callback) { easy.instance_variable_get(:@body_write_callback) }
|
||||
let(:stream) { double(:read_string => "") }
|
||||
context "when body returns not :abort" do
|
||||
it "returns number bigger than 0" do
|
||||
expect(body_write_callback.call(stream, 1, 1, nil) > 0).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when body returns :abort" do
|
||||
before do
|
||||
easy.on_body.clear
|
||||
easy.on_body { :abort }
|
||||
end
|
||||
let(:body_write_callback) { easy.instance_variable_get(:@body_write_callback) }
|
||||
|
||||
it "returns -1 to indicate abort to libcurl" do
|
||||
expect(body_write_callback.call(stream, 1, 1, nil)).to eq(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#header_write_callback" do
|
||||
let(:header_write_callback) { easy.instance_variable_get(:@header_write_callback) }
|
||||
let(:stream) { double(:read_string => "") }
|
||||
context "when header returns not :abort" do
|
||||
it "returns number bigger than 0" do
|
||||
expect(header_write_callback.call(stream, 1, 1, nil) > 0).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "when header returns :abort" do
|
||||
before do
|
||||
easy.on_headers.clear
|
||||
easy.on_headers { :abort }
|
||||
end
|
||||
let(:header_write_callback) { easy.instance_variable_get(:@header_write_callback) }
|
||||
|
||||
it "returns -1 to indicate abort to libcurl" do
|
||||
expect(header_write_callback.call(stream, 1, 1, nil)).to eq(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::DebugInfo do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
|
||||
before do
|
||||
easy.url = "http://localhost:3001/"
|
||||
easy.perform
|
||||
end
|
||||
|
||||
describe "#debug_info" do
|
||||
context "when verbose is not set to true" do
|
||||
it "does not save any debug info after a request" do
|
||||
expect(easy.debug_info.to_a.length).to eq(0)
|
||||
expect(easy.debug_info.to_h.values.flatten.length).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context "when verbose is set to true" do
|
||||
before do
|
||||
easy.verbose = true
|
||||
easy.perform
|
||||
end
|
||||
|
||||
after do
|
||||
easy.verbose = false
|
||||
easy.reset
|
||||
end
|
||||
|
||||
it "saves debug info after a request" do
|
||||
expect(easy.debug_info.to_a.length).to be > 0
|
||||
end
|
||||
|
||||
it "saves request headers" do
|
||||
expect(easy.debug_info.header_out.join).to include('GET / HTTP/1.1')
|
||||
end
|
||||
|
||||
it "saves response headers" do
|
||||
expect(easy.debug_info.header_in.length).to be > 0
|
||||
expect(easy.response_headers).to include(easy.debug_info.header_in.join)
|
||||
end
|
||||
|
||||
it "saves incoming data" do
|
||||
expect(easy.debug_info.data_in.length).to be > 0
|
||||
expect(easy.response_body).to include(easy.debug_info.data_in.join)
|
||||
end
|
||||
|
||||
it "saves debug text" do
|
||||
expect(easy.debug_info.text.length).to be > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Informations do
|
||||
|
||||
describe "#supports_asynch_dns?" do
|
||||
it "returns boolean" do
|
||||
expect([true, false].include? Ethon::Easy.supports_asynch_dns?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#supports_zlib?" do
|
||||
it "returns boolean" do
|
||||
expect([true, false].include? Ethon::Easy.supports_zlib?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#supports_timeout_ms?" do
|
||||
it "returns boolean" do
|
||||
expect([true, false].include? Ethon::Easy.supports_timeout_ms?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Form do
|
||||
let(:hash) { {} }
|
||||
let!(:easy) { Ethon::Easy.new }
|
||||
let(:form) { Ethon::Easy::Form.new(easy, hash) }
|
||||
|
||||
describe ".new" do
|
||||
it "assigns attribute to @params" do
|
||||
expect(form.instance_variable_get(:@params)).to eq(hash)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#first" do
|
||||
it "returns a pointer" do
|
||||
expect(form.first).to be_a(FFI::Pointer)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#last" do
|
||||
it "returns a pointer" do
|
||||
expect(form.first).to be_a(FFI::Pointer)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#multipart?" do
|
||||
before { form.instance_variable_set(:@query_pairs, pairs) }
|
||||
|
||||
context "when query_pairs contains string values" do
|
||||
let(:pairs) { [['a', '1'], ['b', '2']] }
|
||||
|
||||
it "returns false" do
|
||||
expect(form.multipart?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context "when query_pairs contains file" do
|
||||
let(:pairs) { [['a', '1'], ['b', ['path', 'encoding', 'abs_path']]] }
|
||||
|
||||
it "returns true" do
|
||||
expect(form.multipart?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "when options contains multipart=true" do
|
||||
before { form.instance_variable_set(:@multipart, true) }
|
||||
let(:pairs) { [['a', '1'], ['b', '2']] }
|
||||
|
||||
it "returns true" do
|
||||
expect(form.multipart?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#materialize" do
|
||||
before { form.instance_variable_set(:@query_pairs, pairs) }
|
||||
|
||||
context "when query_pairs contains string values" do
|
||||
let(:pairs) { [['a', '1']] }
|
||||
|
||||
it "adds params to form" do
|
||||
expect(Ethon::Curl).to receive(:formadd)
|
||||
form.materialize
|
||||
end
|
||||
end
|
||||
|
||||
context "when query_pairs contains nil" do
|
||||
let(:pairs) { [['a', nil]] }
|
||||
|
||||
it "adds params to form" do
|
||||
expect(Ethon::Curl).to receive(:formadd)
|
||||
form.materialize
|
||||
end
|
||||
end
|
||||
|
||||
context "when query_pairs contains file" do
|
||||
let(:pairs) { [['a', ["file", "type", "path/file"]]] }
|
||||
|
||||
it "adds file to form" do
|
||||
expect(Ethon::Curl).to receive(:formadd)
|
||||
form.materialize
|
||||
end
|
||||
end
|
||||
|
||||
context "when query_pairs contains file and string values" do
|
||||
let(:pairs) { [['a', ["file", "type", "path/file"]], ['b', '1']] }
|
||||
|
||||
it "adds file to form" do
|
||||
expect(Ethon::Curl).to receive(:formadd).twice
|
||||
form.materialize
|
||||
end
|
||||
end
|
||||
|
||||
context "when query_pairs contains file, string and int values" do
|
||||
let(:pairs) { [['a', ["file", "type", "path/file"]], ['b', '1'], ['c', 1]] }
|
||||
|
||||
it "adds file to form" do
|
||||
expect(Ethon::Curl).to receive(:formadd).exactly(3).times
|
||||
form.materialize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Header do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
|
||||
describe "#headers=" do
|
||||
let(:headers) { { 'User-Agent' => 'Ethon' } }
|
||||
|
||||
it "sets header" do
|
||||
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
|
||||
expect(Ethon::Curl).to receive(:set_option)
|
||||
easy.headers = headers
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = headers
|
||||
easy.url = "http://localhost:3001"
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "sends" do
|
||||
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
|
||||
end
|
||||
|
||||
context "when header value contains null byte" do
|
||||
let(:headers) { { 'User-Agent' => "Ethon\0" } }
|
||||
|
||||
it "escapes" do
|
||||
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon\\\\0"')
|
||||
end
|
||||
end
|
||||
|
||||
context "when header value has leading whitespace" do
|
||||
let(:headers) { { 'User-Agent' => " Ethon" } }
|
||||
|
||||
it "removes" do
|
||||
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
|
||||
end
|
||||
end
|
||||
|
||||
context "when header value has traiing whitespace" do
|
||||
let(:headers) { { 'User-Agent' => "Ethon " } }
|
||||
|
||||
it "removes" do
|
||||
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#compose_header" do
|
||||
it "has space in between" do
|
||||
expect(easy.compose_header('a', 'b')).to eq('a: b')
|
||||
end
|
||||
|
||||
context "when value is a symbol" do
|
||||
it "works" do
|
||||
expect{ easy.compose_header('a', :b) }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#header_list" do
|
||||
context "when no set_headers" do
|
||||
it "returns nil" do
|
||||
expect(easy.header_list).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "when set_headers" do
|
||||
it "returns pointer to header list" do
|
||||
easy.headers = {'User-Agent' => 'Custom'}
|
||||
expect(easy.header_list).to be_a(FFI::Pointer)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,177 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Http::Custom do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
let(:params) { nil }
|
||||
let(:form) { nil }
|
||||
let(:custom) { described_class.new("PURGE", url, {:params => params, :body => form}) }
|
||||
|
||||
describe "#setup" do
|
||||
context "when nothing" do
|
||||
it "sets url" do
|
||||
custom.setup(easy)
|
||||
expect(easy.url).to eq(url)
|
||||
end
|
||||
|
||||
it "makes a custom request" do
|
||||
custom.setup(easy)
|
||||
easy.perform
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
|
||||
end
|
||||
end
|
||||
|
||||
context "when params" do
|
||||
let(:params) { {:a => "1&"} }
|
||||
|
||||
it "attaches escaped to url" do
|
||||
custom.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a=1%26")
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
custom.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "is a custom verb" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
|
||||
end
|
||||
|
||||
it "does not use application/x-www-form-urlencoded content type" do
|
||||
expect(easy.response_body).to_not include('"CONTENT_TYPE":"application/x-www-form-urlencoded"')
|
||||
end
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?a=1%26"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when body" do
|
||||
context "when multipart" do
|
||||
let(:form) { {:a => File.open(__FILE__, 'r')} }
|
||||
|
||||
it "sets httppost" do
|
||||
expect(easy).to receive(:httppost=)
|
||||
custom.setup(easy)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
custom.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "is a custom verb" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
|
||||
end
|
||||
|
||||
it "uses multipart/form-data content type" do
|
||||
expect(easy.response_body).to include('"CONTENT_TYPE":"multipart/form-data')
|
||||
end
|
||||
|
||||
it "submits a body" do
|
||||
expect(easy.response_body).to match('"body":".+"')
|
||||
end
|
||||
|
||||
it "submits the data" do
|
||||
expect(easy.response_body).to include('"filename":"custom_spec.rb"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when not multipart" do
|
||||
let(:form) { {:a => "1&b=2"} }
|
||||
let(:encoded) { "a=1%26b%3D2" }
|
||||
|
||||
it "sets escaped copypostfields" do
|
||||
expect(easy).to receive(:copypostfields=).with(encoded)
|
||||
custom.setup(easy)
|
||||
end
|
||||
|
||||
it "sets postfieldsize" do
|
||||
expect(easy).to receive(:postfieldsize=).with(encoded.bytesize)
|
||||
custom.setup(easy)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
custom.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "is a custom verb" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"PURGE"')
|
||||
end
|
||||
|
||||
it "uses multipart/form-data content type" do
|
||||
expect(easy.response_body).to include('"CONTENT_TYPE":"application/x-www-form-urlencoded')
|
||||
end
|
||||
|
||||
it "submits a body" do
|
||||
expect(easy.response_body).to match('"body":"a=1%26b%3D2"')
|
||||
end
|
||||
|
||||
it "submits the data" do
|
||||
expect(easy.response_body).to include('"rack.request.form_hash":{"a":"1&b=2"}')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when string" do
|
||||
let(:form) { "{a: 1}" }
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
custom.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "sends string" do
|
||||
expect(easy.response_body).to include('"body":"{a: 1}"')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when params and body" do
|
||||
let(:form) { {:a => "1"} }
|
||||
let(:params) { {:b => "2"} }
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
custom.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "url contains params" do
|
||||
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?b=2"')
|
||||
end
|
||||
|
||||
it "body contains form" do
|
||||
expect(easy.response_body).to include('"body":"a=1"')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Http::Delete do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
let(:params) { nil }
|
||||
let(:form) { nil }
|
||||
let(:delete) { described_class.new(url, {:params => params, :body => form}) }
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
delete.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "makes a delete request" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"DELETE"')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Http::Get do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
let(:params) { nil }
|
||||
let(:form) { nil }
|
||||
let(:options) { {} }
|
||||
let(:get) { described_class.new(url, {:params => params, :body => form}.merge(options)) }
|
||||
|
||||
describe "#setup" do
|
||||
it "sets url" do
|
||||
get.setup(easy)
|
||||
expect(easy.url).to eq(url)
|
||||
end
|
||||
|
||||
context "when body" do
|
||||
let(:form) { { :a => 1 } }
|
||||
|
||||
it "sets customrequest" do
|
||||
expect(easy).to receive(:customrequest=).with("GET")
|
||||
get.setup(easy)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no body" do
|
||||
it "doesn't set customrequest" do
|
||||
expect(easy).to receive(:customrequest=).never
|
||||
get.setup(easy)
|
||||
end
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
get.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
context "when url already contains params" do
|
||||
let(:url) { "http://localhost:3001/?query=here" }
|
||||
let(:params) { {:a => "1&b=2"} }
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "is a get request" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"GET"')
|
||||
end
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.effective_url).to eq("http://localhost:3001/?query=here&a=1%26b%3D2")
|
||||
end
|
||||
end
|
||||
|
||||
context "when params and no body" do
|
||||
let(:params) { {:a => "1&b=2"} }
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "is a get request" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"GET"')
|
||||
end
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
|
||||
end
|
||||
end
|
||||
|
||||
context "when params and body" do
|
||||
let(:params) { {:a => "1&b=2"} }
|
||||
let(:form) { {:b => "2"} }
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "is a get request" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"GET"')
|
||||
end
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
|
||||
end
|
||||
end
|
||||
|
||||
context "with :escape" do
|
||||
let(:params) { {:a => "1&b=2"} }
|
||||
|
||||
context 'missing' do
|
||||
it "escapes values" do
|
||||
expect(easy.url).to eq("#{url}?a=1%26b%3D2")
|
||||
end
|
||||
end
|
||||
|
||||
context 'nil' do
|
||||
let(:options) { {:escape => nil} }
|
||||
|
||||
it "escapes values" do
|
||||
expect(easy.url).to eq("#{url}?a=1%26b%3D2")
|
||||
end
|
||||
end
|
||||
|
||||
context 'true' do
|
||||
let(:options) { {:escape => true} }
|
||||
|
||||
it "escapes values" do
|
||||
expect(easy.url).to eq("#{url}?a=1%26b%3D2")
|
||||
end
|
||||
end
|
||||
|
||||
context 'false' do
|
||||
let(:options) { {:escape => false} }
|
||||
|
||||
it "sends raw values" do
|
||||
expect(easy.url).to eq("#{url}?a=1&b=2")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Http::Head do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
let(:params) { nil }
|
||||
let(:form) { nil }
|
||||
let(:head) { described_class.new(url, {:params => params, :body => form}) }
|
||||
|
||||
describe "#setup" do
|
||||
context "when nothing" do
|
||||
it "sets nobody" do
|
||||
expect(easy).to receive(:nobody=).with(true)
|
||||
head.setup(easy)
|
||||
end
|
||||
|
||||
it "sets url" do
|
||||
head.setup(easy)
|
||||
expect(easy.url).to eq(url)
|
||||
end
|
||||
end
|
||||
|
||||
context "when params" do
|
||||
let(:params) { {:a => "1&b=2"} }
|
||||
|
||||
it "sets nobody" do
|
||||
expect(easy).to receive(:nobody=).with(true)
|
||||
head.setup(easy)
|
||||
end
|
||||
|
||||
it "attaches escaped to url" do
|
||||
head.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a=1%26b%3D2")
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
head.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "has no body" do
|
||||
expect(easy.response_body).to be_empty
|
||||
end
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
|
||||
end
|
||||
|
||||
context "when url already contains params" do
|
||||
let(:url) { "http://localhost:3001/?query=here" }
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.effective_url).to eq("http://localhost:3001/?query=here&a=1%26b%3D2")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when body" do
|
||||
let(:form) { {:a => 1} }
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
head.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Http::Options do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
let(:params) { nil }
|
||||
let(:form) { nil }
|
||||
let(:options) { described_class.new(url, {:params => params, :body => form}) }
|
||||
|
||||
describe "#setup" do
|
||||
it "sets customrequest" do
|
||||
expect(easy).to receive(:customrequest=).with("OPTIONS")
|
||||
options.setup(easy)
|
||||
end
|
||||
|
||||
it "sets url" do
|
||||
options.setup(easy)
|
||||
expect(easy.url).to eq(url)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
let(:params) { {:a => "1&b=2"} }
|
||||
|
||||
before do
|
||||
options.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "is a options request" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"OPTIONS"')
|
||||
end
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
|
||||
end
|
||||
|
||||
context "when url already contains params" do
|
||||
let(:url) { "http://localhost:3001/?query=here" }
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.effective_url).to eq("http://localhost:3001/?query=here&a=1%26b%3D2")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Http::Patch do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
let(:params) { nil }
|
||||
let(:form) { nil }
|
||||
let(:patch) { described_class.new(url, {:params => params, :body => form}) }
|
||||
|
||||
describe "#setup" do
|
||||
it "sets customrequest" do
|
||||
expect(easy).to receive(:customrequest=).with("PATCH")
|
||||
patch.setup(easy)
|
||||
end
|
||||
|
||||
it "sets url" do
|
||||
patch.setup(easy)
|
||||
expect(easy.url).to eq(url)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
let(:params) { {:a => "1&b=2"} }
|
||||
|
||||
before do
|
||||
patch.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "is a patch request" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"PATCH"')
|
||||
end
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.effective_url).to eq("http://localhost:3001/?a=1%26b%3D2")
|
||||
end
|
||||
|
||||
context "when url already contains params" do
|
||||
let(:url) { "http://localhost:3001/?query=here" }
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.effective_url).to eq("http://localhost:3001/?query=here&a=1%26b%3D2")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,317 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Http::Post do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
let(:params) { nil }
|
||||
let(:form) { nil }
|
||||
let(:options) { Hash.new }
|
||||
let(:post) { described_class.new(url, options.merge({:params => params, :body => form})) }
|
||||
|
||||
describe "#setup" do
|
||||
context "when nothing" do
|
||||
it "sets url" do
|
||||
post.setup(easy)
|
||||
expect(easy.url).to eq(url)
|
||||
end
|
||||
|
||||
it "sets postfield_size" do
|
||||
expect(easy).to receive(:postfieldsize=).with(0)
|
||||
post.setup(easy)
|
||||
end
|
||||
|
||||
it "sets copy_postfields" do
|
||||
expect(easy).to receive(:copypostfields=).with("")
|
||||
post.setup(easy)
|
||||
end
|
||||
|
||||
it "makes a post request" do
|
||||
post.setup(easy)
|
||||
easy.perform
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
|
||||
end
|
||||
end
|
||||
|
||||
context "when params" do
|
||||
let(:params) { {:a => "1&"} }
|
||||
|
||||
it "attaches escaped to url" do
|
||||
post.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a=1%26")
|
||||
end
|
||||
|
||||
context "with arrays" do
|
||||
let(:params) { {:a => %w( foo bar )} }
|
||||
|
||||
context "by default" do
|
||||
it "encodes them with indexes" do
|
||||
post.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a%5B0%5D=foo&a%5B1%5D=bar")
|
||||
end
|
||||
end
|
||||
|
||||
context "when params_encoding is :rack" do
|
||||
let(:options) { {:params_encoding => :rack} }
|
||||
it "encodes them without indexes" do
|
||||
post.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a%5B%5D=foo&a%5B%5D=bar")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with :escape" do
|
||||
context 'missing' do
|
||||
it "escapes values" do
|
||||
post.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a=1%26")
|
||||
end
|
||||
end
|
||||
|
||||
context 'nil' do
|
||||
let(:options) { {:escape => nil} }
|
||||
|
||||
it "escapes values" do
|
||||
post.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a=1%26")
|
||||
end
|
||||
end
|
||||
|
||||
context 'true' do
|
||||
let(:options) { {:escape => true} }
|
||||
|
||||
it "escapes values" do
|
||||
post.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a=1%26")
|
||||
end
|
||||
end
|
||||
|
||||
context 'false' do
|
||||
let(:options) { {:escape => false} }
|
||||
|
||||
it "sends raw values" do
|
||||
post.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a=1&")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "sets postfieldsize" do
|
||||
expect(easy).to receive(:postfieldsize=).with(0)
|
||||
post.setup(easy)
|
||||
end
|
||||
|
||||
it "sets copypostfields" do
|
||||
expect(easy).to receive(:copypostfields=).with("")
|
||||
post.setup(easy)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
let(:postredir) { nil }
|
||||
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
post.setup(easy)
|
||||
easy.postredir = postredir
|
||||
easy.followlocation = true
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "is a post" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
|
||||
end
|
||||
|
||||
it "uses application/x-www-form-urlencoded content type" do
|
||||
expect(easy.response_body).to include('"CONTENT_TYPE":"application/x-www-form-urlencoded"')
|
||||
end
|
||||
|
||||
it "requests parameterized url" do
|
||||
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?a=1%26"')
|
||||
end
|
||||
|
||||
context "when redirection" do
|
||||
let(:url) { "localhost:3001/redirect" }
|
||||
|
||||
context "when no postredirs" do
|
||||
it "is a get" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"GET"')
|
||||
end
|
||||
end
|
||||
|
||||
unless ENV['TRAVIS']
|
||||
context "when postredirs" do
|
||||
let(:postredir) { :post_all }
|
||||
|
||||
it "is a post" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when body" do
|
||||
context "when multipart" do
|
||||
let(:form) { {:a => File.open(__FILE__, 'r')} }
|
||||
|
||||
it "sets httppost" do
|
||||
expect(easy).to receive(:httppost=)
|
||||
post.setup(easy)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
post.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "is a post" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
|
||||
end
|
||||
|
||||
it "uses multipart/form-data content type" do
|
||||
expect(easy.response_body).to include('"CONTENT_TYPE":"multipart/form-data')
|
||||
end
|
||||
|
||||
it "submits a body" do
|
||||
expect(easy.response_body).to match('"body":".+"')
|
||||
end
|
||||
|
||||
it "submits the data" do
|
||||
expect(easy.response_body).to include('"filename":"post_spec.rb"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when not multipart" do
|
||||
let(:form) { {:a => "1&b=2"} }
|
||||
let(:encoded) { "a=1%26b%3D2" }
|
||||
|
||||
it "sets escaped copypostfields" do
|
||||
expect(easy).to receive(:copypostfields=).with(encoded)
|
||||
post.setup(easy)
|
||||
end
|
||||
|
||||
it "sets postfieldsize" do
|
||||
expect(easy).to receive(:postfieldsize=).with(encoded.bytesize)
|
||||
post.setup(easy)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
post.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "is a post" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"POST"')
|
||||
end
|
||||
|
||||
it "uses multipart/form-data content type" do
|
||||
expect(easy.response_body).to include('"CONTENT_TYPE":"application/x-www-form-urlencoded')
|
||||
end
|
||||
|
||||
it "submits a body" do
|
||||
expect(easy.response_body).to match('"body":"a=1%26b%3D2"')
|
||||
end
|
||||
|
||||
it "submits the data" do
|
||||
expect(easy.response_body).to include('"rack.request.form_hash":{"a":"1&b=2"}')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when string" do
|
||||
let(:form) { "{a: 1}" }
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
post.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "sends string" do
|
||||
expect(easy.response_body).to include('"body":"{a: 1}"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when binary with null bytes" do
|
||||
let(:form) { [1, 0, 1].pack('c*') }
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
post.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "sends binary data" do
|
||||
expect(easy.response_body).to include('"body":"\\u0001\\u0000\\u0001"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when arrays" do
|
||||
let(:form) { {:a => %w( foo bar )} }
|
||||
|
||||
context "by default" do
|
||||
it "sets copypostfields with indexed, escaped representation" do
|
||||
expect(easy).to receive(:copypostfields=).with('a%5B0%5D=foo&a%5B1%5D=bar')
|
||||
post.setup(easy)
|
||||
end
|
||||
end
|
||||
|
||||
context "when params_encoding is :rack" do
|
||||
let(:options) { {:params_encoding => :rack} }
|
||||
|
||||
it "sets copypostfields with non-indexed, escaped representation" do
|
||||
expect(easy).to receive(:copypostfields=).with('a%5B%5D=foo&a%5B%5D=bar')
|
||||
post.setup(easy)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when params and body" do
|
||||
let(:form) { {:a => "1"} }
|
||||
let(:params) { {:b => "2"} }
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
post.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "url contains params" do
|
||||
expect(easy.response_body).to include('"REQUEST_URI":"http://localhost:3001/?b=2"')
|
||||
end
|
||||
|
||||
it "body contains form" do
|
||||
expect(easy.response_body).to include('"body":"a=1"')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,168 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Http::Put do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
let(:params) { nil }
|
||||
let(:form) { nil }
|
||||
let(:options) { Hash.new }
|
||||
let(:put) { described_class.new(url, options.merge({:params => params, :body => form})) }
|
||||
|
||||
describe "#setup" do
|
||||
context "when nothing" do
|
||||
it "sets url" do
|
||||
put.setup(easy)
|
||||
expect(easy.url).to eq(url)
|
||||
end
|
||||
|
||||
it "sets upload" do
|
||||
expect(easy).to receive(:upload=).with(true)
|
||||
put.setup(easy)
|
||||
end
|
||||
|
||||
it "sets infilesize" do
|
||||
expect(easy).to receive(:infilesize=).with(0)
|
||||
put.setup(easy)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
it "makes a put request" do
|
||||
put.setup(easy)
|
||||
easy.perform
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"PUT"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when params" do
|
||||
let(:params) { {:a => "1&"} }
|
||||
|
||||
it "attaches escaped to url" do
|
||||
put.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a=1%26")
|
||||
end
|
||||
|
||||
context "with arrays" do
|
||||
let(:params) { {:a => %w( foo bar )} }
|
||||
|
||||
context "by default" do
|
||||
it "encodes them with indexes" do
|
||||
put.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a%5B0%5D=foo&a%5B1%5D=bar")
|
||||
end
|
||||
end
|
||||
|
||||
context "when params_encoding is :rack" do
|
||||
let(:options) { {:params_encoding => :rack} }
|
||||
it "encodes them without indexes" do
|
||||
put.setup(easy)
|
||||
expect(easy.url).to eq("#{url}?a%5B%5D=foo&a%5B%5D=bar")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "sets upload" do
|
||||
expect(easy).to receive(:upload=).with(true)
|
||||
put.setup(easy)
|
||||
end
|
||||
|
||||
it "sets infilesize" do
|
||||
expect(easy).to receive(:infilesize=).with(0)
|
||||
put.setup(easy)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
before do
|
||||
put.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "makes a put request" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"PUT"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when body" do
|
||||
let(:form) { {:a => "1&b=2"} }
|
||||
|
||||
it "sets infilesize" do
|
||||
expect(easy).to receive(:infilesize=).with(11)
|
||||
put.setup(easy)
|
||||
end
|
||||
|
||||
it "sets readfunction" do
|
||||
expect(easy).to receive(:readfunction)
|
||||
put.setup(easy)
|
||||
end
|
||||
|
||||
it "sets upload" do
|
||||
expect(easy).to receive(:upload=).with(true)
|
||||
put.setup(easy)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
context "sending string body" do
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
put.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "makes a put request" do
|
||||
expect(easy.response_body).to include('"REQUEST_METHOD":"PUT"')
|
||||
end
|
||||
|
||||
it "submits a body" do
|
||||
expect(easy.response_body).to include('"body":"a=1%26b%3D2"')
|
||||
end
|
||||
end
|
||||
|
||||
context "when injecting a file as body" do
|
||||
let(:file) { File.open(__FILE__) }
|
||||
let(:easy) do
|
||||
e = Ethon::Easy.new(:url => url, :upload => true)
|
||||
e.set_read_callback(file)
|
||||
e.infilesize = File.size(file.path)
|
||||
e
|
||||
end
|
||||
|
||||
before do
|
||||
easy.headers = { 'Expect' => '' }
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "submits file" do
|
||||
expect(easy.response_body).to include("injecting")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when arrays" do
|
||||
let(:form) { {:a => %w( foo bar )} }
|
||||
|
||||
before do
|
||||
put.setup(easy)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
context "by default" do
|
||||
it "submits an indexed, escaped representation" do
|
||||
expect(easy.response_body).to include('"body":"a%5B0%5D=foo&a%5B1%5D=bar"')
|
||||
end
|
||||
end
|
||||
|
||||
context "when params_encoding is :rack" do
|
||||
let(:options) { {:params_encoding => :rack} }
|
||||
|
||||
it "submits an non-indexed, escaped representation" do
|
||||
expect(easy.response_body).to include('"body":"a%5B%5D=foo&a%5B%5D=bar"')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when params and body"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Http do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
|
||||
describe "#http_request" do
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
let(:action_name) { :get }
|
||||
let(:options) { {} }
|
||||
|
||||
let(:get) { double(:setup) }
|
||||
let(:get_class) { Ethon::Easy::Http::Get }
|
||||
|
||||
it "instanciates action" do
|
||||
expect(get).to receive(:setup)
|
||||
expect(get_class).to receive(:new).and_return(get)
|
||||
easy.http_request(url, action_name, options)
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
[ :get, :post, :put, :delete, :head, :patch, :options ].map do |action|
|
||||
it "returns ok" do
|
||||
easy.http_request(url, action, options)
|
||||
easy.perform
|
||||
expect(easy.return_code).to be(:ok)
|
||||
end
|
||||
|
||||
unless action == :head
|
||||
it "makes a #{action.to_s.upcase} request" do
|
||||
easy.http_request(url, action, options)
|
||||
easy.perform
|
||||
expect(easy.response_body).to include("\"REQUEST_METHOD\":\"#{action.to_s.upcase}\"")
|
||||
end
|
||||
|
||||
it "streams the response body from the #{action.to_s.upcase} request" do
|
||||
bytes_read = 0
|
||||
easy.on_body { |chunk, response| bytes_read += chunk.bytesize }
|
||||
easy.http_request(url, action, options)
|
||||
easy.perform
|
||||
content_length = ((easy.response_headers =~ /Content-Length: (\d+)/) && $1.to_i)
|
||||
expect(bytes_read).to eq(content_length)
|
||||
expect(easy.response_body).to eq("")
|
||||
end
|
||||
|
||||
it "notifies when headers are ready" do
|
||||
headers = []
|
||||
easy.on_headers { |r| headers << r.response_headers }
|
||||
easy.http_request(url, action, options)
|
||||
easy.perform
|
||||
expect(headers).to eq([easy.response_headers])
|
||||
expect(headers.first).to match(/Content-Length: (\d+)/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "makes requests with custom HTTP verbs" do
|
||||
easy.http_request(url, :purge, options)
|
||||
easy.perform
|
||||
expect(easy.response_body).to include(%{"REQUEST_METHOD":"PURGE"})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Informations do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
|
||||
before do
|
||||
easy.url = "http://localhost:3001"
|
||||
easy.perform
|
||||
end
|
||||
|
||||
describe "#httpauth_avail" do
|
||||
it "returns" do
|
||||
expect(easy.httpauth_avail).to be
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_time" do
|
||||
it "returns float" do
|
||||
expect(easy.total_time).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#starttransfer_time" do
|
||||
it "returns float" do
|
||||
expect(easy.starttransfer_time).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#appconnect_time" do
|
||||
it "returns float" do
|
||||
expect(easy.appconnect_time).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#pretransfer_time" do
|
||||
it "returns float" do
|
||||
expect(easy.pretransfer_time).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#connect_time" do
|
||||
it "returns float" do
|
||||
expect(easy.connect_time).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#namelookup_time" do
|
||||
it "returns float" do
|
||||
expect(easy.namelookup_time).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#redirect_time" do
|
||||
it "returns float" do
|
||||
expect(easy.redirect_time).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#effective_url" do
|
||||
it "returns url" do
|
||||
expect(easy.effective_url).to match(/^http:\/\/localhost:3001\/?/)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#primary_ip" do
|
||||
it "returns localhost" do
|
||||
expect(easy.primary_ip).to match(/::1|127\.0\.0\.1/)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#response_code" do
|
||||
it "returns 200" do
|
||||
expect(easy.response_code).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#redirect_count" do
|
||||
it "returns 0" do
|
||||
expect(easy.redirect_count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#redirect_url" do
|
||||
it "returns nil as there is no redirect" do
|
||||
expect(easy.redirect_url).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#request_size" do
|
||||
it "returns 53" do
|
||||
expect(easy.request_size).to eq(53)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#supports_zlib?" do
|
||||
it "returns true" do
|
||||
expect(Kernel).to receive(:warn)
|
||||
expect(easy.supports_zlib?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#size_upload" do
|
||||
it "returns float" do
|
||||
expect(easy.size_upload).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#size_download" do
|
||||
it "returns float" do
|
||||
expect(easy.size_download).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#speed_upload" do
|
||||
it "returns float" do
|
||||
expect(easy.speed_upload).to be_a(Float)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#speed_download" do
|
||||
it "returns float" do
|
||||
expect(easy.speed_download).to be_a(Float)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Mirror do
|
||||
let(:options) { nil }
|
||||
let(:mirror) { described_class.new(options) }
|
||||
|
||||
describe "::INFORMATIONS_TO_LOG" do
|
||||
[
|
||||
:return_code, :response_code, :response_body, :response_headers,
|
||||
:total_time, :starttransfer_time, :appconnect_time,
|
||||
:pretransfer_time, :connect_time, :namelookup_time, :redirect_time,
|
||||
:size_upload, :size_download, :speed_upload, :speed_upload,
|
||||
:effective_url, :primary_ip, :redirect_count, :redirect_url, :debug_info
|
||||
].each do |name|
|
||||
it "contains #{name}" do
|
||||
expect(described_class::INFORMATIONS_TO_MIRROR).to include(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_hash" do
|
||||
let(:options) { {:return_code => 1} }
|
||||
|
||||
it "returns mirror as hash" do
|
||||
expect(mirror.to_hash).to eq(options)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#log_informations" do
|
||||
let(:options) { {:return_code => 1} }
|
||||
|
||||
it "returns hash" do
|
||||
expect(mirror.log_informations).to be_a(Hash)
|
||||
end
|
||||
|
||||
it "only calls methods that exist" do
|
||||
described_class::INFORMATIONS_TO_LOG.each do |method_name|
|
||||
expect(mirror.respond_to? method_name).to eql(true)
|
||||
end
|
||||
end
|
||||
|
||||
it "includes return code" do
|
||||
expect(mirror.log_informations).to include(options)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,271 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Operations do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
|
||||
describe "#handle" do
|
||||
it "returns a pointer" do
|
||||
expect(easy.handle).to be_a(FFI::Pointer)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe "#perform" do
|
||||
let(:url) { nil }
|
||||
let(:timeout) { nil }
|
||||
let(:connect_timeout) { nil }
|
||||
let(:follow_location) { nil }
|
||||
let(:max_redirs) { nil }
|
||||
let(:user_pwd) { nil }
|
||||
let(:http_auth) { nil }
|
||||
let(:headers) { nil }
|
||||
let(:protocols) { nil }
|
||||
let(:redir_protocols) { nil }
|
||||
let(:username) { nil }
|
||||
let(:password) { nil }
|
||||
|
||||
before do
|
||||
Ethon.logger.level = Logger::DEBUG
|
||||
easy.url = url
|
||||
easy.timeout = timeout
|
||||
easy.connecttimeout = connect_timeout
|
||||
easy.followlocation = follow_location
|
||||
easy.maxredirs = max_redirs
|
||||
easy.httpauth = http_auth
|
||||
easy.headers = headers
|
||||
easy.protocols = protocols
|
||||
easy.redir_protocols = redir_protocols
|
||||
|
||||
if user_pwd
|
||||
easy.userpwd = user_pwd
|
||||
else
|
||||
easy.username = username
|
||||
easy.password = password
|
||||
end
|
||||
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "calls Curl.easy_perform" do
|
||||
expect(Ethon::Curl).to receive(:easy_perform)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "calls Curl.easy_cleanup" do
|
||||
expect_any_instance_of(FFI::AutoPointer).to receive(:free)
|
||||
easy.cleanup
|
||||
end
|
||||
|
||||
it "logs" do
|
||||
expect(Ethon.logger).to receive(:debug)
|
||||
easy.perform
|
||||
end
|
||||
|
||||
it "doesn't log after completing because completing could reset" do
|
||||
easy.on_complete{ expect(Ethon.logger).to receive(:debug).never }
|
||||
easy.perform
|
||||
end
|
||||
|
||||
context "when url" do
|
||||
let(:url) { "http://localhost:3001/" }
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to eq(:ok)
|
||||
end
|
||||
|
||||
it "sets response body" do
|
||||
expect(easy.response_body).to be
|
||||
end
|
||||
|
||||
it "sets response headers" do
|
||||
expect(easy.response_headers).to be
|
||||
end
|
||||
|
||||
context "when request timed out" do
|
||||
let(:url) { "http://localhost:3001/?delay=1" }
|
||||
let(:timeout) { 1 }
|
||||
|
||||
it "returns operation_timedout" do
|
||||
expect(easy.return_code).to eq(:operation_timedout)
|
||||
end
|
||||
end
|
||||
|
||||
context "when connection timed out" do
|
||||
let(:url) { "http://localhost:3009" }
|
||||
let(:connect_timeout) { 1 }
|
||||
|
||||
it "returns couldnt_connect" do
|
||||
expect(easy.return_code).to eq(:couldnt_connect)
|
||||
end
|
||||
end
|
||||
|
||||
context "when no follow location" do
|
||||
let(:url) { "http://localhost:3001/redirect" }
|
||||
let(:follow_location) { false }
|
||||
|
||||
it "doesn't follow" do
|
||||
expect(easy.response_code).to eq(302)
|
||||
expect(easy.redirect_url).to eq("http://localhost:3001/")
|
||||
end
|
||||
end
|
||||
|
||||
context "when follow location" do
|
||||
let(:url) { "http://localhost:3001/redirect" }
|
||||
let(:follow_location) { true }
|
||||
|
||||
it "follows" do
|
||||
expect(easy.response_code).to eq(200)
|
||||
expect(easy.redirect_url).to eq(nil)
|
||||
end
|
||||
|
||||
context "when infinite redirect loop" do
|
||||
let(:url) { "http://localhost:3001/bad_redirect" }
|
||||
let(:max_redirs) { 5 }
|
||||
|
||||
context "when max redirect set" do
|
||||
it "follows only x times" do
|
||||
expect(easy.response_code).to eq(302)
|
||||
expect(easy.redirect_url).to eq("http://localhost:3001/bad_redirect")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when user agent" do
|
||||
let(:headers) { { 'User-Agent' => 'Ethon' } }
|
||||
|
||||
it "sets" do
|
||||
expect(easy.response_body).to include('"HTTP_USER_AGENT":"Ethon"')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when auth url" do
|
||||
before { easy.url = url }
|
||||
|
||||
context "when basic auth" do
|
||||
let(:url) { "http://localhost:3001/auth_basic/username/password" }
|
||||
|
||||
context "when no user_pwd" do
|
||||
it "returns 401" do
|
||||
expect(easy.response_code).to eq(401)
|
||||
end
|
||||
end
|
||||
|
||||
context "when invalid user_pwd" do
|
||||
let(:user_pwd) { "invalid:invalid" }
|
||||
|
||||
it "returns 401" do
|
||||
expect(easy.response_code).to eq(401)
|
||||
end
|
||||
end
|
||||
|
||||
context "when valid user_pwd" do
|
||||
let(:user_pwd) { "username:password" }
|
||||
|
||||
it "returns 200" do
|
||||
expect(easy.response_code).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user and password" do
|
||||
let(:username) { "username" }
|
||||
let(:password) { "password" }
|
||||
|
||||
it "returns 200" do
|
||||
expect(easy.response_code).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when ntlm" do
|
||||
let(:url) { "http://localhost:3001/auth_ntlm" }
|
||||
let(:http_auth) { :ntlm }
|
||||
|
||||
context "when no user_pwd" do
|
||||
it "returns 401" do
|
||||
expect(easy.response_code).to eq(401)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user_pwd" do
|
||||
let(:user_pwd) { "username:password" }
|
||||
|
||||
it "returns 200" do
|
||||
expect(easy.response_code).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when protocols" do
|
||||
context "when asking for a allowed url" do
|
||||
let(:url) { "http://localhost:3001" }
|
||||
let(:protocols) { :http }
|
||||
|
||||
it "returns ok" do
|
||||
expect(easy.return_code).to be(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context "when asking for a not allowed url" do
|
||||
let(:url) { "http://localhost:3001" }
|
||||
let(:protocols) { :https }
|
||||
|
||||
it "returns unsupported_protocol" do
|
||||
expect(easy.return_code).to be(:unsupported_protocol)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when multiple protocols" do
|
||||
context "when asking for a allowed url" do
|
||||
let(:protocols) { [:http, :https] }
|
||||
|
||||
context "when http" do
|
||||
let(:url) { "http://localhost:3001" }
|
||||
|
||||
it "returns ok for http" do
|
||||
expect(easy.return_code).to be(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context "when https" do
|
||||
let(:url) { "https://localhost:3001" }
|
||||
|
||||
it "returns ssl_connect_error for https" do
|
||||
expect(easy.return_code).to be(:ssl_connect_error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when asking for a not allowed url" do
|
||||
let(:url) { "ssh://localhost" }
|
||||
let(:protocols) { [:https, :http] }
|
||||
|
||||
it "returns unsupported_protocol" do
|
||||
expect(easy.return_code).to be(:unsupported_protocol)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when redir_protocols" do
|
||||
context "when redirecting to a not allowed url" do
|
||||
let(:url) { "http://localhost:3001/redirect" }
|
||||
let(:follow_location) { true }
|
||||
let(:redir_protocols) { :https }
|
||||
|
||||
it "returns unsupported_protocol" do
|
||||
expect(easy.return_code).to be(:unsupported_protocol)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when no url" do
|
||||
it "returns url_malformat" do
|
||||
expect(easy.perform).to eq(:url_malformat)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,193 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Options do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
|
||||
[
|
||||
:accept_encoding, :cainfo, :capath, :connecttimeout, :connecttimeout_ms, :cookie,
|
||||
:cookiejar, :cookiefile, :copypostfields, :customrequest, :dns_cache_timeout,
|
||||
:followlocation, :forbid_reuse, :http_version, :httpauth, :httpget, :httppost,
|
||||
:infilesize, :interface, :keypasswd, :maxredirs, :nobody, :nosignal,
|
||||
:postfieldsize, :postredir, :protocols, :proxy, :proxyauth, :proxyport, :proxytype,
|
||||
:proxyuserpwd, :readdata, :readfunction, :redir_protocols, :ssl_verifyhost,
|
||||
:ssl_verifypeer, :sslcert, :sslcerttype, :sslkey, :sslkeytype, :sslversion,
|
||||
:timeout, :timeout_ms, :unrestricted_auth, :upload, :url, :useragent,
|
||||
:userpwd, :verbose, :pipewait, :dns_shuffle_addresses, :path_as_is
|
||||
].each do |name|
|
||||
describe "#{name}=" do
|
||||
it "responds_to" do
|
||||
expect(easy).to respond_to("#{name}=")
|
||||
end
|
||||
|
||||
it "sets option" do
|
||||
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
|
||||
expect(Ethon::Curl).to receive(:set_option).with(name, anything, anything)
|
||||
value = case name
|
||||
when :http_version
|
||||
:httpv1_0
|
||||
when :httpauth
|
||||
:basic
|
||||
when :protocols, :redir_protocols
|
||||
:http
|
||||
when :postredir
|
||||
:post_301
|
||||
when :proxytype
|
||||
:http
|
||||
when :sslversion
|
||||
:default
|
||||
when :httppost
|
||||
FFI::Pointer::NULL
|
||||
else
|
||||
1
|
||||
end
|
||||
easy.method("#{name}=").call(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#escape?' do
|
||||
context 'by default' do
|
||||
it 'returns true' do
|
||||
expect(easy.escape?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #escape=nil' do
|
||||
it 'returns true' do
|
||||
easy.escape = nil
|
||||
expect(easy.escape?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #escape=true' do
|
||||
it 'returns true' do
|
||||
easy.escape = true
|
||||
expect(easy.escape?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #escape=false' do
|
||||
it 'returns true' do
|
||||
easy.escape = false
|
||||
expect(easy.escape?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#multipart?' do
|
||||
context 'by default' do
|
||||
it 'returns false' do
|
||||
expect(easy.multipart?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #multipart=nil' do
|
||||
it 'returns false' do
|
||||
easy.multipart = nil
|
||||
expect(easy.multipart?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #multipart=true' do
|
||||
it 'returns true' do
|
||||
easy.multipart = true
|
||||
expect(easy.multipart?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when #multipart=false' do
|
||||
it 'returns false' do
|
||||
easy.multipart = false
|
||||
expect(easy.multipart?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#httppost=" do
|
||||
it "raises unless given a FFI::Pointer" do
|
||||
expect{ easy.httppost = 1 }.to raise_error(Ethon::Errors::InvalidValue)
|
||||
end
|
||||
end
|
||||
|
||||
context "when requesting" do
|
||||
let(:url) { "localhost:3001" }
|
||||
let(:timeout) { nil }
|
||||
let(:timeout_ms) { nil }
|
||||
let(:connecttimeout) { nil }
|
||||
let(:connecttimeout_ms) { nil }
|
||||
let(:userpwd) { nil }
|
||||
|
||||
before do
|
||||
easy.url = url
|
||||
easy.timeout = timeout
|
||||
easy.timeout_ms = timeout_ms
|
||||
easy.connecttimeout = connecttimeout
|
||||
easy.connecttimeout_ms = connecttimeout_ms
|
||||
easy.userpwd = userpwd
|
||||
easy.perform
|
||||
end
|
||||
|
||||
context "when userpwd" do
|
||||
context "when contains /" do
|
||||
let(:url) { "localhost:3001/auth_basic/test/te%2Fst" }
|
||||
let(:userpwd) { "test:te/st" }
|
||||
|
||||
it "works" do
|
||||
expect(easy.response_code).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when timeout" do
|
||||
let(:timeout) { 1 }
|
||||
|
||||
context "when request takes longer" do
|
||||
let(:url) { "localhost:3001?delay=2" }
|
||||
|
||||
it "times out" do
|
||||
expect(easy.return_code).to eq(:operation_timedout)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when connecttimeout" do
|
||||
let(:connecttimeout) { 1 }
|
||||
|
||||
context "when cannot connect" do
|
||||
let(:url) { "localhost:3002" }
|
||||
|
||||
it "times out" do
|
||||
expect(easy.return_code).to eq(:couldnt_connect)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if Ethon::Easy.supports_timeout_ms?
|
||||
context "when timeout_ms" do
|
||||
let(:timeout_ms) { 100 }
|
||||
|
||||
context "when request takes longer" do
|
||||
let(:url) { "localhost:3001?delay=1" }
|
||||
|
||||
it "times out" do
|
||||
expect(easy.return_code).to eq(:operation_timedout)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when connecttimeout_ms" do
|
||||
let(:connecttimeout_ms) { 100 }
|
||||
|
||||
context "when cannot connect" do
|
||||
let(:url) { "localhost:3002" }
|
||||
|
||||
it "times out" do
|
||||
# this can either lead to a timeout or couldnt connect depending on which happens first
|
||||
expect([:couldnt_connect, :operation_timedout]).to include(easy.return_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,235 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Queryable do
|
||||
let(:hash) { {} }
|
||||
let!(:easy) { Ethon::Easy.new }
|
||||
let(:params) { Ethon::Easy::Params.new(easy, hash) }
|
||||
|
||||
describe "#to_s" do
|
||||
context "when query_pairs empty" do
|
||||
before { params.instance_variable_set(:@query_pairs, []) }
|
||||
|
||||
it "returns empty string" do
|
||||
expect(params.to_s).to eq("")
|
||||
end
|
||||
end
|
||||
|
||||
context "when query_pairs not empty" do
|
||||
context "when escape" do
|
||||
before do
|
||||
params.escape = true
|
||||
end
|
||||
|
||||
{
|
||||
'!' => '%21', '*' => '%2A', "'" => '%27', '(' => '%28',
|
||||
')' => '%29', ';' => '%3B', ':' => '%3A', '@' => '%40',
|
||||
'&' => '%26', '=' => '%3D', '+' => '%2B', '$' => '%24',
|
||||
',' => '%2C', '/' => '%2F', '?' => '%3F', '#' => '%23',
|
||||
'[' => '%5B', ']' => '%5D',
|
||||
|
||||
'<' => '%3C', '>' => '%3E', '"' => '%22', '{' => '%7B',
|
||||
'}' => '%7D', '|' => '%7C', '\\' => '%5C', '`' => '%60',
|
||||
'^' => '%5E', '%' => '%25', ' ' => '%20', "\0" => '%00',
|
||||
|
||||
'まつもと' => '%E3%81%BE%E3%81%A4%E3%82%82%E3%81%A8',
|
||||
}.each do |value, percent|
|
||||
it "turns #{value.inspect} into #{percent}" do
|
||||
params.instance_variable_set(:@query_pairs, [[:a, value]])
|
||||
expect(params.to_s).to eq("a=#{percent}")
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
'.' => '%2E', '-' => '%2D', '_' => '%5F', '~' => '%7E',
|
||||
}.each do |value, percent|
|
||||
it "leaves #{value.inspect} instead of turning into #{percent}" do
|
||||
params.instance_variable_set(:@query_pairs, [[:a, value]])
|
||||
expect(params.to_s).to eq("a=#{value}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when no escape" do
|
||||
before { params.instance_variable_set(:@query_pairs, [[:a, 1], [:b, 2]]) }
|
||||
|
||||
it "returns concatenated query string" do
|
||||
expect(params.to_s).to eq("a=1&b=2")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when query_pairs contains a string" do
|
||||
before { params.instance_variable_set(:@query_pairs, ["{a: 1}"]) }
|
||||
|
||||
it "returns correct string" do
|
||||
expect(params.to_s).to eq("{a: 1}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#build_query_pairs" do
|
||||
let(:pairs) { params.method(:build_query_pairs).call(hash) }
|
||||
|
||||
context "when params is empty" do
|
||||
it "returns empty array" do
|
||||
expect(pairs).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "when params is string" do
|
||||
let(:hash) { "{a: 1}" }
|
||||
|
||||
it "wraps it in an array" do
|
||||
expect(pairs).to eq([hash])
|
||||
end
|
||||
end
|
||||
|
||||
context "when params is simple hash" do
|
||||
let(:hash) { {:a => 1, :b => 2} }
|
||||
|
||||
it "transforms" do
|
||||
expect(pairs).to include([:a, 1])
|
||||
expect(pairs).to include([:b, 2])
|
||||
end
|
||||
end
|
||||
|
||||
context "when params is a nested hash" do
|
||||
let(:hash) { {:a => 1, :b => {:c => 2}} }
|
||||
|
||||
it "transforms" do
|
||||
expect(pairs).to include([:a, 1])
|
||||
expect(pairs).to include(["b[c]", 2])
|
||||
end
|
||||
end
|
||||
|
||||
context "when params contains an array" do
|
||||
let(:hash) { {:a => 1, :b => [2, 3]} }
|
||||
|
||||
context "by default" do
|
||||
it "transforms" do
|
||||
expect(pairs).to include([:a, 1])
|
||||
expect(pairs).to include(["b[0]", 2])
|
||||
expect(pairs).to include(["b[1]", 3])
|
||||
end
|
||||
end
|
||||
|
||||
context "when params_encoding is :rack" do
|
||||
before { params.params_encoding = :rack }
|
||||
it "transforms without indexes" do
|
||||
expect(pairs).to include([:a, 1])
|
||||
expect(pairs).to include(["b[]", 2])
|
||||
expect(pairs).to include(["b[]", 3])
|
||||
end
|
||||
end
|
||||
|
||||
context "when params_encoding is :none" do
|
||||
before { params.params_encoding = :none }
|
||||
it "does no transformation" do
|
||||
expect(pairs).to include([:a, 1])
|
||||
expect(pairs).to include([:b, [2, 3]])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when params contains something nested in an array" do
|
||||
context "when string" do
|
||||
let(:hash) { {:a => {:b => ["hello", "world"]}} }
|
||||
|
||||
it "transforms" do
|
||||
expect(pairs).to eq([["a[b][0]", "hello"], ["a[b][1]", "world"]])
|
||||
end
|
||||
end
|
||||
|
||||
context "when hash" do
|
||||
let(:hash) { {:a => {:b => [{:c =>1}, {:d => 2}]}} }
|
||||
|
||||
it "transforms" do
|
||||
expect(pairs).to eq([["a[b][0][c]", 1], ["a[b][1][d]", 2]])
|
||||
end
|
||||
end
|
||||
|
||||
context "when file" do
|
||||
let(:file) { File.open("spec/spec_helper.rb") }
|
||||
let(:file_info) { params.method(:file_info).call(file) }
|
||||
let(:hash) { {:a => {:b => [file]}} }
|
||||
let(:mime_type) { file_info[1] }
|
||||
|
||||
it "transforms" do
|
||||
expect(pairs).to eq([["a[b][0]", file_info]])
|
||||
end
|
||||
|
||||
context "when MIME" do
|
||||
if defined?(MIME)
|
||||
context "when mime type" do
|
||||
it "sets mime type to text" do
|
||||
expect(mime_type).to eq("application/x-ruby")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when no mime type" do
|
||||
let(:file) { Tempfile.new("fubar") }
|
||||
|
||||
it "sets mime type to default application/octet-stream" do
|
||||
expect(mime_type).to eq("application/octet-stream")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when no MIME" do
|
||||
before { hide_const("MIME") }
|
||||
|
||||
it "sets mime type to default application/octet-stream" do
|
||||
expect(mime_type).to eq("application/octet-stream")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context "when params contains file" do
|
||||
let(:file) { Tempfile.new("fubar") }
|
||||
let(:file_info) { params.method(:file_info).call(file) }
|
||||
let(:hash) { {:a => 1, :b => file} }
|
||||
|
||||
it "transforms" do
|
||||
expect(pairs).to include([:a, 1])
|
||||
expect(pairs).to include([:b, file_info])
|
||||
end
|
||||
end
|
||||
|
||||
context "when params key contains a null byte" do
|
||||
let(:hash) { {:a => "1\0" } }
|
||||
|
||||
it "preserves" do
|
||||
expect(pairs).to eq([[:a, "1\0"]])
|
||||
end
|
||||
end
|
||||
|
||||
context "when params value contains a null byte" do
|
||||
let(:hash) { {"a\0" => 1 } }
|
||||
|
||||
it "preserves" do
|
||||
expect(pairs).to eq([["a\0", 1]])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#empty?" do
|
||||
context "when params empty" do
|
||||
it "returns true" do
|
||||
expect(params.empty?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "when params not empty" do
|
||||
let(:hash) { {:a => 1} }
|
||||
|
||||
it "returns false" do
|
||||
expect(params.empty?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,152 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::ResponseCallbacks do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
|
||||
[:on_complete, :on_headers, :on_body, :on_progress].each do |callback_type|
|
||||
describe "##{callback_type}" do
|
||||
it "responds" do
|
||||
expect(easy).to respond_to("#{callback_type}")
|
||||
end
|
||||
|
||||
context "when no block given" do
|
||||
it "returns @#{callback_type}" do
|
||||
expect(easy.send("#{callback_type}")).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context "when block given" do
|
||||
it "stores" do
|
||||
easy.send(callback_type) { p 1 }
|
||||
expect(easy.instance_variable_get("@#{callback_type}").size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when multiple blocks given" do
|
||||
it "stores" do
|
||||
easy.send(callback_type) { p 1 }
|
||||
easy.send(callback_type) { p 2 }
|
||||
expect(easy.instance_variable_get("@#{callback_type}").size).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#complete" do
|
||||
before do
|
||||
easy.on_complete {|r| String.new(r.url) }
|
||||
end
|
||||
|
||||
it "executes blocks and passes self" do
|
||||
expect(String).to receive(:new).with(easy.url)
|
||||
easy.complete
|
||||
end
|
||||
|
||||
context "when @on_complete nil" do
|
||||
it "doesn't raise" do
|
||||
easy.instance_variable_set(:@on_complete, nil)
|
||||
expect{ easy.complete }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#headers" do
|
||||
before do
|
||||
easy.on_headers {|r| String.new(r.url) }
|
||||
end
|
||||
|
||||
it "executes blocks and passes self" do
|
||||
expect(String).to receive(:new).with(easy.url)
|
||||
easy.headers
|
||||
end
|
||||
|
||||
context "when @on_headers nil" do
|
||||
it "doesn't raise" do
|
||||
easy.instance_variable_set(:@on_headers, nil)
|
||||
expect{ easy.headers }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#progress" do
|
||||
context "when requesting for realz" do
|
||||
it "executes callback" do
|
||||
post = Ethon::Easy::Http::Post.new("http://localhost:3001", {:body => "bar=fu"})
|
||||
post.setup(easy)
|
||||
@called = false
|
||||
@has_dltotal = false
|
||||
@has_ultotal = false
|
||||
easy.on_progress { @called = true }
|
||||
easy.on_progress { |dltotal, _, _, _| @has_dltotal ||= true }
|
||||
easy.on_progress { |_, _, ultotal, _| @has_ultotal ||= true }
|
||||
easy.perform
|
||||
expect(@called).to be true
|
||||
expect(@has_dltotal).to be true
|
||||
expect(@has_ultotal).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context "when pretending" do
|
||||
before do
|
||||
@dltotal = nil
|
||||
@dlnow = nil
|
||||
@ultotal = nil
|
||||
@ulnow = nil
|
||||
easy.on_progress { |dltotal, dlnow, ultotal, ulnow| @dltotal = dltotal ; @dlnow = dlnow; @ultotal = ultotal; @ulnow = ulnow }
|
||||
end
|
||||
|
||||
it "executes blocks and passes dltotal" do
|
||||
easy.progress(1, 2, 3, 4)
|
||||
expect(@dltotal).to eq(1)
|
||||
end
|
||||
|
||||
it "executes blocks and passes dlnow" do
|
||||
easy.progress(1, 2, 3, 4)
|
||||
expect(@dlnow).to eq(2)
|
||||
end
|
||||
|
||||
it "executes blocks and passes ultotal" do
|
||||
easy.progress(1, 2, 3, 4)
|
||||
expect(@ultotal).to eq(3)
|
||||
end
|
||||
|
||||
it "executes blocks and passes ulnow" do
|
||||
easy.progress(1, 2, 3, 4)
|
||||
expect(@ulnow).to eq(4)
|
||||
end
|
||||
|
||||
context "when @on_progress nil" do
|
||||
it "doesn't raise" do
|
||||
easy.instance_variable_set(:@on_progress, nil)
|
||||
expect{ easy.progress(1, 2, 3, 4) }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#body" do
|
||||
before do
|
||||
@chunk = nil
|
||||
@r = nil
|
||||
easy.on_body { |chunk, r| @chunk = chunk ; @r = r }
|
||||
end
|
||||
|
||||
it "executes blocks and passes self" do
|
||||
easy.body("the chunk")
|
||||
expect(@r).to be(easy)
|
||||
end
|
||||
|
||||
it "executes blocks and passes chunk" do
|
||||
easy.body("the chunk")
|
||||
expect(@chunk).to eq("the chunk")
|
||||
end
|
||||
|
||||
context "when @on_body nil" do
|
||||
it "doesn't raise" do
|
||||
easy.instance_variable_set(:@on_body, nil)
|
||||
expect{ easy.body("the chunk") }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy::Util do
|
||||
class Dummy
|
||||
include Ethon::Easy::Util
|
||||
end
|
||||
|
||||
let(:klass) { Dummy.new }
|
||||
|
||||
describe "escape_zero_byte" do
|
||||
context "when value has no zero byte" do
|
||||
let(:value) { "hello world" }
|
||||
|
||||
it "returns same value" do
|
||||
expect(klass.escape_zero_byte(value)).to be(value)
|
||||
end
|
||||
end
|
||||
|
||||
context "when value has zero byte" do
|
||||
let(:value) { "hello \0world" }
|
||||
|
||||
it "returns escaped" do
|
||||
expect(klass.escape_zero_byte(value)).to eq("hello \\0world")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,203 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Easy do
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
|
||||
describe ".new" do
|
||||
it "inits curl" do
|
||||
expect(Ethon::Curl).to receive(:init)
|
||||
easy
|
||||
end
|
||||
|
||||
context "when options are empty" do
|
||||
it "sets only callbacks" do
|
||||
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
|
||||
expect(Ethon::Easy).to receive(:set_option).never
|
||||
easy
|
||||
end
|
||||
end
|
||||
|
||||
context "when options not empty" do
|
||||
context "when followlocation is set" do
|
||||
let(:options) { { :followlocation => true } }
|
||||
let(:easy) { Ethon::Easy.new(options) }
|
||||
|
||||
it "sets followlocation" do
|
||||
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
|
||||
expect(Ethon::Curl).to receive(:set_option).with(:followlocation, true, anything)
|
||||
easy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#set_attributes" do
|
||||
context "when options are empty" do
|
||||
it "sets only callbacks" do
|
||||
expect_any_instance_of(Ethon::Easy).to receive(:set_callbacks)
|
||||
expect(Ethon::Easy).to receive(:set_option).never
|
||||
easy
|
||||
end
|
||||
end
|
||||
|
||||
context "when options aren't empty" do
|
||||
context "when valid key" do
|
||||
it "sets" do
|
||||
expect(easy).to receive(:verbose=).with(true)
|
||||
easy.set_attributes({:verbose => true})
|
||||
end
|
||||
end
|
||||
|
||||
context "when invalid key" do
|
||||
it "raises invalid option error" do
|
||||
expect{ easy.set_attributes({:fubar => 1}) }.to raise_error(Ethon::Errors::InvalidOption)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#reset" do
|
||||
before { easy.url = "www.example.com" }
|
||||
|
||||
it "resets url" do
|
||||
easy.reset
|
||||
expect(easy.url).to be_nil
|
||||
end
|
||||
|
||||
it "resets escape?" do
|
||||
easy.escape = false
|
||||
easy.reset
|
||||
expect(easy.escape?).to be_truthy
|
||||
end
|
||||
|
||||
it "resets hash" do
|
||||
easy.reset
|
||||
expect(easy.instance_variable_get(:@hash)).to be_nil
|
||||
end
|
||||
|
||||
it "resets easy handle" do
|
||||
expect(Ethon::Curl).to receive(:easy_reset)
|
||||
easy.reset
|
||||
end
|
||||
|
||||
it "resets on_complete" do
|
||||
easy.on_complete { p 1 }
|
||||
easy.reset
|
||||
expect(easy.on_complete).to be_empty
|
||||
end
|
||||
|
||||
it "resets on_headers" do
|
||||
easy.on_headers { p 1 }
|
||||
easy.reset
|
||||
expect(easy.on_headers).to be_empty
|
||||
end
|
||||
|
||||
it "resets on_body" do
|
||||
easy.on_body { p 1 }
|
||||
easy.reset
|
||||
expect(easy.on_body).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "#dup" do
|
||||
let!(:easy) do
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:3001/"
|
||||
easy.on_complete { 'on_complete' }
|
||||
easy.on_headers { 'on_headers' }
|
||||
easy.on_progress { 'on_progress' }
|
||||
easy.response_body = String.new('test_body')
|
||||
easy.response_headers = String.new('test_headers')
|
||||
easy
|
||||
end
|
||||
let!(:e) { easy.dup }
|
||||
|
||||
it "sets a new handle" do
|
||||
expect(e.handle).not_to eq(easy.handle)
|
||||
end
|
||||
|
||||
it "preserves url" do
|
||||
expect(e.url).to eq(easy.url)
|
||||
end
|
||||
|
||||
it "preserves on_complete callback" do
|
||||
expect(e.on_complete).to be(easy.on_complete)
|
||||
end
|
||||
|
||||
it "preserves on_headers callback" do
|
||||
expect(e.on_headers).to be(easy.on_headers)
|
||||
end
|
||||
|
||||
it 'preserves body_write_callback of original handle' do
|
||||
expect { easy.perform }.to change { easy.response_body }
|
||||
expect { easy.perform }.not_to change { e.response_body }
|
||||
end
|
||||
|
||||
it "preserves on_progress callback" do
|
||||
expect(e.on_progress).to be(easy.on_progress)
|
||||
end
|
||||
|
||||
it 'sets new body_write_callback of duplicated handle' do
|
||||
expect { e.perform }.to change { e.response_body }
|
||||
expect { e.perform }.not_to change { easy.response_body }
|
||||
end
|
||||
|
||||
it 'preserves headers_write_callback of original handle' do
|
||||
expect { easy.perform }.to change { easy.response_headers }
|
||||
expect { easy.perform }.not_to change { e.response_headers }
|
||||
end
|
||||
|
||||
it 'sets new headers_write_callback of duplicated handle' do
|
||||
expect { e.perform }.to change { e.response_headers }
|
||||
expect { e.perform }.not_to change { easy.response_headers }
|
||||
end
|
||||
|
||||
it "resets response_body" do
|
||||
expect(e.response_body).to be_empty
|
||||
end
|
||||
|
||||
it "resets response_headers" do
|
||||
expect(e.response_headers).to be_empty
|
||||
end
|
||||
|
||||
it "sets response_body for duplicated Easy" do
|
||||
e.perform
|
||||
expect(e.response_body).not_to be_empty
|
||||
end
|
||||
|
||||
it "sets response_headers for duplicated Easy" do
|
||||
e.perform
|
||||
expect(e.response_headers).not_to be_empty
|
||||
end
|
||||
|
||||
it "preserves response_body for original Easy" do
|
||||
e.perform
|
||||
expect(easy.response_body).to eq('test_body')
|
||||
end
|
||||
|
||||
it "preserves response_headers for original Easy" do
|
||||
e.perform
|
||||
expect(easy.response_headers).to eq('test_headers')
|
||||
end
|
||||
end
|
||||
|
||||
describe "#mirror" do
|
||||
it "returns a Mirror" do
|
||||
expect(easy.mirror).to be_a(Ethon::Easy::Mirror)
|
||||
end
|
||||
|
||||
it "builds from easy" do
|
||||
expect(Ethon::Easy::Mirror).to receive(:from_easy).with(easy)
|
||||
easy.mirror
|
||||
end
|
||||
end
|
||||
|
||||
describe "#log_inspect" do
|
||||
[ :url, :response_code, :return_code, :total_time ].each do |name|
|
||||
it "contains #{name}" do
|
||||
expect(easy.log_inspect).to match name.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Libc do
|
||||
describe "#getdtablesize", :if => !Ethon::Curl.windows? do
|
||||
it "returns an integer" do
|
||||
expect(Ethon::Libc.getdtablesize).to be_a(Integer)
|
||||
end
|
||||
|
||||
it "returns bigger zero", :if => !Ethon::Curl.windows? do
|
||||
expect(Ethon::Libc.getdtablesize).to_not be_zero
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
require "spec_helper"
|
||||
|
||||
describe Ethon::Loggable do
|
||||
|
||||
describe "#logger=" do
|
||||
|
||||
let(:logger) do
|
||||
Logger.new($stdout).tap do |log|
|
||||
log.level = Logger::INFO
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Ethon.logger = logger
|
||||
end
|
||||
|
||||
it "sets the logger" do
|
||||
expect(Ethon.logger).to eq(logger)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,298 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Multi::Operations do
|
||||
let(:multi) { Ethon::Multi.new }
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
let(:pointer) { FFI::MemoryPointer.new(:int) }
|
||||
|
||||
describe "#handle" do
|
||||
it "returns a pointer" do
|
||||
expect(multi.handle).to be_a(FFI::Pointer)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#running_count" do
|
||||
context "when hydra has no easy" do
|
||||
it "returns nil" do
|
||||
expect(multi.send(:running_count)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when hydra has easy" do
|
||||
before do
|
||||
easy.url = "http://localhost:3001/"
|
||||
multi.add(easy)
|
||||
multi.send(:trigger, pointer)
|
||||
end
|
||||
|
||||
it "returns 1" do
|
||||
expect(multi.send(:running_count)).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when hydra has more easys" do
|
||||
let(:another_easy) { Ethon::Easy.new }
|
||||
|
||||
before do
|
||||
easy.url = "http://localhost:3001/"
|
||||
another_easy.url = "http://localhost:3001/"
|
||||
multi.add(easy)
|
||||
multi.add(another_easy)
|
||||
multi.send(:trigger, pointer)
|
||||
end
|
||||
|
||||
it "returns 2" do
|
||||
expect(multi.send(:running_count)).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#get_timeout" do
|
||||
context "when code ok" do
|
||||
let(:timeout) { 1 }
|
||||
|
||||
before do
|
||||
expect(Ethon::Curl).to receive(:multi_timeout).and_return(:ok)
|
||||
multi.instance_variable_set(:@timeout, double(:read_long => timeout))
|
||||
end
|
||||
|
||||
it "doesn't raise" do
|
||||
expect{ multi.send(:get_timeout) }.to_not raise_error
|
||||
end
|
||||
|
||||
context "when timeout smaller zero" do
|
||||
let(:timeout) { -1 }
|
||||
|
||||
it "returns 1" do
|
||||
expect(multi.send(:get_timeout)).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when timeout bigger or equal zero" do
|
||||
let(:timeout) { 2 }
|
||||
|
||||
it "returns timeout" do
|
||||
expect(multi.send(:get_timeout)).to eq(timeout)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when code not ok" do
|
||||
before { expect(Ethon::Curl).to receive(:multi_timeout).and_return(:not_ok) }
|
||||
|
||||
it "raises MultiTimeout error" do
|
||||
expect{ multi.send(:get_timeout) }.to raise_error(Ethon::Errors::MultiTimeout)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#set_fds" do
|
||||
let(:timeout) { 1 }
|
||||
let(:max_fd) { 1 }
|
||||
|
||||
context "when code ok" do
|
||||
before { expect(Ethon::Curl).to receive(:multi_fdset).and_return(:ok) }
|
||||
|
||||
it "doesn't raise" do
|
||||
expect{ multi.method(:set_fds).call(timeout) }.to_not raise_error
|
||||
end
|
||||
|
||||
context "when max_fd -1" do
|
||||
let(:max_fd) { -1 }
|
||||
|
||||
before do
|
||||
multi.instance_variable_set(:@max_fd, double(:read_int => max_fd))
|
||||
expect(multi).to receive(:sleep).with(0.001)
|
||||
end
|
||||
|
||||
it "waits 100ms" do
|
||||
multi.method(:set_fds).call(timeout)
|
||||
end
|
||||
end
|
||||
|
||||
context "when max_fd not -1" do
|
||||
context "when code smaller zero" do
|
||||
before { expect(Ethon::Curl).to receive(:select).and_return(-1) }
|
||||
|
||||
it "raises Select error" do
|
||||
expect{ multi.method(:set_fds).call(timeout) }.to raise_error(Ethon::Errors::Select)
|
||||
end
|
||||
end
|
||||
|
||||
context "when code bigger or equal zero" do
|
||||
before { expect(Ethon::Curl).to receive(:select).and_return(0) }
|
||||
|
||||
it "doesn't raise" do
|
||||
expect{ multi.method(:set_fds).call(timeout) }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when code not ok" do
|
||||
before { expect(Ethon::Curl).to receive(:multi_fdset).and_return(:not_ok) }
|
||||
|
||||
it "raises MultiFdset error" do
|
||||
expect{ multi.method(:set_fds).call(timeout) }.to raise_error(Ethon::Errors::MultiFdset)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#perform" do
|
||||
context "when no easy handles" do
|
||||
it "returns nil" do
|
||||
expect(multi.perform).to be_nil
|
||||
end
|
||||
|
||||
it "logs" do
|
||||
expect(Ethon.logger).to receive(:debug).twice
|
||||
multi.perform
|
||||
end
|
||||
end
|
||||
|
||||
context "when easy handle" do
|
||||
before do
|
||||
easy.url = "http://localhost:3001/"
|
||||
multi.add(easy)
|
||||
end
|
||||
|
||||
it "requests" do
|
||||
multi.perform
|
||||
end
|
||||
|
||||
it "sets easy" do
|
||||
multi.perform
|
||||
expect(easy.response_code).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "when four easy handles" do
|
||||
let(:easies) do
|
||||
ary = []
|
||||
4.times do
|
||||
ary << another_easy = Ethon::Easy.new
|
||||
another_easy.url = "http://localhost:3001/"
|
||||
end
|
||||
ary
|
||||
end
|
||||
|
||||
before do
|
||||
easies.each { |e| multi.add(e) }
|
||||
multi.perform
|
||||
end
|
||||
|
||||
it "sets response codes" do
|
||||
expect(easies.all?{ |e| e.response_code == 200 }).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#ongoing?" do
|
||||
context "when easy_handles" do
|
||||
before { multi.easy_handles << 1 }
|
||||
|
||||
context "when running_count not greater 0" do
|
||||
before { multi.instance_variable_set(:@running_count, 0) }
|
||||
|
||||
it "returns true" do
|
||||
expect(multi.method(:ongoing?).call).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "when running_count greater 0" do
|
||||
before { multi.instance_variable_set(:@running_count, 1) }
|
||||
|
||||
it "returns true" do
|
||||
expect(multi.method(:ongoing?).call).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when no easy_handles" do
|
||||
context "when running_count not greater 0" do
|
||||
before { multi.instance_variable_set(:@running_count, 0) }
|
||||
|
||||
it "returns false" do
|
||||
expect(multi.method(:ongoing?).call).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context "when running_count greater 0" do
|
||||
before { multi.instance_variable_set(:@running_count, 1) }
|
||||
|
||||
it "returns true" do
|
||||
expect(multi.method(:ongoing?).call).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#init_vars" do
|
||||
it "sets @timeout" do
|
||||
expect(multi.instance_variable_get(:@timeout)).to be_a(FFI::MemoryPointer)
|
||||
end
|
||||
|
||||
it "sets @timeval" do
|
||||
expect(multi.instance_variable_get(:@timeval)).to be_a(Ethon::Curl::Timeval)
|
||||
end
|
||||
|
||||
it "sets @fd_read" do
|
||||
expect(multi.instance_variable_get(:@fd_read)).to be_a(Ethon::Curl::FDSet)
|
||||
end
|
||||
|
||||
it "sets @fd_write" do
|
||||
expect(multi.instance_variable_get(:@fd_write)).to be_a(Ethon::Curl::FDSet)
|
||||
end
|
||||
|
||||
it "sets @fd_excep" do
|
||||
expect(multi.instance_variable_get(:@fd_excep)).to be_a(Ethon::Curl::FDSet)
|
||||
end
|
||||
|
||||
it "sets @max_fd" do
|
||||
expect(multi.instance_variable_get(:@max_fd)).to be_a(FFI::MemoryPointer)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#reset_fds" do
|
||||
after { multi.method(:reset_fds).call }
|
||||
|
||||
it "resets @fd_read" do
|
||||
expect(multi.instance_variable_get(:@fd_read)).to receive(:clear)
|
||||
end
|
||||
|
||||
it "resets @fd_write" do
|
||||
expect(multi.instance_variable_get(:@fd_write)).to receive(:clear)
|
||||
end
|
||||
|
||||
it "resets @fd_excep" do
|
||||
expect(multi.instance_variable_get(:@fd_excep)).to receive(:clear)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#check" do
|
||||
it { skip("untested") }
|
||||
end
|
||||
|
||||
describe "#run" do
|
||||
it { skip("untested") }
|
||||
end
|
||||
|
||||
describe "#trigger" do
|
||||
it "calls multi perform" do
|
||||
expect(Ethon::Curl).to receive(:multi_perform)
|
||||
multi.send(:trigger, pointer)
|
||||
end
|
||||
|
||||
it "sets running count" do
|
||||
multi.instance_variable_set(:@running_count, nil)
|
||||
multi.send(:trigger, pointer)
|
||||
expect(multi.instance_variable_get(:@running_count)).to_not be_nil
|
||||
end
|
||||
|
||||
it "returns multi perform code" do
|
||||
expect(Ethon::Curl).to receive(:multi_perform).and_return(:ok)
|
||||
expect(multi.send(:trigger, pointer)).to eq(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,182 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Multi::Options do
|
||||
let(:multi) { Ethon::Multi.new }
|
||||
|
||||
[
|
||||
:maxconnects, :pipelining, :socketdata, :socketfunction,
|
||||
:timerdata, :timerfunction, :max_total_connections
|
||||
].each do |name|
|
||||
describe "#{name}=" do
|
||||
it "responds_to" do
|
||||
expect(multi).to respond_to("#{name}=")
|
||||
end
|
||||
|
||||
it "sets option" do
|
||||
expect(Ethon::Curl).to receive(:set_option).with(name, anything, anything, anything)
|
||||
multi.method("#{name}=").call(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "socket_action mode" do
|
||||
let(:multi) { Ethon::Multi.new(execution_mode: :socket_action) }
|
||||
|
||||
describe "#socketfunction callbacks" do
|
||||
it "allows multi_code return values" do
|
||||
calls = []
|
||||
multi.socketfunction = proc do |handle, sock, what, userp, socketp|
|
||||
calls << what
|
||||
:ok
|
||||
end
|
||||
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:3001/?delay=1"
|
||||
multi.add(easy)
|
||||
expect(calls).to eq([])
|
||||
5.times do
|
||||
multi.socket_action
|
||||
break unless calls.empty?
|
||||
sleep 0.1
|
||||
end
|
||||
expect(calls.last).to eq(:in).or(eq(:out))
|
||||
multi.delete(easy)
|
||||
expect(calls.last).to eq(:remove)
|
||||
end
|
||||
|
||||
it "allows integer return values (compatibility)" do
|
||||
called = false
|
||||
multi.socketfunction = proc do |handle, sock, what, userp, socketp|
|
||||
called = true
|
||||
0
|
||||
end
|
||||
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:3001/?delay=1"
|
||||
multi.add(easy)
|
||||
5.times do
|
||||
multi.socket_action
|
||||
break if called
|
||||
sleep 0.1
|
||||
end
|
||||
multi.delete(easy)
|
||||
|
||||
expect(called).to be_truthy
|
||||
end
|
||||
|
||||
it "errors on invalid return codes" do
|
||||
called = false
|
||||
multi.socketfunction = proc do |handle, sock, what, userp, socketp|
|
||||
called = true
|
||||
"hi"
|
||||
end
|
||||
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:3001/?delay=1"
|
||||
multi.add(easy)
|
||||
expect {
|
||||
5.times do
|
||||
multi.socket_action
|
||||
break if called
|
||||
sleep 0.1
|
||||
end
|
||||
}.to raise_error(ArgumentError)
|
||||
expect { multi.delete(easy) }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#timerfunction callbacks" do
|
||||
it "allows multi_code return values" do
|
||||
calls = []
|
||||
multi.timerfunction = proc do |handle, timeout_ms, userp|
|
||||
calls << timeout_ms
|
||||
:ok
|
||||
end
|
||||
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:3001/?delay=1"
|
||||
multi.add(easy)
|
||||
expect(calls.last).to be >= 0 # adds an immediate timeout
|
||||
|
||||
multi.delete(easy)
|
||||
expect(calls.last).to eq(-1) # cancels the timer
|
||||
end
|
||||
|
||||
it "allows integer return values (compatibility)" do
|
||||
called = false
|
||||
multi.timerfunction = proc do |handle, timeout_ms, userp|
|
||||
called = true
|
||||
0
|
||||
end
|
||||
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:3001/?delay=1"
|
||||
multi.add(easy)
|
||||
multi.socket_action
|
||||
multi.delete(easy)
|
||||
|
||||
expect(called).to be_truthy
|
||||
end
|
||||
|
||||
it "errors on invalid return codes" do
|
||||
called = false
|
||||
multi.timerfunction = proc do |handle, timeout_ms, userp|
|
||||
called = true
|
||||
"hi"
|
||||
end
|
||||
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:3001/?delay=1"
|
||||
expect { multi.add(easy) }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#value_for" do
|
||||
context "when option in bool" do
|
||||
context "when value true" do
|
||||
let(:value) { true }
|
||||
|
||||
it "returns 1" do
|
||||
expect(multi.method(:value_for).call(value, :bool)).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when value false" do
|
||||
let(:value) { false }
|
||||
|
||||
it "returns 0" do
|
||||
expect(multi.method(:value_for).call(value, :bool)).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context "when value in int" do
|
||||
let(:value) { "2" }
|
||||
|
||||
it "returns value casted to int" do
|
||||
expect(multi.method(:value_for).call(value, :int)).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context "when value in unspecific_options" do
|
||||
context "when value a string" do
|
||||
let(:value) { "www.example.\0com" }
|
||||
|
||||
it "returns zero byte escaped string" do
|
||||
expect(multi.method(:value_for).call(value, nil)).to eq("www.example.\\0com")
|
||||
end
|
||||
end
|
||||
|
||||
context "when value not a string" do
|
||||
let(:value) { 1 }
|
||||
|
||||
it "returns value" do
|
||||
expect(multi.method(:value_for).call(value, nil)).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Multi::Stack do
|
||||
let(:multi) { Ethon::Multi.new }
|
||||
let(:easy) { Ethon::Easy.new }
|
||||
|
||||
describe "#add" do
|
||||
context "when easy already added" do
|
||||
before { multi.add(easy) }
|
||||
|
||||
it "returns nil" do
|
||||
expect(multi.add(easy)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "when easy new" do
|
||||
it "adds easy to multi" do
|
||||
expect(Ethon::Curl).to receive(:multi_add_handle).and_return(:ok)
|
||||
multi.add(easy)
|
||||
end
|
||||
|
||||
it "adds easy to easy_handles" do
|
||||
multi.add(easy)
|
||||
expect(multi.easy_handles).to include(easy)
|
||||
end
|
||||
end
|
||||
|
||||
context "when multi_add_handle fails" do
|
||||
it "raises multi add error" do
|
||||
expect(Ethon::Curl).to receive(:multi_add_handle).and_return(:bad_easy_handle)
|
||||
expect{ multi.add(easy) }.to raise_error(Ethon::Errors::MultiAdd)
|
||||
end
|
||||
end
|
||||
|
||||
context "when multi cleaned up before" do
|
||||
it "raises multi add error" do
|
||||
Ethon::Curl.multi_cleanup(multi.handle)
|
||||
expect{ multi.add(easy) }.to raise_error(Ethon::Errors::MultiAdd)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#delete" do
|
||||
context "when easy in easy_handles" do
|
||||
before { multi.add(easy) }
|
||||
|
||||
it "deletes easy from multi" do
|
||||
expect(Ethon::Curl).to receive(:multi_remove_handle).and_return(:ok)
|
||||
multi.delete(easy)
|
||||
end
|
||||
|
||||
it "deletes easy from easy_handles" do
|
||||
multi.delete(easy)
|
||||
expect(multi.easy_handles).to_not include(easy)
|
||||
end
|
||||
end
|
||||
|
||||
context "when easy is not in easy_handles" do
|
||||
it "does nothing" do
|
||||
expect(Ethon::Curl).to receive(:multi_add_handle).and_return(:ok)
|
||||
multi.add(easy)
|
||||
end
|
||||
|
||||
it "adds easy to easy_handles" do
|
||||
multi.add(easy)
|
||||
expect(multi.easy_handles).to include(easy)
|
||||
end
|
||||
end
|
||||
|
||||
context "when multi_remove_handle fails" do
|
||||
before { multi.add(easy) }
|
||||
|
||||
it "raises multi remove error" do
|
||||
expect(Ethon::Curl).to receive(:multi_remove_handle).and_return(:bad_easy_handle)
|
||||
expect{ multi.delete(easy) }.to raise_error(Ethon::Errors::MultiRemove)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,152 @@
|
||||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ethon::Multi do
|
||||
describe ".new" do
|
||||
it "inits curl" do
|
||||
expect(Ethon::Curl).to receive(:init)
|
||||
Ethon::Multi.new
|
||||
end
|
||||
|
||||
context "with default options" do
|
||||
it "allows running #perform with the default execution_mode" do
|
||||
Ethon::Multi.new.perform
|
||||
end
|
||||
|
||||
it "refuses to run #socket_action" do
|
||||
expect { Ethon::Multi.new.socket_action }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
context "when options not empty" do
|
||||
context "when pipelining is set" do
|
||||
let(:options) { { :pipelining => true } }
|
||||
|
||||
it "sets pipelining" do
|
||||
expect_any_instance_of(Ethon::Multi).to receive(:pipelining=).with(true)
|
||||
Ethon::Multi.new(options)
|
||||
end
|
||||
end
|
||||
|
||||
context "when execution_mode option is :socket_action" do
|
||||
let(:options) { { :execution_mode => :socket_action } }
|
||||
let(:multi) { Ethon::Multi.new(options) }
|
||||
|
||||
it "refuses to run #perform" do
|
||||
expect { multi.perform }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "allows running #socket_action" do
|
||||
multi.socket_action
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#socket_action" do
|
||||
let(:options) { { :execution_mode => :socket_action } }
|
||||
let(:select_state) { { :readers => [], :writers => [], :timeout => 0 } }
|
||||
let(:multi) {
|
||||
multi = Ethon::Multi.new(options)
|
||||
multi.timerfunction = proc do |handle, timeout_ms, userp|
|
||||
timeout_ms = nil if timeout_ms == -1
|
||||
select_state[:timeout] = timeout_ms
|
||||
:ok
|
||||
end
|
||||
multi.socketfunction = proc do |handle, sock, what, userp, socketp|
|
||||
case what
|
||||
when :remove
|
||||
select_state[:readers].delete(sock)
|
||||
select_state[:writers].delete(sock)
|
||||
when :in
|
||||
select_state[:readers].push(sock) unless select_state[:readers].include? sock
|
||||
select_state[:writers].delete(sock)
|
||||
when :out
|
||||
select_state[:readers].delete(sock)
|
||||
select_state[:writers].push(sock) unless select_state[:writers].include? sock
|
||||
when :inout
|
||||
select_state[:readers].push(sock) unless select_state[:readers].include? sock
|
||||
select_state[:writers].push(sock) unless select_state[:writers].include? sock
|
||||
else
|
||||
raise ArgumentError, "invalid value for 'what' in socketfunction callback"
|
||||
end
|
||||
:ok
|
||||
end
|
||||
multi
|
||||
}
|
||||
|
||||
def fds_to_ios(fds)
|
||||
fds.map do |fd|
|
||||
IO.for_fd(fd).tap { |io| io.autoclose = false }
|
||||
end
|
||||
end
|
||||
|
||||
def perform_socket_action_until_complete
|
||||
multi.socket_action # start things off
|
||||
|
||||
while multi.ongoing?
|
||||
readers, writers, _ = IO.select(
|
||||
fds_to_ios(select_state[:readers]),
|
||||
fds_to_ios(select_state[:writers]),
|
||||
[],
|
||||
select_state[:timeout]
|
||||
)
|
||||
|
||||
to_notify = Hash.new { |hash, key| hash[key] = [] }
|
||||
unless readers.nil?
|
||||
readers.each do |reader|
|
||||
to_notify[reader] << :in
|
||||
end
|
||||
end
|
||||
unless writers.nil?
|
||||
writers.each do |writer|
|
||||
to_notify[writer] << :out
|
||||
end
|
||||
end
|
||||
|
||||
to_notify.each do |io, readiness|
|
||||
multi.socket_action(io, readiness)
|
||||
end
|
||||
|
||||
# if we didn't have anything to notify, then we timed out
|
||||
multi.socket_action if to_notify.empty?
|
||||
end
|
||||
ensure
|
||||
multi.easy_handles.dup.each do |h|
|
||||
multi.delete(h)
|
||||
end
|
||||
end
|
||||
|
||||
it "supports an end-to-end request" do
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:3001/"
|
||||
multi.add(easy)
|
||||
|
||||
perform_socket_action_until_complete
|
||||
|
||||
expect(multi.ongoing?).to eq(false)
|
||||
end
|
||||
|
||||
it "supports multiple concurrent requests" do
|
||||
handles = []
|
||||
10.times do
|
||||
easy = Ethon::Easy.new
|
||||
easy.url = "http://localhost:3001/?delay=1"
|
||||
multi.add(easy)
|
||||
handles << easy
|
||||
end
|
||||
|
||||
start = Time.now
|
||||
perform_socket_action_until_complete
|
||||
duration = Time.now - start
|
||||
|
||||
# these should have happened concurrently
|
||||
expect(duration).to be < 2
|
||||
expect(multi.ongoing?).to eq(false)
|
||||
|
||||
handles.each do |handle|
|
||||
expect(handle.response_code).to eq(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
||||
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
||||
|
||||
require 'bundler'
|
||||
Bundler.setup
|
||||
require "ethon"
|
||||
require 'rspec'
|
||||
|
||||
if defined? require_relative
|
||||
require_relative 'support/localhost_server'
|
||||
require_relative 'support/server'
|
||||
else
|
||||
require 'support/localhost_server'
|
||||
require 'support/server'
|
||||
end
|
||||
|
||||
# Ethon.logger = Logger.new($stdout).tap do |log|
|
||||
# log.level = Logger::DEBUG
|
||||
# end
|
||||
|
||||
RSpec.configure do |config|
|
||||
# config.order = :rand
|
||||
|
||||
config.before(:suite) do
|
||||
LocalhostServer.new(TESTSERVER.new, 3001)
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user