From e7d308ab6919f0abcc9b20300de8e2d8d8192114 Mon Sep 17 00:00:00 2001 From: Michael Woods Date: Sun, 28 Dec 2025 00:09:47 -0500 Subject: [PATCH] Jobs interface is looking really good now.. --- packetserver/http/routers/jobs.py | 26 ++++++++++++++++++--- packetserver/http/templates/job_detail.html | 26 ++++++++++++++++++++- packetserver/http/templates/job_new.html | 10 ++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/packetserver/http/routers/jobs.py b/packetserver/http/routers/jobs.py index 138cf57..21f12db 100644 --- a/packetserver/http/routers/jobs.py +++ b/packetserver/http/routers/jobs.py @@ -7,6 +7,7 @@ import logging import base64 import json import gzip +import shlex from traceback import format_exc from packetserver.http.dependencies import get_current_http_user @@ -20,6 +21,16 @@ from packetserver.server.jobs import RunnerFile router = APIRouter(prefix="/api/v1", tags=["jobs"]) dashboard_router = APIRouter(tags=["jobs"]) +def tokenize_cmd(cmd: str) -> list[str]: + """ + Tokenize a command string with basic shell-like quoting support. + Uses shlex with posix=True for proper "double" and 'single' quote handling. + """ + try: + return shlex.split(cmd) + except ValueError as e: + raise HTTPException(status_code=400, detail=f"Invalid command quoting: {e}") + class JobSummary(BaseModel): id: int cmd: Union[str, List[str]] @@ -161,6 +172,7 @@ async def create_job_from_form( env_values: List[str] = Form(default=[]), files: List[UploadFile] = File(default=[]), include_db: Optional[str] = Form(None), + shell_mode: Optional[str] = Form(None), current_user: HttpUser = Depends(get_current_http_user) ): # Build env dict from parallel lists @@ -176,6 +188,15 @@ async def create_job_from_form( content = await upload.read() files_dict[upload.filename] = base64.b64encode(content).decode('ascii') + if shell_mode == "on": + # Pass entire raw cmd string to bash -c + cmd_args = ["bash", "-c", cmd.strip()] + else: + # Use shlex to respect quotes + cmd_args = tokenize_cmd(cmd.strip()) + if not cmd_args: + raise HTTPException(status_code=400, detail="Command cannot be empty") + if include_db == "on": try: username_lower = current_user.username.lower() @@ -191,14 +212,13 @@ async def create_job_from_form( # Prepare payload for the existing API payload = { - "cmd": [part.strip() for part in cmd.split() if part.strip()], # split on whitespace, like shell + "cmd": cmd_args, "env": env if env else None, "files": files_dict if files_dict else None } # Call the API internally - from packetserver.http.routers.jobs import create_job as api_create_job - response = await api_create_job( + response = await create_job( payload=JobCreate(**{k: v for k, v in payload.items() if v is not None}), db=db, current_user=current_user diff --git a/packetserver/http/templates/job_detail.html b/packetserver/http/templates/job_detail.html index f049b90..017c225 100644 --- a/packetserver/http/templates/job_detail.html +++ b/packetserver/http/templates/job_detail.html @@ -10,9 +10,33 @@
Command + {% if job.cmd|length > 1 or ('bash' in job.cmd and job.cmd|length == 3) %} + Multi-arg + {% elif job.cmd|length == 1 %} + Single arg + {% endif %}
-
{% if job.cmd is string %}{{ job.cmd }}{% else %}{{ job.cmd|join(' ') }}{% endif %}
+ {% if job.cmd is string %} + +
{{ job.cmd }}
+ Legacy single-string command + {% else %} + +
    + {% for arg in job.cmd %} +
  1. + {{ arg | e }} +
  2. + {% endfor %} +
+ {% if job.cmd[:2] == ['bash', '-c'] %} +
+

Full command passed to bash -c:

+
{{ job.cmd[2] }}
+ This job used shell mode + {% endif %} + {% endif %}
diff --git a/packetserver/http/templates/job_new.html b/packetserver/http/templates/job_new.html index be11bd6..f8e0901 100644 --- a/packetserver/http/templates/job_new.html +++ b/packetserver/http/templates/job_new.html @@ -13,6 +13,16 @@
Space-separated command and arguments (like a shell command).
+
+ + +