class DuoApi

A Ruby implementation of the Duo API

Constants

BACKOFF_FACTOR
INITIAL_BACKOFF_WAIT_SECS
MAX_BACKOFF_WAIT_SECS

Constants for handling rate limit backoff

RATE_LIMITED_RESP_CODE

Attributes

ca_file[RW]

Public Class Methods

new(ikey, skey, host, proxy = nil, ca_file: nil) click to toggle source
# File lib/duo_api.rb, line 19
def initialize(ikey, skey, host, proxy = nil, ca_file: nil)
  @ikey = ikey
  @skey = skey
  @host = host
  if proxy.nil?
    @proxy = []
  else
    proxy_uri = URI.parse proxy
    @proxy = [
      proxy_uri.host,
      proxy_uri.port,
      proxy_uri.user,
      proxy_uri.password
    ]
  end
  @ca_file = ca_file ||
             File.join(File.dirname(__FILE__), '..', 'ca_certs.pem')
end

Public Instance Methods

request(method, path, params = nil) click to toggle source
# File lib/duo_api.rb, line 38
def request(method, path, params = nil)
  uri = request_uri(path, params)
  current_date, signed = sign(method, uri.host, path, params)

  request = Net::HTTP.const_get(method.capitalize).new uri.to_s
  request.basic_auth(@ikey, signed)
  request['Date'] = current_date
  request['User-Agent'] = 'duo_api_ruby/1.2.0'

  Net::HTTP.start(uri.host, uri.port, *@proxy,
                  use_ssl: true, ca_file: @ca_file,
                  verify_mode: OpenSSL::SSL::VERIFY_PEER) do |http|
    wait_secs = INITIAL_BACKOFF_WAIT_SECS
    while true do
      resp = http.request(request)
      if resp.code != RATE_LIMITED_RESP_CODE or wait_secs > MAX_BACKOFF_WAIT_SECS
          return resp
      end
      random_offset = rand()
      sleep(wait_secs + random_offset)
      wait_secs *= BACKOFF_FACTOR
    end
  end
end

Private Instance Methods

canonicalize(method, host, path, params, options = {}) click to toggle source
# File lib/duo_api.rb, line 95
def canonicalize(method, host, path, params, options = {})
  options[:date] ||= time
  canon = [
    options[:date],
    method.upcase,
    host.downcase,
    path,
    encode_params(params)
  ]
  [options[:date], canon.join("\n")]
end
encode_key_val(k, v) click to toggle source
# File lib/duo_api.rb, line 65
def encode_key_val(k, v)
  # encode the key and the value for a url
  key = URI.encode(k.to_s, @@encode_regex)
  value = URI.encode(v.to_s, @@encode_regex)
  key + '=' + value
end
encode_params(params_hash = nil) click to toggle source
# File lib/duo_api.rb, line 72
def encode_params(params_hash = nil)
  return '' if params_hash.nil?
  params_hash.sort.map do |k, v|
    # when it is an array, we want to add that as another param
    # eg. next_offset = ['1547486297000', '5bea1c1e-612c-4f1d-b310-75fd31385b15']
    if v.is_a?(Array)
      encode_key_val(k, v[0]) + '&' + encode_key_val(k, v[1])
    else
      encode_key_val(k, v)
    end
  end.join('&')
end
request_uri(path, params = nil) click to toggle source
# File lib/duo_api.rb, line 89
def request_uri(path, params = nil)
  u = 'https://' + @host + path
  u += '?' + encode_params(params) unless params.nil?
  URI.parse(u)
end
sign(method, host, path, params, options = {}) click to toggle source
# File lib/duo_api.rb, line 107
def sign(method, host, path, params, options = {})
  date, canon = canonicalize(method, host, path, params, date: options[:date])
  [date, OpenSSL::HMAC.hexdigest('sha1', @skey, canon)]
end
time() click to toggle source
# File lib/duo_api.rb, line 85
def time
  Time.now.rfc2822
end