| Server IP : 142.11.234.102 / Your IP : 216.73.217.33 Web Server : Apache System : Linux dal-shared-66.hostwindsdns.com 4.18.0-513.24.1.lve.1.el8.x86_64 #1 SMP Thu May 9 15:10:09 UTC 2024 x86_64 User : krnuyqrm ( 1183) PHP Version : 7.4.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : OFF | Pkexec : OFF Directory : /opt/cloudlinux/venv/lib/python3.11/site-packages/clcagefslib/webisolation/crontab/ |
Upload File : |
# -*- coding: utf-8 -*-
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2025 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
"""Parsing and serialization functions for crontab entries."""
from .constants import (
CRON_ENV_ASSIGNMENT_PATTERN,
CRON_NICKNAME_PATTERN,
CRON_SCHEDULE_PATTERN,
WEBSITE_CRON_BEGIN_PATTERN,
WEBSITE_CRON_END_PATTERN,
)
from .structure import (
BeginWebsite,
CommentLine,
CrontabEntry,
CrontabStructure,
EndWebsite,
EnvAssignmentLine,
ParsedCrontabLine,
)
# Defense-in-depth reset lines emitted at the top of every docroot section
# whenever the global scope carries any env-assignment. crond honours
# env-assignment lines in textual order (per crontab(5)) for all following
# jobs, so a user-supplied `SHELL=/path/to/fake` in global_records would apply
# to the wrapped docroot jobs below it and let a caller-controlled interpreter
# defeat the isolation wrapper. Reset the interpreter-selection knobs to safe
# defaults at the top of each wrapped section so the global assignment cannot
# reach the docroot jobs textually below. See F-31 / CLOS-5418.
_DOCROOT_ENV_RESET_LINES: tuple[bytes, ...] = (
b"SHELL=/bin/bash\n",
b"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin\n",
b"BASH_ENV=\n",
)
def parse_crontab_line(line: bytes) -> CrontabEntry:
"""
Parse a crontab line into a cron entry, comment, or website marker.
Args:
line: A single line from crontab output (bytes)
"""
stripped = line.strip()
# Check for website cron begin marker
begin_match = WEBSITE_CRON_BEGIN_PATTERN.match(stripped)
if begin_match:
docroot_bytes = begin_match.group(1).strip()
docroot = docroot_bytes.decode("utf-8", errors="replace")
return BeginWebsite(docroot=docroot)
# Check for website cron end marker
if WEBSITE_CRON_END_PATTERN.match(stripped):
return EndWebsite()
# Handle empty lines and comments
if not stripped:
return CommentLine(content=line)
if stripped.startswith(b"#"):
return CommentLine(content=line)
# Environment-assignment line (`name = value`) — must be classified
# distinctly from comments so the wrap step can drop these in per-site
# sections; otherwise crond honours them and a user-supplied `SHELL=`
# would run before the isolation wrapper for following job lines.
if CRON_ENV_ASSIGNMENT_PATTERN.match(stripped):
return EnvAssignmentLine(content=line)
# Nickname schedule (`@hourly command`, `@reboot command`, ...) — a
# single-token cron(5) schedule form the 5-field CRON_SCHEDULE_PATTERN
# below never matches. Without this branch such lines fall through to
# CommentLine and skip the wrap loop in process_save, letting a
# site-scoped caller plant a nickname job that crond executes without
# the cagefs_enter_site confinement. Classify as ParsedCrontabLine so
# the wrap loop treats it like any other schedule/command pair.
nickname_match = CRON_NICKNAME_PATTERN.match(stripped)
if nickname_match:
schedule = nickname_match.group(1)
command = nickname_match.group(2)
if line.endswith(b"\n"):
command += b"\n"
return ParsedCrontabLine(schedule=schedule, command=command)
# Try to match cron schedule + command
schedule_match = CRON_SCHEDULE_PATTERN.match(stripped)
if not schedule_match:
return CommentLine(content=line)
schedule = schedule_match.group(1)
command = schedule_match.group(2)
if line.endswith(b"\n"):
command += b"\n"
return ParsedCrontabLine(
schedule=schedule,
command=command,
)
def parse_crontab_structure(input_data: bytes) -> CrontabStructure:
"""
Parse crontab file into structured format with user records and docroot sections.
Supports both old format (with isolation wrapper prefixes) and new format
(with section markers).
Args:
input_data: bytes containing crontab entries
Returns:
CrontabStructure: Structure containing global_records and docroot_sections
"""
lines = input_data.splitlines(keepends=True)
crontab_records: dict[
str | None,
list[CrontabEntry],
] = {}
current_docroot: str | None = None
for line in lines:
parsed = parse_crontab_line(line)
if isinstance(parsed, BeginWebsite):
current_docroot = parsed.docroot
continue
if isinstance(parsed, EndWebsite):
current_docroot = None
continue
crontab_records.setdefault(current_docroot, []).append(parsed)
global_records = crontab_records.pop(None, [])
return CrontabStructure(
global_records=global_records,
docroot_sections=crontab_records,
)
def write_crontab_structure(structure: CrontabStructure) -> bytes:
"""
Write crontab structure to bytes in new format.
Format:
- User records at the top
- Then docroot sections with markers:
## WEBSITE CRON BEGIN /path/to/docroot
... entries ...
## WEBSITE CRON END
Args:
structure: CrontabStructure with user_records and docroot_sections
Returns:
bytes: Crontab content in new format
"""
result_parts = entries_to_str_list(structure.global_records)
# F-31: only pay the reset when a global env-assignment is actually
# present to bleed. Without one, crond has nothing to carry across the
# section marker and the reset would be visible noise inside `crontab -l`
# for docroot cages.
needs_env_reset = any(
isinstance(entry, EnvAssignmentLine) for entry in structure.global_records
)
# Add docroot sections (preserve insertion order)
for docroot in structure.docroot_sections.keys():
# Strip any EnvAssignmentLine entries from the section body: process_save
# already refuses to persist them from tenant input, and stripping here
# keeps the serializer idempotent — otherwise a section that we
# ourselves reset-prefixed on a previous write would re-parse those
# reset lines as section EnvAssignmentLine entries, and each subsequent
# write would prepend another set, growing without bound.
entries = [
entry
for entry in structure.docroot_sections[docroot]
if not isinstance(entry, EnvAssignmentLine)
]
if not entries:
continue
if '\n' in docroot or '\r' in docroot:
raise ValueError(f'Invalid docroot: {docroot!r}')
result_parts.append(f"## WEBSITE CRON BEGIN {docroot}\n".encode("utf-8"))
if needs_env_reset:
result_parts.extend(_DOCROOT_ENV_RESET_LINES)
result_parts.extend(entries_to_str_list(entries))
result_parts.append(b"## WEBSITE CRON END\n")
return b"".join(result_parts)
def entries_to_str_list(entries: list[CrontabEntry], without_wrapper: bool = False) -> list[bytes]:
"""
Converts parsed crontab entries into a string list.
"""
lines = []
for entry in entries:
if isinstance(entry, ParsedCrontabLine):
line = (
entry.schedule
+ b" "
+ (entry.get_clean_command() if without_wrapper else entry.command)
)
if not line.endswith(b"\n"):
line += b"\n"
lines.append(line)
elif isinstance(entry, CommentLine):
lines.append(entry.content)
elif isinstance(entry, EnvAssignmentLine):
lines.append(entry.content)
return lines