self.__request_msg = Message(Message.PARSE)
self.__response_renderer = MessageRenderer()
- # The following attribute(s) are essentially private and constant,
- # but defined as "protected" so that test code can customize them.
- # They should not be overridden for any other purposes.
+ # The following attribute(s) are essentially private, but defined as
+ # "protected" so that test code can customize/inspect them.
+ # They should not be overridden/referenced for any other purposes.
#
# DDNS Protocol handling class.
self._UpdateSessionClass = isc.ddns.session.UpdateSession
+ # Outstanding TCP context: fileno=>context_obj
+ self._tcp_ctxs = {}
class InternalError(Exception):
'''Exception for internal errors in an update session.
sock.sendto(data, dest)
else:
tcp_ctx = DNSTCPContext(sock)
- tcp_ctx.send(data)
- tcp_ctx.close()
+ send_result = tcp_ctx.send(data)
+ if send_result == DNSTCPContext.SENDING:
+ self._tcp_ctxs[sock.fileno] = tcp_ctx
+ elif send_result == DNSTCPContext.CLOSED:
+ raise socket.error("socket error in TCP send")
+ else:
+ tcp_ctx.close()
except socket.error as ex:
logger.error(DDNS_RESPONSE_SOCKET_ERROR, ClientFormatter(dest), ex)
return False
from isc.cc.session import SessionTimeout, SessionError, ProtocolError
from isc.datasrc import DataSourceClient
from isc.config.ccsession import create_answer
+from isc.server_common.dns_tcp import DNSTCPContext
import ddns
import errno
import os
self._sent_data = None
self._sent_addr = None
self._close_called = 0 # number of calls to close()
+ self.__send_cc = 0 # waterline of the send buffer (can be reset)
# customizable by tests; if set to True, sendto() will throw after
# recording the parameters.
self._raise_on_send = False
+ self._send_buflen = None # imaginary send buffer for partial send
def fileno(self):
return self.__fileno
def getpeername(self):
if self._raise_on_send:
raise socket.error('test socket failure')
def send(self, data):
+ if self._raise_on_send:
+ raise socket.error(errno.EPIPE, 'faked connection disruption')
+ elif self._send_buflen is None:
+ available_space = len(data)
+ else:
+ available_space = self._send_buflen - self.__send_cc
+ if available_space == 0:
+ # if there's no space, (assuming it's nonblocking mode) raise
+ # EAGAIN.
+ raise socket.error(errno.EAGAIN,
+ "Resource temporarily unavailable")
+ # determine the sendable part of the data, record it, update "buffer".
+ cc = min(available_space, len(data))
if self._sent_data is None:
- self._sent_data = data
+ self._sent_data = data[:cc]
else:
- self._sent_data += data
- return len(data)
+ self._sent_data += data[:cc]
+ self.__send_cc += cc
+ return cc
def setblocking(self, on):
# We only need a faked NO-OP implementation.
pass
'''Clear internal instrumental data.'''
self._sent_data = None
self._sent_addr = None
+ def make_send_ready(self):
+ # pretend that the accrued data has been cleared, making room in
+ # the send buffer.
+ self.__send_cc = 0
class FakeSessionReceiver:
"""
# In the current implementation, the socket should be closed
# immedidately after a successful send.
self.assertEqual(1, s._close_called)
+ # TCP context shouldn't be held in the server.
+ self.assertEqual(0, len(self.server._tcp_ctxs))
+
+ def test_tcp_request_incomplete(self):
+ # set the size of the socket "send buffer" to a small value, which
+ # should cause partial send.
+ s = self.__sock
+ s.proto = socket.IPPROTO_TCP
+ s._send_buflen = 7
+ # before this request there should be no outstanding TCP context.
+ self.assertEqual(0, len(self.server._tcp_ctxs))
+ self.assertTrue(self.server.handle_request((s, TEST_SERVER6,
+ TEST_CLIENT6,
+ create_msg())))
+ # Only the part of data that fit the send buffer should be transmitted.
+ self.assertEqual(s._send_buflen, len(s._sent_data))
+ # the socket is not yet closed.
+ self.assertEqual(0, s._close_called)
+ # and a new context is stored in the server.
+ self.assertEqual(1, len(self.server._tcp_ctxs))
+
+ # clear the "send buffer" of the fake socket, and continue the send
+ # by hand. The next attempt should complete the send, and the combined
+ # data should be the expected response.
+ s.make_send_ready()
+ self.assertEqual(DNSTCPContext.SEND_DONE,
+ self.server._tcp_ctxs[s.fileno].send_ready())
+ self.server._tcp_ctxs[s.fileno].close() # explicit close per convention
+ self.check_update_response(s._sent_data, Rcode.NOERROR(), tcp=True)
+
+ def test_tcp_request_error(self):
+ # initial send() on the TCP socket will fail. The request handling
+ # will be considered failure.
+ s = self.__sock
+ s.proto = socket.IPPROTO_TCP
+ s._raise_on_send = True
+ self.assertFalse(self.server.handle_request((s, TEST_SERVER6,
+ TEST_CLIENT6,
+ create_msg())))
+ # the socket should have been closed.
+ self.assertEqual(1, s._close_called)
def test_request_message(self):
'''Test if the request message stores RRs separately.'''