Skip to content

Commit 838ad69

Browse files
authored
Merge pull request #27 from vesellov/master
testing clTRID field on EPP response to make sure stream sequence is not broken
2 parents 48337d2 + 0aa8d76 commit 838ad69

File tree

5 files changed

+124
-29
lines changed

5 files changed

+124
-29
lines changed

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
setup_params = dict(
44
name='epp-python-client',
5-
version='0.0.13',
5+
version='0.0.14',
66
author='Veselin Penev',
77
author_email='[email protected]',
88
packages=find_packages(where='src'),

src/epp/epp_client.py

+33-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import struct
55
import hashlib
66
import time
7+
import re
78

89
from bs4 import BeautifulSoup
910

@@ -56,12 +57,20 @@ class EPPConnectionAlreadyClosedError(Exception):
5657
class EPPResponseEmptyError(Exception):
5758
pass
5859

60+
61+
class EPPRequestFailedError(Exception):
62+
pass
63+
64+
65+
class EPPStreamSequenceBrokenError(Exception):
66+
pass
67+
5968
#------------------------------------------------------------------------------
6069

6170

6271
class EPPConnection:
6372

64-
def __init__(self, host, port, user, password, verbose=False, raise_errors=False, return_soup=None):
73+
def __init__(self, host, port, user, password, verbose=False, raise_errors=True, return_soup=None):
6574
self.host = host
6675
self.port = int(port)
6776
self.user = user
@@ -73,6 +82,7 @@ def __init__(self, host, port, user, password, verbose=False, raise_errors=False
7382
self.verbose = verbose
7483
self.raise_errors = raise_errors
7584
self.return_soup = return_soup
85+
self.cltrid_regexp = re.compile(r'<clTRID>(\w+?)</clTRID>')
7686

7787
def open(self, timeout=15):
7888
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -152,7 +162,7 @@ def read(self):
152162
if self.verbose:
153163
logger.exception('failed to receive EPP response')
154164
return None
155-
if ret is None:
165+
if not ret:
156166
if self.verbose:
157167
logger.error('nothing was received from EPP connection')
158168
raise EPPResponseEmptyError()
@@ -181,13 +191,30 @@ def write(self, xml):
181191
return ret
182192

183193
def call(self, cmd, soup=None, quite=False):
194+
cltrid_request = ''
195+
r_req = self.cltrid_regexp.search(cmd)
196+
if r_req:
197+
cltrid_request = r_req.group(1)
184198
if self.write(cmd):
185199
if self.verbose and not quite:
186-
logger.debug('sent %d bytes:\n%s\n', len(cmd), cmd)
200+
logger.debug('sent %d bytes [%s]:\n%s\n', len(cmd), cltrid_request, cmd)
201+
else:
202+
if self.verbose:
203+
logger.exception('failed to send EPP request')
204+
raise EPPRequestFailedError()
187205
raw = self.read()
188-
if raw:
189-
if self.verbose and not quite:
190-
logger.debug('received %d bytes:\n%s', len(raw), raw.decode())
206+
if not raw:
207+
if self.verbose:
208+
logger.exception('received empty EPP response')
209+
return ''
210+
cltrid_response = ''
211+
r_resp = self.cltrid_regexp.search(raw.decode())
212+
if r_resp:
213+
cltrid_response = r_resp.group(1)
214+
if self.verbose and not quite:
215+
logger.debug('received %d bytes [%s]:\n%s', len(raw), cltrid_response, raw.decode())
216+
if cltrid_request and cltrid_response and cltrid_request != cltrid_response:
217+
raise EPPStreamSequenceBrokenError()
191218
if soup is True or (self.return_soup is True and soup is not False):
192219
try:
193220
soup = BeautifulSoup(raw, "lxml")

src/epp/rpc_server.py

+19-15
Original file line numberDiff line numberDiff line change
@@ -257,20 +257,24 @@ def do_process_epp_command(self, request_json):
257257
cmd = request_json['cmd']
258258
args = request_json.get('args', {})
259259
except KeyError as exc:
260-
logger.exception('failed processing epp command')
261-
return {'error': 'failed reading epp request: %r' % exc, }
260+
logger.exception('failed processing EPP command')
261+
return {'error': 'failed reading EPP request: %r' % exc, }
262262

263263
try:
264264
if self.verbose:
265265
if self.verbose_poll or cmd not in ['poll_req', 'poll_ack', ]:
266266
logger.debug('request: [%s] %r', cmd, args)
267267
response_xml = self.do_epp_request(cmd, args)
268-
269-
except (epp_client.EPPConnectionAlreadyClosedError, epp_client.EPPResponseEmptyError, ) as exc:
268+
except (
269+
epp_client.EPPConnectionAlreadyClosedError,
270+
epp_client.EPPResponseEmptyError,
271+
epp_client.EPPRequestFailedError,
272+
epp_client.EPPStreamSequenceBrokenError,
273+
) as exc:
270274
if not self.epp_reconnect:
271275
if self.verbose:
272276
logger.critical('EPP connection closed: %r', exc)
273-
return {'error': 'failed processing epp command: %r' % exc, }
277+
return {'error': 'failed processing EPP command: %r' % exc, }
274278
if self.verbose:
275279
logger.critical('about to restart EPP connection, because of %r', exc)
276280
try:
@@ -281,16 +285,16 @@ def do_process_epp_command(self, request_json):
281285
self.connect_epp()
282286
response_xml = self.do_epp_request(cmd, args)
283287
except Exception as exc:
284-
logger.exception('epp command retry failed')
285-
return {'error': 'failed processing epp command: %r' % exc, }
288+
logger.exception('EPP command retry failed')
289+
return {'error': 'failed processing EPP command: %r' % exc, }
286290

287291
except Exception as exc:
288-
logger.exception('failed processing epp command')
289-
return {'error': 'failed processing epp command: %r' % exc, }
292+
logger.exception('failed processing EPP command')
293+
return {'error': 'failed processing EPP command: %r' % exc, }
290294

291-
if response_xml is None:
292-
logger.error('UNKNOWN COMMAND: %r', cmd)
293-
return {'error': 'unknown command: %r' % cmd, }
295+
if not response_xml:
296+
logger.exception('unknown command or empty response received: %r', cmd)
297+
return {'error': 'unknown command or empty response received: %r' % cmd, }
294298

295299
try:
296300
response_json = json.loads(xml2json.xml2json(response_xml, XML2JsonOptions(), strip_ns=1, strip=1))
@@ -300,10 +304,10 @@ def do_process_epp_command(self, request_json):
300304
response_json = json.loads(xml2json.xml2json(response_xml.encode('ascii', errors='ignore'), XML2JsonOptions(), strip_ns=1, strip=1))
301305
except Exception as exc:
302306
logger.exception('xml2json failed')
303-
return {'error': 'failed reading epp response: %r' % exc, }
307+
return {'error': 'failed reading EPP response: %r' % exc, }
304308
except Exception as exc:
305-
logger.exception('failed reading epp response')
306-
return {'error': 'failed reading epp response: %r' % exc, }
309+
logger.exception('failed reading EPP response')
310+
return {'error': 'failed reading EPP response: %r' % exc, }
307311

308312
try:
309313
code = response_json['epp']['response']['result']['@code']

tests/epp/test_epp_client.py

+64
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@
4242
<trID><svTRID>1618663648910</svTRID></trID></response></epp>'''
4343

4444

45+
sample_domain_check_command_response = b'''<?xml version="1.0" encoding="UTF-8"?>
46+
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
47+
xsi:schemaLocation="urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd"><response><result code="1000">
48+
<msg>Command completed successfully</msg></result>
49+
<resData><domain:chkData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0"
50+
xsi:schemaLocation="urn:ietf:params:xml:ns:domain-1.0 domain-1.0.xsd">
51+
<domain:cd><domain:name avail="0">test.com</domain:name><domain:reason>(00) The domain exists</domain:reason>
52+
</domain:cd></domain:chkData></resData><trID>
53+
<clTRID>bde35ab00a221ebc0e557c3d6fd6b693</clTRID><svTRID>1634623283678</svTRID></trID></response></epp>
54+
'''
55+
56+
4557
def conn():
4658
logging.getLogger('epp.client').setLevel(logging.DEBUG)
4759
return epp_client.EPPConnection(
@@ -188,3 +200,55 @@ def test_call_with_soup(self, mock_wrap_socket, mock_socket_connect):
188200
assert resp.find('result').get('code') == '2000'
189201
assert resp.find('msg').text == 'Unknown command'
190202
assert c.close() is True
203+
204+
@mock.patch('socket.socket.connect')
205+
@mock.patch('ssl.wrap_socket')
206+
def test_call_cltrid_is_matching(self, mock_wrap_socket, mock_socket_connect):
207+
mock_socket_connect.return_value = True
208+
fake_stream = io.BytesIO(
209+
fake_response(sample_greeting) +
210+
fake_response(sample_login_response) +
211+
fake_response(sample_domain_check_command_response) +
212+
fake_response(sample_logout_response))
213+
setattr(fake_stream, 'send', lambda _: True)
214+
mock_wrap_socket.return_value = fake_stream
215+
c = conn()
216+
assert c.open() is True
217+
assert c.call(cmd='''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
218+
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
219+
<command>
220+
<check>
221+
<domain:check xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
222+
<domain:name>test.com</domain:name>
223+
</domain:check>
224+
</check>
225+
<clTRID>bde35ab00a221ebc0e557c3d6fd6b693</clTRID>
226+
</command>
227+
</epp>''') == sample_domain_check_command_response
228+
assert c.close() is True
229+
230+
@mock.patch('socket.socket.connect')
231+
@mock.patch('ssl.wrap_socket')
232+
def test_call_cltrid_missmatch(self, mock_wrap_socket, mock_socket_connect):
233+
mock_socket_connect.return_value = True
234+
fake_stream = io.BytesIO(
235+
fake_response(sample_greeting) +
236+
fake_response(sample_login_response) +
237+
fake_response(sample_domain_check_command_response) +
238+
fake_response(sample_logout_response))
239+
setattr(fake_stream, 'send', lambda _: True)
240+
mock_wrap_socket.return_value = fake_stream
241+
c = conn()
242+
assert c.open() is True
243+
with pytest.raises(epp_client.EPPStreamSequenceBrokenError):
244+
c.call(cmd='''<?xml version="1.0" encoding="UTF-8" standalone="no"?>
245+
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
246+
<command>
247+
<check>
248+
<domain:check xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
249+
<domain:name>test.com</domain:name>
250+
</domain:check>
251+
</check>
252+
<clTRID>bde35ab00a221ebc0e557c3d6fd6b693isdifferent</clTRID>
253+
</command>
254+
</epp>''')

tests/epp/test_rpc_server.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,21 @@ class TestEPP_RPC_Server(object):
8080
def test_bad_rpc_request(self):
8181
assert srv().do_process_epp_command(
8282
{'bad': 'request'}
83-
)['error'] == "failed reading epp request: KeyError('cmd')"
83+
)['error'] == "failed reading EPP request: KeyError('cmd')"
8484

8585
def test_unknown_command(self):
8686
assert srv(
8787
xml_response='bad response'
8888
).do_process_epp_command(
8989
{'cmd': 'something_crazy'}
90-
)['error'] == "unknown command: 'something_crazy'"
90+
)['error'] == "unknown command or empty response received: 'something_crazy'"
9191

9292
def test_bad_rpc_response(self):
9393
assert srv(
9494
xml_response='bad response'
9595
).do_process_epp_command(
9696
{'cmd': 'poll_req'}
97-
)['error'] == "failed reading epp response: ParseError('syntax error: line 1, column 0')"
97+
)['error'] == "failed reading EPP response: ParseError('syntax error: line 1, column 0')"
9898

9999
def test_cmd_poll_req(self):
100100
verify_cmd(
@@ -514,12 +514,12 @@ def test_cmd_contact_delete(self):
514514
xml_response='''<?xml version="1.0" encoding="UTF-8"?><epp xmlns="urn:ietf:params:xml:ns:epp-1.0"
515515
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd">
516516
<response><result code="1000"><msg>Command completed successfully</msg></result><msgQ count="2" id="786"/>
517-
<trID><clTRID>13ed22ad5eea887a64688edf51db893b</clTRID><svTRID>1618921358722</svTRID></trID></response></epp>''',
517+
<trID><clTRID>c5bc8f94103f1a47019a09049dff5aec</clTRID><svTRID>1618921358722</svTRID></trID></response></epp>''',
518518
json_response={'epp': {
519519
'@{http://www.w3.org/2001/XMLSchema-instance}schemaLocation': 'urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd',
520520
'response': {'msgQ': {'@count': '2', '@id': '786'},
521521
'result': {'@code': '1000', 'msg': 'Command completed successfully'},
522-
'trID': {'clTRID': '13ed22ad5eea887a64688edf51db893b', 'svTRID': '1618921358722'}, },
522+
'trID': {'clTRID': 'c5bc8f94103f1a47019a09049dff5aec', 'svTRID': '1618921358722'}, },
523523
}, },
524524
)
525525

