Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion discord/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
import time
import warnings
from math import floor
from typing import IO, TYPE_CHECKING, Any, Callable, Generic, TypeVar
from typing import IO, TYPE_CHECKING, Any, Callable, Generic, Literal, TypeVar, overload

from .enums import SpeakingState
from .errors import ClientException
Expand Down Expand Up @@ -309,6 +309,11 @@ def cleanup(self) -> None:
self._process = self._stdout = self._stdin = self._stderr = MISSING


DEFAULT_PROTOCOL_WHITELIST: set[str] = frozenset(
{"file", "http", "https", "tcp", "tls", "crypto", "pipe", "fd", "cache"}
)


class FFmpegPCMAudio(FFmpegAudio):
"""An audio source from FFmpeg (or AVConv).

Expand All @@ -325,6 +330,16 @@ class FFmpegPCMAudio(FFmpegAudio):
The input that ffmpeg will take and convert to PCM bytes.
If ``pipe`` is ``True`` then this is a file-like object that is
passed to the stdin of ffmpeg.

.. warning::

The ``source`` parameter is passed directly to your executable's ``-i`` flag and
interpreted through its full protocol machinery. This means it can accept
``file://`` paths (local file read), ``http(s)://`` to internal services
(SSRF), ``concat:`` (multi-file read), and other dangerous schemes.
You should never pass attacker-controlled values (e.g. raw user input from a chat
command) to ``source`` without validation.

executable: :class:`str`
The executable name (and path) to use. Defaults to ``ffmpeg``.

Expand All @@ -342,13 +357,45 @@ class FFmpegPCMAudio(FFmpegAudio):
Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
options: Optional[:class:`str`]
Extra command line arguments to pass to ffmpeg after the ``-i`` flag.
protocol_whitelist: Optional[:class:`set[str]`]
A comma-separated list of protocols that ffmpeg is allowed to use.
Defaults to ``"file,http,https,tcp,tls,crypto,pipe,fd,cache"``, which
blocks dangerous schemes such as ``concat:``, ``subfile:``, ``data:``,
and ``gopher:``. Set to ``None`` to disable the whitelist entirely
(not recommended unless you are certain of the input source).

Raises
------
ClientException
The subprocess failed to be created.
"""

@overload
def __init__(
self,
source: io.BufferedIOBase,
*,
executable: str = ...,
pipe: Literal[True] = ...,
stderr: IO[bytes] | None = ...,
before_options: str | None = ...,
options: str | None = ...,
protocol_whitelist: set[str] | None = ...,
) -> None: ...

@overload
def __init__(
self,
source: str,
*,
executable: str = ...,
pipe: Literal[False] = ...,
stderr: IO[bytes] | None = ...,
before_options: str | None = ...,
options: str | None = ...,
protocol_whitelist: set[str] | None = ...,
) -> None: ...

def __init__(
self,
source: str | io.BufferedIOBase,
Expand All @@ -358,6 +405,7 @@ def __init__(
stderr: IO[bytes] | None = None,
before_options: str | None = None,
options: str | None = None,
protocol_whitelist: set[str] | None = DEFAULT_PROTOCOL_WHITELIST,
) -> None:
args = []
subprocess_kwargs = {
Expand All @@ -368,6 +416,9 @@ def __init__(
if isinstance(before_options, str):
args.extend(shlex.split(before_options))

if protocol_whitelist is not None:
args.extend(["-protocol_whitelist", ",".join(protocol_whitelist)])

args.append("-i")
args.append("-" if pipe else source)

Expand Down Expand Up @@ -430,6 +481,16 @@ class FFmpegOpusAudio(FFmpegAudio):
The input that ffmpeg will take and convert to Opus bytes.
If ``pipe`` is ``True`` then this is a file-like object that is
passed to the stdin of ffmpeg.

.. warning::

The ``source`` parameter is passed directly to your executable's ``-i`` flag and
interpreted through its full protocol machinery. This means it can accept
``file://`` paths (local file read), ``http(s)://`` to internal services
(SSRF), ``concat:`` (multi-file read), and other dangerous schemes.
You should never pass attacker-controlled values (e.g. raw user input from a chat
command) to ``source`` without validation.

bitrate: :class:`int`
The bitrate in kbps to encode the output to. Defaults to ``128``.
codec: Optional[:class:`str`]
Expand All @@ -456,13 +517,49 @@ class FFmpegOpusAudio(FFmpegAudio):
Extra command line arguments to pass to ffmpeg before the ``-i`` flag.
options: Optional[:class:`str`]
Extra command line arguments to pass to ffmpeg after the ``-i`` flag.
protocol_whitelist: Optional[:class:`set[str]`]
A set of protocols that ffmpeg is allowed to use.
Defaults to ``{"file", "http", "https", "tcp", "tls", "crypto", "pipe", "fd", "cache"}``, which
blocks dangerous schemes such as ``concat:``, ``subfile:``, ``data:``,
and ``gopher:``. Set to ``None`` to disable the whitelist entirely
(not recommended unless you are certain of the input source).

Raises
------
ClientException
The subprocess failed to be created.
"""

@overload
def __init__(
self,
source: io.BufferedIOBase,
*,
bitrate: int | None = None,
codec: str | None = None,
executable: str = ...,
pipe: Literal[True] = ...,
stderr: IO[bytes] | None = ...,
before_options: str | None = ...,
options: str | None = ...,
protocol_whitelist: set[str] | None = ...,
) -> None: ...

@overload
def __init__(
self,
source: str,
*,
bitrate: int | None = None,
codec: str | None = None,
executable: str = ...,
pipe: Literal[False] = ...,
stderr: IO[bytes] | None = ...,
before_options: str | None = ...,
options: str | None = ...,
protocol_whitelist: set[str] | None = ...,
) -> None: ...

def __init__(
self,
source: str | io.BufferedIOBase,
Expand All @@ -474,6 +571,7 @@ def __init__(
stderr: IO[bytes] | None = None,
before_options: str | None = None,
options: str | None = None,
protocol_whitelist: set[str] | None = DEFAULT_PROTOCOL_WHITELIST,
) -> None:
args = []
subprocess_kwargs = {
Expand All @@ -484,6 +582,9 @@ def __init__(
if isinstance(before_options, str):
args.extend(shlex.split(before_options))

if protocol_whitelist is not None:
args.extend(["-protocol_whitelist", ",".join(protocol_whitelist)])

args.append("-i")
args.append("-" if pipe else source)

Expand Down
Loading