lib/ethon/easy.rb



# 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