|
1 | 1 | import sys |
| 2 | +import os |
| 3 | +import pandas as pd |
2 | 4 |
|
3 | 5 | from uid2_client import BidstreamClient |
| 6 | +from uid2_client.decryption_status import DecryptionStatus |
| 7 | +from uid2_client.encryption import EncryptionError |
4 | 8 |
|
5 | 9 |
|
6 | 10 | # this sample client decrypts an advertising token into a raw UID2 |
7 | 11 | # to demonstrate decryption for DSPs |
| 12 | +# Can process a single token or read UID2s from an Excel file |
8 | 13 |
|
9 | 14 | def _usage(): |
10 | | - print('Usage: python3 sample_bidstream_client.py <base_url> <auth_key> <secret_key> <domain_name> <ad_token>', file=sys.stderr) |
| 15 | + print('Usage: python3 sample_bidstream_client.py <base_url> <auth_key> <secret_key> <domain_name> <ad_token_or_excel_file>', file=sys.stderr) |
| 16 | + print(' If the 5th argument is an .xlsx file, it will read UID2s from the "UID" column in the "GAM" sheet', file=sys.stderr) |
11 | 17 | sys.exit(1) |
12 | 18 |
|
13 | 19 |
|
| 20 | +def get_error_summary(exception): |
| 21 | + """Extract a concise root cause message from an exception.""" |
| 22 | + # For EncryptionError with "invalid payload", show the underlying exception |
| 23 | + if isinstance(exception, EncryptionError): |
| 24 | + error_msg = str(exception) |
| 25 | + # If it's an "invalid payload" error, always show the underlying cause |
| 26 | + if error_msg == 'invalid payload' and exception.__cause__: |
| 27 | + root_cause = str(exception.__cause__) |
| 28 | + # Extract the first line and clean it up |
| 29 | + root_line = root_cause.split('\n')[0].strip() |
| 30 | + # Remove common exception prefixes if present |
| 31 | + if root_line.startswith(exception.__cause__.__class__.__name__ + ':'): |
| 32 | + root_line = root_line.split(':', 1)[1].strip() |
| 33 | + # Combine with "invalid payload" context |
| 34 | + return f"invalid payload: {root_line}" |
| 35 | + # For other EncryptionError messages, use as-is |
| 36 | + return error_msg.split('\n')[0].strip() |
| 37 | + |
| 38 | + # For other exceptions, check for chained exceptions (root cause) |
| 39 | + if exception.__cause__: |
| 40 | + root_cause = str(exception.__cause__) |
| 41 | + # Extract the first line and clean it up |
| 42 | + root_line = root_cause.split('\n')[0].strip() |
| 43 | + # Remove common exception prefixes if present |
| 44 | + if root_line.startswith(exception.__cause__.__class__.__name__ + ':'): |
| 45 | + root_line = root_line.split(':', 1)[1].strip() |
| 46 | + return root_line |
| 47 | + |
| 48 | + # Otherwise, extract the exception message |
| 49 | + error_msg = str(exception) |
| 50 | + # Remove exception class name prefix if present (e.g., "ValueError: message") |
| 51 | + if ':' in error_msg and error_msg.split(':')[0].strip() == exception.__class__.__name__: |
| 52 | + error_msg = error_msg.split(':', 1)[1].strip() |
| 53 | + |
| 54 | + return error_msg.split('\n')[0].strip() |
| 55 | + |
| 56 | + |
| 57 | +def decrypt_token(client, ad_token, domain_name, index=None): |
| 58 | + """Decrypt a single token and return the result with error handling.""" |
| 59 | + token_suffix = ad_token[-6:] if len(ad_token) >= 6 else ad_token |
| 60 | + try: |
| 61 | + decrypt_result = client.decrypt_token_into_raw_uid(ad_token, domain_name) |
| 62 | + |
| 63 | + result = { |
| 64 | + 'index': index, |
| 65 | + 'token': ad_token[:50] + '...' if len(ad_token) > 50 else ad_token, |
| 66 | + 'token_suffix': token_suffix, |
| 67 | + 'status': decrypt_result.status, |
| 68 | + 'uid': decrypt_result.uid, |
| 69 | + 'established': decrypt_result.established, |
| 70 | + 'site_id': decrypt_result.site_id, |
| 71 | + 'identity_type': decrypt_result.identity_type, |
| 72 | + 'advertising_token_version': decrypt_result.advertising_token_version, |
| 73 | + 'is_client_side_generated': decrypt_result.is_client_side_generated, |
| 74 | + 'error': None, |
| 75 | + } |
| 76 | + return result |
| 77 | + except EncryptionError as e: |
| 78 | + # Handle encryption errors - extract root cause |
| 79 | + return { |
| 80 | + 'index': index, |
| 81 | + 'token': ad_token[:50] + '...' if len(ad_token) > 50 else ad_token, |
| 82 | + 'token_suffix': token_suffix, |
| 83 | + 'status': None, |
| 84 | + 'uid': None, |
| 85 | + 'established': None, |
| 86 | + 'site_id': None, |
| 87 | + 'identity_type': None, |
| 88 | + 'advertising_token_version': None, |
| 89 | + 'is_client_side_generated': None, |
| 90 | + 'error': get_error_summary(e), |
| 91 | + } |
| 92 | + except Exception as e: |
| 93 | + # Handle any other unexpected errors - extract root cause |
| 94 | + return { |
| 95 | + 'index': index, |
| 96 | + 'token': ad_token[:50] + '...' if len(ad_token) > 50 else ad_token, |
| 97 | + 'token_suffix': token_suffix, |
| 98 | + 'status': None, |
| 99 | + 'uid': None, |
| 100 | + 'established': None, |
| 101 | + 'site_id': None, |
| 102 | + 'identity_type': None, |
| 103 | + 'advertising_token_version': None, |
| 104 | + 'is_client_side_generated': None, |
| 105 | + 'error': get_error_summary(e), |
| 106 | + } |
| 107 | + |
| 108 | + |
| 109 | +def print_result(result): |
| 110 | + """Print decryption result in a formatted way.""" |
| 111 | + token_suffix = result.get('token_suffix', '') |
| 112 | + if result['index'] is not None: |
| 113 | + print(f"\n{'='*60}") |
| 114 | + if token_suffix: |
| 115 | + print(f"Token #{result['index'] + 1} (last 6 chars: {token_suffix}): {result['token']}") |
| 116 | + else: |
| 117 | + print(f"Token #{result['index'] + 1}: {result['token']}") |
| 118 | + print(f"{'='*60}") |
| 119 | + else: |
| 120 | + print(f"\n{'='*60}") |
| 121 | + if token_suffix: |
| 122 | + print(f"Token (last 6 chars: {token_suffix}): {result['token']}") |
| 123 | + else: |
| 124 | + print(f"Token: {result['token']}") |
| 125 | + print(f"{'='*60}") |
| 126 | + |
| 127 | + # Check if there was an error or if status indicates failure |
| 128 | + if result['error'] is not None: |
| 129 | + print(f"ERROR: {result['error']}") |
| 130 | + elif result['status'] is None: |
| 131 | + print(f"ERROR: Unknown error occurred") |
| 132 | + elif result['status'] != DecryptionStatus.SUCCESS: |
| 133 | + print(f"ERROR: {result['status'].value}") |
| 134 | + else: |
| 135 | + print(f"Status = {result['status'].name} ({result['status'].value})") |
| 136 | + print(f"UID = {result['uid']}") |
| 137 | + print(f"Established = {result['established']}") |
| 138 | + print(f"Site ID = {result['site_id']}") |
| 139 | + print(f"Identity Type = {result['identity_type']}") |
| 140 | + print(f"Advertising Token Version = {result['advertising_token_version']}") |
| 141 | + print(f"Is Client Side Generated = {result['is_client_side_generated']}") |
| 142 | + |
| 143 | + |
14 | 144 | if len(sys.argv) < 6: |
15 | 145 | _usage() |
16 | 146 |
|
17 | 147 | base_url = sys.argv[1] |
18 | 148 | auth_key = sys.argv[2] |
19 | 149 | secret_key = sys.argv[3] |
20 | 150 | domain_name = sys.argv[4] |
21 | | -ad_token = sys.argv[5] |
| 151 | +input_arg = sys.argv[5] |
22 | 152 |
|
| 153 | +# Initialize client |
23 | 154 | client = BidstreamClient(base_url, auth_key, secret_key) |
24 | 155 | refresh_response = client.refresh() |
25 | 156 | if not refresh_response.success: |
26 | | - print('Failed to refresh keys due to =', refresh_response.reason) |
| 157 | + print('Failed to refresh keys due to =', refresh_response.reason, file=sys.stderr) |
27 | 158 | sys.exit(1) |
28 | 159 |
|
29 | | -decrypt_result = client.decrypt_token_into_raw_uid(ad_token, domain_name) |
30 | | - |
31 | | -print('Status =', decrypt_result.status) |
32 | | -print('UID =', decrypt_result.uid) |
33 | | -print('Established =', decrypt_result.established) |
34 | | -print('Site ID =', decrypt_result.site_id) |
35 | | -print('Identity Type =', decrypt_result.identity_type) |
36 | | -print('Advertising Token Version =', decrypt_result.advertising_token_version) |
37 | | -print('Is Client Side Generated =', decrypt_result.is_client_side_generated) |
| 160 | +# Check if input is an Excel file |
| 161 | +if input_arg.endswith('.xlsx') and os.path.exists(input_arg): |
| 162 | + # Read UID2s from Excel file |
| 163 | + print(f"Reading UID2s from Excel file: {input_arg}", file=sys.stderr) |
| 164 | + try: |
| 165 | + df = pd.read_excel(input_arg, sheet_name='GAM') |
| 166 | + |
| 167 | + # Get the UID2 column (it's called 'UID' in the file) |
| 168 | + if 'UID' not in df.columns: |
| 169 | + print(f"Error: 'UID' column not found in GAM sheet. Available columns: {df.columns.tolist()}", file=sys.stderr) |
| 170 | + sys.exit(1) |
| 171 | + |
| 172 | + # Filter out invalid entries (like "Bad Envelope") |
| 173 | + uid2_tokens = df['UID'].dropna().astype(str) |
| 174 | + uid2_tokens = uid2_tokens[uid2_tokens != 'Bad Envelope'] |
| 175 | + uid2_tokens = uid2_tokens[uid2_tokens.str.strip() != ''].tolist() |
| 176 | + |
| 177 | + print(f"Found {len(uid2_tokens)} valid UID2 tokens to decrypt", file=sys.stderr) |
| 178 | + |
| 179 | + # Decrypt each token sequentially |
| 180 | + results = [] |
| 181 | + for idx, token in enumerate(uid2_tokens): |
| 182 | + # Print the last 6 characters of the token first |
| 183 | + token_suffix = token[-6:] if len(token) >= 6 else token |
| 184 | + print(f"\nProcessing token {idx + 1}/{len(uid2_tokens)} (last 6 chars: {token_suffix})...", file=sys.stderr) |
| 185 | + try: |
| 186 | + result = decrypt_token(client, token, domain_name, index=idx) |
| 187 | + results.append(result) |
| 188 | + |
| 189 | + # Print one-line error summary if failed, otherwise full result |
| 190 | + if result['error'] is not None: |
| 191 | + print(f"Token #{idx + 1} ({token_suffix}) FAILED: {result['error']}") |
| 192 | + elif result['status'] is not None and result['status'] != DecryptionStatus.SUCCESS: |
| 193 | + print(f"Token #{idx + 1} ({token_suffix}) FAILED: {result['status'].value}") |
| 194 | + else: |
| 195 | + print_result(result) |
| 196 | + except Exception as e: |
| 197 | + # Catch any unexpected errors during processing |
| 198 | + token_suffix = token[-6:] if len(token) >= 6 else token |
| 199 | + error_summary = get_error_summary(e) |
| 200 | + error_result = { |
| 201 | + 'index': idx, |
| 202 | + 'token': token[:50] + '...' if len(token) > 50 else token, |
| 203 | + 'token_suffix': token_suffix, |
| 204 | + 'status': None, |
| 205 | + 'uid': None, |
| 206 | + 'established': None, |
| 207 | + 'site_id': None, |
| 208 | + 'identity_type': None, |
| 209 | + 'advertising_token_version': None, |
| 210 | + 'is_client_side_generated': None, |
| 211 | + 'error': error_summary, |
| 212 | + } |
| 213 | + results.append(error_result) |
| 214 | + print(f"Token #{idx + 1} ({token_suffix}) FAILED: {error_summary}") |
| 215 | + |
| 216 | + # Print summary |
| 217 | + print(f"\n{'='*60}") |
| 218 | + print(f"SUMMARY") |
| 219 | + print(f"{'='*60}") |
| 220 | + print(f"Total tokens processed: {len(results)}") |
| 221 | + successful = sum(1 for r in results if r.get('status') == DecryptionStatus.SUCCESS) |
| 222 | + print(f"Successful decryptions: {successful}") |
| 223 | + print(f"Failed decryptions: {len(results) - successful}") |
| 224 | + |
| 225 | + except Exception as e: |
| 226 | + print(f"Error reading Excel file: {e}", file=sys.stderr) |
| 227 | + sys.exit(1) |
| 228 | +else: |
| 229 | + # Process single token |
| 230 | + try: |
| 231 | + result = decrypt_token(client, input_arg, domain_name, index=None) |
| 232 | + print_result(result) |
| 233 | + except Exception as e: |
| 234 | + print(f"ERROR: {str(e)}", file=sys.stderr) |
| 235 | + sys.exit(1) |
0 commit comments