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
+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