nominet-epp 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Geoff Garside
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = nominet-epp
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 Geoff Garside. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "nominet-epp"
8
+ gem.summary = %Q{Nominet EPP (Extensible Provisioning Protocol) Client}
9
+ gem.description = %Q{Client for communicating with the Nominet EPP}
10
+ gem.email = "geoff@geoffgarside.co.uk"
11
+ gem.homepage = "http://github.com/geoffgarside/nominet-epp"
12
+ gem.authors = ["Geoff Garside"]
13
+ gem.add_development_dependency "shoulda", ">= 0"
14
+ gem.add_development_dependency "yard", ">= 0"
15
+
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ gem.add_dependency "epp-client", ">= 0"
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/test_*.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+ begin
32
+ require 'rcov/rcovtask'
33
+ Rcov::RcovTask.new do |test|
34
+ test.libs << 'test'
35
+ test.pattern = 'test/**/test_*.rb'
36
+ test.verbose = true
37
+ end
38
+ rescue LoadError
39
+ task :rcov do
40
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
41
+ end
42
+ end
43
+
44
+ task :test => :check_dependencies
45
+
46
+ task :default => :test
47
+
48
+ begin
49
+ require 'yard'
50
+ YARD::Rake::YardocTask.new
51
+ rescue LoadError
52
+ task :yardoc do
53
+ abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
54
+ end
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
@@ -0,0 +1,139 @@
1
+ require 'epp-client'
2
+ require 'time'
3
+
4
+ require File.dirname(__FILE__) + '/nominet-epp/operations'
5
+ require File.dirname(__FILE__) + '/nominet-epp/helpers'
6
+
7
+ # Nominet EPP Module
8
+ module NominetEPP
9
+ # Front end interface Client to the NominetEPP Service
10
+ class Client
11
+ # Default Nominet Service URNs
12
+ DEFAULT_SERVICES = %w(http://www.nominet.org.uk/epp/xml/nom-domain-2.0
13
+ http://www.nominet.org.uk/epp/xml/nom-notifications-2.0
14
+ urn:ietf:params:xml:ns:host-1.0)
15
+
16
+ # Create a new instance of NominetEPP::Client
17
+ #
18
+ # @param [String] tag Nominet TAG
19
+ # @param [String] passwd Nominet TAG EPP Password
20
+ # @param [String] server Nominet EPP Server address
21
+ def initialize(tag, passwd, server = 'epp.nominet.org.uk')
22
+ @tag, @server = tag, server
23
+ @client = EPP::Client.new(tag, passwd, server, :services => DEFAULT_SERVICES)
24
+ end
25
+
26
+ # @see Object#inspect
27
+ def inspect
28
+ "#<#{self.class} #{@tag}@#{@server}>"
29
+ end
30
+
31
+ # @return [Hash] Nominet Namespaces by prefixes
32
+ def namespaces
33
+ { :domain => 'http://www.nominet.org.uk/epp/xml/nom-domain-2.0',
34
+ :account => 'http://www.nominet.org.uk/epp/xml/nom-account-2.0',
35
+ :contact => 'http://www.nominet.org.uk/epp/xml/nom-contact-2.0',
36
+ :tag => 'http://www.nominet.org.uk/epp/xml/nom-tag-1.0',
37
+ :n => 'http://www.nominet.org.uk/epp/xml/nom-notifications-2.0' }
38
+ end
39
+
40
+ # @return [Hash] Nominet Schema Locations by prefix
41
+ def schemaLocations
42
+ { :domain => 'http://www.nominet.org.uk/epp/xml/nom-domain-2.0 nom-domain-2.0.xsd',
43
+ :account => 'http://www.nominet.org.uk/epp/xml/nom-account-2.0 nom-account-2.0.xsd',
44
+ :contact => 'http://www.nominet.org.uk/epp/xml/nom-contact-2.0 nom-contact-1.0.xsd',
45
+ :tag => 'http://www.nominet.org.uk/epp/xml/nom-tag-1.0 nom-tag-1.0.xsd',
46
+ :n => 'http://www.nominet.org.uk/epp/xml/nom-notifications-2.0 nom-notifications-2.0.xsd' }
47
+ end
48
+
49
+ include Helpers
50
+
51
+ include Operations::Check
52
+ include Operations::Create
53
+ include Operations::Delete
54
+ include Operations::Fork
55
+ include Operations::Hello
56
+ include Operations::Info
57
+ include Operations::List
58
+ include Operations::Lock
59
+ include Operations::Merge
60
+ include Operations::Poll
61
+ include Operations::Renew
62
+ include Operations::Transfer
63
+ include Operations::Unlock
64
+ include Operations::Unrenew
65
+ include Operations::Update
66
+
67
+ private
68
+ # @param [XML::Node] node XML Node to find the value from
69
+ # @param [String] xpath XPath Expression for the value
70
+ # @return [String] node value
71
+ def node_value(node, xpath)
72
+ n = node.find(xpath, namespaces).first
73
+ n && n.content.strip
74
+ end
75
+
76
+ # @param [String] name XML Element name
77
+ # @param [Symbol] ns_prefix Namespace Prefix
78
+ # @yield [node, ns] block to populate node
79
+ # @yieldparam [XML::Node] node XML Node to populate
80
+ # @yieldparam [XML::Namespace] ns XML Namespace of the node
81
+ # @return [XML::Node] newly created node
82
+ def new_node(name, ns_prefix, &block)
83
+ node = XML::Node.new(name)
84
+ node.namespaces.namespace = ns = XML::Namespace.new(node, ns_prefix.to_s, namespaces[ns_prefix])
85
+ node['xsi:schemaLocation'] = schemaLocations[ns_prefix]
86
+
87
+ case block.arity
88
+ when 0
89
+ yield
90
+ when 1
91
+ yield node
92
+ when 2
93
+ yield node, ns
94
+ end
95
+
96
+ node
97
+ end
98
+
99
+ # @param [String] node_name XML Element name
100
+ # @yield [node, ns] block to populate node
101
+ # @return [XML::Node] new node in :domain namespace
102
+ # @see new_node
103
+ def domain(node_name, &block)
104
+ new_node(node_name, :domain, &block)
105
+ end
106
+
107
+ # @param [String] node_name XML Element name
108
+ # @yield [node, ns] block to populate node
109
+ # @return [XML::Node] new node in :account namespace
110
+ # @see new_node
111
+ def account(node_name, &block)
112
+ new_node(node_name, :account, &block)
113
+ end
114
+
115
+ # @param [String] node_name XML Element name
116
+ # @yield [node, ns] block to populate node
117
+ # @return [XML::Node] new node in :contact namespace
118
+ # @see new_node
119
+ def contact(node_name, &block)
120
+ new_node(node_name, :contact, &block)
121
+ end
122
+
123
+ # @param [String] node_name XML Element name
124
+ # @yield [node, ns] block to populate node
125
+ # @return [XML::Node] new node in :tag namespace
126
+ # @see new_node
127
+ def tag(node_name, &block)
128
+ new_node(node_name, :tag, &block)
129
+ end
130
+
131
+ # @param [String] node_name XML Element name
132
+ # @yield [node, ns] block to populate node
133
+ # @return [XML::Node] new node in :notification namespace
134
+ # @see new_node
135
+ def notification(node_name, &block)
136
+ new_node(node_name, :n, &block)
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,83 @@
1
+ module NominetEPP
2
+ # Helper methods
3
+ module Helpers
4
+ # @param [String, Array] nameservers Nameserver host name or array of host names or hashes
5
+ # @param [XML::Namespace] ns XML Namespace to create the elements with
6
+ # @return [XML::Node] +ns+ element of +host+ elements
7
+ # @see domain_host_xml
8
+ def domain_ns_xml(nameservers, ns)
9
+ ns_el = XML::Node.new('ns', nil, ns)
10
+
11
+ case nameservers.class
12
+ when String
13
+ ns_el << (XML::Node.new('host', nil, ns) << XML::Node.new('hostName', nameservers, ns))
14
+ when Array
15
+ nameservers.each do |nameserver|
16
+ ns_el << domain_host_xml(nameserver, ns)
17
+ end
18
+ else
19
+ raise ArgumentError, "nameservers must either be a string or array of strings"
20
+ end
21
+
22
+ ns_el
23
+ end
24
+
25
+ # @param [String, Hash] nameserver Nameserver host name or hash of +:name+ and +:v4+ or +:v6+ address
26
+ # @param [XML::Namespace] ns XML Namespace to create the elements with
27
+ # @return [XML::Node] +host+ element with +hostName+ and optionally +ip+ subelements
28
+ def domain_host_xml(nameserver, ns)
29
+ host = XML::Node.new('host', nil, ns)
30
+
31
+ case nameserver.class
32
+ when String
33
+ host << XML::Node.new('hostName', nameserver, ns)
34
+ when Hash
35
+ host << XML::Node.new('hostName', nameserver[:name], ns)
36
+ if nameserver[:v4]
37
+ n = XML::Node.new('hostAddr', nameserver[:v4], ns)
38
+ n['ip'] = 'v4'
39
+ host << n
40
+ end
41
+ if nameserver[:v6]
42
+ n = XML::Node.new('hostAddr', nameserver[:v6], ns)
43
+ n['ip'] = 'v6'
44
+ host << n
45
+ end
46
+ end
47
+
48
+ host
49
+ end
50
+
51
+ # Adds the attributes from the fields to the +node+.
52
+ #
53
+ # @param [Hash] fields Account attributes to marshal
54
+ # @param [XML::Node] node XML Node to add the attributes to
55
+ # @param [XML::Namespace] ns XML Namespace to create the elements under
56
+ def account_fields_xml(fields, node, ns)
57
+ fields.each do |k,v|
58
+ case k
59
+ when :contacts
60
+ v.each do |c|
61
+ node << (c_node = XML::Node.new('contact', nil, ns))
62
+ c_node['order'] = c.delete(:order)
63
+ c_node['type'] = 'admin'
64
+
65
+ c_node << contact(c.delete(:action) || 'create') do |cn, cns|
66
+ c.each do |l,w|
67
+ cn << XML::Node.new(l, w, cns)
68
+ end
69
+ end
70
+ end
71
+ when :addr # Limitation, can only handle one addr at a time
72
+ node << (a_node = XML::Node.new('addr', nil, ns))
73
+ a_node['type'] = 'admin'
74
+ v.each do |l,w|
75
+ a_node << XML::Node.new(l, w, ns)
76
+ end
77
+ else
78
+ node << XML::Node.new(k.to_s.gsub('_', '-'), v, ns)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,20 @@
1
+ module NominetEPP
2
+ # Module to enclose the Operations
3
+ module Operations
4
+ autoload :Check, File.dirname(__FILE__) + '/operations/check.rb'
5
+ autoload :Create, File.dirname(__FILE__) + '/operations/create.rb'
6
+ autoload :Delete, File.dirname(__FILE__) + '/operations/delete.rb'
7
+ autoload :Fork, File.dirname(__FILE__) + '/operations/fork.rb'
8
+ autoload :Hello, File.dirname(__FILE__) + '/operations/hello.rb'
9
+ autoload :Info, File.dirname(__FILE__) + '/operations/info.rb'
10
+ autoload :List, File.dirname(__FILE__) + '/operations/list.rb'
11
+ autoload :Lock, File.dirname(__FILE__) + '/operations/lock.rb'
12
+ autoload :Merge, File.dirname(__FILE__) + '/operations/merge.rb'
13
+ autoload :Poll, File.dirname(__FILE__) + '/operations/poll.rb'
14
+ autoload :Renew, File.dirname(__FILE__) + '/operations/renew.rb'
15
+ autoload :Transfer, File.dirname(__FILE__) + '/operations/transfer.rb'
16
+ autoload :Unlock, File.dirname(__FILE__) + '/operations/unlock.rb'
17
+ autoload :Unrenew, File.dirname(__FILE__) + '/operations/unrenew.rb'
18
+ autoload :Update, File.dirname(__FILE__) + '/operations/update.rb'
19
+ end
20
+ end
@@ -0,0 +1,39 @@
1
+ module NominetEPP
2
+ module Operations
3
+ # EPP Check Operation
4
+ module Check
5
+ # Check the availablity of one or more domain names
6
+ #
7
+ # @param [String, ...] *names List of names to check
8
+ # @return [false] request failed
9
+ # @return [true] the domain name is available
10
+ # @return [Hash<String,Boolean>] availability by domain name
11
+ def check(*names)
12
+ resp = @client.check do
13
+ domain('check') do |node, ns|
14
+ names.each do |name|
15
+ node << XML::Node.new('name', name, ns)
16
+ end
17
+ end
18
+ end
19
+
20
+ return false unless resp.success?
21
+
22
+ @check_limit = check_abuse_limit(resp.data)
23
+ results = resp.data.find('//domain:name', namespaces)
24
+ if results.size > 1
25
+ hash = {}
26
+ results.each {|node| hash[node.content.strip] = (node['avail'] == '1') }
27
+ hash
28
+ else
29
+ results.first['avail'] == '1'
30
+ end
31
+ end
32
+
33
+ protected
34
+ def check_abuse_limit(data)
35
+ data.find('//domain:chkData/@abuse-limit', namespaces).first.value.to_i
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,166 @@
1
+ module NominetEPP
2
+ module Operations
3
+ # EPP Create Operation
4
+ module Create
5
+ # Register a domain name with Nominet.
6
+ #
7
+ # The returned hash has the following keys
8
+ # - (String) +:name+ -- Domain name registered
9
+ # - (Time) +:crDate+ -- Domain creation date
10
+ # - (Time) +:exDate+ -- Domain expiration date
11
+ # - (Hash) +:account+ -- Created account data (if returned)
12
+ #
13
+ # The +:account+ hash contains the following keys
14
+ # - (String) +:roid+ -- Account ID
15
+ # - (String) +:name+ -- Account Name
16
+ # - (Array) +:contacts+ -- Account contacts
17
+ #
18
+ # The +:contacts+ array contains hashes of the following keys
19
+ # - (String) +:roid+ -- Contact ID
20
+ # - (String) +:name+ -- Contact Name
21
+ # - (String) +:type+ -- Contact type if set
22
+ # - (Integer) +:order+ -- Contacts order in the list of contacts
23
+ #
24
+ # @param [String] name Domain name to register
25
+ # @param [String] acct
26
+ # @param [Array] nameservers Nameservers to set for the domain
27
+ # @param [Hash] options Registration options
28
+ # @option options [String] :period
29
+ # @option options [String] :first_bill
30
+ # @option options [String] :recur_bill
31
+ # @option options [String] :auto_bill
32
+ # @option options [String] :next_bill
33
+ # @option options [String] :notes
34
+ # @raise [ArgumentError] invalid option keys
35
+ # @return [false] registration failed
36
+ # @return [Hash] Domain creation data
37
+ def create(name, acct, nameservers, options = {})
38
+ raise ArgumentError, "options allowed keys are period, first_bill, recur_bill, auto_bill, next_bill, notes" unless (options.keys - [:period, :first_bill, :recur_bill, :auto_bill, :next_bill, :notes]).empty?
39
+
40
+ resp = @client.create do
41
+ domain('create') do |node, ns|
42
+ node << XML::Node.new('name', name, ns)
43
+
44
+ options[:period] && node << XML::Node.new('period', options.delete(:period), ns)
45
+
46
+ acct_xml = XML::Node.new('account', nil, ns)
47
+ acct_xml << create_account(acct, ns)
48
+ node << acct_xml
49
+
50
+ node << domain_ns_xml(nameservers, ns)
51
+
52
+ options.each do |key, value|
53
+ node << XML::Node.new(key.to_s.gsub('_', '-'), value, ns)
54
+ end
55
+ end
56
+ end
57
+
58
+ return false unless resp.success?
59
+
60
+ creData = resp.data.find('/domain:creData', namespaces).first
61
+ h = { :name => node_value(creData, 'domain:name'),
62
+ :crDate => Time.parse(node_value(creData, 'domain:crDate')),
63
+ :exDate => Time.parse(node_value(creData, 'domain:exDate')) }
64
+ unless creData.find('domain:account').first.nil?
65
+ h[:account] = created_account(creData.find('domain:account/account:creData', namespaces).first)
66
+ end
67
+ h
68
+ end
69
+
70
+ private
71
+ # Create the account payload
72
+ #
73
+ # @param [String, Hash] acct Account ID or New account fields
74
+ # @param [XML::Namespace] domain_ns +:domain+ namespace
75
+ # @raise [ArgumentError] acct must be a String or a Hash of account fields
76
+ # @return [XML::Node]
77
+ def create_account(acct, domain_ns)
78
+ if acct.kind_of?(String)
79
+ XML::Node.new('account-id', acct, domain_ns)
80
+ elsif acct.kind_of?(Hash)
81
+ account('create') do |node, ns|
82
+ node << XML::Node.new('name', acct[:name], ns)
83
+ node << XML::Node.new('trad-name', acct[:trad_name], ns)
84
+ node << XML::Node.new('type', acct[:type], ns)
85
+ node << XML::Node.new('opt-out', acct[:opt_out], ns)
86
+
87
+ acct[:contacts].each_with_index do |cont, i|
88
+ c = XML::Node.new('contact', nil, ns)
89
+ c['order'] = i
90
+ node << (c << create_account_contact(cont))
91
+ end
92
+
93
+ node << create_account_address(acct[:addr], ns)
94
+ end
95
+ else
96
+ raise ArgumentError, "acct must be String or Hash"
97
+ end
98
+ end
99
+
100
+ # Create account contact payload
101
+ #
102
+ # @param [Hash] cont Contact fields
103
+ # @raise [ArgumentError] invalid contact fields
104
+ # @raise [ArgumentError] name or email key is missing
105
+ # @return [XML::Node]
106
+ def create_account_contact(cont)
107
+ raise ArgumentError, "Contact allowed keys are name, email, phone and mobile" unless (cont.keys -[:name, :email, :phone, :mobile]).empty?
108
+ raise ArgumentError, "Contact requires name and email keys" unless cont.has_key?(:name) && cont.has_key?(:email)
109
+
110
+ contact('create') do |node, ns|
111
+ cont.each do |key, value|
112
+ node << XML::Node.new(key, value, ns)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Create contact address
118
+ #
119
+ # @param [Hash] addr Address fields
120
+ # @raise [ArgumentError] invalid keys in addr
121
+ # @return [XML::Node]
122
+ def create_account_address(addr, ns)
123
+ raise ArgumentError, "Address allowed keys are street, locality, city, county, postcode, country" unless (addr.keys - [:street, :locality, :city, :county, :postcode, :country]).empty?
124
+
125
+ addr = XML::Node.new('addr', nil, ns)
126
+ addr.each do |key, value|
127
+ addr << XML::Node.new(key, value, ns)
128
+ end
129
+
130
+ addr
131
+ end
132
+
133
+ # Collects created account information
134
+ #
135
+ # @param [XML::Node] creData XML
136
+ # @return [Hash]
137
+ def created_account(creData)
138
+ { :roid => node_value(creData, 'account:roid'),
139
+ :name => node_value(creData, 'account:name'),
140
+ :contacts => created_contacts(creData.find(
141
+ 'account:contact', namespaces)) }
142
+ end
143
+
144
+ # Collects created account contacts
145
+ #
146
+ # @param [XML::Node] account_contacts Account contacts
147
+ # @return [Hash]
148
+ def created_contacts(account_contacts)
149
+ account_contacts.map do |cont|
150
+ { :type => cont['type'],
151
+ :order => cont['order'] }.merge(
152
+ created_contact(cont.find('contact:creData', namespaces).first))
153
+ end
154
+ end
155
+
156
+ # Collects create contact information
157
+ #
158
+ # @param [XML::Node] creData XML
159
+ # @return [Hash]
160
+ def created_contact(creData)
161
+ { :roid => node_value(creData, 'contact:roid'),
162
+ :name => node_value(creData, 'contact:name') }
163
+ end
164
+ end
165
+ end
166
+ end