Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+41
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
*.gem
.bundle
Gemfile.lock
.DS_Store
.yardoc
doc
coverage
.idea
+3
View File
@@ -0,0 +1,3 @@
--tty
--color
--format documentation
+375
View File
@@ -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
+43
View File
@@ -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
+10
View File
@@ -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
+20
View File
@@ -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.
+118
View File
@@ -0,0 +1,118 @@
[![Gem Version](https://badge.fury.io/rb/ethon.svg)](https://badge.fury.io/rb/ethon)
[![Build Status](https://github.com/typhoeus/ethon/workflows/Ruby/badge.svg)](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.
+40
View File
@@ -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
+26
View File
@@ -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
+36
View File
@@ -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
+90
View File
@@ -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
+315
View File
@@ -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
+17
View File
@@ -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
+21
View File
@@ -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
+126
View File
@@ -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
+104
View File
@@ -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
+28
View File
@@ -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