From 7f0545a8ae056d20715bd33b73684612e6d99bed Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Tue, 8 Sep 2015 17:55:23 -0300 Subject: [PATCH 01/10] adding gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40df40e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build/ +dist/ +*.pyc +*.pyo +*.egg-info From c447b610143a1382d30cb77c038452cd92496a7f Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Thu, 10 Sep 2015 18:07:53 -0300 Subject: [PATCH 02/10] cleanup for better code reading --- epp/EPP.py | 60 +++++++++++++++++++++++++++++++++------- epp/commands/__init__.py | 4 +-- epp/commands/contact.py | 8 +++--- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/epp/EPP.py b/epp/EPP.py index 84cb436..b732484 100755 --- a/epp/EPP.py +++ b/epp/EPP.py @@ -1,11 +1,10 @@ #!/usr/bin/env python -import argparse +import commands import socket import ssl import struct + from BeautifulSoup import BeautifulStoneSoup -import commands -from commands import contact class EPP: @@ -16,12 +15,14 @@ def __init__(self, **kwargs): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.settimeout(2) self.socket.connect((self.config['host'], self.config['port'])) + try: self.ssl = ssl.wrap_socket(self.socket) except socket.error: print "ERROR: Could not setup a secure connection." print "Check whether your IP is allowed to connect to the host." exit(1) + self.format_32 = self.format_32() self.login() @@ -37,16 +38,20 @@ def __del__(self): def format_32(self): # Get the size of C integers. We need 32 bits unsigned. format_32 = ">I" + if struct.calcsize(format_32) < 4: - format_32 = ">L" + format_32 = ">L" + if struct.calcsize(format_32) != 4: raise Exception("Cannot find a 32 bits integer") elif struct.calcsize(format_32) > 4: - format_32 = ">H" + format_32 = ">H" + if struct.calcsize(format_32) != 4: raise Exception("Cannot find a 32 bits integer") else: pass + return format_32 def int_from_net(self, data): @@ -60,23 +65,28 @@ def cmd(self, cmd, silent=False): soup = BeautifulStoneSoup(self.read()) response = soup.find('response') result = soup.find('result') + try: code = int(result.get('code')) except AttributeError: print "\nERROR: Could not get result code, exiting." exit(1) + if not silent or code not in (1000, 1300, 1500): print("- [%d] %s" % (code, result.msg.text)) if code == 2308: return False if code == 2502: return False + return response def read(self): length = self.ssl.read(4) + if length: i = self.int_from_net(length)-4 + return self.ssl.read(i) def write(self, xml): @@ -84,7 +94,9 @@ def write(self, xml): # +4 for the length field itself (section 4 mandates that) # +2 for the CRLF at the end length = self.int_to_net(len(epp_as_string) + 4 + 2) + self.ssl.send(length) + return self.ssl.send(epp_as_string + "\r\n") def login(self): @@ -97,19 +109,23 @@ def login(self): """ Login """ xml = commands.login % self.config + if not self.cmd(xml, silent=True): exit(1) def logout(self): cmd = commands.logout + return self.cmd(cmd, silent=True) def poll(self): cmd = commands.poll + return self.cmd(cmd) class EPPObject: + def __init__(self, epp): self.epp = epp @@ -124,27 +140,32 @@ def __getitem__(self, key): class Contact(EPPObject): + def __init__(self, epp, handle=False, **kwargs): self.epp = epp self.handle = handle + for k, v in kwargs.items(): setattr(self, k, v) def __unicode__(self): try: self.name != '' - return "[%(handle)s] %(name)s, %(street)s, %(pc)s %(city)s (%(cc)s)" % self + return ("[%(handle)s] %(name)s, %(street)s, " + + "%(pc)s %(city)s (%(cc)s)") % self except: return self.handle def available(self): cmd = commands.contact.available % self res = self.epp.cmd(cmd, silent=True) + return res.resdata.find('contact:id').get('avail') == 'true' def create(self): cmd = commands.contact.create % self res = self.epp.cmd(cmd).resdata + return res.find('contact:id').text def info(self): @@ -153,42 +174,53 @@ def info(self): self.roid = res.find('contact:roid').text self.status = res.find('contact:status').get('s') self.name = res.find('contact:name').text + try: self.street = res.find('contact:street').text except AttributeError: pass + self.city = res.find('contact:city').text + try: self.pc = res.find('contact:pc').text except AttributeError: pass + self.cc = res.find('contact:cc').text self.voice = res.find('contact:voice').text self.email = res.find('contact:email').text + return self def update(self): cmd = commands.contact.update % self + return self.epp.cmd(cmd) class Domain(EPPObject): + def __init__(self, epp, domain): self.domain = domain self.epp = epp self.roid = "" self.status = "" - #self.ns = Nameserver(epp) + # self.ns = Nameserver(epp) def __unicode__(self): - return "[%(domain)s] status: %(status)s, registrant: %(registrant)s, admin: %(admin)s, tech: %(tech)s" % self + return ("[%(domain)s] status: %(status)s, " + + "registrant: %(registrant)s, admin: %(admin)s, " + + "tech: %(tech)s") % self def available(self): cmd = commands.available % self.domain res = self.epp.cmd(cmd) + if not res: # exception would be more fitting return False + return res.resdata.find('domain:name').get('avail') == 'true' def create(self, contacts, ns): @@ -206,6 +238,7 @@ def delete(self, undo=False): cmd = commands.canceldelete % self.domain else: cmd = commands.delete % self.domain + return self.epp.cmd(cmd) def info(self): @@ -214,13 +247,17 @@ def info(self): self.roid = res.find('domain:roid').text self.status = res.find('domain:status').get('s') self.registrant = Contact(self.epp, res.find('domain:registrant').text) - self.admin = Contact(self.epp, res.find('domain:contact', type='admin').text) - self.tech = Contact(self.epp, res.find('domain:contact', type='tech').text) + self.admin = Contact(self.epp, res.find('domain:contact', + type='admin').text) + self.tech = Contact(self.epp, res.find('domain:contact', + type='tech').text) + return self def token(self): cmd = commands.info % self.domain res = self.epp.cmd(cmd) + return res.resdata.find('domain:pw').text def transfer(self, token): @@ -228,10 +265,12 @@ def transfer(self, token): 'domain': self.domain, 'token': token, }) + return self.epp.cmd(cmd) class Nameserver(EPPObject): + def __init__(self, epp, nameserver=False): self.nameserver = nameserver self.epp = epp @@ -242,4 +281,5 @@ def __unicode__(self): def get_ip(self): cmd = commands.nameserver % self.nameserver res = self.epp.cmd(cmd) + return res.resdata.find('host:addr').text diff --git a/epp/commands/__init__.py b/epp/commands/__init__.py index 66fb116..791ea54 100644 --- a/epp/commands/__init__.py +++ b/epp/commands/__init__.py @@ -13,7 +13,7 @@ - %(domain)s @@ -35,7 +35,7 @@ %s diff --git a/epp/commands/contact.py b/epp/commands/contact.py index 40cc9dd..a7ffcd3 100644 --- a/epp/commands/contact.py +++ b/epp/commands/contact.py @@ -5,7 +5,7 @@ %(handle)s - + ABC-12345 @@ -28,7 +28,7 @@ %(pc)s %(cc)s - + %(voice)s %(fax)s %(email)s @@ -85,9 +85,9 @@ %(voice)s %(fax)s %(email)s - + ABC-12345 -""" \ No newline at end of file +""" From f31b693a9fcf11de77b7cece8aeca710a59c18be Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Thu, 10 Sep 2015 18:10:37 -0300 Subject: [PATCH 03/10] return response from Domain create --- epp/EPP.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/epp/EPP.py b/epp/EPP.py index b732484..cedabcc 100755 --- a/epp/EPP.py +++ b/epp/EPP.py @@ -231,7 +231,8 @@ def create(self, contacts, ns): 'admin': contacts['admin'], 'tech': contacts['tech'], }) - res = self.epp.cmd(cmd) + + return self.epp.cmd(cmd) def delete(self, undo=False): if undo: From eaefc15673fb9af46f2d52cc76270ba77e0358da Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Thu, 10 Sep 2015 21:44:15 -0300 Subject: [PATCH 04/10] improve package structure --- README.md | 2 +- epp/__init__.py | 1 + epp/{EPP.py => core.py} | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename epp/{EPP.py => core.py} (100%) diff --git a/README.md b/README.md index 88e2572..583dd03 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ EPP client written in Python. Development is still in an early phase, but most o Usage ----- - from EPP import EPP, Contact, Domain, Nameserver + from epp import EPP, Contact, Domain, Nameserver config = { 'host': '%s.domain-registry.nl' % ('testdrs' if args['test'] else 'drs'), diff --git a/epp/__init__.py b/epp/__init__.py index e69de29..451d0c0 100644 --- a/epp/__init__.py +++ b/epp/__init__.py @@ -0,0 +1 @@ +from .core import EPP, Contact, Domain, Nameserver diff --git a/epp/EPP.py b/epp/core.py similarity index 100% rename from epp/EPP.py rename to epp/core.py From 44204c10064b9414c68008af915f81d56c75d358 Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Thu, 10 Sep 2015 22:16:35 -0300 Subject: [PATCH 05/10] add certificate support --- README.md | 1 + epp/core.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 583dd03..e5cae99 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Usage 'port': 700, 'user': , 'pass': , + 'cert': , # Optional parameter } contacts = { 'registrant': 'NEX001077-NEXTG', diff --git a/epp/core.py b/epp/core.py index cedabcc..84c3fb3 100755 --- a/epp/core.py +++ b/epp/core.py @@ -17,7 +17,8 @@ def __init__(self, **kwargs): self.socket.connect((self.config['host'], self.config['port'])) try: - self.ssl = ssl.wrap_socket(self.socket) + self.ssl = ssl.wrap_socket(self.socket, + certfile=self.config.get('certfile')) except socket.error: print "ERROR: Could not setup a secure connection." print "Check whether your IP is allowed to connect to the host." From 698d6b72706d1fb3a1f39eaa97d9c875fce673a1 Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Wed, 14 Oct 2015 00:06:18 -0300 Subject: [PATCH 06/10] fix use correct param name --- epp/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/epp/core.py b/epp/core.py index 84c3fb3..ed2523d 100755 --- a/epp/core.py +++ b/epp/core.py @@ -18,7 +18,7 @@ def __init__(self, **kwargs): try: self.ssl = ssl.wrap_socket(self.socket, - certfile=self.config.get('certfile')) + certfile=self.config.get('cert')) except socket.error: print "ERROR: Could not setup a secure connection." print "Check whether your IP is allowed to connect to the host." From f9d90430111f078dcec51f4d37410f11c195ec7b Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Wed, 14 Oct 2015 14:26:05 -0300 Subject: [PATCH 07/10] fix Domain.available() and raise exceptions instead of using exit(1) --- epp/core.py | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/epp/core.py b/epp/core.py index ed2523d..2ee8927 100755 --- a/epp/core.py +++ b/epp/core.py @@ -20,9 +20,11 @@ def __init__(self, **kwargs): self.ssl = ssl.wrap_socket(self.socket, certfile=self.config.get('cert')) except socket.error: - print "ERROR: Could not setup a secure connection." - print "Check whether your IP is allowed to connect to the host." - exit(1) + raise socket.error( + 'ERROR: Could not setup a secure connection. \n' + 'Check whether your IP is allowed to connect to the host, ' + 'or if you have a valid certificate.' + ) self.format_32 = self.format_32() self.login() @@ -63,15 +65,19 @@ def int_to_net(self, value): def cmd(self, cmd, silent=False): self.write(cmd) - soup = BeautifulStoneSoup(self.read()) + + raw_response = self.read() + if not raw_response: + raise Exception('ERROR: Empty response') + + soup = BeautifulStoneSoup(raw_response) response = soup.find('response') result = soup.find('result') try: code = int(result.get('code')) except AttributeError: - print "\nERROR: Could not get result code, exiting." - exit(1) + raise AttributeError("ERROR: Could not get result code.") if not silent or code not in (1000, 1300, 1500): print("- [%d] %s" % (code, result.msg.text)) @@ -86,7 +92,7 @@ def read(self): length = self.ssl.read(4) if length: - i = self.int_from_net(length)-4 + i = self.int_from_net(length) - 4 return self.ssl.read(i) @@ -112,7 +118,7 @@ def login(self): xml = commands.login % self.config if not self.cmd(xml, silent=True): - exit(1) + raise Exception('Error: Unable to login') def logout(self): cmd = commands.logout @@ -207,6 +213,7 @@ def __init__(self, epp, domain): self.epp = epp self.roid = "" self.status = "" + ssl.wrap_socket # self.ns = Nameserver(epp) def __unicode__(self): @@ -215,14 +222,17 @@ def __unicode__(self): "tech: %(tech)s") % self def available(self): + """Checks for domain availability""" + cmd = commands.available % self.domain res = self.epp.cmd(cmd) - if not res: - # exception would be more fitting - return False + # https://tools.ietf.org/html/rfc5731#section-3.1.1 + # "1" or "true" means that the object can be provisioned. + # "0" or "false" means that the object can not be provisioned. + avail = res.resdata.find('domain:name').get('avail') - return res.resdata.find('domain:name').get('avail') == 'true' + return avail == 'true' or avail == '1' def create(self, contacts, ns): cmd = commands.create % dict({ From 3d8281fbe3216b81dda03fe70daaf79c21520222 Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Wed, 14 Oct 2015 15:59:08 -0300 Subject: [PATCH 08/10] better module organization --- epp/__init__.py | 7 +- epp/contact.py | 64 ++++++++++ epp/core.py | 297 ---------------------------------------------- epp/domain.py | 79 ++++++++++++ epp/epp.py | 148 +++++++++++++++++++++++ epp/nameserver.py | 20 ++++ 6 files changed, 317 insertions(+), 298 deletions(-) create mode 100644 epp/contact.py delete mode 100755 epp/core.py create mode 100644 epp/domain.py create mode 100644 epp/epp.py create mode 100644 epp/nameserver.py diff --git a/epp/__init__.py b/epp/__init__.py index 451d0c0..290fb2b 100644 --- a/epp/__init__.py +++ b/epp/__init__.py @@ -1 +1,6 @@ -from .core import EPP, Contact, Domain, Nameserver +# -*- coding: utf-8 -*- + +from .contact import Contact +from .domain import Domain +from .epp import EPP +from .nameserver import Nameserver diff --git a/epp/contact.py b/epp/contact.py new file mode 100644 index 0000000..00e65b6 --- /dev/null +++ b/epp/contact.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +from . import commands +from .epp import EPPObject + + +class Contact(EPPObject): + + def __init__(self, epp, handle=False, **kwargs): + self.epp = epp + self.handle = handle + + for k, v in kwargs.items(): + setattr(self, k, v) + + def __unicode__(self): + try: + self.name != '' + return ("[%(handle)s] %(name)s, %(street)s, " + + "%(pc)s %(city)s (%(cc)s)") % self + except: + return self.handle + + def available(self): + cmd = commands.contact.available % self + res = self.epp.cmd(cmd, silent=True) + + return res.resdata.find('contact:id').get('avail') == 'true' + + def create(self): + cmd = commands.contact.create % self + res = self.epp.cmd(cmd).resdata + + return res.find('contact:id').text + + def info(self): + cmd = commands.contact.info % self + res = self.epp.cmd(cmd).resdata + self.roid = res.find('contact:roid').text + self.status = res.find('contact:status').get('s') + self.name = res.find('contact:name').text + + try: + self.street = res.find('contact:street').text + except AttributeError: + pass + + self.city = res.find('contact:city').text + + try: + self.pc = res.find('contact:pc').text + except AttributeError: + pass + + self.cc = res.find('contact:cc').text + self.voice = res.find('contact:voice').text + self.email = res.find('contact:email').text + + return self + + def update(self): + cmd = commands.contact.update % self + + return self.epp.cmd(cmd) diff --git a/epp/core.py b/epp/core.py deleted file mode 100755 index 2ee8927..0000000 --- a/epp/core.py +++ /dev/null @@ -1,297 +0,0 @@ -#!/usr/bin/env python -import commands -import socket -import ssl -import struct - -from BeautifulSoup import BeautifulStoneSoup - - -class EPP: - - def __init__(self, **kwargs): - self.config = kwargs - self.connected = False - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.settimeout(2) - self.socket.connect((self.config['host'], self.config['port'])) - - try: - self.ssl = ssl.wrap_socket(self.socket, - certfile=self.config.get('cert')) - except socket.error: - raise socket.error( - 'ERROR: Could not setup a secure connection. \n' - 'Check whether your IP is allowed to connect to the host, ' - 'or if you have a valid certificate.' - ) - - self.format_32 = self.format_32() - self.login() - - def __del__(self): - try: - self.logout() - self.socket.close() - except TypeError: - """ Will occur when not properly connected """ - pass - - # http://www.bortzmeyer.org/4934.html - def format_32(self): - # Get the size of C integers. We need 32 bits unsigned. - format_32 = ">I" - - if struct.calcsize(format_32) < 4: - format_32 = ">L" - - if struct.calcsize(format_32) != 4: - raise Exception("Cannot find a 32 bits integer") - elif struct.calcsize(format_32) > 4: - format_32 = ">H" - - if struct.calcsize(format_32) != 4: - raise Exception("Cannot find a 32 bits integer") - else: - pass - - return format_32 - - def int_from_net(self, data): - return struct.unpack(self.format_32, data)[0] - - def int_to_net(self, value): - return struct.pack(self.format_32, value) - - def cmd(self, cmd, silent=False): - self.write(cmd) - - raw_response = self.read() - if not raw_response: - raise Exception('ERROR: Empty response') - - soup = BeautifulStoneSoup(raw_response) - response = soup.find('response') - result = soup.find('result') - - try: - code = int(result.get('code')) - except AttributeError: - raise AttributeError("ERROR: Could not get result code.") - - if not silent or code not in (1000, 1300, 1500): - print("- [%d] %s" % (code, result.msg.text)) - if code == 2308: - return False - if code == 2502: - return False - - return response - - def read(self): - length = self.ssl.read(4) - - if length: - i = self.int_from_net(length) - 4 - - return self.ssl.read(i) - - def write(self, xml): - epp_as_string = xml - # +4 for the length field itself (section 4 mandates that) - # +2 for the CRLF at the end - length = self.int_to_net(len(epp_as_string) + 4 + 2) - - self.ssl.send(length) - - return self.ssl.send(epp_as_string + "\r\n") - - def login(self): - """ Read greeting """ - greeting = self.read() - soup = BeautifulStoneSoup(greeting) - svid = soup.find('svid') - version = soup.find('version') - print("Connected to %s (v%s)\n" % (svid.text, version.text)) - - """ Login """ - xml = commands.login % self.config - - if not self.cmd(xml, silent=True): - raise Exception('Error: Unable to login') - - def logout(self): - cmd = commands.logout - - return self.cmd(cmd, silent=True) - - def poll(self): - cmd = commands.poll - - return self.cmd(cmd) - - -class EPPObject: - - def __init__(self, epp): - self.epp = epp - - def __str__(self): - return unicode(self).encode('utf-8') - - def __getitem__(self, key): - try: - return getattr(self, key) - except AttributeError: - pass - - -class Contact(EPPObject): - - def __init__(self, epp, handle=False, **kwargs): - self.epp = epp - self.handle = handle - - for k, v in kwargs.items(): - setattr(self, k, v) - - def __unicode__(self): - try: - self.name != '' - return ("[%(handle)s] %(name)s, %(street)s, " + - "%(pc)s %(city)s (%(cc)s)") % self - except: - return self.handle - - def available(self): - cmd = commands.contact.available % self - res = self.epp.cmd(cmd, silent=True) - - return res.resdata.find('contact:id').get('avail') == 'true' - - def create(self): - cmd = commands.contact.create % self - res = self.epp.cmd(cmd).resdata - - return res.find('contact:id').text - - def info(self): - cmd = commands.contact.info % self - res = self.epp.cmd(cmd).resdata - self.roid = res.find('contact:roid').text - self.status = res.find('contact:status').get('s') - self.name = res.find('contact:name').text - - try: - self.street = res.find('contact:street').text - except AttributeError: - pass - - self.city = res.find('contact:city').text - - try: - self.pc = res.find('contact:pc').text - except AttributeError: - pass - - self.cc = res.find('contact:cc').text - self.voice = res.find('contact:voice').text - self.email = res.find('contact:email').text - - return self - - def update(self): - cmd = commands.contact.update % self - - return self.epp.cmd(cmd) - - -class Domain(EPPObject): - - def __init__(self, epp, domain): - self.domain = domain - self.epp = epp - self.roid = "" - self.status = "" - ssl.wrap_socket - # self.ns = Nameserver(epp) - - def __unicode__(self): - return ("[%(domain)s] status: %(status)s, " + - "registrant: %(registrant)s, admin: %(admin)s, " + - "tech: %(tech)s") % self - - def available(self): - """Checks for domain availability""" - - cmd = commands.available % self.domain - res = self.epp.cmd(cmd) - - # https://tools.ietf.org/html/rfc5731#section-3.1.1 - # "1" or "true" means that the object can be provisioned. - # "0" or "false" means that the object can not be provisioned. - avail = res.resdata.find('domain:name').get('avail') - - return avail == 'true' or avail == '1' - - def create(self, contacts, ns): - cmd = commands.create % dict({ - 'domain': self.domain, - 'ns': ns[0], - 'registrant': contacts['registrant'], - 'admin': contacts['admin'], - 'tech': contacts['tech'], - }) - - return self.epp.cmd(cmd) - - def delete(self, undo=False): - if undo: - cmd = commands.canceldelete % self.domain - else: - cmd = commands.delete % self.domain - - return self.epp.cmd(cmd) - - def info(self): - cmd = commands.info % self.domain - res = self.epp.cmd(cmd).resdata - self.roid = res.find('domain:roid').text - self.status = res.find('domain:status').get('s') - self.registrant = Contact(self.epp, res.find('domain:registrant').text) - self.admin = Contact(self.epp, res.find('domain:contact', - type='admin').text) - self.tech = Contact(self.epp, res.find('domain:contact', - type='tech').text) - - return self - - def token(self): - cmd = commands.info % self.domain - res = self.epp.cmd(cmd) - - return res.resdata.find('domain:pw').text - - def transfer(self, token): - cmd = commands.transfer % dict({ - 'domain': self.domain, - 'token': token, - }) - - return self.epp.cmd(cmd) - - -class Nameserver(EPPObject): - - def __init__(self, epp, nameserver=False): - self.nameserver = nameserver - self.epp = epp - - def __unicode__(self): - return self.nameserver - - def get_ip(self): - cmd = commands.nameserver % self.nameserver - res = self.epp.cmd(cmd) - - return res.resdata.find('host:addr').text diff --git a/epp/domain.py b/epp/domain.py new file mode 100644 index 0000000..4d2b262 --- /dev/null +++ b/epp/domain.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +from . import commands +from .contact import Contact +from .epp import EPPObject + + +class Domain(EPPObject): + + def __init__(self, epp, domain): + self.domain = domain + self.epp = epp + self.roid = "" + self.status = "" + # self.ns = Nameserver(epp) + + def __unicode__(self): + return ("[%(domain)s] status: %(status)s, " + + "registrant: %(registrant)s, admin: %(admin)s, " + + "tech: %(tech)s") % self + + def available(self): + """Checks for domain availability""" + + cmd = commands.available % self.domain + res = self.epp.cmd(cmd) + + # https://tools.ietf.org/html/rfc5731#section-3.1.1 + # "1" or "true" means that the object can be provisioned. + # "0" or "false" means that the object can not be provisioned. + avail = res.resdata.find('domain:name').get('avail') + + return avail == 'true' or avail == '1' + + def create(self, contacts, ns): + cmd = commands.create % dict({ + 'domain': self.domain, + 'ns': ns[0], + 'registrant': contacts['registrant'], + 'admin': contacts['admin'], + 'tech': contacts['tech'], + }) + + return self.epp.cmd(cmd) + + def delete(self, undo=False): + if undo: + cmd = commands.canceldelete % self.domain + else: + cmd = commands.delete % self.domain + + return self.epp.cmd(cmd) + + def info(self): + cmd = commands.info % self.domain + res = self.epp.cmd(cmd).resdata + self.roid = res.find('domain:roid').text + self.status = res.find('domain:status').get('s') + self.registrant = Contact(self.epp, res.find('domain:registrant').text) + self.admin = Contact(self.epp, res.find('domain:contact', + type='admin').text) + self.tech = Contact(self.epp, res.find('domain:contact', + type='tech').text) + + return self + + def token(self): + cmd = commands.info % self.domain + res = self.epp.cmd(cmd) + + return res.resdata.find('domain:pw').text + + def transfer(self, token): + cmd = commands.transfer % dict({ + 'domain': self.domain, + 'token': token, + }) + + return self.epp.cmd(cmd) diff --git a/epp/epp.py b/epp/epp.py new file mode 100644 index 0000000..7a0082e --- /dev/null +++ b/epp/epp.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- + +import socket +import ssl +import struct + +from BeautifulSoup import BeautifulStoneSoup + +from . import commands + + +class EPP: + + def __init__(self, **kwargs): + self.config = kwargs + self.connected = False + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.settimeout(2) + self.socket.connect((self.config['host'], self.config['port'])) + + try: + self.ssl = ssl.wrap_socket(self.socket, + certfile=self.config.get('cert')) + except socket.error: + raise socket.error( + 'ERROR: Could not setup a secure connection. \n' + 'Check whether your IP is allowed to connect to the host, ' + 'or if you have a valid certificate.' + ) + + self.format_32 = self.format_32() + self.login() + + def __del__(self): + try: + self.logout() + self.socket.close() + except TypeError: + """ Will occur when not properly connected """ + pass + + # http://www.bortzmeyer.org/4934.html + def format_32(self): + # Get the size of C integers. We need 32 bits unsigned. + format_32 = ">I" + + if struct.calcsize(format_32) < 4: + format_32 = ">L" + + if struct.calcsize(format_32) != 4: + raise Exception("Cannot find a 32 bits integer") + elif struct.calcsize(format_32) > 4: + format_32 = ">H" + + if struct.calcsize(format_32) != 4: + raise Exception("Cannot find a 32 bits integer") + else: + pass + + return format_32 + + def int_from_net(self, data): + return struct.unpack(self.format_32, data)[0] + + def int_to_net(self, value): + return struct.pack(self.format_32, value) + + def cmd(self, cmd, silent=False): + self.write(cmd) + + raw_response = self.read() + if not raw_response: + raise Exception('ERROR: Empty response') + + soup = BeautifulStoneSoup(raw_response) + response = soup.find('response') + result = soup.find('result') + + try: + code = int(result.get('code')) + except AttributeError: + raise AttributeError("ERROR: Could not get result code.") + + if not silent or code not in (1000, 1300, 1500): + print("- [%d] %s" % (code, result.msg.text)) + if code == 2308: + return False + if code == 2502: + return False + + return response + + def read(self): + length = self.ssl.read(4) + + if length: + i = self.int_from_net(length) - 4 + + return self.ssl.read(i) + + def write(self, xml): + epp_as_string = xml + # +4 for the length field itself (section 4 mandates that) + # +2 for the CRLF at the end + length = self.int_to_net(len(epp_as_string) + 4 + 2) + + self.ssl.send(length) + + return self.ssl.send(epp_as_string + "\r\n") + + def login(self): + """ Read greeting """ + greeting = self.read() + soup = BeautifulStoneSoup(greeting) + svid = soup.find('svid') + version = soup.find('version') + print("Connected to %s (v%s)\n" % (svid.text, version.text)) + + """ Login """ + xml = commands.login % self.config + + if not self.cmd(xml, silent=True): + raise Exception('Error: Unable to login') + + def logout(self): + cmd = commands.logout + + return self.cmd(cmd, silent=True) + + def poll(self): + cmd = commands.poll + + return self.cmd(cmd) + + +class EPPObject: + + def __init__(self, epp): + self.epp = epp + + def __str__(self): + return unicode(self).encode('utf-8') + + def __getitem__(self, key): + try: + return getattr(self, key) + except AttributeError: + pass diff --git a/epp/nameserver.py b/epp/nameserver.py new file mode 100644 index 0000000..a5ef436 --- /dev/null +++ b/epp/nameserver.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +from . import commands +from .epp import EPPObject + + +class Nameserver(EPPObject): + + def __init__(self, epp, nameserver=False): + self.nameserver = nameserver + self.epp = epp + + def __unicode__(self): + return self.nameserver + + def get_ip(self): + cmd = commands.nameserver % self.nameserver + res = self.epp.cmd(cmd) + + return res.resdata.find('host:addr').text From 95cc583bce72348601313ec779680e6df00e63cc Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Wed, 14 Oct 2015 17:42:29 -0300 Subject: [PATCH 09/10] add CommandException for more detailed errors and tie silent option to EPP object for more consistent behavior --- epp/contact.py | 2 +- epp/epp.py | 26 +++++++++++++++----------- epp/exceptions.py | 5 +++++ 3 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 epp/exceptions.py diff --git a/epp/contact.py b/epp/contact.py index 00e65b6..7bd5e1b 100644 --- a/epp/contact.py +++ b/epp/contact.py @@ -23,7 +23,7 @@ def __unicode__(self): def available(self): cmd = commands.contact.available % self - res = self.epp.cmd(cmd, silent=True) + res = self.epp.cmd(cmd) return res.resdata.find('contact:id').get('avail') == 'true' diff --git a/epp/epp.py b/epp/epp.py index 7a0082e..5cfb2fd 100644 --- a/epp/epp.py +++ b/epp/epp.py @@ -7,13 +7,16 @@ from BeautifulSoup import BeautifulStoneSoup from . import commands +from .exceptions import CommandException class EPP: - def __init__(self, **kwargs): + def __init__(self, silent=True, **kwargs): + self.silent = silent self.config = kwargs self.connected = False + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.settimeout(2) self.socket.connect((self.config['host'], self.config['port'])) @@ -65,28 +68,29 @@ def int_from_net(self, data): def int_to_net(self, value): return struct.pack(self.format_32, value) - def cmd(self, cmd, silent=False): + def cmd(self, cmd): self.write(cmd) raw_response = self.read() if not raw_response: - raise Exception('ERROR: Empty response') + raise CommandException('Empty response') soup = BeautifulStoneSoup(raw_response) response = soup.find('response') result = soup.find('result') + import ipdb; ipdb.set_trace() + try: code = int(result.get('code')) except AttributeError: - raise AttributeError("ERROR: Could not get result code.") + raise CommandException("Could not get result code") - if not silent or code not in (1000, 1300, 1500): + if not self.silent: print("- [%d] %s" % (code, result.msg.text)) - if code == 2308: - return False - if code == 2502: - return False + + if code not in (1000, 1300, 1500): + raise CommandException('[{0}] {1}'.format(code, result.msg.text)) return response @@ -119,13 +123,13 @@ def login(self): """ Login """ xml = commands.login % self.config - if not self.cmd(xml, silent=True): + if not self.cmd(xml): raise Exception('Error: Unable to login') def logout(self): cmd = commands.logout - return self.cmd(cmd, silent=True) + return self.cmd(cmd) def poll(self): cmd = commands.poll diff --git a/epp/exceptions.py b/epp/exceptions.py new file mode 100644 index 0000000..f17f788 --- /dev/null +++ b/epp/exceptions.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + + +class CommandException(Exception): + pass From 77a88a747bd02647a01a6a49ed2860e1cbd1ee2a Mon Sep 17 00:00:00 2001 From: Humberto Rocha Date: Wed, 14 Oct 2015 17:44:39 -0300 Subject: [PATCH 10/10] remove forgotten ipdb --- epp/epp.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/epp/epp.py b/epp/epp.py index 5cfb2fd..d70333c 100644 --- a/epp/epp.py +++ b/epp/epp.py @@ -79,8 +79,6 @@ def cmd(self, cmd): response = soup.find('response') result = soup.find('result') - import ipdb; ipdb.set_trace() - try: code = int(result.get('code')) except AttributeError: