|
5 | 5 | import os |
6 | 6 | import platform |
7 | 7 | import random |
| 8 | +import shlex |
| 9 | +import subprocess |
8 | 10 | import sys |
9 | 11 | import time |
10 | 12 | import traceback |
@@ -366,12 +368,15 @@ class MissingURLError(ZulipError): |
366 | 368 | class UnrecoverableNetworkError(ZulipError): |
367 | 369 | pass |
368 | 370 |
|
| 371 | +class APIKeyRetrievalError(ZulipError): |
| 372 | + pass |
369 | 373 |
|
370 | 374 | class Client: |
371 | 375 | def __init__( |
372 | 376 | self, |
373 | 377 | email: Optional[str] = None, |
374 | 378 | api_key: Optional[str] = None, |
| 379 | + passcmd: Optional[str] = None, |
375 | 380 | config_file: Optional[str] = None, |
376 | 381 | verbose: bool = False, |
377 | 382 | retry_on_errors: bool = True, |
@@ -424,8 +429,29 @@ def __init__( |
424 | 429 | config = ConfigParser() |
425 | 430 | with open(config_file) as f: |
426 | 431 | config.read_file(f, config_file) |
427 | | - if api_key is None: |
| 432 | + if api_key is None and config.has_option("api", "key"): |
428 | 433 | api_key = config.get("api", "key") |
| 434 | + if passcmd is None and config.has_option("api", "passcmd"): |
| 435 | + passcmd = config.get("api", "passcmd") |
| 436 | + malicious_chars = {";", "|", "&", ">", "<", "`", "$", "\\", "\n", "\r"} |
| 437 | + if any(char in passcmd for char in malicious_chars): |
| 438 | + raise APIKeyRetrievalError( |
| 439 | + f"Invalid characters detected in passcmd: {passcmd!r}" |
| 440 | + ) |
| 441 | + try: |
| 442 | + cmd_parts = shlex.split(passcmd) |
| 443 | + except ValueError as err: |
| 444 | + raise APIKeyRetrievalError( |
| 445 | + f"Failed to parse passcmd '{passcmd}': {err!s}" |
| 446 | + ) from err |
| 447 | + try: |
| 448 | + result = subprocess.run(cmd_parts, capture_output=True, check=True) |
| 449 | + api_key = result.stdout.decode().strip() |
| 450 | + except subprocess.CalledProcessError as err: |
| 451 | + raise APIKeyRetrievalError( |
| 452 | + f'Failed to retrieve API key using passcmd "{passcmd}".' |
| 453 | + f"Command exited with return code {err.returncode}." |
| 454 | + ) from err |
429 | 455 | if email is None: |
430 | 456 | email = config.get("api", "email") |
431 | 457 | if site is None and config.has_option("api", "site"): |
|
0 commit comments