1""" 2""" 3 4# Created on 2013.07.15 5# 6# Author: Giovanni Cannata 7# 8# Copyright 2013 - 2020 Giovanni Cannata 9# 10# This file is part of ldap3. 11# 12# ldap3 is free software: you can redistribute it and/or modify 13# it under the terms of the GNU Lesser General Public License as published 14# by the Free Software Foundation, either version 3 of the License, or 15# (at your option) any later version. 16# 17# ldap3 is distributed in the hope that it will be useful, 18# but WITHOUT ANY WARRANTY; without even the implied warranty of 19# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20# GNU Lesser General Public License for more details. 21# 22# You should have received a copy of the GNU Lesser General Public License 23# along with ldap3 in the COPYING and COPYING.LESSER files. 24# If not, see <http://www.gnu.org/licenses/>. 25 26import socket 27 28from .. import SEQUENCE_TYPES, get_config_parameter 29from ..core.exceptions import LDAPSocketReceiveError, communication_exception_factory, LDAPExceptionError, LDAPExtensionError, LDAPOperationResult 30from ..strategy.base import BaseStrategy, SESSION_TERMINATED_BY_SERVER, RESPONSE_COMPLETE, TRANSACTION_ERROR 31from ..protocol.rfc4511 import LDAPMessage 32from ..utils.log import log, log_enabled, ERROR, NETWORK, EXTENDED, format_ldap_message 33from ..utils.asn1 import decoder, decode_message_fast 34 35LDAP_MESSAGE_TEMPLATE = LDAPMessage() 36 37 38# noinspection PyProtectedMember 39class SyncStrategy(BaseStrategy): 40 """ 41 This strategy is synchronous. You send the request and get the response 42 Requests return a boolean value to indicate the result of the requested Operation 43 Connection.response will contain the whole LDAP response for the messageId requested in a dict form 44 Connection.request will contain the result LDAP message in a dict form 45 """ 46 47 def __init__(self, ldap_connection): 48 BaseStrategy.__init__(self, ldap_connection) 49 self.sync = True 50 self.no_real_dsa = False 51 self.pooled = False 52 self.can_stream = False 53 self.socket_size = get_config_parameter('SOCKET_SIZE') 54 55 def open(self, reset_usage=True, read_server_info=True): 56 BaseStrategy.open(self, reset_usage, read_server_info) 57 if read_server_info and not self.connection._deferred_open: 58 try: 59 self.connection.refresh_server_info() 60 except LDAPOperationResult: # catch errors from server if raise_exception = True 61 self.connection.server._dsa_info = None 62 self.connection.server._schema_info = None 63 64 def _start_listen(self): 65 if not self.connection.listening and not self.connection.closed: 66 self.connection.listening = True 67 68 def receiving(self): 69 """ 70 Receives data over the socket 71 Checks if the socket is closed 72 """ 73 messages = [] 74 receiving = True 75 unprocessed = b'' 76 data = b'' 77 get_more_data = True 78 exc = None 79 while receiving: 80 if get_more_data: 81 try: 82 data = self.connection.socket.recv(self.socket_size) 83 except (OSError, socket.error, AttributeError) as e: 84 self.connection.last_error = 'error receiving data: ' + str(e) 85 try: # try to close the connection before raising exception 86 self.close() 87 except (socket.error, LDAPExceptionError): 88 pass 89 if log_enabled(ERROR): 90 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 91 # raise communication_exception_factory(LDAPSocketReceiveError, exc)(self.connection.last_error) 92 raise communication_exception_factory(LDAPSocketReceiveError, type(e)(str(e)))(self.connection.last_error) 93 unprocessed += data 94 if len(data) > 0: 95 length = BaseStrategy.compute_ldap_message_size(unprocessed) 96 if length == -1: # too few data to decode message length 97 get_more_data = True 98 continue 99 if len(unprocessed) < length: 100 get_more_data = True 101 else: 102 if log_enabled(NETWORK): 103 log(NETWORK, 'received %d bytes via <%s>', len(unprocessed[:length]), self.connection) 104 messages.append(unprocessed[:length]) 105 unprocessed = unprocessed[length:] 106 get_more_data = False 107 if len(unprocessed) == 0: 108 receiving = False 109 else: 110 receiving = False 111 112 if log_enabled(NETWORK): 113 log(NETWORK, 'received %d ldap messages via <%s>', len(messages), self.connection) 114 return messages 115 116 def post_send_single_response(self, message_id): 117 """ 118 Executed after an Operation Request (except Search) 119 Returns the result message or None 120 """ 121 responses, result = self.get_response(message_id) 122 self.connection.result = result 123 if result['type'] == 'intermediateResponse': # checks that all responses are intermediates (there should be only one) 124 for response in responses: 125 if response['type'] != 'intermediateResponse': 126 self.connection.last_error = 'multiple messages received error' 127 if log_enabled(ERROR): 128 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 129 raise LDAPSocketReceiveError(self.connection.last_error) 130 131 responses.append(result) 132 return responses 133 134 def post_send_search(self, message_id): 135 """ 136 Executed after a search request 137 Returns the result message and store in connection.response the objects found 138 """ 139 responses, result = self.get_response(message_id) 140 self.connection.result = result 141 if isinstance(responses, SEQUENCE_TYPES): 142 self.connection.response = responses[:] # copy search result entries 143 return responses 144 145 self.connection.last_error = 'error receiving response' 146 if log_enabled(ERROR): 147 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 148 raise LDAPSocketReceiveError(self.connection.last_error) 149 150 def _get_response(self, message_id, timeout): 151 """ 152 Performs the capture of LDAP response for SyncStrategy 153 """ 154 ldap_responses = [] 155 response_complete = False 156 while not response_complete: 157 responses = self.receiving() 158 if responses: 159 for response in responses: 160 if len(response) > 0: 161 if self.connection.usage: 162 self.connection._usage.update_received_message(len(response)) 163 if self.connection.fast_decoder: 164 ldap_resp = decode_message_fast(response) 165 dict_response = self.decode_response_fast(ldap_resp) 166 else: 167 ldap_resp, _ = decoder.decode(response, asn1Spec=LDAP_MESSAGE_TEMPLATE) # unprocessed unused because receiving() waits for the whole message 168 dict_response = self.decode_response(ldap_resp) 169 if log_enabled(EXTENDED): 170 log(EXTENDED, 'ldap message received via <%s>:%s', self.connection, format_ldap_message(ldap_resp, '<<')) 171 if int(ldap_resp['messageID']) == message_id: 172 ldap_responses.append(dict_response) 173 if dict_response['type'] not in ['searchResEntry', 'searchResRef', 'intermediateResponse']: 174 response_complete = True 175 elif int(ldap_resp['messageID']) == 0: # 0 is reserved for 'Unsolicited Notification' from server as per RFC4511 (paragraph 4.4) 176 if dict_response['responseName'] == '1.3.6.1.4.1.1466.20036': # Notice of Disconnection as per RFC4511 (paragraph 4.4.1) 177 return SESSION_TERMINATED_BY_SERVER 178 elif dict_response['responseName'] == '2.16.840.1.113719.1.27.103.4': # Novell LDAP transaction error unsolicited notification 179 return TRANSACTION_ERROR 180 else: 181 self.connection.last_error = 'unknown unsolicited notification from server' 182 if log_enabled(ERROR): 183 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 184 raise LDAPSocketReceiveError(self.connection.last_error) 185 elif int(ldap_resp['messageID']) != message_id and dict_response['type'] == 'extendedResp': 186 self.connection.last_error = 'multiple extended responses to a single extended request' 187 if log_enabled(ERROR): 188 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 189 raise LDAPExtensionError(self.connection.last_error) 190 # pass # ignore message with invalid messageId when receiving multiple extendedResp. This is not allowed by RFC4511 but some LDAP server do it 191 else: 192 self.connection.last_error = 'invalid messageId received' 193 if log_enabled(ERROR): 194 log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 195 raise LDAPSocketReceiveError(self.connection.last_error) 196 # response = unprocessed 197 # if response: # if this statement is removed unprocessed data will be processed as another message 198 # self.connection.last_error = 'unprocessed substrate error' 199 # if log_enabled(ERROR): 200 # log(ERROR, '<%s> for <%s>', self.connection.last_error, self.connection) 201 # raise LDAPSocketReceiveError(self.connection.last_error) 202 else: 203 return SESSION_TERMINATED_BY_SERVER 204 ldap_responses.append(RESPONSE_COMPLETE) 205 206 return ldap_responses 207 208 def set_stream(self, value): 209 raise NotImplementedError 210 211 def get_stream(self): 212 raise NotImplementedError 213