|
5 | 5 | import os
|
6 | 6 | import platform
|
7 | 7 | import random
|
| 8 | +import subprocess |
| 9 | +import shlex |
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,28 @@ 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 e: |
| 444 | + raise APIKeyRetrievalError( |
| 445 | + f"Failed to parse passcmd '{passcmd}': {str(e)}") |
| 446 | + try: |
| 447 | + result = subprocess.run(cmd_parts, capture_output=True, check=True) |
| 448 | + api_key = result.stdout.decode().strip() |
| 449 | + except subprocess.CalledProcessError as e: |
| 450 | + raise APIKeyRetrievalError( |
| 451 | + f"Failed to retrieve API key using passcmd '{passcmd}'. " |
| 452 | + f"Command exited with return code {e.returncode}." |
| 453 | + ) |
429 | 454 | if email is None:
|
430 | 455 | email = config.get("api", "email")
|
431 | 456 | if site is None and config.has_option("api", "site"):
|
@@ -512,6 +537,14 @@ def __init__(
|
512 | 537 | self.feature_level: int = server_settings.get("zulip_feature_level", 0)
|
513 | 538 | assert self.zulip_version is not None
|
514 | 539 |
|
| 540 | + def get_api_key(self, passcmd: str) -> Optional[str]: |
| 541 | + # run the passcmd command and get the API key |
| 542 | + result = subprocess.run(passcmd.split(), capture_output=True, check=False) |
| 543 | + if result.returncode == 0: |
| 544 | + return result.stdout.decode().strip() |
| 545 | + else: |
| 546 | + raise RuntimeError("Error: Unable to retrieve API key.") |
| 547 | + |
515 | 548 | def ensure_session(self) -> None:
|
516 | 549 | # Check if the session has been created already, and return
|
517 | 550 | # immediately if so.
|
|
0 commit comments