Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
294 lines (257 sloc) 10.3 KB
require "rubygems"
require "bundler/setup"
require 'savon'
require 'curb'
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 => '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 :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 => '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
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)
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 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.
def client!
@client = Savon::Client.new do
wsdl.document = WSDL
end
@client.http.headers["Cookie"] = cookie
return @client
end
# Returns a Savon::Client object to be used in the connection.
# This object is re-used and cached as @client.
def client
@client ||= client!
end
# Returns Array with all possible SOAP WSDL actions.
def actions
client.wsdl.soap_actions
end
# Returns the response.to_hash (raw Savon::SOAP::Response is also stored in @response).
# Examples:
# hash_response = transip.request(:get_domain_names)
# hash_response[:get_domain_names_response][:return][:item] # => ["your.domain", "names.list"]
# For more info see the Transip API docs.
# Be sure to rescue all the errors.. since it is hardcore error throwing.
def request(action, options = nil)
if options.nil?
@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
Something went wrong with that request. Please try again.