Skip to content
Draft
131 changes: 127 additions & 4 deletions discord/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@
import threading
import time
import warnings
from collections.abc import Sequence
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 All @@ -52,7 +53,6 @@

from .voice import VoiceClient


AT = TypeVar("AT", bound="AudioSource")
FT = TypeVar("FT", bound="FFmpegOpusAudio")

Expand Down Expand Up @@ -309,6 +309,19 @@ def cleanup(self) -> None:
self._process = self._stdout = self._stdin = self._stderr = MISSING


DEFAULT_PROTOCOL_WHITELIST: Sequence[str] = (
"file",
"http",
"https",
"tcp",
"tls",
"crypto",
"pipe",
"fd",
"cache",
)


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

Expand All @@ -325,6 +338,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 +365,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:`abc.Sequence[str]`]
A sequence 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: Sequence[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: Sequence[str] | None = ...,
) -> None: ...

def __init__(
self,
source: str | io.BufferedIOBase,
Expand All @@ -358,6 +413,7 @@ def __init__(
stderr: IO[bytes] | None = None,
before_options: str | None = None,
options: str | None = None,
protocol_whitelist: Sequence[str] | None = DEFAULT_PROTOCOL_WHITELIST,
) -> None:
args = []
subprocess_kwargs = {
Expand All @@ -366,7 +422,17 @@ def __init__(
}

if isinstance(before_options, str):
args.extend(shlex.split(before_options))
user_args = shlex.split(before_options)
if "-protocol_whitelist" in user_args:
protocol_whitelist = None
warnings.warn(
"the protocol_whitelist argument is being ignored because -protocol_whitelist was found in before_options.",
UserWarning,
)
args.extend(user_args)

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 +496,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 +532,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:`abc.Sequence[str]`]
A sequence 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: Sequence[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: Sequence[str] | None = ...,
) -> None: ...

def __init__(
self,
source: str | io.BufferedIOBase,
Expand All @@ -474,6 +586,7 @@ def __init__(
stderr: IO[bytes] | None = None,
before_options: str | None = None,
options: str | None = None,
protocol_whitelist: Sequence[str] | None = DEFAULT_PROTOCOL_WHITELIST,
) -> None:
args = []
subprocess_kwargs = {
Expand All @@ -482,7 +595,17 @@ def __init__(
}

if isinstance(before_options, str):
args.extend(shlex.split(before_options))
user_args = shlex.split(before_options)
if "-protocol_whitelist" in user_args:
protocol_whitelist = None
warnings.warn(
"the protocol_whitelist argument is being ignored because -protocol_whitelist was found in before_options.",
UserWarning,
)
args.extend(user_args)

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