Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added support for full API v5.0 (including VPS services).

  • Loading branch information...
commit c377a74d46ca2ba52f9e869ac3d56df64d54348d 1 parent ba36211
@joost authored
View
6 .gitignore
@@ -1 +1,5 @@
-*.gem
+*.gem
+.DS_Store
+.bundle/
+vendor/
+tmp/
View
36 README.rdoc
@@ -1,19 +1,17 @@
= TransIP API
-Ruby gem to use the full TransIP API (v4.2).
-Uses an updated version of savon, and implements the new request signing method that the guys at TransIP have introduced into their api. So far, i have only tested the :get_domain_names, :get_info and :set_dns_entries calls.
+Ruby gem to use the full TransIP (www.transip.nl) API (v5.0).
+Uses an updated version of savon and implements the new request signing method that the guys at TransIP have introduced into their API.
-The transip API makes use of public/private key encryption. You need to use the TransIP
-control panel to give your server access to the api, and to generate a key. You can then
-use the key together with your username to gain access to the api
+The TransIP API makes use of public/private key encryption. You need to use the TransIP
+control panel to give your server access to the API and to generate a key. You can then
+use the key together with your username to gain access to the API.
For more info see:
* <b>The origin of this code:</b> https://github.com/joost/transip
* <b>TransIP API Docs:</b> https://www.transip.nl/g/api
-Credits for full rewrite to work with new TransIP API version go to Richard Bronkhorst (https://github.com/richmans).
-
== Install
Install from rubygems (rubygems.org/gems/transip):
@@ -46,19 +44,37 @@ For the most up-to-date documentation see the source files.
Setup the API client:
# use this in production
- transip = Transip.new(username: 'your_username', key: 'your_private_rsa_key', ip: '12.34.12.3', mode: :readwrite)
+ transip = Transip::DomainClient.new(username: 'your_username', key: 'your_private_rsa_key', ip: '12.34.12.3', mode: :readwrite)
+
+You can use Transip::DomainClient, Transip::VpsClient, Transip::ColocationClient, Transip::WebhostingClient and Transip::ForwardClient.
In development you can leave out the ip. To test request use :readonly mode.
+If you store your private key in a seperate file you can do:
+
+ transip = Transip::DomainClient.new(username: 'your_username', key_file: 'path_to_your_private_key_file', ip: '12.34.12.3', mode: :readwrite)
+
+=== DomainClient
+
+ transip = Transip::DomainClient.new(username: 'your_username', key: 'your_private_rsa_key', ip: '12.34.12.3', mode: :readwrite)
+
transip.actions # => [:batch_check_availability, :check_availability, :get_whois, :get_domain_names, :get_info, :batch_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, :get_all_tld_infos, :get_tld_info, :get_current_domain_action, :retry_current_domain_action_with_new_data, :retry_transfer_with_different_auth_code, :cancel_domain_action]
transip.request(:get_domain_names)
transip.request(:get_info, :domain_name => 'example.com')
transip.request(:get_whois, :domain_name => 'example.com')
transip.request(:set_dns_entries, :domain_name => 'example.com', :dns_entries => [Transip::DnsEntry.new('test', 5.inutes, 'A', '74.125.77.147')])
- transip.request(:register, Transip::Domain.new('example.com', nil, nil, [Transip::DnsEntry.new('test', 5.minutes, 'A', '74.125.77.147')]))
+ transip.request(:register, Transip::Domain.new('example.com', nil, nil, [Transip::DnsEntry.new('test', 300, 'A', '74.125.77.147')]))
transip.request(:set_contacts, :domain_name => 'example.com', :contacts => [Transip::WhoisContact.new('type', 'first', 'middle', 'last', 'company', 'kvk', 'companyType', 'street', 'number', 'postalCode', 'city', 'phoneNumber', 'faxNumber', 'email', 'country')])
+=== VpsClient
+
+ transip_vps = Transip::VpsClient.new(username: 'your_username', key: 'your_private_rsa_key', ip: '12.34.12.3', mode: :readwrite)
+
+ transip_vps.actions # => [:get_available_products, :get_available_addons, :get_active_addons_for_vps, :get_available_upgrades, :get_available_addons_for_vps, :get_cancellable_addons_for_vps, :order_vps, :order_addon, :order_private_network, :upgrade_vps, :cancel_vps, :cancel_addon, :cancel_private_network, :get_private_networks_by_vps, :get_all_private_networks, :add_vps_to_private_network, :remove_vps_from_private_network, :get_amount_of_traffic_used, :start, :stop, :reset, :create_snapshot, :revert_snapshot, :remove_snapshot, :get_vps, :get_vpses, :get_snapshots_by_vps, :get_operating_systems, :install_operating_system, :get_ips_for_vps, :get_all_ips, :add_ipv6_to_vps, :update_ptr_record]
+
+== Contribute and licence
+
Please feel free to contribute and send me a pull request via Github!
-Copyright (c) 2013 Richard Bronkhorst, released under the MIT license
+(C)opyright 2014 Joost Hietbrink / Richard Bronkhorst, released under the MIT license
View
247 lib/transip.rb
@@ -9,14 +9,17 @@
require 'ipaddr'
require File.expand_path '../transip/version', __FILE__
+require File.expand_path '../transip/client', __FILE__
+require File.expand_path '../transip/api_error', __FILE__
+
#
-# Implements the www.transip.nl API (v4.2). For more info see: https://www.transip.nl/g/api/
+# Implements the www.transip.nl API (v5.0). For more info see: https://www.transip.nl/g/api/
#
# The transip API makes use of public/private key encryption. You need to use the TransIP
# control panel to give your server access to the api, and to generate a key. You can then
# use the key together with your username to gain access to the api
# Usage:
-# transip = Transip.new(:username => 'api_username', :key => private_key, :ip => '12.34.12.3', :mode => 'readwrite') # use this in production
+# transip = Transip::DomainClient.new(:username => 'api_username', :key => private_key, :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 => 'example.com')
@@ -25,17 +28,7 @@
# transip.request(:set_contacts, :domain_name => 'example.com', :contacts => [Transip::WhoisContact.new('type', 'first', 'middle', 'last', 'company', 'kvk', 'companyType', 'street','number','postalCode','city','phoneNumber','faxNumber','email','country')])
# transip.request(:register, Transip::Domain.new('example.com', nil, nil, [Transip::DnsEntry.new('test', 5.minutes, 'A', '74.125.77.147')]))
#
-class Transip
- SERVICE = 'DomainService'
- WSDL = 'https://api.transip.nl/wsdl/?service=DomainService'
- API_VERSION = '4.2'
-
- attr_accessor :username, :password, :ip, :mode, :hash
- attr_reader :response
-
- # Following Error needs to be catched in your code!
- class ApiError < RuntimeError
- end
+module Transip
# Following subclasses are actually not needed (as you can also
# do the same by just creating hashes..).
@@ -186,231 +179,13 @@ class Domain < TransipStruct.new(:name, :nameservers, :contacts, :dns_entries, :
class Tld < TransipStruct.new(:name, :price, :renewal_price, :capabilities, :registration_period_length, :cancel_time_frame)
end
- # Options:
- # * username
- # * ip
- # * key
- # * mode
- #
- # Example:
- # transip = Transip.new(:username => 'api_username', :ip => '12.34.12.3', :key => mykey, :mode => 'readwrite') # use this in production
- def initialize(options = {})
- @key = options[:key]
- @username = options[:username]
- @ip = options[:ip]
- raise ArgumentError, "The :username, :ip and :key options are required!" if @username.nil? or @key.nil?
-
- @mode = options[:mode] || :readonly
- @endpoint = options[:endpoint] || 'api.transip.nl'
- if options[:password]
- @password = options[:password]
- end
- @savon_options = {
- :wsdl => WSDL
- }
- # 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_options[:log] = false # disable logging
- @savon_options[:log_level] = :info # changing the log level
- end
-
-
- # Make Savon log to Rails.logger and turn_off_debugging!
- def use_with_rails!
- if Rails.env.production?
- self.turn_off_debugging!
- end
- @savon_options[:logger] = Rails.logger # using the Rails logger
- end
-
- # yes, i know, it smells bad
- def convert_array_to_hash(array)
- result = {}
- array.each_with_index do |value, index|
- result[index] = value
- end
- result
- end
-
- def urlencode(input)
- output = URI.encode_www_form_component(input)
- output.gsub!('+', '%20')
- output.gsub!('%7E', '~')
- output
- end
-
- def serialize_parameters(parameters, key_prefix=nil)
- parameters = parameters.to_hash.values.first if parameters.is_a? TransipStruct
- parameters = convert_array_to_hash(parameters) if parameters.is_a? Array
- if not parameters.is_a? Hash
- return urlencode(parameters)
- end
-
- encoded_parameters = []
- parameters.each do |key, value|
- next if key.to_s == '@xsi:type'
- encoded_key = (key_prefix.nil?) ? urlencode(key) : "#{key_prefix}[#{urlencode(key)}]"
- if value.is_a? Hash or value.is_a? Array or value.is_a? TransipStruct
- encoded_parameters << serialize_parameters(value, encoded_key)
- else
- encoded_value = urlencode(value)
- encoded_parameters << "#{encoded_key}=#{encoded_value}"
- end
- end
-
- encoded_parameters = encoded_parameters.join("&")
- #puts encoded_parameters.split('&').join("\n")
- encoded_parameters
- end
-
-
- # does all the techy stuff to calculate transip's sick authentication scheme:
- # a hash with all the request information is subsequently:
- # serialized like a www form
- # SHA512 digested
- # asn1 header added
- # private key encrypted
- # Base64 encoded
- # URL encoded
- # I think the guys at transip were trying to use their entire crypto-toolbox!
- def signature(method, parameters, time, nonce)
- formatted_method = method.to_s.lower_camelcase
- parameters ||= {}
- input = convert_array_to_hash(parameters.values)
- options = {
- '__method' => formatted_method,
- '__service' => SERVICE,
- '__hostname' => @endpoint,
- '__timestamp' => time,
- '__nonce' => nonce
-
- }
- input.merge!(options)
- raise "Invalid RSA key" unless @key =~ /-----BEGIN (RSA )?PRIVATE KEY-----(.*)-----END (RSA )?PRIVATE KEY-----/sim
- serialized_input = serialize_parameters(input)
-
- digest = Digest::SHA512.new.digest(serialized_input)
- asn_header = "\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40"
-
- # convert asn_header literal to ASCII-8BIT
- if RUBY_VERSION.split('.')[0] == "2"
- asn = asn_header.b + digest
- else
- asn = asn_header + digest
- end
- private_key = OpenSSL::PKey::RSA.new(@key)
- encrypted_asn = private_key.private_encrypt(asn)
- readable_encrypted_asn = Base64.encode64(encrypted_asn)
- urlencode(readable_encrypted_asn)
- end
-
- def to_cookies(content)
- content.map do |item|
- HTTPI::Cookie.new item
- end
- end
-
-
- # Used for authentication
- def cookies(method, parameters)
- time = Time.new.to_i
- #strip out the -'s because transip requires the nonce to be between 6 and 32 chars
- nonce = SecureRandom.uuid.gsub("-", '')
- result = to_cookies [ "login=#{self.username}",
- "mode=#{self.mode}",
- "timestamp=#{time}",
- "nonce=#{nonce}",
- "clientVersion=#{API_VERSION}",
- "signature=#{signature(method, parameters, time, nonce)}"
-
- ]
- #puts signature(method, parameters, time, nonce)
- result
- 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(@savon_options) do
- namespaces(
- "xmlns:enc" => "http://schemas.xmlsoap.org/soap/encoding/"
- )
- end
- 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
+# VPS related methods
+# Available from TransIp v5.0.
- # Returns Array with all possible SOAP WSDL actions.
- def actions
- client.operations
- end
-
- # This makes sure that arrays are properly encoded as soap-arrays by Gyoku
- def fix_array_definitions(options)
- result = {}
- options.each do |key, value|
- if value.is_a? Array and value.size > 0
- entry_name = value.first.class.name.split(":").last
- result[key] = {
- 'item' => {:content! => value, :'@xsi:type' => "tns:#{entry_name}"},
- :'@xsi:type' => "tns:ArrayOf#{entry_name}",
- :'@enc:arrayType' => "tns:#{entry_name}[#{value.size}]"
- }
- else
- result[key] = value
- end
- end
- result
+ class Vps < TransipStruct.new(:name, :description, :operating_system, :disk_size, :memory_size, :cpus, :status, :ip_address, :vnc_hostname, :vnc_port_number, :vnc_password, :is_blocked, :is_customer_locked)
end
-
- # converts the savon response object to something we can return to the caller
- # - A TransipStruct object
- # - An array of TransipStructs
- # - nil
- def process_response(response)
- response = response.to_hash.values.first[:return] rescue nil
- TransipStruct.from_soap(response)
-
+ class VpsService < TransipStruct.new(:name, :description, :operating_system, :disk_size, :memory_size, :cpus, :status, :ip_address, :vnc_hostname, :vnc_port_number, :vnc_password, :is_blocked, :is_customer_locked)
end
- # This is the main request function
- # throws ApiError
- # returns response object (can be TransipStruct or Array of TransipStruct)
- def request(action, options = nil)
- formatted_action = action.to_s.lower_camelcase
- parameters = {
- # for some reason, the transip server wants the body root tag to be
- # the name of the action.
- :message_tag => formatted_action
- }
- options = options.to_hash if options.is_a? Transip::TransipStruct
-
- if options.is_a? Hash
- xml_options = fix_array_definitions(options)
- elsif options.nil?
- xml_options = nil
- else
- raise "Invalid parameter format (should be nil, hash or TransipStruct"
- end
- parameters[:message] = xml_options
- parameters[:cookies] = cookies(action, options)
- #puts parameters.inspect
- response = client.call(action, parameters)
-
- process_response(response)
- rescue Savon::SOAPFault => e
- raise ApiError.new(e), e.message.sub(/^\(\d+\)\s+/,'') # We raise our own error (FIXME: Correct?).
- end
-end
+end
View
5 lib/transip/api_error.rb
@@ -0,0 +1,5 @@
+module Transip
+ # Following Error needs to be catched in your code!
+ class ApiError < RuntimeError
+ end
+end
View
271 lib/transip/client.rb
@@ -0,0 +1,271 @@
+module Transip
+
+ class Client
+
+ API_VERSION = '5.0'
+ API_SERVICE = 'DomainService'
+
+ attr_accessor :username, :password, :ip, :mode, :hash
+ attr_reader :response
+
+ def api_version
+ # We use self.class:: here to not use parentclass constant.
+ @api_version || self.class::API_VERSION
+ end
+
+ def api_service
+ @api_service || self.class::API_SERVICE
+ end
+
+ def wsdl
+ "https://api.transip.nl/wsdl/?service=#{api_service}"
+ end
+
+ # Options:
+ # * username - Your login name on the TransIP website.
+ # * ip - needed in production
+ # * key / key_file - key is one of your private keys (these can be requested via your Controlpanel). key_file is path to file containing key.
+ # * mode - :readonly, :readwrite
+ #
+ # Example:
+ # transip = Transip.new(:username => 'api_username', :ip => '12.34.12.3', :key => mykey, :mode => 'readwrite') # use this in production
+ def initialize(options = {})
+ @key = options[:key] || (options[:key_file] && File.read(options[:key_file]))
+ @username = options[:username]
+ @ip = options[:ip]
+ @api_version = options[:api_version]
+ @api_service = options[:api_service]
+ raise ArgumentError, "The :username, :ip and :key options are required!" if @username.nil? or @key.nil?
+
+ @mode = options[:mode] || :readonly
+ @endpoint = options[:endpoint] || 'api.transip.nl'
+ if options[:password]
+ @password = options[:password]
+ end
+ @savon_options = {
+ :wsdl => wsdl
+ }
+ # 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_options[:log] = false # disable logging
+ @savon_options[:log_level] = :info # changing the log level
+ end
+
+ # Make Savon log to Rails.logger and turn_off_debugging!
+ def use_with_rails!
+ if Rails.env.production?
+ self.turn_off_debugging!
+ end
+ @savon_options[:logger] = Rails.logger # using the Rails logger
+ end
+
+ # yes, i know, it smells bad
+ def convert_array_to_hash(array)
+ result = {}
+ array.each_with_index do |value, index|
+ result[index] = value
+ end
+ result
+ end
+
+ def urlencode(input)
+ output = URI.encode_www_form_component(input)
+ output.gsub!('+', '%20')
+ output.gsub!('%7E', '~')
+ output
+ end
+
+ def serialize_parameters(parameters, key_prefix=nil)
+ parameters = parameters.to_hash.values.first if parameters.is_a? TransipStruct
+ parameters = convert_array_to_hash(parameters) if parameters.is_a? Array
+ if not parameters.is_a? Hash
+ return urlencode(parameters)
+ end
+
+ encoded_parameters = []
+ parameters.each do |key, value|
+ next if key.to_s == '@xsi:type'
+ encoded_key = (key_prefix.nil?) ? urlencode(key) : "#{key_prefix}[#{urlencode(key)}]"
+ if value.is_a? Hash or value.is_a? Array or value.is_a? TransipStruct
+ encoded_parameters << serialize_parameters(value, encoded_key)
+ else
+ encoded_value = urlencode(value)
+ encoded_parameters << "#{encoded_key}=#{encoded_value}"
+ end
+ end
+
+ encoded_parameters = encoded_parameters.join("&")
+ puts encoded_parameters.split('&').join("\n")
+ encoded_parameters
+ end
+
+ # does all the techy stuff to calculate transip's sick authentication scheme:
+ # a hash with all the request information is subsequently:
+ # serialized like a www form
+ # SHA512 digested
+ # asn1 header added
+ # private key encrypted
+ # Base64 encoded
+ # URL encoded
+ # I think the guys at transip were trying to use their entire crypto-toolbox!
+ def signature(method, parameters, time, nonce)
+ formatted_method = method.to_s.lower_camelcase
+ parameters ||= {}
+ input = convert_array_to_hash(parameters.values)
+ options = {
+ '__method' => formatted_method,
+ '__service' => api_service,
+ '__hostname' => @endpoint,
+ '__timestamp' => time,
+ '__nonce' => nonce
+
+ }
+ input.merge!(options)
+ raise "Invalid RSA key" unless @key =~ /-----BEGIN (RSA )?PRIVATE KEY-----(.*)-----END (RSA )?PRIVATE KEY-----/sim
+ serialized_input = serialize_parameters(input)
+
+ digest = Digest::SHA512.new.digest(serialized_input)
+ asn_header = "\x30\x51\x30\x0d\x06\x09\x60\x86\x48\x01\x65\x03\x04\x02\x03\x05\x00\x04\x40"
+
+ # convert asn_header literal to ASCII-8BIT
+ if RUBY_VERSION.split('.')[0] == "2"
+ asn = asn_header.b + digest
+ else
+ asn = asn_header + digest
+ end
+ private_key = OpenSSL::PKey::RSA.new(@key)
+ encrypted_asn = private_key.private_encrypt(asn)
+ readable_encrypted_asn = Base64.encode64(encrypted_asn)
+ urlencode(readable_encrypted_asn)
+ end
+
+ def to_cookies(content)
+ content.map do |item|
+ HTTPI::Cookie.new item
+ end
+ end
+
+ # Used for authentication
+ def cookies(method, parameters)
+ time = Time.new.to_i
+ #strip out the -'s because transip requires the nonce to be between 6 and 32 chars
+ nonce = SecureRandom.uuid.gsub("-", '')
+ result = to_cookies [ "login=#{self.username}",
+ "mode=#{self.mode}",
+ "timestamp=#{time}",
+ "nonce=#{nonce}",
+ "clientVersion=#{api_version}",
+ "signature=#{signature(method, parameters, time, nonce)}"
+
+ ]
+ #puts signature(method, parameters, time, nonce)
+ result
+ 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(@savon_options) do
+ namespaces(
+ "xmlns:enc" => "http://schemas.xmlsoap.org/soap/encoding/"
+ )
+ end
+ 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.operations
+ end
+
+ # This makes sure that arrays are properly encoded as soap-arrays by Gyoku
+ def fix_array_definitions(options)
+ result = {}
+ options.each do |key, value|
+ if value.is_a?(Array) and (value.size > 0)
+ entry_name = value.first.class.name.split(":").last
+ result[key] = {
+ 'item' => {:content! => value, :'@xsi:type' => "tns:#{entry_name}"},
+ :'@xsi:type' => "tns:ArrayOf#{entry_name}",
+ :'@enc:arrayType' => "tns:#{entry_name}[#{value.size}]"
+ }
+ else
+ result[key] = value
+ end
+ end
+ result
+ end
+
+ # converts the savon response object to something we can return to the caller
+ # - A TransipStruct object
+ # - An array of TransipStructs
+ # - nil
+ def process_response(response)
+ response = response.to_hash.values.first[:return] rescue nil
+ TransipStruct.from_soap(response)
+ end
+
+ # This is the main request function
+ # throws ApiError
+ # returns response object (can be TransipStruct or Array of TransipStruct)
+ def request(action, options = nil)
+ formatted_action = action.to_s.lower_camelcase
+ parameters = {
+ # for some reason, the transip server wants the body root tag to be
+ # the name of the action.
+ :message_tag => formatted_action
+ }
+ options = options.to_hash if options.is_a?(Transip::TransipStruct)
+
+ if options.is_a?(Hash)
+ xml_options = fix_array_definitions(options)
+ elsif options.nil?
+ xml_options = nil
+ else
+ raise "Invalid parameter format (should be nil, hash or TransipStruct)"
+ end
+ parameters[:message] = xml_options
+ parameters[:cookies] = cookies(action, options)
+ #puts parameters.inspect
+ response = client.call(action, parameters)
+
+ process_response(response)
+ rescue Savon::SOAPFault => e
+ raise ApiError.new(e), e.message.sub(/^\(\d+\)\s+/,'') # We raise our own error (FIXME: Correct?).
+ end
+ end
+
+ # 'Aliased' by Transip::Client.
+ class DomainClient < Client;end
+
+ # We name it VpsClient instead of VpsService since the latter is already in use by
+ # the TransipStruct.
+ class VpsClient < Client
+ API_SERVICE = 'VpsService'
+ end
+
+ class ColocationClient < Client
+ API_SERVICE = 'ColocationService'
+ end
+
+ class WebhostingClient < Client
+ API_SERVICE = 'WebhostingService'
+ end
+
+ class ForwardClient < Client
+ API_SERVICE = 'ForwardService'
+ end
+
+end
View
4 lib/transip/version.rb
@@ -1,3 +1,3 @@
-class Transip
- VERSION = "0.3.7"
+module Transip
+ VERSION = "0.4.0"
end
View
4 transip.gemspec
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
spec.version = Transip::VERSION
spec.authors = ["Joost Hietbrink", "Richard Bronkhorst"]
spec.email = ["joost@joopp.com"]
- spec.description = %q{Ruby gem to use the full TransIP API (v4.2).}
- spec.summary = %q{Ruby gem to use the full TransIP API (v4.2).}
+ spec.description = %q{Ruby gem to use the full TransIP API (v5.0).}
+ spec.summary = %q{Ruby gem to use the full TransIP API (v5.0).}
spec.homepage = %q{http://github.com/joost/transip}
spec.license = "MIT"
Please sign in to comment.
Something went wrong with that request. Please try again.