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)