lib/cucumber/formatter/http_io.rb



require 'net/http'
require 'tempfile'
require 'shellwords'

module Cucumber
  module Formatter
    class HTTPIO
      class << self
        # Returns an IO that will write to a HTTP request's body
        # https_verify_mode can be set to OpenSSL::SSL::VERIFY_NONE
        # to ignore unsigned certificate - setting to nil will verify the certificate
        def open(url, https_verify_mode = nil, reporter = nil)
          @https_verify_mode = https_verify_mode
          uri, method, headers = CurlOptionParser.parse(url)
          IOHTTPBuffer.new(uri, method, headers, https_verify_mode, reporter)
        end
      end
    end

    class CurlOptionParser
      def self.parse(options)
        args = Shellwords.split(options)

        url = nil
        http_method = 'PUT'
        headers = {}

        until args.empty?
          arg = args.shift
          case arg
          when '-X', '--request'
            http_method = remove_arg_for(args, arg)
          when '-H'
            header_arg = remove_arg_for(args, arg)
            headers = headers.merge(parse_header(header_arg))
          else
            raise StandardError, "#{options} was not a valid curl command. Can't set url to #{arg} it is already set to #{url}" if url

            url = arg
          end
        end
        raise StandardError, "#{options} was not a valid curl command" unless url

        [
          url,
          http_method,
          headers
        ]
      end

      def self.remove_arg_for(args, arg)
        return args.shift unless args.empty?

        raise StandardError, "Missing argument for #{arg}"
      end

      def self.parse_header(header_arg)
        parts = header_arg.split(':', 2)
        raise StandardError, "#{header_arg} was not a valid header" unless parts.length == 2

        { parts[0].strip => parts[1].strip }
      end
    end

    class IOHTTPBuffer
      attr_reader :uri, :method, :headers

      def initialize(uri, method, headers = {}, https_verify_mode = nil, reporter = nil)
        @uri = URI(uri)
        @method = method
        @headers = headers
        @write_io = Tempfile.new('cucumber', encoding: 'UTF-8')
        @https_verify_mode = https_verify_mode
        @reporter = reporter || NoReporter.new
      end

      def close
        response = send_content(@uri, @method, @headers)
        @reporter.report(response.body)
        @write_io.close
        return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)

        raise StandardError, "request to #{uri} failed with status #{response.code}"
      end

      def write(data)
        @write_io.write(data)
      end

      def flush
        @write_io.flush
      end

      def closed?
        @write_io.closed?
      end

      private

      def send_content(uri, method, headers, attempt = 10)
        content = (method == 'GET' ? StringIO.new : @write_io)
        http = build_client(uri, @https_verify_mode)

        raise StandardError, "request to #{uri} failed (too many redirections)" if attempt <= 0

        req = build_request(
          uri,
          method,
          headers.merge(
            'Content-Length' => content.size.to_s
          )
        )

        content.rewind
        req.body_stream = content

        begin
          response = http.request(req)
        rescue SystemCallError
          # We may get the redirect response before pushing the file.
          response = http.request(build_request(uri, method, headers))
        end

        case response
        when Net::HTTPAccepted
          send_content(URI(response['Location']), 'PUT', {}, attempt - 1) if response['Location']
        when Net::HTTPRedirection
          send_content(URI(response['Location']), method, headers, attempt - 1)
        end
        response
      end

      def build_request(uri, method, headers)
        method_class_name = "#{method[0].upcase}#{method[1..].downcase}"
        req = Net::HTTP.const_get(method_class_name).new(uri)
        headers.each do |header, value|
          req[header] = value
        end
        req
      end

      def build_client(uri, https_verify_mode)
        http = Net::HTTP.new(uri.hostname, uri.port)
        if uri.scheme == 'https'
          http.use_ssl = true
          http.verify_mode = https_verify_mode if https_verify_mode
        end
        http
      end
    end
  end
end