| """ |
| Verify that a given JSON replay file fulfills basic expectations. |
| """ |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import argparse |
| import json |
| import jsonschema |
| import sys |
| |
| |
| expected_sensitive_value = \ |
| '''0000000 0000001 0000002 0000003 0000004 0000005 0000006 0000007 0000008 0000009 000000a 000000b 000000c 000000d ''' \ |
| '''000000e 000000f 0000010 0000011 0000012 0000013 0000014 0000015 0000016 0000017 0000018 0000019 000001a 000001b ''' \ |
| '''000001c 000001d 000001e 000001f 0000020 0000021 0000022 0000023 0000024 0000025 0000026 0000027 0000028 0000029 ''' \ |
| '''000002a 000002b 000002c 000002d 000002e 000002f 0000030 0000031 0000032 0000033 0000034 0000035 0000036 0000037 ''' \ |
| '''0000038 0000039 000003a 000003b 000003c 000003d 000003e 000003f 0000040 0000041 0000042 0000043 0000044 0000045 ''' \ |
| '''0000046 0000047 0000048 0000049 000004a 000004b 000004c 000004d 000004e 000004f 0000050 0000051 0000052 0000053 ''' \ |
| '''0000054 0000055 0000056 0000057 0000058 0000059 000005a 000005b 000005c 000005d 000005e 000005f 0000060 0000061 ''' \ |
| '''0000062 0000063 0000064 0000065 0000066 0000067 0000068 0000069 000006a 000006b 000006c 000006d 000006e 000006f ''' \ |
| '''0000070 0000071 0000072 0000073 0000074 0000075 0000076 0000077 0000078 0000079 000007a 000007b 000007c 000007d ''' \ |
| '''000007e 000007f 0000080 0000081 0000082 0000083 0000084 0000085 0000086 0000087 0000088 0000089 000008a 000008b ''' \ |
| '''000008c 000008d 000008e 000008f 0000090 0000091 0000092 0000093 0000094 0000095 0000096 0000097 0000098 0000099 ''' \ |
| '''000009a 000009b 000009c 000009d 000009e 000009f 00000a0 00000a1 00000a2 00000a3 00000a4 00000a5 00000a6 00000a7 ''' \ |
| '''00000a8 00000a9 00000aa 00000ab 00000ac 00000ad 00000ae 00000af 00000b0 00000b1 00000b2 00000b3 00000b4 00000b5 ''' \ |
| '''00000b6 00000b7 00000b8 00000b9 00000ba 00000bb 00000bc 00000bd 00000be 00000bf 00000c0 00000c1 00000c2 00000c3 ''' \ |
| '''00000c4 00000c5 00000c6 00000c7 00000c8 00000c9 00000ca 00000cb 00000cc 00000cd 00000ce 00000cf 00000d0 00000d1 ''' \ |
| '''00000d2 00000d3 00000d4 00000d5 00000d6 00000d7 00000d8 00000d9 00000da 00000db 00000dc 00000dd 00000de 00000df ''' \ |
| '''00000e0 00000e1 00000e2 00000e3 00000e4 00000e5 00000e6 00000e7 00000e8 00000e9 00000ea 00000eb 00000ec 00000ed ''' \ |
| '''00000ee 00000ef 00000f0 00000f1 00000f2 00000f3 00000f4 00000f5 00000f6 00000f7 00000f8 00000f9 00000fa 00000fb ''' \ |
| '''00000fc 00000fd 00000fe 00000ff''' |
| |
| |
| def validate_json(schema_json, replay_json): |
| """ |
| Validate the replay file against the provided schema. |
| """ |
| try: |
| jsonschema.validate(instance=replay_json, schema=schema_json) |
| except jsonschema.ValidationError as e: |
| print("The replay file does not validate against the schema: {}".format(e)) |
| return False |
| else: |
| return True |
| |
| |
| def verify_there_was_a_transaction(replay_json): |
| """ |
| Verify that the replay file has a sensible looking transaction. |
| """ |
| try: |
| transactions = replay_json['sessions'][0]['transactions'] |
| except KeyError: |
| print("The replay file did not have transactions in it.") |
| return False |
| |
| if len(transactions) < 1: |
| print("There are no transactions in the replay file.") |
| return False |
| transaction = transactions[0] |
| if not ('client-request' in transaction and 'proxy-response' in transaction): |
| print("There was not request and response in the transaction of the replay file.") |
| return False |
| |
| return True |
| |
| |
| def verify_request_target(replay_json, request_target): |
| """ |
| Verify that the 'url' element of the first transaction contains the request target. |
| """ |
| try: |
| url = replay_json['sessions'][0]['transactions'][0]['client-request']['url'] |
| except KeyError: |
| print("The replay file did not have a first transaction with a url element.") |
| return False |
| |
| if url != request_target: |
| print("Mismatched request target. Expected: {}, received: {}".format(request_target, url)) |
| return False |
| return True |
| |
| |
| def verify_client_request_size(replay_json, client_request_size): |
| """ |
| Verify that the 'url' element of the first transaction contains the request target. |
| """ |
| try: |
| size = int(replay_json['sessions'][0]['transactions'][0]['client-request']['content']['size']) |
| except KeyError: |
| print("The replay file did not have content size element in the first client-request.") |
| return False |
| |
| if size != client_request_size: |
| print("Mismatched client-request request size. Expected: {}, received: {}".format( |
| client_request_size, size)) |
| return False |
| return True |
| |
| |
| def verify_sensitive_fields_not_dumped(replay_json, sensitive_fields): |
| """ |
| Verify that all of the cookie fields have the expected value. |
| """ |
| message_types = ['client-request', 'proxy-request', 'server-response', 'proxy-response'] |
| try: |
| for session in replay_json['sessions']: |
| for transaction in session['transactions']: |
| for message_type in transaction: |
| if message_type not in message_types: |
| continue |
| message = transaction[message_type] |
| for field in message['headers']['fields']: |
| field_name = field[0].lower() |
| if field_name in sensitive_fields: |
| field_value = field[1] |
| if field_value not in expected_sensitive_value: |
| print("Found an unexpected cookie: {}: {}".format(field[0], field[1])) |
| return False |
| |
| except KeyError: |
| print("Could not find headers in the replay file.") |
| return False |
| |
| return True |
| |
| |
| def verify_protocols_helper(replay_json, expected_protocols, is_client): |
| try: |
| if is_client: |
| protocol_node = replay_json['sessions'][0]['protocol'] |
| else: |
| protocol_node = replay_json['sessions'][0]['transactions'][0]['proxy-request']['protocol'] |
| |
| dumped_protocols = '' |
| is_first_protocol = True |
| for protocol in protocol_node: |
| if not is_first_protocol: |
| dumped_protocols += "," |
| is_first_protocol = False |
| dumped_protocols += protocol['name'] |
| |
| if dumped_protocols == expected_protocols: |
| return True |
| else: |
| host_name = "client" if is_client else "server" |
| print('Unexpected protocol {} stack. Expected: "{}", found: "{}".'.format( |
| host_name, expected_protocols, dumped_protocols)) |
| return False |
| except KeyError: |
| print("Could not find {} protocol stack node in the replay file.".format(host_name)) |
| return False |
| |
| |
| def verify_client_protocols(replay_json, expected_protocols): |
| return verify_protocols_helper(replay_json, expected_protocols, is_client=True) |
| |
| |
| def verify_server_protocols(replay_json, expected_protocols): |
| return verify_protocols_helper(replay_json, expected_protocols, is_client=False) |
| |
| |
| def get_tls_features(replay_json, is_client): |
| session = replay_json['sessions'][0] |
| if is_client: |
| protocol_parent = session['protocol'] |
| else: |
| protocol_parent = session['transactions'][0]['proxy-request']['protocol'] |
| |
| for protocol in protocol_parent: |
| if protocol['name'] == "tls": |
| return protocol |
| else: |
| raise KeyError('The "tls" protocol node was not found.') |
| |
| |
| def verify_tls_features(expected_tls_features, found_tls_features): |
| for expected_tls_feature in expected_tls_features.split(','): |
| expected_key, expected_value = expected_tls_feature.split(':') |
| try: |
| found_value = found_tls_features[expected_key] |
| except KeyError: |
| print("Could not find tls feature in the replay file: {}".format(expected_key)) |
| return False |
| value_matches = False |
| if isinstance(found_value, bool) or (found_value in ('true', 'false')): |
| if expected_value.lower() == "false": |
| expected_value = False |
| elif expected_value.lower() == "true": |
| expected_value = True |
| else: |
| raise ValueError("Cannot convert expected value to a boolean: {}, found: {}".format( |
| expected_value, found_value)) |
| if found_value == 'true': |
| found_value = True |
| elif found_value == 'false': |
| found_value = False |
| value_matches = found_value == bool(expected_value) |
| |
| elif isinstance(found_value, str): |
| value_matches = found_value == expected_value |
| elif isinstance(found_value, int): |
| value_matches = found_value == int(expected_value) |
| else: |
| raise ValueError("Cannot determine type of found value: {}".format( |
| found_value)) |
| |
| if not value_matches: |
| print('Mismatched value for "{}", expected "{}", received "{}"'.format( |
| expected_key, expected_value, found_value)) |
| return False |
| return True |
| |
| |
| def verify_client_tls_features(replay_json, expected_tls_features): |
| try: |
| tls_features = get_tls_features(replay_json, is_client=True) |
| except KeyError: |
| print("Could not find client tls node in the replay file.") |
| return False |
| |
| if not verify_tls_features(expected_tls_features, tls_features): |
| print('Failed to verify client tls features in "{}"'.format( |
| expected_tls_features)) |
| return False |
| return True |
| |
| |
| def verify_server_tls_features(replay_json, expected_tls_features): |
| try: |
| tls_features = get_tls_features(replay_json, is_client=False) |
| except KeyError: |
| print("Could not find server tls node in the replay file.") |
| return False |
| |
| if not verify_tls_features(expected_tls_features, tls_features): |
| print('Failed to verify server tls features in "{}"'.format( |
| expected_tls_features)) |
| return False |
| return True |
| |
| |
| def parse_args(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("schema_file", |
| type=argparse.FileType('r'), |
| help="The schema in which to interpret validate the replay file.") |
| parser.add_argument("replay_file", |
| type=argparse.FileType('r'), |
| help="The replay file to validate.") |
| parser.add_argument("--request-target", |
| help="The request target ('url' element) to expect in the replay file.") |
| parser.add_argument("--client-request-size", |
| type=int, |
| help="The expected size value in the client-request node.") |
| parser.add_argument("--sensitive-fields", |
| action="append", |
| help="The fields that are considered sensitive and replaced with insensitive values.") |
| parser.add_argument("--client-protocols", |
| help="The comma-separated sequence of protocols to expect for the client connection.") |
| parser.add_argument("--server-protocols", |
| help="The comma-separated sequence of protocols to expect for the server connection.") |
| parser.add_argument("--client-tls-features", |
| help="The TLS values to expect for the client connection.") |
| parser.add_argument("--server-tls-features", |
| help="The TLS values to expect for the server connection.") |
| return parser.parse_args() |
| |
| |
| def main(): |
| args = parse_args() |
| |
| schema_json = json.load(args.schema_file) |
| replay_json = json.load(args.replay_file) |
| |
| if not validate_json(schema_json, replay_json): |
| return 1 |
| |
| # Verifying that there is a transaction in the replay file may seem |
| # unnecessary since the replay file validated against the schema. But a JSON |
| # file that doesn't have conflicting entry names will pass the schema. For |
| # instance, this passes against our replay schema: |
| # |
| # {"name": "Bob", "languages": ["English", "French"]} |
| # |
| # Thus we do the following sanity check to make sure that the replay file |
| # appears to have some transaction in it. |
| if not verify_there_was_a_transaction(replay_json): |
| return 1 |
| |
| if args.request_target and not verify_request_target(replay_json, args.request_target): |
| return 1 |
| |
| if args.client_request_size and not verify_client_request_size(replay_json, args.client_request_size): |
| return 1 |
| |
| if args.sensitive_fields and not verify_sensitive_fields_not_dumped(replay_json, args.sensitive_fields): |
| return 1 |
| |
| if args.client_protocols and not verify_client_protocols(replay_json, args.client_protocols): |
| return 1 |
| |
| if args.server_protocols and not verify_server_protocols(replay_json, args.server_protocols): |
| return 1 |
| |
| if args.client_tls_features and not verify_client_tls_features(replay_json, args.client_tls_features): |
| return 1 |
| |
| if args.server_tls_features and not verify_server_tls_features(replay_json, args.server_tls_features): |
| return 1 |
| |
| return 0 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |