def _send_request(self, parser, rendered_data): """ Send a request and invoke the response parser. @param parser: A parser to parse the data @type parser: Function pointer @param rendered_data: The request's rendered data to send @type rendered_data: Str @return: The response from the server @rtype : HttpResponse """ try: sock = messaging.HttpSock(self._connection_settings) except TransportLayerException as error: RAW_LOGGING(f"{error!s}") # connection failed return HttpResponse() success, response = sock.sendRecv( rendered_data, req_timeout_sec=Settings().max_request_execution_time) if not success: RAW_LOGGING(response.to_str) Monitor().increment_requests_count(self.__class__.__name__) return response
def apply_checkers(checkers, renderings, global_lock): """ Calls each enabled checker from a list of Checker objects @param checkers: A list of checkers to apply @type checkers: List[Checker] @param renderings: Object containing the rendered sequence information @type renderings: RenderedSequence @param global_lock: Lock object used for sync of more than one fuzzing jobs. @type global_lock: thread.Lock @return: None @rtype : None """ for checker in checkers: try: if checker.enabled: RAW_LOGGING( f"Checker: {checker.__class__.__name__} kicks in\n") checker.apply(renderings, global_lock) RAW_LOGGING( f"Checker: {checker.__class__.__name__} kicks out\n") except Exception as error: print(f"Exception {error!s} applying checker {checker}") raise
def sendRecv(self, message, req_timeout_sec=600): """ Sends a specified request to the server and waits for a response @param message: Message to be sent. @type message : Str @param req_timeout_sec: The time, in seconds, to wait for request to complete @type req_timeout_sec : Int @return: False if failure, True if success Response if True returned, Error if False returned @rtype : Tuple (Bool, String) """ try: self._sendRequest(message) if not Settings().use_test_socket: response = HttpResponse(self._recvResponse(req_timeout_sec)) else: response = self._sock.recv() RAW_LOGGING(f'Received: {response.to_str!r}\n') return (True, response) except TransportLayerException as error: response = HttpResponse(str(error).strip('"\'')) if 'timed out' in str(error): response._status_code = TIMEOUT_CODE RAW_LOGGING( f"Reached max req_timeout_sec of {req_timeout_sec}.") elif self._contains_connection_closed(str(error)): response._status_code = CONNECTION_CLOSED_CODE RAW_LOGGING(f"{error!s}") return (False, response) finally: self._closeSocket()
def _refresh(self, last_request): """ Refresh server state and response mapping @param last_request: Last request to fuzz @type last_request: Request @return: The new sequence that was sent during refresh @rtype: Sequence """ # replay the whole sequence except the last request new_seq = self._execute_start_of_sequence() # re-send the last request and analyze the response w.r.t body schema initial_response, response_to_parse = self._render_and_send_data( new_seq, last_request) if not initial_response: return None hints = self._map_response_to_current_body_schema(response_to_parse) if self._acc_response: for tag in hints: self._response_values[tag] = hints[tag] else: self._response_values = hints self._refresh_req = False RAW_LOGGING("Done refreshing the sequence") return new_seq
def _execute_start_of_sequence(self): """ Send all requests in the sequence up until the last request @return: None @rtype : None """ # Copied from InvalidDynamicObjectChecker RAW_LOGGING("Re-rendering and sending start of sequence") new_seq = sequences.Sequence([]) for request in self._sequence.requests[:-1]: new_seq = new_seq + sequences.Sequence(request) initial_response, response_to_parse = self._render_and_send_data( new_seq, request) # Check to make sure a bug wasn't uncovered while executing the # sequence if initial_response: if initial_response.has_bug_code(): self._print_suspect_sequence(new_seq, initial_response) BugBuckets.Instance().update_bug_buckets( new_seq, initial_response.status_code, origin=self.__class__.__name__) if self._acc_response: hints = self._map_response_to_current_body_schema( response_to_parse) for tag in hints: self._response_values[tag] = hints[tag] return new_seq
def _render_hijack_request(self, req): """ Render the last request of the sequence and inspect the status code of the response. If it's any of 20x, we have probably hit a bug. @param req: The hijack request. @type req: Request Class object. @return: None @rtype : None """ self._checker_log.checker_print("Hijack request rendering") RAW_LOGGING("Hijack request rendering") rendered_data, parser = req.render_current( self._req_collection.candidate_values_pool ) rendered_data = self._sequence.resolve_dependencies(rendered_data) rendered_data = self._change_user_identity(rendered_data) response = self._send_request(parser, rendered_data) request_utilities.call_response_parser(parser, response) if response and self._rule_violation(self._sequence, response): self._print_suspect_sequence(self._sequence, response) BugBuckets.Instance().update_bug_buckets( self._sequence, response.status_code, origin=self.__class__.__name__, reproduce=False )
def _render_attacker_subsequence(self, req): """ Helper to render attacker user and try to hijack @param target_type objects. @param req: The hijack request. @type req: Request Class object. @return: None @rtype : None """ # Render subsequnce up to before any producer of @param consumed_types. consumed_types = req.consumes for stopping_length, req in enumerate(self._sequence): # Stop before producing the target type. if req.produces.intersection(consumed_types): break for i in range(stopping_length): request = self._sequence.requests[i] rendered_data, parser = request.render_current( self._req_collection.candidate_values_pool ) rendered_data = self._sequence.resolve_dependencies(rendered_data) rendered_data = self._change_user_identity(rendered_data) response = self._send_request(parser, rendered_data) request_utilities.call_response_parser(parser, response) self._checker_log.checker_print("Subsequence rendering up to: {}".\ format(stopping_length)) RAW_LOGGING(f"Subsequence rendering up to: {stopping_length}")
def decode_buf(buf): try: return buf.decode(UTF8) except Exception as ex: if self.ignore_decoding_failures: RAW_LOGGING(f'Failed to decode data due to {ex}. \ Trying again while ignoring offending bytes.') return buf.decode(UTF8, "ignore") else: raise
def _RAW_LOGGING(log_str): """ Wrapper for the raw network logging function. Necessary to avoid circular dependency with logging. @param log_str: The string to log @type log_str: Str @return: None @rtype : None """ from utils.logger import raw_network_logging as RAW_LOGGING RAW_LOGGING(log_str)
def _namespace_rule(self): """ Try to hijack objects of @param target_types and use them via a secondary attacker user. @param target_types: The types of the target object to attemp hijack. @type target_types: Set @return: None @rtype : None """ # For the target types (target dynamic objects), get the latest # values which we know will exist due to the previous rendering. # We will later on use these old values atop a new rendering. hijacked_values = {} consumed_types = self._sequence.consumes consumed_types = set(itertools.chain(*consumed_types)) # Exit the checker and do not re-render if nothing is consumed since # the checker will have nothing to work on anyways. if not consumed_types: return # Render only last request if not in exhaustive (expensive) mode. # If that last request does not consume anything, stop here. if self._mode != 'exhaustive' and not self._sequence.last_request.consumes: return self._render_original_sequence_start(self._sequence) for type in consumed_types: hijacked_values[type] = dependencies.get_variable(type) self._checker_log.checker_print(f"Hijacked values: {hijacked_values}") RAW_LOGGING(f"Hijacked values: {hijacked_values}") for i, req in enumerate(self._sequence): # Render only last request if not in exhaustive (expensive) mode. if self._mode != 'exhaustive' and i != self._sequence.length - 1: continue # Skip requests that are not consumers. if not req.consumes: continue dependencies.reset_tlb() self._render_attacker_subsequence(req) # Feed hijacked values. for type in hijacked_values: dependencies.set_variable(type, hijacked_values[type]) self._render_hijack_request(req)
def resolve_dependencies(self, data): """ Renders dependent variables. @param data: The rendered payload with dependency placeholders. @type data: String @return: The rendered payload with dependency placeholders substituted by the respective values parsed from the appropriate responses. @rtype : String """ data = str(data).split(dependencies.RDELIM) for i in range(1, len(data), 2): var_name = data[i] data[i] = dependencies.get_variable(var_name) if data[i] == 'None': RAW_LOGGING(f'Dynamic object {var_name} is set to None!') return "".join(data)
def _render_last_request(self, seq): """ Render the last request of the sequence and inspect the status code of the response. If it's any of 20x, we have probably hit a bug. @param seq: The sequence whose last request we will try to render. @type seq: Sequence Class object. @return: None @rtype : None """ request = seq.last_request for rendered_data, parser in\ request.render_iter(self._req_collection.candidate_values_pool, skip=request._current_combination_id): # Hold the lock (because other workers may be rendering the same # request) and check whether the current rendering is known from the # past to lead to invalid status codes. If so, skip the current # rendering. if self._lock is not None: self._lock.acquire() should_skip = Monitor().is_invalid_rendering(request) if self._lock is not None: self._lock.release() # Skip the loop and don't forget to increase the counter. if should_skip: RAW_LOGGING("Skipping rendering: {}".\ format(request._current_combination_id)) request._current_combination_id += 1 continue rendered_data = seq.resolve_dependencies(rendered_data) response = self._send_request(parser, rendered_data) request_utilities.call_response_parser(parser, response) # Append the rendered data to the sent list as we will not be rendering # with the sequence's render function seq.append_data_to_sent_list(rendered_data, parser, response) if response and self._rule_violation(seq, response): self._print_suspect_sequence(seq, response) BugBuckets.Instance().update_bug_buckets( seq, response.status_code, origin=self.__class__.__name__)
def _render_original_sequence(self, seq): """ Helper to re-render original sequence to have better checkers' independence. @param seq: The sequence whose last request we will try to render. @type seq: Sequence Class object. @return: None @rtype : None """ self._checker_log.checker_print("\nRe-rendering original sequence") RAW_LOGGING("Re-rendering original sequence") for request in seq: rendered_data, parser = request.render_current( self._req_collection.candidate_values_pool) rendered_data = seq.resolve_dependencies(rendered_data) response = self._send_request(parser, rendered_data) request_utilities.call_response_parser(parser, response)
def _render_original_sequence_start(self, seq): """ Helper to re-render the start of the original sequence to create the appropriate dynamic objects. Does not send the final target request. @param seq: The sequence whose last request we will try to render. @type seq: Sequence Class object. @return: None @rtype : None """ self._checker_log.checker_print("\nRe-rendering start of original sequence") RAW_LOGGING("Re-rendering start of original sequence") for request in seq.requests[:-1]: rendered_data, parser = request.render_current( self._req_collection.candidate_values_pool ) rendered_data = seq.resolve_dependencies(rendered_data) response = self._send_request(parser, rendered_data) request_utilities.call_response_parser(parser, response)
def _sendRequest(self, message): """ Sends message via current instance of socket object. @param message: Message to be sent. @type message : Str @return: None @rtype : None """ def _get_end_of_header(message): return message.index(DELIM) def _get_start_of_body(message): return _get_end_of_header(message) + len(DELIM) def _append_to_header(message, content): header = message[:_get_end_of_header(message )] + "\r\n" + content + DELIM return header + message[_get_start_of_body(message):] if "Content-Length: " not in message: contentlen = len(message[_get_start_of_body(message):]) message = _append_to_header(message, f"Content-Length: {contentlen}") if self.connection_settings.include_user_agent: message = _append_to_header( message, f"User-Agent: restler/{Settings().version}") # Attempt to throttle the request if necessary self._begin_throttle_request() try: RAW_LOGGING(f'Sending: {message!r}\n') self._sock.sendall(message.encode(UTF8)) except Exception as error: raise TransportLayerException(f"Exception Sending Data: {error!s}") finally: self._end_throttle_request()
def try_async_poll(request_data, response, max_async_wait_time): """ Helper that will poll the server until a certain resource becomes available. This function will check the response from a PUT or POST request to see if it contains one of the known async resource creations strings and, if so, will poll the URL specified in the response until the resource is said to be available or a max timeout has been reached. @param request_data: The request that was sent's data string @type request_data: Str @param response: The response returned after @request was sent @type response: Str @param max_async_wait_time: The maximum amount of time we will wait (in seconds) for the resource to become available @type max_async_wait_time: Int @return: A tuple containing: - The response for parsing, which will either be the @response passed to this function or the response from the GET request if it was an async request. - A boolean that is True if there was an error when creating the resource - A Boolean that tells the caller whether or not async polling was attempted @rtype : Tuple(HttpResponse, boolean, boolean) """ from utils.logger import raw_network_logging as RAW_LOGGING from utils.logger import print_async_results as LOG_RESULTS response_to_parse = response resource_error = False async_waited = False if Settings().wait_for_async_resource_creation and max_async_wait_time > 0\ and (request_data.startswith("PUT") or request_data.startswith("POST")): # Get the request used for polling the resource availability data, data_in_poll_response = get_polling_request(response) if data: async_waited = True poll_wait_seconds = 1 start_time = time.time() RAW_LOGGING("Waiting for resource to be available...") while (time.time() - start_time) < max_async_wait_time: try: # Send the polling request poll_response = request_utilities.send_request_data(data) time_str = str(round((time.time() - start_time), 2)) if data_in_poll_response: # If this returned a '200' status code, the response should contain the parsable data. # Otherwise, continue to poll as the resource has not yet been created. These types will # return a '202 - Accepted' while the resource is still being created. if poll_response.status_code == '200': LOG_RESULTS( request_data, f"Resource creation succeeded after {time_str} seconds." ) # Break and return the polling response to be parsed response_to_parse = poll_response break else: # The data is not in the polling response and must be fetched separately. # This comes from Azure-Async responses that do not contain a Location: field # Check for the status of the resource response_body = json.loads(poll_response.json_body) done = str(response_body["status"]).lower() if done == "succeeded": LOG_RESULTS( request_data, f"Resource creation succeeded after {time_str} seconds." ) get_response = try_parse_GET_request(request_data) if get_response: response_to_parse = get_response break elif done == "failed": LOG_RESULTS( request_data, f"The server reported that the resource creation Failed after {time_str} seconds." ) resource_error = True break except json.JSONDecodeError as err: LOG_RESULTS( request_data, "Failed to parse body of async response, retrying.") # This may have been due to a connection failure. Retry until max_async_wait_time. time.sleep(poll_wait_seconds) continue except Exception as err: LOG_RESULTS( request_data, f"An exception occurred while parsing the poll message: {err!s}" ) break try: if not poll_response.status_code.startswith('2'): LOG_RESULTS( request_data, f"Resource creation failed after {time_str} seconds" f" because status code '{poll_response.status_code}' was received." ) break except Exception as err: LOG_RESULTS( request_data, f"An exception occurred while parsing the poll message: {err!s}" ) break time.sleep(poll_wait_seconds) else: RAW_LOGGING( "Resource polling timed out before the resource was available." ) LOG_RESULTS( request_data, f"Failed to create resource in {max_async_wait_time} seconds." ) RAW_LOGGING("Attempting to get resources from GET request...") get_response = try_parse_GET_request(request_data) if get_response: # Use response from the GET request for parsing. If the GET request failed, # the caller will know to try and use the original PUT response for parsing response_to_parse = get_response return response_to_parse, resource_error, async_waited
def get_polling_request(response): """ Helper that exctracts the URL from an azure-async-operation response and then creates and returns a valid GET request for it. @param response: The response to parse for the URL @type response: Str @return: Tuple containing the rendered data for the GET request and a boolean that indicates whether or not to expect the parsable data to be included in the polling request. @rtype : Tuple(str, bool) """ from utils.logger import raw_network_logging as RAW_LOGGING data_in_poll_response = False if not response or not response.to_str: return None, False # In our experience with Azure cloud services, there have been two types of indications for async resource creation. # One that contains "Azure-AsyncOperation:<polling-url>" in the response and one that contains "Location:<polling-url>" # in the response. # - For responses that contain only the Azure-AsyncOperation type, the response from the GET request sent # to the supplied polling-URL returns only a "status" that says InProgress, Succeeded, or Failed. Any additional, # parsable, data about the resource must be retrieved by creating and sending a GET request to the original URI. # - For responses that contain the Location: type, the GET request sent to the provided polling-url will include the # parsable resource data along with its "provisioningState", which can be Creating, Succeeded, or Failed. # The Location: types will also be returned with "202 - Accepted" status codes. These responses will look very much # like the original response from the PUT request, so they can be parsed as they would normally. # - It is possible to see both Azure-Async AND Location in the response. If this is the case (at least in our experience), # the response will act as though it is the Location type and the response will contain the parsable data. # See https://github.com/Azure/autorest/blob/master/docs/extensions/readme.md#x-ms-long-running-operation-options # for more information on async resource creation in Azure. LocationSearchStr = "Location: " AzureAsyncSearchStr = "Azure-AsyncOperation: " response_str = response.to_str if response.status_code == '202' or response.status_code == '201' and LocationSearchStr in response_str: url = response_str.split(LocationSearchStr)[1] data_in_poll_response = True elif AzureAsyncSearchStr in response_str: url = response_str.split(AzureAsyncSearchStr)[1] else: return None, False url_split = url.split("https://") if len(url_split) > 1: url = url_split[1] else: return None, False try: url_part = url.partition('/') hostname = url_part[0] url = url_part[1] + url_part[2] token_value = request_utilities.get_latest_token_value() if token_value is None: token_value = '' # Create the GET request to poll the url req_data = 'GET ' + url.split('\r\n')[0] +\ " HTTP/1.1\r\nAccept: application/json\r\nHost: " +\ hostname + "\r\nContent-Type: application/json\r\n" +\ token_value + "\r\n" return req_data, data_in_poll_response except Exception as err: # Log any exception and then return that no polling request exists. # It is better to skip polling and continue the fuzzing run than to crash or end. # The side effect will be that RESTler may attempt to use a resource before it is # created, which may lower coverage during a smoke test, but could also potentially # lead to a bug being uncovered in the service being fuzzed. RAW_LOGGING(f"Exception while creating the polling request: {err!s}") return None, False
def apply_destructors(self, destructors, max_aged_objects=100): """ Background task trying to delete evicted objects that are in @overflowing dictionary. @param overflowing: Dictionary of overflowing objects. We need to issue deletes to all these objects @type overflowing: Dict @param destructors: The Request class objects required to delete. @type destructors: Dict @param max_aged_objects: Maximum number of objects we allow to age and will retry to delete (since delete is idempotent this is fine). @type max_aged_objects: Int @return: None @rtype : None NOTE: This function is invoked without any lock since overflowing objects are already dead (not referenced by anything) and are just aging here. """ if not self.overflowing: return from engine.errors import TransportLayerException from engine.transport_layer import messaging from utils.logger import raw_network_logging as RAW_LOGGING from utils.logger import garbage_collector_logging as CUSTOM_LOGGING # For each object in the overflowing area, whose destructor is # available, render the corresponding request, send the request, # and then check the status code. If the resource has been determined # to be removed, delete the object from the overflow area. # At the end keep track of only up to @param max_aged_objects # remaining objects. for type in destructors: destructor = destructors[type] deleted_list = [] if self.overflowing[type]: CUSTOM_LOGGING("{}: Trying garbage collection of * {} * objects".\ format(formatting.timestamp(), len(self.overflowing[type]))) CUSTOM_LOGGING(f"{type}: {self.overflowing[type]}") # Iterate in reverse to give priority to newest resources for value in reversed(self.overflowing[type]): rendered_data, _ = destructor.\ render_current(self.req_collection.candidate_values_pool) # replace dynamic parameters fully_rendered_data = str(rendered_data) fully_rendered_data = fully_rendered_data.replace( RDELIM + type + RDELIM, value) if fully_rendered_data: try: # Establish connection to the server sock = messaging.HttpSock( Settings().connection_settings) except TransportLayerException as error: RAW_LOGGING(f"{error!s}") return # Send the request and receive the response success, response = sock.sendRecv(fully_rendered_data) if success: self.monitor.increment_requests_count('gc') else: RAW_LOGGING(response.to_str) # Check to see if the DELETE operation is complete try: if response.status_code in DELETED_CODES: deleted_list.append(value) except Exception: pass # Remove deleted items from the to-delete cache for value in deleted_list: self.overflowing[type].remove(value) self.overflowing[type] = self.overflowing[type][-max_aged_objects:]
def render(self, candidate_values_pool, lock, preprocessing=False, postprocessing=False): """ Core routine that performs the rendering of restler sequences. In principal all requests of a sequence are being constantly rendered with a specific values combination @param request._current_combination_id which we know in the past led to a valid rendering and only the last request of the sequence is being rendered iteratively with all feasible value combinations. Each time a "valid rendering" is found for the last request of the sequence (where "valid rendering" is defined according to "VALID_CODES"), the routine returns a new sequence which has an end-to-end (i.e., all requests) "valid rendering" and can be added in the sequences collection in order to be used in the future as a building block for longer sequences. @param candidate_values_pool: The pool of values for primitive types. @type candidate_values_pool: Dict @param lock: Lock object used for sync of more than one fuzzing jobs. @type lock: thread.Lock object @param preprocessing: Set to true if rendering during preprocessing @type preprocessing: Bool @return: A RenderedSequence object containing the sequence, the final request's response, whether or not the final request received a valid status code, and a FailureInformation enum if there was a failure or bug detected during rendering. @rtype : RenderedSequence """ # Try rendering all primitive type value combinations for last request request = self.last_request # for clarity reasons, don't log requests whose render iterator is over if request._current_combination_id <\ request.num_combinations(candidate_values_pool): CUSTOM_LOGGING(self, candidate_values_pool) self._sent_request_data_list = [] for rendered_data, parser in\ request.render_iter(candidate_values_pool, skip=request._current_combination_id, preprocessing=preprocessing): # Hold the lock (because other workers may be rendering the same # request) and check whether the current rendering is known from the # past to lead to invalid status codes. If so, skip the current # rendering. if lock is not None: lock.acquire() should_skip = Monitor().is_invalid_rendering(request) if lock is not None: lock.release() # Skip the loop and don't forget to increase the counter. if should_skip: RAW_LOGGING("Skipping rendering: {}".\ format(request._current_combination_id)) request._current_combination_id += 1 continue # Clean up internal state self.status_codes = [] dependencies.reset_tlb() sequence_failed = False # Step A: Static template rendering # Render last known valid combination of primitive type values # for every request until the last for i in range(len(self.requests) - 1): prev_request = self.requests[i] prev_rendered_data, prev_parser =\ prev_request.render_current(candidate_values_pool, preprocessing=preprocessing) # substitute reference placeholders with resolved values if not Settings().ignore_dependencies: prev_rendered_data =\ self.resolve_dependencies(prev_rendered_data) prev_req_async_wait = Settings( ).get_max_async_resource_creation_time(prev_request.request_id) prev_producer_timing_delay = Settings( ).get_producer_timing_delay(prev_request.request_id) prev_response = request_utilities.send_request_data( prev_rendered_data) prev_response_to_parse, resource_error, async_waited = async_request_utilities.try_async_poll( prev_rendered_data, prev_response, prev_req_async_wait) prev_parser_threw_exception = False # Response may not exist if there was an error sending the request or a timeout if prev_parser and prev_response_to_parse: prev_parser_threw_exception = not request_utilities.call_response_parser( prev_parser, prev_response_to_parse, prev_request) prev_status_code = prev_response.status_code # If the async logic waited for the resource, this wait already included the required # producer timing delay. Here, set the producer timing delay to zero, so this wait is # skipped both below for this request and during replay if async_waited: prev_producer_timing_delay = 0 else: prev_req_async_wait = 0 self.append_data_to_sent_list(prev_rendered_data, prev_parser, prev_response, prev_producer_timing_delay, prev_req_async_wait) if not prev_status_code: logger.write_to_main( f"Error: Failed to get status code during valid sequence re-rendering.\n" ) sequence_failed = True break if prev_response.has_bug_code(): BugBuckets.Instance().update_bug_buckets(self, prev_status_code, reproduce=False, lock=lock) sequence_failed = True break if prev_parser_threw_exception: logger.write_to_main( "Error: Parser exception occurred during valid sequence re-rendering.\n" ) sequence_failed = True break if resource_error: logger.write_to_main( "Error: The resource was left in a Failed state after creation during valid sequence re-rendering.\n" ) sequence_failed = True break # If the previous request is a resource generator and we did not perform an async resource # creation wait, then wait for the specified duration in order for the backend to have a # chance to create the resource. if prev_producer_timing_delay > 0 and prev_request.is_resource_generator( ): print( f"Pausing for {prev_producer_timing_delay} seconds, request is a generator..." ) time.sleep(prev_producer_timing_delay) # register latest client/server interaction timestamp_micro = int(time.time() * 10**6) self.status_codes.append( status_codes_monitor.RequestExecutionStatus( timestamp_micro, prev_request.hex_definition, prev_status_code, prev_response.has_valid_code(), False)) if sequence_failed: self.status_codes.append( status_codes_monitor.RequestExecutionStatus( int(time.time() * 10**6), request.hex_definition, RESTLER_INVALID_CODE, False, True)) Monitor().update_status_codes_monitor(self, self.status_codes, lock) return RenderedSequence( failure_info=FailureInformation.SEQUENCE) # Step B: Dynamic template rendering # substitute reference placeholders with ressoved values # for the last request if not Settings().ignore_dependencies: rendered_data = self.resolve_dependencies(rendered_data) # Render candidate value combinations seeking for valid error codes request._current_combination_id += 1 req_async_wait = Settings().get_max_async_resource_creation_time( request.request_id) response = request_utilities.send_request_data(rendered_data) response_to_parse, resource_error, _ = async_request_utilities.try_async_poll( rendered_data, response, req_async_wait) parser_exception_occurred = False # Response may not exist if there was an error sending the request or a timeout if parser and response_to_parse: parser_exception_occurred = not request_utilities.call_response_parser( parser, response_to_parse, request) status_code = response.status_code if not status_code: return RenderedSequence(None) self.append_data_to_sent_list(rendered_data, parser, response, max_async_wait_time=req_async_wait) rendering_is_valid = not parser_exception_occurred\ and not resource_error\ and response.has_valid_code() # register latest client/server interaction and add to the status codes list response_datetime = datetime.datetime.now(datetime.timezone.utc) timestamp_micro = int(response_datetime.timestamp() * 10**6) self.status_codes.append( status_codes_monitor.RequestExecutionStatus( timestamp_micro, request.hex_definition, status_code, rendering_is_valid, False)) # add sequence's error codes to bug buckets. if response.has_bug_code(): BugBuckets.Instance().update_bug_buckets(self, status_code, lock=lock) Monitor().update_status_codes_monitor(self, self.status_codes, lock) # Register current rendering's status. if lock is not None: lock.acquire() Monitor().update_renderings_monitor(request, rendering_is_valid) if lock is not None: lock.release() if Monitor().remaining_time_budget <= 0 and not postprocessing: raise TimeOutException("Exceeded Timeout") if lock is not None: lock.acquire() # Deep copying here will try copying anything the class has access # to including the shared client monitor, which we update in the # above code block holding the lock, but then we release the # lock and one thread can be updating while another is copying. # This is a typlical nasty read after write syncronization bug. duplicate = copy.deepcopy(self) if lock is not None: lock.release() datetime_format = "%Y-%m-%d %H:%M:%S" # return a rendered clone if response indicates a valid status code if rendering_is_valid or Settings().ignore_feedback: return RenderedSequence( duplicate, valid=True, final_request_response=response, response_datetime=response_datetime.strftime( datetime_format)) else: information = None if response.has_valid_code(): if parser_exception_occurred: information = FailureInformation.PARSER elif resource_error: information = FailureInformation.RESOURCE_CREATION elif response.has_bug_code(): information = FailureInformation.BUG return RenderedSequence( duplicate, valid=False, failure_info=information, final_request_response=response, response_datetime=response_datetime.strftime( datetime_format)) return RenderedSequence(None)