lib/onering/api.rb in onering-client-0.0.41 vs lib/onering/api.rb in onering-client-0.0.42

- old
+ new

@@ -1,6 +1,6 @@ -require 'net/https' +require 'rest_client' require 'uri' require 'multi_json' require 'yaml' require 'addressable/uri' require 'deep_merge' @@ -10,10 +10,11 @@ module Errors class NotConnected < Exception; end class ClientError < Exception; end class ServerError < Exception; end class ConnectionTimeout < Exception; end + class ClientPemNotFound < Exception; end end class Base include Onering::Util @@ -27,10 +28,14 @@ DEFAULT_CLIENT_PEM=[ "~/.onering/client.pem", "/etc/onering/client.pem" ] + DEFAULT_VALIDATION_PEM=[ + "/etc/onering/validation.pem" + ] + class<<self def connect(options={}) # list all existing config files from least specific to most @_configfiles = ([options[:config]] + DEFAULT_OPTIONS_FILE).compact.select{|i| (File.exists?(File.expand_path(i)) rescue false) @@ -49,25 +54,104 @@ @_uri = Addressable::URI.parse("#{options[:host]}/#{DEFAULT_PATH}") else @_uri = Addressable::URI.parse("#{@_config['url'] || DEFAULT_BASE}/#{@_config['apiroot'] || DEFAULT_PATH}") end - if @_uri - @_pemfile = ([options[:pemfile], @_config['pemfile']]+DEFAULT_CLIENT_PEM).compact.select{|i| - (File.exists?(File.expand_path(i)) rescue false) - }.first + unless @_uri.nil? + begin + @_pemfile = ([options[:pemfile], @_config['pemfile']]+DEFAULT_CLIENT_PEM).compact.select{|i| + (File.exists?((File.expand_path(i) rescue i)) rescue nil) + }.compact.first - if @_pemfile - @_pem = File.read(File.expand_path(@_pemfile)) - @_http = Net::HTTP.new(@_uri.host, (@_uri.port || 443)) - @_http.open_timeout = 30 - @_http.read_timeout = 120 - @_http.use_ssl = true - @_http.cert = OpenSSL::X509::Certificate.new(@_pem) - @_http.key = OpenSSL::PKey::RSA.new(@_pem) - @_http.verify_mode = OpenSSL::SSL::VERIFY_NONE + raise Errors::ClientPemNotFound if @_pemfile.nil? + + @_pem = File.read((File.expand_path(@_pemfile) rescue @_pemfile)) + + @rest = RestClient::Resource.new("#{@_uri.scheme}://#{@_uri.host}:#{@_uri.port || 443}", { + :timeout => 120, + :open_timeout => 30, + :ssl_client_cert => OpenSSL::X509::Certificate.new(@_pem), + :ssl_client_key => OpenSSL::PKey::RSA.new(@_pem), + :verify_peer => OpenSSL::SSL::VERIFY_PEER + }) + + rescue Errors::ClientPemNotFound + # client PEM not present, attempt autoregistration + STDERR.puts("Onering client.pem not found, attempting automatic registration...") + + @_validation = ([options[:validationfile], @_config['validationfile']]+DEFAULT_VALIDATION_PEM).compact.select{|i| + (File.exists?((File.expand_path(i) rescue i)) rescue nil) + }.compact.first + + if @_validation.nil? + raise Errors::ClientError.new("Cannot automatically register client, cannot find validation.pem") + end + + @_validation = File.read(@_validation) + + @rest = RestClient::Resource.new("#{@_uri.scheme}://#{@_uri.host}:#{@_uri.port || 443}", { + :timeout => 120, + :open_timeout => 30, + :ssl_client_cert => OpenSSL::X509::Certificate.new(@_validation), + :ssl_client_key => OpenSSL::PKey::RSA.new(@_validation), + :verify_peer => OpenSSL::SSL::VERIFY_PEER + }) + + + clients = [{ + :path => "/etc/onering", + :name => (@_config['id'] || File.read("/etc/hardware.id")).strip.chomp, + :keyname => 'system', + :autodelete => true + },{ + :path => "~/.onering", + :name => ENV['USER'], + :keyname => 'cli', + :autodelete => false + }] + + # attempt to autoregister clients from least specific to most (machine account then user account) + clients.each do |client| + # determine if we can create this client + client[:path] = (File.expand_path(client[:path]) rescue client[:path]) + next unless File.writable?(File.dirname(client[:path])) + File.mkdir(client[:path]) unless File.directory?(client[:path]) + next unless File.writable?(client[:path]) + + begin + response = @rest["/api/users/#{client[:name]}/keys/#{client[:keyname]}"].get({ + :params => { + :cert => 'pem', + :autodelete => client[:autodelete] + } + }) + + rescue RestClient::Forbidden + STDERR.puts("Cannot re-download key '#{client[:keyname]}' for client #{client[:name]}. Please remove the client key from Onering and try again.") + next + + rescue RestClient::Exception => e + raise Errors::ClientError.new("HTTP #{e.http_code}: #{e.message}") + end + + + File.open("#{client[:path]}/client.pem", "w") do |file| + file.puts(response.to_str) + STDERR.puts("Successfully registered client key #{client[:name]}:#{client[:keyname]}, key is at #{file.path}") + break + end + end + + if clients.select{|i| p = "#{i[:path]}/client.pem"; File.exists?((File.expand_path(p) rescue p)) }.empty? + raise Errors::ClientError.new("Unable to register a Onering client.") + end + + retry end + + else + raise Errors::ClientError.new("Could not parse API URL.") end end def request(endpoint, options={}) options = @_config.merge(options) @@ -75,45 +159,40 @@ request = nil uri = Addressable::URI.parse("#{@_uri.to_s}/#{endpoint}") uri.query_values = options[:fields] if options[:fields] - raise Errors::NotConnected unless @_http + raise Errors::NotConnected unless @rest - case options[:method] - when :post - request = Net::HTTP::Post.new(uri.request_uri) - request['Content-Type'] = 'application/json' - request.body = MultiJson.dump(options[:data]) if options[:data] + begin + case options[:method] + when :post + data = (options[:data].nil? ? nil : MultiJson.dump(options[:data])) - when :delete - request = Net::HTTP::Delete.new(uri.request_uri) + response = @rest[uri.request_uri].post(data, { + :content_type => 'application/json' + }) - else - request = Net::HTTP::Get.new(uri.request_uri) + when :delete + response = @rest[uri.request_uri].delete() - end + when :head + response = @rest[uri.request_uri].head() + else + response = @rest[uri.request_uri].get() + end - response = @_http.request(request) + rescue RestClient::Unauthorized => e + raise Errors::ClientError.new("You are not authorized to perform this request") - if response.code.to_i >= 400 - rv = (MultiJson.load(response.body) rescue {}) unless response.body.empty? + rescue RestClient::Exception => e + raise Errors::ClientError.new("(HTTP #{e.http_code}) #{e.class.name}: #{e.message}") + end - if rv['errors'] - msg = "#{rv['errors']['type']}: #{rv['errors']['message']}" - end - - if response.code.to_i >= 500 - raise Errors::ServerError.new("HTTP #{response.code}: #{msg}") - else - raise Errors::ClientError.new("HTTP #{response.code}: #{msg}") - end - else - if response['Content-Type'] == 'application/json' - rv = (response.body.empty? ? nil : MultiJson.load(response.body)) - else - rv = response.body - end + begin + rv = (response.empty? ? nil : MultiJson.load(response)) + rescue Exception + rv = response end rv end