
* upgrade to latest pip for in_docker_test_script.sh * Bump gunicorn to 22.0.0 (security) Changelog: https://docs.gunicorn.org/en/stable/news.html#id1 use utime to notify workers liveness migrate setup to pyproject.toml fix numerous security vulnerabilities in HTTP parser (closing some request smuggling vectors) parsing additional requests is no longer attempted past unsupported request framing on HTTP versions < 1.1 support for chunked transfer is refused (only used in exploits) requests conflicting configured or passed SCRIPT_NAME now produce a verbose error Trailer fields are no longer inspected for headers indicating secure scheme support Python 3.12 ** Breaking changes ** minimum version is Python 3.7 the limitations on valid characters in the HTTP method have been bounded to Internet Standards requests specifying unsupported transfer coding (order) are refused by default (rare) HTTP methods are no longer casefolded by default (IANA method registry contains none affected) HTTP methods containing the number sign (#) are no longer accepted by default (rare) HTTP versions < 1.0 or >= 2.0 are no longer accepted by default (rare, only HTTP/1.1 is supported) HTTP versions consisting of multiple digits or containing a prefix/suffix are no longer accepted HTTP header field names Gunicorn cannot safely map to variables are silently dropped, as in other software HTTP headers with empty field name are refused by default (no legitimate use cases, used in exploits) requests with both Transfer-Encoding and Content-Length are refused by default (such a message might indicate an attempt to perform request smuggling) empty transfer codings are no longer permitted (reportedly seen with really old & broken proxies) ** SECURITY ** fix CVE-2024-1135 * Remove TYPE_CHECKING guard for pydantic v1 imports Retain TYPE_CHECKING guard in v1 fallback to force pyright into pydantic.v1 namespace * Run unit tests with pydantic v1 now that v2 is installed via poetry
355 lines
10 KiB
Python
355 lines
10 KiB
Python
"""The Reflex config."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import os
|
|
import sys
|
|
import urllib.parse
|
|
from typing import Any, Dict, List, Optional, Set
|
|
|
|
try:
|
|
import pydantic.v1 as pydantic
|
|
except ModuleNotFoundError:
|
|
import pydantic
|
|
|
|
from reflex_cli.constants.hosting import Hosting
|
|
|
|
from reflex import constants
|
|
from reflex.base import Base
|
|
from reflex.utils import console
|
|
|
|
|
|
class DBConfig(Base):
|
|
"""Database config."""
|
|
|
|
engine: str
|
|
username: Optional[str] = ""
|
|
password: Optional[str] = ""
|
|
host: Optional[str] = ""
|
|
port: Optional[int] = None
|
|
database: str
|
|
|
|
@classmethod
|
|
def postgresql(
|
|
cls,
|
|
database: str,
|
|
username: str,
|
|
password: str | None = None,
|
|
host: str | None = None,
|
|
port: int | None = 5432,
|
|
) -> DBConfig:
|
|
"""Create an instance with postgresql engine.
|
|
|
|
Args:
|
|
database: Database name.
|
|
username: Database username.
|
|
password: Database password.
|
|
host: Database host.
|
|
port: Database port.
|
|
|
|
Returns:
|
|
DBConfig instance.
|
|
"""
|
|
return cls(
|
|
engine="postgresql",
|
|
username=username,
|
|
password=password,
|
|
host=host,
|
|
port=port,
|
|
database=database,
|
|
)
|
|
|
|
@classmethod
|
|
def postgresql_psycopg2(
|
|
cls,
|
|
database: str,
|
|
username: str,
|
|
password: str | None = None,
|
|
host: str | None = None,
|
|
port: int | None = 5432,
|
|
) -> DBConfig:
|
|
"""Create an instance with postgresql+psycopg2 engine.
|
|
|
|
Args:
|
|
database: Database name.
|
|
username: Database username.
|
|
password: Database password.
|
|
host: Database host.
|
|
port: Database port.
|
|
|
|
Returns:
|
|
DBConfig instance.
|
|
"""
|
|
return cls(
|
|
engine="postgresql+psycopg2",
|
|
username=username,
|
|
password=password,
|
|
host=host,
|
|
port=port,
|
|
database=database,
|
|
)
|
|
|
|
@classmethod
|
|
def sqlite(
|
|
cls,
|
|
database: str,
|
|
) -> DBConfig:
|
|
"""Create an instance with sqlite engine.
|
|
|
|
Args:
|
|
database: Database name.
|
|
|
|
Returns:
|
|
DBConfig instance.
|
|
"""
|
|
return cls(
|
|
engine="sqlite",
|
|
database=database,
|
|
)
|
|
|
|
def get_url(self) -> str:
|
|
"""Get database URL.
|
|
|
|
Returns:
|
|
The database URL.
|
|
"""
|
|
host = (
|
|
f"{self.host}:{self.port}" if self.host and self.port else self.host or ""
|
|
)
|
|
username = urllib.parse.quote_plus(self.username) if self.username else ""
|
|
password = urllib.parse.quote_plus(self.password) if self.password else ""
|
|
|
|
if username:
|
|
path = f"{username}:{password}@{host}" if password else f"{username}@{host}"
|
|
else:
|
|
path = f"{host}"
|
|
|
|
return f"{self.engine}://{path}/{self.database}"
|
|
|
|
|
|
class Config(Base):
|
|
"""The config defines runtime settings for the app.
|
|
|
|
By default, the config is defined in an `rxconfig.py` file in the root of the app.
|
|
|
|
```python
|
|
# rxconfig.py
|
|
import reflex as rx
|
|
|
|
config = rx.Config(
|
|
app_name="myapp",
|
|
api_url="http://localhost:8000",
|
|
)
|
|
```
|
|
|
|
Every config value can be overridden by an environment variable with the same name in uppercase.
|
|
For example, `db_url` can be overridden by setting the `DB_URL` environment variable.
|
|
|
|
See the [configuration](https://reflex.dev/docs/getting-started/configuration/) docs for more info.
|
|
"""
|
|
|
|
class Config:
|
|
"""Pydantic config for the config."""
|
|
|
|
validate_assignment = True
|
|
|
|
# The name of the app (should match the name of the app directory).
|
|
app_name: str
|
|
|
|
# The log level to use.
|
|
loglevel: constants.LogLevel = constants.LogLevel.INFO
|
|
|
|
# The port to run the frontend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
|
|
frontend_port: int = 3000
|
|
|
|
# The path to run the frontend on. For example, "/app" will run the frontend on http://localhost:3000/app
|
|
frontend_path: str = ""
|
|
|
|
# The port to run the backend on. NOTE: When running in dev mode, the next available port will be used if this is taken.
|
|
backend_port: int = 8000
|
|
|
|
# The backend url the frontend will connect to. This must be updated if the backend is hosted elsewhere, or in production.
|
|
api_url: str = f"http://localhost:{backend_port}"
|
|
|
|
# The url the frontend will be hosted on.
|
|
deploy_url: Optional[str] = f"http://localhost:{frontend_port}"
|
|
|
|
# The url the backend will be hosted on.
|
|
backend_host: str = "0.0.0.0"
|
|
|
|
# The database url used by rx.Model.
|
|
db_url: Optional[str] = "sqlite:///reflex.db"
|
|
|
|
# The redis url
|
|
redis_url: Optional[str] = None
|
|
|
|
# Telemetry opt-in.
|
|
telemetry_enabled: bool = True
|
|
|
|
# The bun path
|
|
bun_path: str = constants.Bun.DEFAULT_PATH
|
|
|
|
# List of origins that are allowed to connect to the backend API.
|
|
cors_allowed_origins: List[str] = ["*"]
|
|
|
|
# Tailwind config.
|
|
tailwind: Optional[Dict[str, Any]] = {}
|
|
|
|
# Timeout when launching the gunicorn server. TODO(rename this to backend_timeout?)
|
|
timeout: int = 120
|
|
|
|
# Whether to enable or disable nextJS gzip compression.
|
|
next_compression: bool = True
|
|
|
|
# Additional frontend packages to install.
|
|
frontend_packages: List[str] = []
|
|
|
|
# The hosting service backend URL.
|
|
cp_backend_url: str = Hosting.CP_BACKEND_URL
|
|
# The hosting service frontend URL.
|
|
cp_web_url: str = Hosting.CP_WEB_URL
|
|
|
|
# The worker class used in production mode
|
|
gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
|
|
|
|
# Attributes that were explicitly set by the user.
|
|
_non_default_attributes: Set[str] = pydantic.PrivateAttr(set())
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Initialize the config values.
|
|
|
|
Args:
|
|
*args: The args to pass to the Pydantic init method.
|
|
**kwargs: The kwargs to pass to the Pydantic init method.
|
|
"""
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Update the config from environment variables.
|
|
env_kwargs = self.update_from_env()
|
|
for key, env_value in env_kwargs.items():
|
|
setattr(self, key, env_value)
|
|
|
|
# Update default URLs if ports were set
|
|
kwargs.update(env_kwargs)
|
|
self._non_default_attributes.update(kwargs)
|
|
self._replace_defaults(**kwargs)
|
|
|
|
@property
|
|
def module(self) -> str:
|
|
"""Get the module name of the app.
|
|
|
|
Returns:
|
|
The module name.
|
|
"""
|
|
return ".".join([self.app_name, self.app_name])
|
|
|
|
def update_from_env(self) -> dict[str, Any]:
|
|
"""Update the config values based on set environment variables.
|
|
|
|
Returns:
|
|
The updated config values.
|
|
|
|
Raises:
|
|
ValueError: If an environment variable is set to an invalid type.
|
|
"""
|
|
updated_values = {}
|
|
# Iterate over the fields.
|
|
for key, field in self.__fields__.items():
|
|
# The env var name is the key in uppercase.
|
|
env_var = os.environ.get(key.upper())
|
|
|
|
# If the env var is set, override the config value.
|
|
if env_var is not None:
|
|
if key.upper() != "DB_URL":
|
|
console.info(
|
|
f"Overriding config value {key} with env var {key.upper()}={env_var}"
|
|
)
|
|
|
|
# Convert the env var to the expected type.
|
|
try:
|
|
if issubclass(field.type_, bool):
|
|
# special handling for bool values
|
|
env_var = env_var.lower() in ["true", "1", "yes"]
|
|
else:
|
|
env_var = field.type_(env_var)
|
|
except ValueError:
|
|
console.error(
|
|
f"Could not convert {key.upper()}={env_var} to type {field.type_}"
|
|
)
|
|
raise
|
|
|
|
# Set the value.
|
|
updated_values[key] = env_var
|
|
|
|
return updated_values
|
|
|
|
def get_event_namespace(self) -> str:
|
|
"""Get the path that the backend Websocket server lists on.
|
|
|
|
Returns:
|
|
The namespace for websocket.
|
|
"""
|
|
event_url = constants.Endpoint.EVENT.get_url()
|
|
return urllib.parse.urlsplit(event_url).path
|
|
|
|
def _replace_defaults(self, **kwargs):
|
|
"""Replace formatted defaults when the caller provides updates.
|
|
|
|
Args:
|
|
**kwargs: The kwargs passed to the config or from the env.
|
|
"""
|
|
if "api_url" not in self._non_default_attributes and "backend_port" in kwargs:
|
|
self.api_url = f"http://localhost:{kwargs['backend_port']}"
|
|
|
|
if (
|
|
"deploy_url" not in self._non_default_attributes
|
|
and "frontend_port" in kwargs
|
|
):
|
|
self.deploy_url = f"http://localhost:{kwargs['frontend_port']}"
|
|
|
|
# If running in Github Codespaces, override API_URL
|
|
codespace_name = os.getenv("CODESPACE_NAME")
|
|
if "api_url" not in self._non_default_attributes and codespace_name:
|
|
GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
|
|
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
|
|
)
|
|
if codespace_name:
|
|
self.api_url = (
|
|
f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
|
|
f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
|
|
)
|
|
|
|
def _set_persistent(self, **kwargs):
|
|
"""Set values in this config and in the environment so they persist into subprocess.
|
|
|
|
Args:
|
|
**kwargs: The kwargs passed to the config.
|
|
"""
|
|
for key, value in kwargs.items():
|
|
if value is not None:
|
|
os.environ[key.upper()] = str(value)
|
|
setattr(self, key, value)
|
|
self._non_default_attributes.update(kwargs)
|
|
self._replace_defaults(**kwargs)
|
|
|
|
|
|
def get_config(reload: bool = False) -> Config:
|
|
"""Get the app config.
|
|
|
|
Args:
|
|
reload: Re-import the rxconfig module from disk
|
|
|
|
Returns:
|
|
The app config.
|
|
"""
|
|
sys.path.insert(0, os.getcwd())
|
|
try:
|
|
rxconfig = __import__(constants.Config.MODULE)
|
|
if reload:
|
|
importlib.reload(rxconfig)
|
|
return rxconfig.config
|
|
|
|
except ImportError:
|
|
return Config(app_name="") # type: ignore
|