lib/transip.rb in transip-0.1.0 vs lib/transip.rb in transip-0.2.0

- old
+ new

@@ -6,51 +6,208 @@ require 'digest/md5' # # Implements the www.transip.nl API (v2). For more info see: https://www.transip.nl/g/api/ # # Usage: -# transip = Transip.new('username', '12.34.12.3') # will use readonly mode -# transip = Transip.new('username', '12.34.12.3', :readwrite) # use this in production -# transip.generate_hash('your_api_password') # Use this to generate a authentication hash -# transip.hash = 'your_hash' # Or use this to directly set the hash (so you don't have to use your password in your code) +# transip = Transip.new(:username => 'api_username') # will try to determine IP (will not work behind NAT) and uses readonly mode +# transip = Transip.new(:username => 'api_username', :ip => '12.34.12.3', :mode => 'readwrite') # use this in production # transip.actions # => [:check_availability, :get_whois, :get_domain_names, :get_info, :get_auth_code, :get_is_locked, :register, :cancel, :transfer_with_owner_change, :transfer_without_owner_change, :set_nameservers, :set_lock, :unset_lock, :set_dns_entries, :set_owner, :set_contacts] # transip.request(:get_domain_names) # transip.request(:get_info, :domain_name => 'yelloyello.be') +# transip.request_with_ip4_fix(:check_availability, :domain_name => 'yelloyello.be') +# transip.request_with_ip4_fix(:get_info, :domain_name => 'one_of_your_domains.com') +# transip.request(:get_whois, :domain_name => 'google.com') +# transip.request(:set_dns_entries, :domain_name => 'bdgg.nl', :dns_entries => [Transip::DnsEntry.new('test', 5.minutes, 'A', '74.125.77.147')]) +# transip.request(:register, Transip::Domain.new('newdomain.com', nil, nil, [Transip::DnsEntry.new('test', 5.minutes, 'A', '74.125.77.147')])) # +# Some other methods: +# transip.generate_hash # Use this to generate a authentication hash +# transip.hash = 'your_hash' # Or use this to directly set the hash (so you don't have to use your password in your code) +# transip.client! # This returns a new Savon::Client. It is cached in transip.client so when you update your username, password or hash call this method! +# # Credits: # Savon Gem - See: http://savonrb.com/. Wouldn't be so simple without it! class Transip WSDL = 'https://api.transip.nl/wsdl/?service=DomainService' - attr_accessor :login, :ip, :mode, :hash + attr_accessor :username, :password, :ip, :mode, :hash attr_reader :response + # Following Error needs to be catched in your code! + class ApiError < RuntimeError + + IP4_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/ + + # Returns true if we have a authentication error and gets ip from error msg. + # "Wrong API credentials (bad hash); called from IP 213.86.41.114" + def ip4_authentication_error? + self.message.to_s =~ /called from IP\s(#{IP4_REGEXP})/ # "Wrong API credentials (bad hash); called from IP 213.86.41.114" + @error_msg_ip = $1 + !@error_msg_ip.nil? + end + + # Returns the ip coming from the error msg. + def error_msg_ip + @error_msg_ip || ip4_authentication_error? && @error_msg_ip + end + + end + + # Following subclasses are actually not needed (as you can also + # do the same by just creating hashes..). + + class TransipStruct < Struct + + # See Rails' underscore method. + def underscore(string) + string.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). + gsub(/([a-z\d])([A-Z])/,'\1_\2'). + tr("-", "_"). + downcase + end + + # Converts Transip::DnsEntry into :dns_entry + def class_name_to_sym + self.underscore(self.class.name.split('::').last).to_sym + end + + # Gyoku.xml (see: https://github.com/rubiii/gyoku) is used by Savon. + # It calls to_s on unknown Objects. We use it to convert + def to_s + Gyoku.xml(self.to_hash) + end + + # See what happens here: http://snippets.dzone.com/posts/show/302 + def members_to_hash + Hash[*members.collect {|m| [m, self.send(m)]}.flatten] + end + + def to_hash + { self.class_name_to_sym => self.members_to_hash } + end + + end + + # name - String (Eg. '@' or 'www') + # expire - Integer (1.day) + # type - String (Eg. A, AAAA, CNAME, MX, NS, TXT, SRV) + # content - String (Eg. '10 mail', '127.0.0.1' or 'www') + class DnsEntry < TransipStruct.new(:name, :expire, :type, :content) + end + + # hostname - string + # ipv4 - string + # ipv6 - string (optional) + class Nameserver < TransipStruct.new(:name, :ipv4, :ipv6) + end + + # type - string + # first_name - string + # middle_name - string + # last_name - string + # company_name - string + # company_kvk - string + # company_type - string ('BV', 'BVI/O', 'COOP', 'CV'..) (see WhoisContact.php) + # street - string + # number - string (streetnumber) + # postal_code - string + # city - string + # phone_number - string + # fax_number - string + # email - string + # country - string (one of the ISO country abbrevs, must be lowercase) ('nl', 'de', ) (see WhoisContact.php) + class WhoisContact < TransipStruct.new(:type, :first_name, :middle_name, :last_name, :company_name, :company_kvk, :company_type, :street, :number, :postal_code, :city, :phone_number, :fax_number, :email, :country) + end + + # company_name - string + # support_email - string + # company_url - string + # terms_of_usage_url - string + # banner_line1 - string + # banner_line2 - string + # banner_line3 - string + class DomainBranding < TransipStruct.new(:company_name, :support_email, :company_url, :terms_of_usage_url, :banner_line1, :banner_line2, :banner_line3) + end + + # name - String + # nameservers - Array of Transip::Nameserver + # contacts - Array of Transip::WhoisContact + # dns_entries - Array of Transip::DnsEntry + # branding - Transip::DomainBranding + class Domain < TransipStruct.new(:name, :nameservers, :contacts, :dns_entries, :branding) + end + + # Options: + # * username + # * ip + # * password + # * mode + # # Example: - # transip = Transip.new('username', '12.34.12.3') # will use readonly mode - # transip = Transip.new('username', '12.34.12.3', 'readwrite') # use this in production - def initialize(login, ip, mode = :readonly) - @login = login - @ip = ip - @mode = mode + # transip = Transip.new(:username => 'api_username') # will try to determine IP (will not work behind NAT) and uses readonly mode + # transip = Transip.new(:username => 'api_username', :ip => '12.34.12.3', :mode => 'readwrite') # use this in production + def initialize(options = {}) + @username = options[:username] + raise ArgumentError, "The :username options is required!" if @username.nil? + @ip = options[:ip] || self.class.local_ip + @mode = options[:mode] || :readonly + if options[:password] + @password = options[:password] + self.generate_hash + end + + # By default we don't want to debug! + self.turn_off_debugging! end + # By default we don't want to debug! + # Changing might impact other Savon usages. + def turn_off_debugging! + Savon.configure do |config| + config.log = false # disable logging + config.log_level = :info # changing the log level + end + end + + # Make Savon log. + # Changing might impact other Savon usages. + def turn_on_debugging! + Savon.configure do |config| + config.log = true + config.log_level = :debug + end + end + + # Make Savon log to Rails.logger and turn_off_debugging! + def use_with_rails! + Savon.configure do |config| + if Rails.env.production? + self.turn_off_debugging! + # else + # self.turn_on_debugging! + end + config.logger = Rails.logger # using the Rails logger + end + end + # Generates the needed authentication hash. # # NOTE: The password is NOT your general TransIP password # but one specially for the API. Configure it in the Control # Panel. - def generate_hash(password) - digest_string = "#{login}:#{password}@#{ip}" + def generate_hash + raise StandardError, "Need username and password to (re)generate the authentication hash." if self.username.nil? || self.password.nil? + digest_string = "#{self.username}:#{self.password}@#{self.ip}" digest = Digest::MD5.hexdigest(digest_string) - @hash = digest + self.hash = digest end # Used as authentication def cookie - raise StandardError, "Don't have an authentication hash yet. Please set a hash using generate_hash('your_api_password') or hash= method." if hash.blank? - "login=#{login}; hash=#{hash}; mode=#{mode}; " + raise StandardError, "Don't have an authentication hash yet. Please set a hash using generate_hash or hash= method." if hash.blank? + "login=#{self.username}; hash=#{self.hash}; mode=#{self.mode}; " end # Same as client method but initializes a brand new fresh client. # You have to use this one when you want to re-set the mode (readwrite, readonly), # or authentication details of your client. @@ -84,12 +241,54 @@ @response = client.request(action) elsif options.is_a?(Hash) @response = client.request(action) do soap.body = options end + elsif options.class < Transip::TransipStruct + # If we call request(:register, Transip::Domain.new('newdomain.com')) we turn the Transip::Domain into a Hash. + @response = client.request(action) do + soap.body = options.to_hash + end else raise ArgumentError, "Expected options to be nil or a Hash!" end @response.to_hash + rescue Savon::SOAP::Fault => e + raise ApiError.new(e), e.message.sub(/^\(\d+\)\s+/,'') # We raise our own error (FIXME: Correct?). + # TODO: Curl::Err::HostResolutionError, Couldn't resolve host name + end + + # This is voodoo. Use it only if you know voodoo kung-fu. + # + # The method fixes the ip that is set. It uses the error from + # Transip to set the ip and re-request an authentication hash. + # + # It only works if you set password (via the password= method)! + def request_with_ip4_fix(*args) + self.request(*args) + rescue ApiError => e + if e.ip4_authentication_error? + if !(@ip == e.error_msg_ip) # If not the same IP we try it with this IP.. + self.ip = e.error_msg_ip + self.generate_hash # Generate a new authentication hash. + self.client! # Update the client with the new authentication hash in the cookie! + return self.request(*args) + end + end + raise # If we haven't returned anything.. we raise the ApiError again. + end + +private + + # Find my local_ip.. + def self.local_ip + orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true # turn off reverse DNS resolution temporarily + + UDPSocket.open do |s| + s.connect('74.125.77.147', 1) # Connects to a Google IP '74.125.77.147'. + s.addr.last + end + ensure + Socket.do_not_reverse_lookup = orig end end \ No newline at end of file