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