🏡 index : quine-global-labs/mirror-cockpit-project-cockpit.git

# Copyright (C) 2014 Red Hat, Inc.
# SPDX-License-Identifier: GPL-3.0-or-later

import base64
import logging
import re
import secrets

logger = logging.getLogger(__name__)


def parse_type(challenge: str) -> tuple[str, str]:
    """Extract the auth type from a challenge string.

    Returns (type, remainder) where type is lowercased.
    Raises ValueError if the challenge is invalid.
    """
    logger.debug("parsing type from challenge %r", challenge)

    auth_type, *remainder = re.split(r"[: ] *", challenge, maxsplit=1)
    if not auth_type:
        raise ValueError('invalid "authorize" message')

    logger.debug("parsed type %r, remainder %r", auth_type, remainder)
    return auth_type.lower(), remainder[0] if remainder else ""


def parse_subject(challenge: str) -> tuple[str, str]:
    """Extract the subject from a challenge string (after the type).

    Returns (subject, remainder).
    Raises ValueError if the challenge is invalid.
    """
    logger.debug("parsing subject from challenge %r", challenge)

    _, after_type = parse_type(challenge)

    subject, *remainder = re.split(r"[: ] *", after_type, maxsplit=1)
    if not subject:
        raise ValueError('invalid "authorize" message "challenge": no subject')

    logger.debug("parsed subject %r, remainder %r", subject, remainder)
    return subject, remainder[0] if remainder else ""


def parse_basic(challenge: str) -> tuple[str, str, str]:
    """Parse a Basic auth challenge.

    Returns (user, password, known_hosts).
    The format is: Basic base64(user:password\0known_hosts)
    Raises ValueError if the challenge is invalid.
    """
    logger.debug("parsing basic auth from %r", challenge)

    auth_type, remainder = parse_type(challenge)
    if auth_type != "basic":
        raise ValueError("invalid prefix in Basic header")

    token, _, _ = remainder.partition(" ")
    if not token:
        logger.debug("empty basic auth token")
        return "", "", ""

    try:
        decoded = base64.b64decode(token).decode("utf-8")
    except ValueError as exc:
        raise ValueError("invalid base64 data in Basic header") from exc

    if ":" not in decoded:
        raise ValueError("invalid base64 data in Basic header")

    user_password, _, known_hosts = decoded.partition("\0")
    user, _, password = user_password.partition(":")
    logger.debug("parsed basic auth for user %r", user)
    return user, password, known_hosts


def build_basic(
    user: str | None, password: str | None, known_hosts: str | None = None
) -> str:
    """Build a Basic auth response string.

    Format: Basic base64(user:password\0known_hosts)
    """
    user = user or ""
    password = password or ""
    logger.debug("building basic auth for user %r", user)

    content = f"{user}:{password}"
    if known_hosts:
        content = f"{content}\0{known_hosts}"
    encoded = base64.b64encode(content.encode("utf-8")).decode("ascii")
    return f"Basic {encoded}"


def parse_negotiate(challenge: str) -> bytes:
    """Parse a Negotiate auth challenge.

    Returns the decoded token bytes.
    Raises ValueError if the challenge is invalid.
    """
    logger.debug("parsing negotiate auth from %r", challenge)

    auth_type, remainder = parse_type(challenge)
    if auth_type != "negotiate":
        raise ValueError("invalid prefix in Negotiate header")

    token, _, _ = remainder.partition(" ")

    try:
        data = base64.b64decode(token)
    except ValueError as exc:
        raise ValueError("invalid base64 data in Negotiate header") from exc

    logger.debug("parsed negotiate token of length %r", len(data))
    return data


def build_negotiate(data: bytes | None) -> str:
    """Build a Negotiate auth response string."""
    if not data:
        logger.debug("building empty negotiate response")
        return "Negotiate"

    logger.debug("building negotiate response for %r bytes", len(data))
    encoded = base64.b64encode(data).decode("ascii")
    return f"Negotiate {encoded}"


def parse_x_conversation(challenge: str) -> tuple[str, str]:
    """Parse an X-Conversation challenge.

    Returns (conversation_id, prompt).
    Raises ValueError if the challenge is invalid.
    """
    logger.debug("parsing x-conversation from %r", challenge)

    auth_type, _ = parse_type(challenge)
    if auth_type != "x-conversation":
        raise ValueError("invalid prefix in X-Conversation header")

    conversation, remainder = parse_subject(challenge)

    token, _, _ = remainder.partition(" ")

    try:
        prompt = base64.b64decode(token).decode("utf-8") if token else ""
    except ValueError as exc:
        raise ValueError("invalid base64 data in X-Conversation header") from exc

    logger.debug("parsed x-conversation %r with prompt %r", conversation, prompt)
    return conversation, prompt


def build_x_conversation(
    prompt: str | None, conversation: str | None = None
) -> tuple[str, str]:
    """Build an X-Conversation challenge string.

    If conversation is None, generates a new one.
    Returns (response_string, conversation_id).
    Raises ValueError if conversation is empty string.
    """
    prompt = prompt or ""
    logger.debug(
        "building x-conversation with prompt %r, conversation %r", prompt, conversation
    )

    if conversation is None:
        conversation = secrets.token_urlsafe(16)
        logger.debug("generated new conversation id %r", conversation)

    if not conversation:
        raise ValueError("invalid conversation nonce")

    if prompt:
        encoded = base64.b64encode(prompt.encode("utf-8")).decode("ascii")
        response = f"X-Conversation {conversation} {encoded}"
    else:
        response = f"X-Conversation {conversation}"

    return response, conversation