module Spectre::Curl

Public Class Methods

curl(name, secure: false, &block) click to toggle source
# File lib/spectre/curl.rb, line 157
def curl name, secure: false, &block
  req = {
    'use_ssl' => secure,
  }

  if @@http_cfg.key? name
    req.merge! @@http_cfg[name]
    raise "No `base_url' set for HTTP client '#{name}'. Check your HTTP config in your environment." if !req['base_url']
  else
    req['base_url'] = name
  end

  SpectreHttpRequest.new(req).instance_eval(&block) if block_given?

  invoke(req)
end
curl_request() click to toggle source
# File lib/spectre/curl.rb, line 174
def curl_request
  raise 'No request has been invoked yet' unless @@request
  @@request
end
curl_response() click to toggle source
# File lib/spectre/curl.rb, line 179
def curl_response
  raise 'There is no response. No request has been invoked yet.' unless @@response
  @@response
end
register(mod) click to toggle source
# File lib/spectre/curl.rb, line 184
def register mod
  raise 'Module must not be nil' unless mod
  @@modules << mod
end

Private Class Methods

header_to_s(headers) click to toggle source
# File lib/spectre/curl.rb, line 214
def header_to_s headers
  s = ''

  return s unless headers

  headers.each do |header|
    key = header[0].to_s
    value = header[1].to_s
    value = '*****' if is_secure?(key) and not @@debug
    s += "#{key.ljust(30, '.')}: #{value}\n"
  end

  s
end
invoke(req) click to toggle source
# File lib/spectre/curl.rb, line 229
def invoke req
  cmd = [@@curl_path]

  if req['cert'] or req['use_ssl']
    scheme = 'https'
  else
    scheme = 'http'
  end

  uri = req['base_url']

  if not uri.match /http(?:s)?:\/\//
    uri = scheme + '://' + uri
  end

  if req['path']
    uri += '/' if !uri.end_with? '/'
    uri += req['path']
  end

  if req['query']
    uri += '?'
    uri += req['query']
      .map { |x| x.join '='}
      .join('&')
  end

  cmd.append('"' + uri + '"')
  cmd.append('-X', req['method']) unless req['method'] == 'GET' or (req['body'] and req['method'] == 'POST')

  # Call all registered modules
  @@modules.each do |mod|
    mod.on_req(req, cmd) if mod.respond_to? :on_req
  end

  # Add headers to curl command
  req['headers'].each do |header|
    cmd.append('-H', '"' + header.join(':') + '"')
  end if req['headers']

  # Add request body
  if req['body'] != nil and not req['body'].empty?
    req_body = try_format_json(req['body']).gsub(/"/, '\\"')
    cmd.append('-d', '"' + req_body + '"')
  elsif ['POST', 'PUT', 'PATCH'].include? req['method'].upcase
    cmd.append('-d', '"\n"')
  end

  # Add certificate path if one if given
  if req['cert']
    raise "Certificate '#{req['cert']}' does not exist" unless File.exists? req['cert']
    cmd.append('--cacert', req['cert'])
  elsif req['use_ssl'] or uri.start_with? 'https'
    cmd.append('-k')
  end

  cmd.append('-i')
  cmd.append('-v')

  @@request = OpenStruct.new(req)

  sys_cmd = cmd.join(' ')

  @@logger.debug(sys_cmd)

  req_id = SecureRandom.uuid()[0..5]

  req_log = "[>] #{req_id} #{req['method']} #{uri}\n"
  req_log += header_to_s(req['headers'])
  req_log += try_format_json(req['body'], pretty: true)

  @@logger.info(req_log)

  start_time = Time.now

  stdin, stdout, stderr, wait_thr = Open3.popen3(sys_cmd)

  end_time = Time.now

  output = stdout.gets(nil)
  stdout.close

  debug_log = stderr.gets(nil)
  stderr.close

  # debug_log.lines.each { |x| @@logger.debug x unless x.empty? }

  raise "Unable to request #{uri}. Please check if this service is reachable." unless output

  @@logger.debug("[<] #{req_id} stdout:\n#{output}")

  header, body = output.split /\r?\n\r?\n/

  result = header.lines.first

  exit_code = wait_thr.value.exitstatus

  raise Exception.new "An error occured while executing curl:\n#{debug_log.lines.map { |x| not x.empty? }}" unless exit_code == 0

  # Parse protocol, version, status code and status message from response
  match = /^(?<protocol>[A-Za-z0-9]+)\/(?<version>\d+\.?\d*) (?<code>\d+) (?<message>.*)/.match result

  raise "Unexpected result from curl request:\n#{result}" unless match

  res_headers = header.lines[1..-1]
    .map { |x| /^(?<key>[A-Za-z0-9-]+):\s*(?<value>.*)$/.match x }
    .select { |x| x != nil }
    .map { |x| [x[:key].downcase, x[:value]] }

  res = {
    protocol: match[:protocol],
    version: match[:version],
    code: match[:code].to_i,
    message: match[:message],
    headers: Hash[res_headers],
    body: body
  }

  # Call all registered modules
  @@modules.each do |mod|
    mod.on_res(res, output) if mod.respond_to? :on_res
  end

  res_log = "[<] #{req_id} #{res[:code]} #{res[:message]} (#{end_time - start_time}s)\n"
  res_headers.each do |header|
    res_log += "#{header[0].to_s.ljust(30, '.')}: #{header[1].to_s}\n"
  end

  if res[:body] != nil and not res[:body].empty?
    res_log += try_format_json(res[:body], pretty: true)
  end

  @@logger.info res_log

  @@response = SpectreHttpResponse.new(res)

  raise "Response did not indicate success: #{@@response.code} #{@@response.message}" if req['ensure_success'] and not @@response.success?

  @@response
end
is_secure?(key) click to toggle source
# File lib/spectre/curl.rb, line 210
def is_secure? key
  @@secure_keys.any? { |x| key.to_s.downcase.include? x.downcase  }
end
try_format_json(str, pretty: false) click to toggle source
# File lib/spectre/curl.rb, line 191
def try_format_json str, pretty: false
  return str unless str or str.empty?

  begin
    json = JSON.parse(str)
    json.obfuscate!(@@secure_keys) if not @@debug

    if pretty
      str = JSON.pretty_generate(json)
    else
      str = JSON.dump(json)
    end
  rescue
    # do nothing
  end

  str
end