├── epp ├── __init__.py ├── commands │ ├── contact.py │ └── __init__.py └── EPP.py ├── setup.py └── README.md /epp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='Python-EPP', 5 | version='0.1.0', 6 | author='Jochem Oosterveen', 7 | author_email='jochem@oosterveen.net', 8 | packages=find_packages(), 9 | description='Python EPP client', 10 | long_description=( 11 | "Python-EPP provides an interface to the Extensible Provisioning " 12 | "Protocol (EPP), which is being used for communication between domain " 13 | "name registries and domain name registrars." 14 | ), 15 | install_requires=[ 16 | "BeautifulSoup >= 3.2.1", 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python-EPP 2 | ========== 3 | 4 | [![PyPI version](https://badge.fury.io/py/Python-EPP.png)](http://badge.fury.io/py/Python-EPP) 5 | 6 | EPP client written in Python. Development is still in an early phase, but most of this should actually work. Tested against the Dutch SIDN DRS5 interface. 7 | 8 | Usage 9 | ----- 10 | 11 | from EPP import EPP, Contact, Domain, Nameserver 12 | 13 | config = { 14 | 'host': '%s.domain-registry.nl' % ('testdrs' if args['test'] else 'drs'), 15 | 'port': 700, 16 | 'user': , 17 | 'pass': , 18 | } 19 | contacts = { 20 | 'registrant': 'NEX001077-NEXTG', 21 | 'admin': 'FJF000131-NEXTG', 22 | 'tech': 'JOO011933-NEXTG', 23 | } 24 | ns = ['ns.nextgear.nl', 'ns2.nextgear.nl'] 25 | 26 | """ This wil automatically handle the greeting and login """ 27 | epp = EPP(**config) 28 | 29 | """ Get the token for a given domain """ 30 | domain = Domain(epp, 'nextgear.nl') 31 | print domain.token() 32 | 33 | """ Lookup the IP for a given nameserver """ 34 | ns = Nameserver(epp, ns[0]) 35 | print ns.get_ip() 36 | 37 | """ Get contact information for a given handle """ 38 | contact = Contact(epp, 'JOO011933-NEXTG') 39 | print contact.info() 40 | -------------------------------------------------------------------------------- /epp/commands/contact.py: -------------------------------------------------------------------------------- 1 | available = """ 2 | 3 | 4 | 5 | 7 | %(handle)s 8 | 9 | 10 | ABC-12345 11 | 12 | """ 13 | 14 | create = """ 15 | 16 | 17 | 18 | 20 | sh8013 21 | 22 | %(name)s 23 | %(org)s 24 | 25 | %(street)s 26 | %(city)s 27 | %(sp)s 28 | %(pc)s 29 | %(cc)s 30 | 31 | 32 | %(voice)s 33 | %(fax)s 34 | %(email)s 35 | 36 | 2fooBAR 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | BV 45 | 8764654.0 46 | 47 | 48 | 49 | 50 | ABC-12345 51 | 52 | """ 53 | 54 | info = """ 55 | 56 | 57 | 58 | 60 | %(handle)s 61 | 62 | 63 | ABC-12345 64 | 65 | """ 66 | 67 | update = """ 68 | 69 | 70 | 71 | 73 | %(handle)s 74 | 75 | 76 | %(name)s 77 | %(org)s 78 | 79 | %(street)s 80 | %(city)s 81 | %(pc)s 82 | %(cc)s 83 | 84 | 85 | %(voice)s 86 | %(fax)s 87 | %(email)s 88 | 89 | 90 | 91 | ABC-12345 92 | 93 | """ -------------------------------------------------------------------------------- /epp/commands/__init__.py: -------------------------------------------------------------------------------- 1 | available = """ 2 | 3 | 4 | 5 | 6 | %s 7 | 8 | 9 | 10 | """ 11 | 12 | create = """ 13 | 14 | 15 | 16 | 18 | %(domain)s 19 | 20 | %(ns)s 21 | 22 | %(registrant)s 23 | %(admin)s 24 | %(tech)s 25 | 26 | 2fooBAR 27 | 28 | 29 | 30 | ABC-12345 31 | 32 | """ 33 | 34 | canceldelete = """ 35 | 36 | 37 | 40 | 41 | %s 42 | 43 | OMVDC10T10 44 | 45 | 46 | """ 47 | 48 | delete = """ 49 | 50 | 51 | 52 | 54 | %s 55 | 56 | 57 | TestVWDNC10T20 58 | 59 | """ 60 | 61 | info = """ 62 | 63 | 64 | 65 | 66 | %s 67 | 68 | 69 | ABC-12345 70 | 71 | """ 72 | 73 | login = """ 74 | 75 | 76 | 77 | %(user)s 78 | %(pass)s 79 | 80 | 1.0 81 | en 82 | 83 | 84 | urn:ietf:params:xml:ns:contact-1.0 85 | urn:ietf:params:xml:ns:host-1.0 86 | urn:ietf:params:xml:ns:domain-1.0 87 | 88 | urn:ietf:params:xml:ns:sidn-ext-epp-1.0 89 | 90 | 91 | 92 | 93 | """ 94 | 95 | logout = """ 96 | 98 | 99 | 100 | 101 | """ 102 | 103 | nameserver = """ 104 | 105 | 106 | %s 107 | 108 | 109 | ABC-12345 110 | 111 | """ 112 | 113 | poll = """ 114 | 115 | 116 | 117 | ABC-12345 118 | 119 | """ 120 | 121 | transfer = """ 122 | 123 | 124 | 125 | 127 | %(domain)s 128 | 129 | %(token)s 130 | 131 | 132 | 133 | C0101C10T10 134 | 135 | """ 136 | 137 | transferstatus = """ 138 | 139 | 140 | 141 | 143 | %s 144 | 145 | 146 | CHKTEST1 147 | 148 | """ 149 | -------------------------------------------------------------------------------- /epp/EPP.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import argparse 3 | import socket 4 | import ssl 5 | import struct 6 | from BeautifulSoup import BeautifulStoneSoup 7 | import commands 8 | from commands import contact 9 | 10 | 11 | class EPP: 12 | 13 | def __init__(self, **kwargs): 14 | self.config = kwargs 15 | self.connected = False 16 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 17 | self.socket.settimeout(2) 18 | self.socket.connect((self.config['host'], self.config['port'])) 19 | try: 20 | self.ssl = ssl.wrap_socket(self.socket) 21 | except socket.error: 22 | print "ERROR: Could not setup a secure connection." 23 | print "Check whether your IP is allowed to connect to the host." 24 | exit(1) 25 | self.format_32 = self.format_32() 26 | self.login() 27 | 28 | def __del__(self): 29 | try: 30 | self.logout() 31 | self.socket.close() 32 | except TypeError: 33 | """ Will occur when not properly connected """ 34 | pass 35 | 36 | # http://www.bortzmeyer.org/4934.html 37 | def format_32(self): 38 | # Get the size of C integers. We need 32 bits unsigned. 39 | format_32 = ">I" 40 | if struct.calcsize(format_32) < 4: 41 | format_32 = ">L" 42 | if struct.calcsize(format_32) != 4: 43 | raise Exception("Cannot find a 32 bits integer") 44 | elif struct.calcsize(format_32) > 4: 45 | format_32 = ">H" 46 | if struct.calcsize(format_32) != 4: 47 | raise Exception("Cannot find a 32 bits integer") 48 | else: 49 | pass 50 | return format_32 51 | 52 | def int_from_net(self, data): 53 | return struct.unpack(self.format_32, data)[0] 54 | 55 | def int_to_net(self, value): 56 | return struct.pack(self.format_32, value) 57 | 58 | def cmd(self, cmd, silent=False): 59 | self.write(cmd) 60 | soup = BeautifulStoneSoup(self.read()) 61 | response = soup.find('response') 62 | result = soup.find('result') 63 | try: 64 | code = int(result.get('code')) 65 | except AttributeError: 66 | print "\nERROR: Could not get result code, exiting." 67 | exit(1) 68 | if not silent or code not in (1000, 1300, 1500): 69 | print("- [%d] %s" % (code, result.msg.text)) 70 | if code == 2308: 71 | return False 72 | if code == 2502: 73 | return False 74 | return response 75 | 76 | def read(self): 77 | length = self.ssl.read(4) 78 | if length: 79 | i = self.int_from_net(length)-4 80 | return self.ssl.read(i) 81 | 82 | def write(self, xml): 83 | epp_as_string = xml 84 | # +4 for the length field itself (section 4 mandates that) 85 | # +2 for the CRLF at the end 86 | length = self.int_to_net(len(epp_as_string) + 4 + 2) 87 | self.ssl.send(length) 88 | return self.ssl.send(epp_as_string + "\r\n") 89 | 90 | def login(self): 91 | """ Read greeting """ 92 | greeting = self.read() 93 | soup = BeautifulStoneSoup(greeting) 94 | svid = soup.find('svid') 95 | version = soup.find('version') 96 | print("Connected to %s (v%s)\n" % (svid.text, version.text)) 97 | 98 | """ Login """ 99 | xml = commands.login % self.config 100 | if not self.cmd(xml, silent=True): 101 | exit(1) 102 | 103 | def logout(self): 104 | cmd = commands.logout 105 | return self.cmd(cmd, silent=True) 106 | 107 | def poll(self): 108 | cmd = commands.poll 109 | return self.cmd(cmd) 110 | 111 | 112 | class EPPObject: 113 | def __init__(self, epp): 114 | self.epp = epp 115 | 116 | def __str__(self): 117 | return unicode(self).encode('utf-8') 118 | 119 | def __getitem__(self, key): 120 | try: 121 | return getattr(self, key) 122 | except AttributeError: 123 | pass 124 | 125 | 126 | class Contact(EPPObject): 127 | def __init__(self, epp, handle=False, **kwargs): 128 | self.epp = epp 129 | self.handle = handle 130 | for k, v in kwargs.items(): 131 | setattr(self, k, v) 132 | 133 | def __unicode__(self): 134 | try: 135 | self.name != '' 136 | return "[%(handle)s] %(name)s, %(street)s, %(pc)s %(city)s (%(cc)s)" % self 137 | except: 138 | return self.handle 139 | 140 | def available(self): 141 | cmd = commands.contact.available % self 142 | res = self.epp.cmd(cmd, silent=True) 143 | return res.resdata.find('contact:id').get('avail') == 'true' 144 | 145 | def create(self): 146 | cmd = commands.contact.create % self 147 | res = self.epp.cmd(cmd).resdata 148 | return res.find('contact:id').text 149 | 150 | def info(self): 151 | cmd = commands.contact.info % self 152 | res = self.epp.cmd(cmd).resdata 153 | self.roid = res.find('contact:roid').text 154 | self.status = res.find('contact:status').get('s') 155 | self.name = res.find('contact:name').text 156 | try: 157 | self.street = res.find('contact:street').text 158 | except AttributeError: 159 | pass 160 | self.city = res.find('contact:city').text 161 | try: 162 | self.pc = res.find('contact:pc').text 163 | except AttributeError: 164 | pass 165 | self.cc = res.find('contact:cc').text 166 | self.voice = res.find('contact:voice').text 167 | self.email = res.find('contact:email').text 168 | return self 169 | 170 | def update(self): 171 | cmd = commands.contact.update % self 172 | return self.epp.cmd(cmd) 173 | 174 | 175 | class Domain(EPPObject): 176 | def __init__(self, epp, domain): 177 | self.domain = domain 178 | self.epp = epp 179 | self.roid = "" 180 | self.status = "" 181 | #self.ns = Nameserver(epp) 182 | 183 | def __unicode__(self): 184 | return "[%(domain)s] status: %(status)s, registrant: %(registrant)s, admin: %(admin)s, tech: %(tech)s" % self 185 | 186 | def available(self): 187 | cmd = commands.available % self.domain 188 | res = self.epp.cmd(cmd) 189 | if not res: 190 | # exception would be more fitting 191 | return False 192 | return res.resdata.find('domain:name').get('avail') == 'true' 193 | 194 | def create(self, contacts, ns): 195 | cmd = commands.create % dict({ 196 | 'domain': self.domain, 197 | 'ns': ns[0], 198 | 'registrant': contacts['registrant'], 199 | 'admin': contacts['admin'], 200 | 'tech': contacts['tech'], 201 | }) 202 | res = self.epp.cmd(cmd) 203 | 204 | def delete(self, undo=False): 205 | if undo: 206 | cmd = commands.canceldelete % self.domain 207 | else: 208 | cmd = commands.delete % self.domain 209 | return self.epp.cmd(cmd) 210 | 211 | def info(self): 212 | cmd = commands.info % self.domain 213 | res = self.epp.cmd(cmd).resdata 214 | self.roid = res.find('domain:roid').text 215 | self.status = res.find('domain:status').get('s') 216 | self.registrant = Contact(self.epp, res.find('domain:registrant').text) 217 | self.admin = Contact(self.epp, res.find('domain:contact', type='admin').text) 218 | self.tech = Contact(self.epp, res.find('domain:contact', type='tech').text) 219 | return self 220 | 221 | def token(self): 222 | cmd = commands.info % self.domain 223 | res = self.epp.cmd(cmd) 224 | return res.resdata.find('domain:pw').text 225 | 226 | def transfer(self, token): 227 | cmd = commands.transfer % dict({ 228 | 'domain': self.domain, 229 | 'token': token, 230 | }) 231 | return self.epp.cmd(cmd) 232 | 233 | 234 | class Nameserver(EPPObject): 235 | def __init__(self, epp, nameserver=False): 236 | self.nameserver = nameserver 237 | self.epp = epp 238 | 239 | def __unicode__(self): 240 | return self.nameserver 241 | 242 | def get_ip(self): 243 | cmd = commands.nameserver % self.nameserver 244 | res = self.epp.cmd(cmd) 245 | return res.resdata.find('host:addr').text 246 | --------------------------------------------------------------------------------