github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-core-master/rest_api/sawtooth_rest_api/route_handlers.py (about) 1 # Copyright 2016, 2017 Intel Corporation 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 # ------------------------------------------------------------------------------ 15 16 import asyncio 17 import re 18 import logging 19 import json 20 import base64 21 from aiohttp import web 22 23 # pylint: disable=no-name-in-module,import-error 24 # needed for the google.protobuf imports to pass pylint 25 from google.protobuf.json_format import MessageToDict 26 from google.protobuf.message import DecodeError 27 28 from sawtooth_rest_api.protobuf.validator_pb2 import Message 29 30 import sawtooth_rest_api.exceptions as errors 31 import sawtooth_rest_api.error_handlers as error_handlers 32 from sawtooth_rest_api.messaging import DisconnectError 33 from sawtooth_rest_api.messaging import SendBackoffTimeoutError 34 from sawtooth_rest_api.protobuf import client_transaction_pb2 35 from sawtooth_rest_api.protobuf import client_list_control_pb2 36 from sawtooth_rest_api.protobuf import client_batch_submit_pb2 37 from sawtooth_rest_api.protobuf import client_state_pb2 38 from sawtooth_rest_api.protobuf import client_block_pb2 39 from sawtooth_rest_api.protobuf import client_batch_pb2 40 from sawtooth_rest_api.protobuf import client_receipt_pb2 41 from sawtooth_rest_api.protobuf import client_peers_pb2 42 from sawtooth_rest_api.protobuf import client_status_pb2 43 from sawtooth_rest_api.protobuf.block_pb2 import BlockHeader 44 from sawtooth_rest_api.protobuf.batch_pb2 import BatchList 45 from sawtooth_rest_api.protobuf.batch_pb2 import BatchHeader 46 from sawtooth_rest_api.protobuf.transaction_pb2 import TransactionHeader 47 48 # pylint: disable=too-many-lines 49 50 DEFAULT_TIMEOUT = 300 51 LOGGER = logging.getLogger(__name__) 52 53 54 class CounterWrapper(): 55 def __init__(self, counter=None): 56 self._counter = counter 57 58 def inc(self): 59 if self._counter: 60 self._counter.inc() 61 62 63 class NoopTimerContext(): 64 def __enter__(self): 65 pass 66 67 def __exit__(self, exception_type, exception_value, traceback): 68 pass 69 70 def stop(self): 71 pass 72 73 74 class TimerWrapper(): 75 def __init__(self, timer=None): 76 self._timer = timer 77 self._noop = NoopTimerContext() 78 79 def time(self): 80 if self._timer: 81 return self._timer.time() 82 return self._noop 83 84 85 class RouteHandler(object): 86 """Contains a number of aiohttp handlers for endpoints in the Rest Api. 87 88 Each handler takes an aiohttp Request object, and uses the data in 89 that request to send Protobuf message to a validator. The Protobuf response 90 is then parsed, and finally an aiohttp Response object is sent back 91 to the client with JSON formatted data and metadata. 92 93 If something goes wrong, an aiohttp HTTP exception is raised or returned 94 instead. 95 96 Args: 97 connection (:obj: messaging.Connection): The object that communicates 98 with the validator. 99 timeout (int, optional): The time in seconds before the Api should 100 cancel a request and report that the validator is unavailable. 101 """ 102 103 def __init__( 104 self, loop, connection, 105 timeout=DEFAULT_TIMEOUT, metrics_registry=None): 106 self._loop = loop 107 self._connection = connection 108 self._timeout = timeout 109 if metrics_registry: 110 self._post_batches_count = CounterWrapper( 111 metrics_registry.counter('post_batches_count')) 112 self._post_batches_error = CounterWrapper( 113 metrics_registry.counter('post_batches_error')) 114 self._post_batches_total_time = TimerWrapper( 115 metrics_registry.timer('post_batches_total_time')) 116 self._post_batches_validator_time = TimerWrapper( 117 metrics_registry.timer('post_batches_validator_time')) 118 else: 119 self._post_batches_count = CounterWrapper() 120 self._post_batches_error = CounterWrapper() 121 self._post_batches_total_time = TimerWrapper() 122 self._post_batches_validator_time = TimerWrapper() 123 124 async def submit_batches(self, request): 125 """Accepts a binary encoded BatchList and submits it to the validator. 126 127 Request: 128 body: octet-stream BatchList of one or more Batches 129 Response: 130 status: 131 - 202: Batches submitted and pending 132 link: /batches or /batch_statuses link for submitted batches 133 134 """ 135 timer_ctx = self._post_batches_total_time.time() 136 self._post_batches_count.inc() 137 138 # Parse request 139 if request.headers['Content-Type'] != 'application/octet-stream': 140 LOGGER.debug( 141 'Submission headers had wrong Content-Type: %s', 142 request.headers['Content-Type']) 143 self._post_batches_error.inc() 144 raise errors.SubmissionWrongContentType() 145 146 body = await request.read() 147 if not body: 148 LOGGER.debug('Submission contained an empty body') 149 self._post_batches_error.inc() 150 raise errors.NoBatchesSubmitted() 151 152 try: 153 batch_list = BatchList() 154 batch_list.ParseFromString(body) 155 except DecodeError: 156 LOGGER.debug('Submission body could not be decoded: %s', body) 157 self._post_batches_error.inc() 158 raise errors.BadProtobufSubmitted() 159 160 # Query validator 161 error_traps = [error_handlers.BatchInvalidTrap, 162 error_handlers.BatchQueueFullTrap] 163 validator_query = client_batch_submit_pb2.ClientBatchSubmitRequest( 164 batches=batch_list.batches) 165 166 with self._post_batches_validator_time.time(): 167 await self._query_validator( 168 Message.CLIENT_BATCH_SUBMIT_REQUEST, 169 client_batch_submit_pb2.ClientBatchSubmitResponse, 170 validator_query, 171 error_traps) 172 173 # Build response envelope 174 id_string = ','.join(b.header_signature for b in batch_list.batches) 175 176 status = 202 177 link = self._build_url(request, path='/batch_statuses', id=id_string) 178 179 retval = self._wrap_response( 180 request, 181 metadata={'link': link}, 182 status=status) 183 184 timer_ctx.stop() 185 return retval 186 187 async def list_statuses(self, request): 188 """Fetches the committed status of batches by either a POST or GET. 189 190 Request: 191 body: A JSON array of one or more id strings (if POST) 192 query: 193 - id: A comma separated list of up to 15 ids (if GET) 194 - wait: Request should not return until all batches committed 195 196 Response: 197 data: A JSON object, with batch ids as keys, and statuses as values 198 link: The /batch_statuses link queried (if GET) 199 """ 200 error_traps = [error_handlers.StatusResponseMissing] 201 202 # Parse batch ids from POST body, or query paramaters 203 if request.method == 'POST': 204 if request.headers['Content-Type'] != 'application/json': 205 LOGGER.debug( 206 'Request headers had wrong Content-Type: %s', 207 request.headers['Content-Type']) 208 raise errors.StatusWrongContentType() 209 210 ids = await request.json() 211 212 if (not ids 213 or not isinstance(ids, list) 214 or not all(isinstance(i, str) for i in ids)): 215 LOGGER.debug('Request body was invalid: %s', ids) 216 raise errors.StatusBodyInvalid() 217 for i in ids: 218 self._validate_id(i) 219 220 else: 221 ids = self._get_filter_ids(request) 222 if not ids: 223 LOGGER.debug('Request for statuses missing id query') 224 raise errors.StatusIdQueryInvalid() 225 226 # Query validator 227 validator_query = \ 228 client_batch_submit_pb2.ClientBatchStatusRequest( 229 batch_ids=ids) 230 self._set_wait(request, validator_query) 231 232 response = await self._query_validator( 233 Message.CLIENT_BATCH_STATUS_REQUEST, 234 client_batch_submit_pb2.ClientBatchStatusResponse, 235 validator_query, 236 error_traps) 237 238 # Send response 239 if request.method != 'POST': 240 metadata = self._get_metadata(request, response) 241 else: 242 metadata = None 243 244 data = self._drop_id_prefixes( 245 self._drop_empty_props(response['batch_statuses'])) 246 247 return self._wrap_response(request, data=data, metadata=metadata) 248 249 async def list_state(self, request): 250 """Fetches list of data entries, optionally filtered by address prefix. 251 252 Request: 253 query: 254 - head: The id of the block to use as the head of the chain 255 - address: Return entries whose addresses begin with this 256 prefix 257 258 Response: 259 data: An array of leaf objects with address and data keys 260 head: The head used for this query (most recent if unspecified) 261 link: The link to this exact query, including head block 262 paging: Paging info and nav, like total resources and a next link 263 """ 264 paging_controls = self._get_paging_controls(request) 265 266 head, root = await self._head_to_root(request.url.query.get( 267 'head', None)) 268 validator_query = client_state_pb2.ClientStateListRequest( 269 state_root=root, 270 address=request.url.query.get('address', None), 271 sorting=self._get_sorting_message(request, "default"), 272 paging=self._make_paging_message(paging_controls)) 273 274 response = await self._query_validator( 275 Message.CLIENT_STATE_LIST_REQUEST, 276 client_state_pb2.ClientStateListResponse, 277 validator_query) 278 279 return self._wrap_paginated_response( 280 request=request, 281 response=response, 282 controls=paging_controls, 283 data=response.get('entries', []), 284 head=head) 285 286 async def fetch_state(self, request): 287 """Fetches data from a specific address in the validator's state tree. 288 289 Request: 290 query: 291 - head: The id of the block to use as the head of the chain 292 - address: The 70 character address of the data to be fetched 293 294 Response: 295 data: The base64 encoded binary data stored at that address 296 head: The head used for this query (most recent if unspecified) 297 link: The link to this exact query, including head block 298 """ 299 error_traps = [ 300 error_handlers.InvalidAddressTrap, 301 error_handlers.StateNotFoundTrap] 302 303 address = request.match_info.get('address', '') 304 head = request.url.query.get('head', None) 305 306 head, root = await self._head_to_root(head) 307 response = await self._query_validator( 308 Message.CLIENT_STATE_GET_REQUEST, 309 client_state_pb2.ClientStateGetResponse, 310 client_state_pb2.ClientStateGetRequest( 311 state_root=root, address=address), 312 error_traps) 313 314 return self._wrap_response( 315 request, 316 data=response['value'], 317 metadata=self._get_metadata(request, response, head=head)) 318 319 async def list_blocks(self, request): 320 """Fetches list of blocks from validator, optionally filtered by id. 321 322 Request: 323 query: 324 - head: The id of the block to use as the head of the chain 325 - id: Comma separated list of block ids to include in results 326 327 Response: 328 data: JSON array of fully expanded Block objects 329 head: The head used for this query (most recent if unspecified) 330 link: The link to this exact query, including head block 331 paging: Paging info and nav, like total resources and a next link 332 """ 333 paging_controls = self._get_paging_controls(request) 334 validator_query = client_block_pb2.ClientBlockListRequest( 335 head_id=self._get_head_id(request), 336 block_ids=self._get_filter_ids(request), 337 sorting=self._get_sorting_message(request, "block_num"), 338 paging=self._make_paging_message(paging_controls)) 339 340 response = await self._query_validator( 341 Message.CLIENT_BLOCK_LIST_REQUEST, 342 client_block_pb2.ClientBlockListResponse, 343 validator_query) 344 345 return self._wrap_paginated_response( 346 request=request, 347 response=response, 348 controls=paging_controls, 349 data=[self._expand_block(b) for b in response['blocks']]) 350 351 async def fetch_block(self, request): 352 """Fetches a specific block from the validator, specified by id. 353 Request: 354 path: 355 - block_id: The 128-character id of the block to be fetched 356 357 Response: 358 data: A JSON object with the data from the fully expanded Block 359 link: The link to this exact query 360 """ 361 error_traps = [error_handlers.BlockNotFoundTrap] 362 363 block_id = request.match_info.get('block_id', '') 364 self._validate_id(block_id) 365 366 response = await self._query_validator( 367 Message.CLIENT_BLOCK_GET_BY_ID_REQUEST, 368 client_block_pb2.ClientBlockGetResponse, 369 client_block_pb2.ClientBlockGetByIdRequest(block_id=block_id), 370 error_traps) 371 372 return self._wrap_response( 373 request, 374 data=self._expand_block(response['block']), 375 metadata=self._get_metadata(request, response)) 376 377 async def list_batches(self, request): 378 """Fetches list of batches from validator, optionally filtered by id. 379 380 Request: 381 query: 382 - head: The id of the block to use as the head of the chain 383 - id: Comma separated list of batch ids to include in results 384 385 Response: 386 data: JSON array of fully expanded Batch objects 387 head: The head used for this query (most recent if unspecified) 388 link: The link to this exact query, including head block 389 paging: Paging info and nav, like total resources and a next link 390 """ 391 paging_controls = self._get_paging_controls(request) 392 validator_query = client_batch_pb2.ClientBatchListRequest( 393 head_id=self._get_head_id(request), 394 batch_ids=self._get_filter_ids(request), 395 sorting=self._get_sorting_message(request, "default"), 396 paging=self._make_paging_message(paging_controls)) 397 398 response = await self._query_validator( 399 Message.CLIENT_BATCH_LIST_REQUEST, 400 client_batch_pb2.ClientBatchListResponse, 401 validator_query) 402 403 return self._wrap_paginated_response( 404 request=request, 405 response=response, 406 controls=paging_controls, 407 data=[self._expand_batch(b) for b in response['batches']]) 408 409 async def fetch_batch(self, request): 410 """Fetches a specific batch from the validator, specified by id. 411 412 Request: 413 path: 414 - batch_id: The 128-character id of the batch to be fetched 415 416 Response: 417 data: A JSON object with the data from the fully expanded Batch 418 link: The link to this exact query 419 """ 420 error_traps = [error_handlers.BatchNotFoundTrap] 421 422 batch_id = request.match_info.get('batch_id', '') 423 self._validate_id(batch_id) 424 425 response = await self._query_validator( 426 Message.CLIENT_BATCH_GET_REQUEST, 427 client_batch_pb2.ClientBatchGetResponse, 428 client_batch_pb2.ClientBatchGetRequest(batch_id=batch_id), 429 error_traps) 430 431 return self._wrap_response( 432 request, 433 data=self._expand_batch(response['batch']), 434 metadata=self._get_metadata(request, response)) 435 436 async def list_transactions(self, request): 437 """Fetches list of txns from validator, optionally filtered by id. 438 439 Request: 440 query: 441 - head: The id of the block to use as the head of the chain 442 - id: Comma separated list of txn ids to include in results 443 444 Response: 445 data: JSON array of Transaction objects with expanded headers 446 head: The head used for this query (most recent if unspecified) 447 link: The link to this exact query, including head block 448 paging: Paging info and nav, like total resources and a next link 449 """ 450 paging_controls = self._get_paging_controls(request) 451 validator_query = client_transaction_pb2.ClientTransactionListRequest( 452 head_id=self._get_head_id(request), 453 transaction_ids=self._get_filter_ids(request), 454 sorting=self._get_sorting_message(request, "default"), 455 paging=self._make_paging_message(paging_controls)) 456 457 response = await self._query_validator( 458 Message.CLIENT_TRANSACTION_LIST_REQUEST, 459 client_transaction_pb2.ClientTransactionListResponse, 460 validator_query) 461 462 data = [self._expand_transaction(t) for t in response['transactions']] 463 464 return self._wrap_paginated_response( 465 request=request, 466 response=response, 467 controls=paging_controls, 468 data=data) 469 470 async def fetch_transaction(self, request): 471 """Fetches a specific transaction from the validator, specified by id. 472 473 Request: 474 path: 475 - transaction_id: The 128-character id of the txn to be fetched 476 477 Response: 478 data: A JSON object with the data from the expanded Transaction 479 link: The link to this exact query 480 """ 481 error_traps = [error_handlers.TransactionNotFoundTrap] 482 483 txn_id = request.match_info.get('transaction_id', '') 484 self._validate_id(txn_id) 485 486 response = await self._query_validator( 487 Message.CLIENT_TRANSACTION_GET_REQUEST, 488 client_transaction_pb2.ClientTransactionGetResponse, 489 client_transaction_pb2.ClientTransactionGetRequest( 490 transaction_id=txn_id), 491 error_traps) 492 493 return self._wrap_response( 494 request, 495 data=self._expand_transaction(response['transaction']), 496 metadata=self._get_metadata(request, response)) 497 498 async def list_receipts(self, request): 499 """Fetches the receipts for transaction by either a POST or GET. 500 501 Request: 502 body: A JSON array of one or more transaction id strings (if POST) 503 query: 504 - id: A comma separated list of up to 15 ids (if GET) 505 - wait: Request should return as soon as some receipts are 506 available 507 508 Response: 509 data: A JSON object, with transaction ids as keys, and receipts as 510 values 511 link: The /receipts link queried (if GET) 512 """ 513 error_traps = [error_handlers.ReceiptNotFoundTrap] 514 515 # Parse transaction ids from POST body, or query paramaters 516 if request.method == 'POST': 517 if request.headers['Content-Type'] != 'application/json': 518 LOGGER.debug( 519 'Request headers had wrong Content-Type: %s', 520 request.headers['Content-Type']) 521 raise errors.ReceiptWrongContentType() 522 523 ids = await request.json() 524 525 if (not ids 526 or not isinstance(ids, list) 527 or not all(isinstance(i, str) for i in ids)): 528 LOGGER.debug('Request body was invalid: %s', ids) 529 raise errors.ReceiptBodyInvalid() 530 for i in ids: 531 self._validate_id(i) 532 533 else: 534 ids = self._get_filter_ids(request) 535 if not ids: 536 LOGGER.debug('Request for receipts missing id query') 537 raise errors.ReceiptIdQueryInvalid() 538 539 # Query validator 540 validator_query = \ 541 client_receipt_pb2.ClientReceiptGetRequest( 542 transaction_ids=ids) 543 self._set_wait(request, validator_query) 544 545 response = await self._query_validator( 546 Message.CLIENT_RECEIPT_GET_REQUEST, 547 client_receipt_pb2.ClientReceiptGetResponse, 548 validator_query, 549 error_traps) 550 551 # Send response 552 if request.method != 'POST': 553 metadata = self._get_metadata(request, response) 554 else: 555 metadata = None 556 557 data = self._drop_id_prefixes( 558 self._drop_empty_props(response['receipts'])) 559 560 return self._wrap_response(request, data=data, metadata=metadata) 561 562 async def fetch_peers(self, request): 563 """Fetches the peers from the validator. 564 Request: 565 566 Response: 567 data: JSON array of peer endpoints 568 link: The link to this exact query 569 """ 570 571 response = await self._query_validator( 572 Message.CLIENT_PEERS_GET_REQUEST, 573 client_peers_pb2.ClientPeersGetResponse, 574 client_peers_pb2.ClientPeersGetRequest()) 575 576 return self._wrap_response( 577 request, 578 data=response['peers'], 579 metadata=self._get_metadata(request, response)) 580 581 async def fetch_status(self, request): 582 '''Fetches information pertaining to the valiator's status.''' 583 584 response = await self._query_validator( 585 Message.CLIENT_STATUS_GET_REQUEST, 586 client_status_pb2.ClientStatusGetResponse, 587 client_status_pb2.ClientStatusGetRequest()) 588 589 return self._wrap_response( 590 request, 591 data={ 592 'peers': response['peers'], 593 'endpoint': response['endpoint'] 594 }, 595 metadata=self._get_metadata(request, response)) 596 597 async def _query_validator(self, request_type, response_proto, 598 payload, error_traps=None): 599 """Sends a request to the validator and parses the response. 600 """ 601 LOGGER.debug( 602 'Sending %s request to validator', 603 self._get_type_name(request_type)) 604 605 payload_bytes = payload.SerializeToString() 606 response = await self._send_request(request_type, payload_bytes) 607 content = self._parse_response(response_proto, response) 608 609 LOGGER.debug( 610 'Received %s response from validator with status %s', 611 self._get_type_name(response.message_type), 612 self._get_status_name(response_proto, content.status)) 613 614 self._check_status_errors(response_proto, content, error_traps) 615 return self._message_to_dict(content) 616 617 async def _send_request(self, request_type, payload): 618 """Uses an executor to send an asynchronous ZMQ request to the 619 validator with the handler's Connection 620 """ 621 try: 622 return await self._connection.send( 623 message_type=request_type, 624 message_content=payload, 625 timeout=self._timeout) 626 except DisconnectError: 627 LOGGER.warning('Validator disconnected while waiting for response') 628 raise errors.ValidatorDisconnected() 629 except asyncio.TimeoutError: 630 LOGGER.warning('Timed out while waiting for validator response') 631 raise errors.ValidatorTimedOut() 632 except SendBackoffTimeoutError: 633 LOGGER.warning('Failed sending message - Backoff timed out') 634 raise errors.SendBackoffTimeout() 635 636 async def _head_to_root(self, block_id): 637 error_traps = [error_handlers.BlockNotFoundTrap] 638 if block_id: 639 response = await self._query_validator( 640 Message.CLIENT_BLOCK_GET_BY_ID_REQUEST, 641 client_block_pb2.ClientBlockGetResponse, 642 client_block_pb2.ClientBlockGetByIdRequest(block_id=block_id), 643 error_traps) 644 block = self._expand_block(response['block']) 645 else: 646 response = await self._query_validator( 647 Message.CLIENT_BLOCK_LIST_REQUEST, 648 client_block_pb2.ClientBlockListResponse, 649 client_block_pb2.ClientBlockListRequest( 650 paging=client_list_control_pb2.ClientPagingControls( 651 limit=1)), 652 error_traps) 653 block = self._expand_block(response['blocks'][0]) 654 return ( 655 block['header_signature'], 656 block['header']['state_root_hash'], 657 ) 658 659 @staticmethod 660 def _parse_response(proto, response): 661 """Parses the content from a validator response Message. 662 """ 663 try: 664 content = proto() 665 content.ParseFromString(response.content) 666 return content 667 except (DecodeError, AttributeError): 668 LOGGER.error('Validator response was not parsable: %s', response) 669 raise errors.ValidatorResponseInvalid() 670 671 @staticmethod 672 def _check_status_errors(proto, content, error_traps=None): 673 """Raises HTTPErrors based on error statuses sent from validator. 674 Checks for common statuses and runs route specific error traps. 675 """ 676 if content.status == proto.OK: 677 return 678 679 try: 680 if content.status == proto.INTERNAL_ERROR: 681 raise errors.UnknownValidatorError() 682 except AttributeError: 683 # Not every protobuf has every status enum, so pass AttributeErrors 684 pass 685 686 try: 687 if content.status == proto.NOT_READY: 688 raise errors.ValidatorNotReady() 689 except AttributeError: 690 pass 691 692 try: 693 if content.status == proto.NO_ROOT: 694 raise errors.HeadNotFound() 695 except AttributeError: 696 pass 697 698 try: 699 if content.status == proto.INVALID_PAGING: 700 raise errors.PagingInvalid() 701 except AttributeError: 702 pass 703 704 try: 705 if content.status == proto.INVALID_SORT: 706 raise errors.SortInvalid() 707 except AttributeError: 708 pass 709 710 # Check custom error traps from the particular route message 711 if error_traps is not None: 712 for trap in error_traps: 713 trap.check(content.status) 714 715 @staticmethod 716 def _wrap_response(request, data=None, metadata=None, status=200): 717 """Creates the JSON response envelope to be sent back to the client. 718 """ 719 envelope = metadata or {} 720 721 if data is not None: 722 envelope['data'] = data 723 724 return web.Response( 725 status=status, 726 content_type='application/json', 727 text=json.dumps( 728 envelope, 729 indent=2, 730 separators=(',', ': '), 731 sort_keys=True)) 732 733 @classmethod 734 def _wrap_paginated_response(cls, request, response, controls, data, 735 head=None): 736 """Builds the metadata for a pagingated response and wraps everying in 737 a JSON encoded web.Response 738 """ 739 paging_response = response['paging'] 740 if head is None: 741 head = response['head_id'] 742 link = cls._build_url( 743 request, 744 head=head, 745 start=paging_response['start'], 746 limit=paging_response['limit']) 747 748 paging = {} 749 limit = controls.get('limit') 750 start = controls.get("start") 751 paging["limit"] = limit 752 paging["start"] = start 753 # If there are no resources, there should be nothing else in paging 754 if paging_response.get("next") == "": 755 return cls._wrap_response( 756 request, 757 data=data, 758 metadata={ 759 'head': head, 760 'link': link, 761 'paging': paging 762 }) 763 764 next_id = paging_response['next'] 765 paging['next_position'] = next_id 766 767 # Builds paging urls specific to this response 768 def build_pg_url(start=None): 769 return cls._build_url(request, head=head, limit=limit, start=start) 770 771 paging['next'] = build_pg_url(paging_response['next']) 772 773 return cls._wrap_response( 774 request, 775 data=data, 776 metadata={ 777 'head': head, 778 'link': link, 779 'paging': paging 780 }) 781 782 @classmethod 783 def _get_metadata(cls, request, response, head=None): 784 """Parses out the head and link properties based on the HTTP Request 785 from the client, and the Protobuf response from the validator. 786 """ 787 head = response.get('head_id', head) 788 metadata = {'link': cls._build_url(request, head=head)} 789 790 if head is not None: 791 metadata['head'] = head 792 return metadata 793 794 @classmethod 795 def _build_url(cls, request, path=None, **changes): 796 """Builds a response URL by overriding the original queries with 797 specified change queries. Change queries set to None will not be used. 798 Setting a change query to False will remove it even if there is an 799 original query with a value. 800 """ 801 changes = {k: v for k, v in changes.items() if v is not None} 802 queries = {**request.url.query, **changes} 803 queries = {k: v for k, v in queries.items() if v is not False} 804 query_strings = [] 805 806 def add_query(key): 807 query_strings.append('{}={}'.format(key, queries[key]) 808 if queries[key] != '' else key) 809 810 def del_query(key): 811 queries.pop(key, None) 812 813 if 'head' in queries: 814 add_query('head') 815 del_query('head') 816 817 if 'start' in changes: 818 add_query('start') 819 elif 'start' in queries: 820 add_query('start') 821 822 del_query('start') 823 824 if 'limit' in queries: 825 add_query('limit') 826 del_query('limit') 827 828 for key in sorted(queries): 829 add_query(key) 830 831 scheme = cls._get_forwarded(request, 'proto') or request.url.scheme 832 host = cls._get_forwarded(request, 'host') or request.host 833 forwarded_path = cls._get_forwarded(request, 'path') 834 path = path if path is not None else request.path 835 query = '?' + '&'.join(query_strings) if query_strings else '' 836 837 url = '{}://{}{}{}{}'.format(scheme, host, forwarded_path, path, query) 838 return url 839 840 @staticmethod 841 def _get_forwarded(request, key): 842 """Gets a forwarded value from the `Forwarded` header if present, or 843 the equivalent `X-Forwarded-` header if not. If neither is present, 844 returns an empty string. 845 """ 846 forwarded = request.headers.get('Forwarded', '') 847 match = re.search( 848 r'(?<={}=).+?(?=[\s,;]|$)'.format(key), 849 forwarded, 850 re.IGNORECASE) 851 852 if match is not None: 853 header = match.group(0) 854 855 if header[0] == '"' and header[-1] == '"': 856 return header[1:-1] 857 858 return header 859 860 return request.headers.get('X-Forwarded-{}'.format(key.title()), '') 861 862 @classmethod 863 def _expand_block(cls, block): 864 """Deserializes a Block's header, and the header of its Batches. 865 """ 866 cls._parse_header(BlockHeader, block) 867 if 'batches' in block: 868 block['batches'] = [cls._expand_batch(b) for b in block['batches']] 869 return block 870 871 @classmethod 872 def _expand_batch(cls, batch): 873 """Deserializes a Batch's header, and the header of its Transactions. 874 """ 875 cls._parse_header(BatchHeader, batch) 876 if 'transactions' in batch: 877 batch['transactions'] = [ 878 cls._expand_transaction(t) for t in batch['transactions']] 879 return batch 880 881 @classmethod 882 def _expand_transaction(cls, transaction): 883 """Deserializes a Transaction's header. 884 """ 885 return cls._parse_header(TransactionHeader, transaction) 886 887 @classmethod 888 def _parse_header(cls, header_proto, resource): 889 """Deserializes a resource's base64 encoded Protobuf header. 890 """ 891 header = header_proto() 892 try: 893 header_bytes = base64.b64decode(resource['header']) 894 header.ParseFromString(header_bytes) 895 except (KeyError, TypeError, ValueError, DecodeError): 896 header = resource.get('header', None) 897 LOGGER.error( 898 'The validator sent a resource with %s %s', 899 'a missing header' if header is None else 'an invalid header:', 900 header or '') 901 raise errors.ResourceHeaderInvalid() 902 903 resource['header'] = cls._message_to_dict(header) 904 return resource 905 906 @staticmethod 907 def _get_paging_controls(request): 908 """Parses start and/or limit queries into a paging controls dict. 909 """ 910 start = request.url.query.get('start', None) 911 limit = request.url.query.get('limit', None) 912 controls = {} 913 914 if limit is not None: 915 try: 916 controls['limit'] = int(limit) 917 except ValueError: 918 LOGGER.debug('Request query had an invalid limit: %s', limit) 919 raise errors.CountInvalid() 920 921 if controls['limit'] <= 0: 922 LOGGER.debug('Request query had an invalid limit: %s', limit) 923 raise errors.CountInvalid() 924 925 if start is not None: 926 controls['start'] = start 927 928 return controls 929 930 @staticmethod 931 def _make_paging_message(controls): 932 """Turns a raw paging controls dict into Protobuf ClientPagingControls. 933 """ 934 935 return client_list_control_pb2.ClientPagingControls( 936 start=controls.get('start', None), 937 limit=controls.get('limit', None)) 938 939 @staticmethod 940 def _get_sorting_message(request, key): 941 """Parses the reverse query into a list of ClientSortControls protobuf 942 messages. 943 """ 944 control_list = [] 945 reverse = request.url.query.get('reverse', None) 946 if reverse is None: 947 return control_list 948 949 if reverse.lower() == "": 950 control_list.append(client_list_control_pb2.ClientSortControls( 951 reverse=True, 952 keys=key.split(",") 953 )) 954 elif reverse.lower() != 'false': 955 control_list.append(client_list_control_pb2.ClientSortControls( 956 reverse=True, 957 keys=reverse.split(",") 958 )) 959 960 return control_list 961 962 def _set_wait(self, request, validator_query): 963 """Parses the `wait` query parameter, and sets the corresponding 964 `wait` and `timeout` properties in the validator query. 965 """ 966 wait = request.url.query.get('wait', 'false') 967 if wait.lower() != 'false': 968 validator_query.wait = True 969 try: 970 validator_query.timeout = int(wait) 971 except ValueError: 972 # By default, waits for 95% of REST API's configured timeout 973 validator_query.timeout = int(self._timeout * 0.95) 974 975 def _drop_empty_props(self, item): 976 """Remove properties with empty strings from nested dicts. 977 """ 978 if isinstance(item, list): 979 return [self._drop_empty_props(i) for i in item] 980 if isinstance(item, dict): 981 return { 982 k: self._drop_empty_props(v) 983 for k, v in item.items() if v != '' 984 } 985 return item 986 987 def _drop_id_prefixes(self, item): 988 """Rename keys ending in 'id', to just be 'id' for nested dicts. 989 """ 990 if isinstance(item, list): 991 return [self._drop_id_prefixes(i) for i in item] 992 if isinstance(item, dict): 993 return { 994 'id' if k.endswith('id') else k: self._drop_id_prefixes(v) 995 for k, v in item.items() 996 } 997 return item 998 999 @classmethod 1000 def _get_head_id(cls, request): 1001 """Fetches the request's head query, and validates if present. 1002 """ 1003 head_id = request.url.query.get('head', None) 1004 1005 if head_id is not None: 1006 cls._validate_id(head_id) 1007 1008 return head_id 1009 1010 @classmethod 1011 def _get_filter_ids(cls, request): 1012 """Parses the `id` filter paramter from the url query. 1013 """ 1014 id_query = request.url.query.get('id', None) 1015 1016 if id_query is None: 1017 return None 1018 1019 filter_ids = id_query.split(',') 1020 for filter_id in filter_ids: 1021 cls._validate_id(filter_id) 1022 1023 return filter_ids 1024 1025 @staticmethod 1026 def _validate_id(resource_id): 1027 """Confirms a header_signature is 128 hex characters, raising an 1028 ApiError if not. 1029 """ 1030 if not re.fullmatch('[0-9a-f]{128}', resource_id): 1031 raise errors.InvalidResourceId(resource_id) 1032 1033 @staticmethod 1034 def _message_to_dict(message): 1035 """Converts a Protobuf object to a python dict with desired settings. 1036 """ 1037 return MessageToDict( 1038 message, 1039 including_default_value_fields=True, 1040 preserving_proto_field_name=True) 1041 1042 @staticmethod 1043 def _get_type_name(type_enum): 1044 return Message.MessageType.Name(type_enum) 1045 1046 @staticmethod 1047 def _get_status_name(proto, status_enum): 1048 try: 1049 return proto.Status.Name(status_enum) 1050 except ValueError: 1051 return 'Unknown ({})'.format(status_enum)