@@ -802,13 +802,13 @@ def test_cmd_domain_update(self):
802802
xml_response='''<?xml version="1.0" encoding="UTF-8"?><epp xmlns="urn:ietf:params:xml:ns:epp-1.0"
803803
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd">
804804
<response><result code="1000"><msg>Command completed successfully</msg></result><msgQ count="2" id="786"/>
805-
<trID><clTRID>dd8f602b0de383fcf4a63ee89f8bc3ca</clTRID><svTRID>1619202519557</svTRID></trID></response></epp>''',
805+
<trID><clTRID>c5bc8f94103f1a47019a09049dff5aec</clTRID><svTRID>1619202519557</svTRID></trID></response></epp>''',
806806
json_response={'epp': {
807807
'@{http://www.w3.org/2001/XMLSchema-instance}schemaLocation': 'urn:ietf:params:xml:ns:epp-1.0 epp-1.0.xsd',
808808
'response': {'msgQ': {
809809
'@count': '2', '@id': '786'},
810810
'result': {'@code': '1000', 'msg': 'Command completed successfully'},
811-
'trID': {'clTRID': 'dd8f602b0de383fcf4a63ee89f8bc3ca', 'svTRID': '1619202519557'}, },
811+
'trID': {'clTRID': 'c5bc8f94103f1a47019a09049dff5aec', 'svTRID': '1619202519557'}, },
812812
}, },
813813
)
814814

0 commit comments

Comments
 (0)