Skip to content

Commit

Permalink
Add option to enable proxy header
Browse files Browse the repository at this point in the history
Add a new option that configure the list of trusted address to parse the proxy headers from.
The parsing is handled by `uvicorn` which currently only support `x-forwarded-{for,proto}` headers.

Closes #8626, closes #8427
  • Loading branch information
FirelightFlagboy committed Oct 8, 2024
1 parent d634e8c commit ab99e7b
Show file tree
Hide file tree
Showing 9 changed files with 57 additions and 79 deletions.
46 changes: 18 additions & 28 deletions docs/HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,24 @@

# Hosting Server

- [Hosting Server](#hosting-server)
- [Requirements](#requirements)
- [Hosting](#hosting)
- [Installation](#installation)
- [Run](#run)
- [Settings](#settings)
- [Host](#host)
- [Port](#port)
- [Database URL](#database-url)
- [Database connections](#database-connections)
- [Blockstore URL](#blockstore-url)
- [Administration token](#administration-token)
- [SSL](#ssl)
- [Logs](#logs)
- [Email](#email)
- [Webhooks](#webhooks)
- [SSE Keepalive](#sse-keepalive)
- [Sentry](#sentry)
- [Debug](#debug)
- [Requirements](#requirements)
- [Hosting](#hosting)
- [Installation](#installation)
- [Run](#run)
- [Settings](#settings)
- [Host](#host)
- [Port](#port)
- [Database URL](#database-url)
- [Database connections](#database-connections)
- [Blockstore URL](#blockstore-url)
- [Administration token](#administration-token)
- [SSL](#ssl)
- [Logs](#logs)
- [Email](#email)
- [Webhooks](#webhooks)
- [SSE Keepalive](#sse-keepalive)
- [Sentry](#sentry)
- [Debug](#debug)

## Requirements

Expand Down Expand Up @@ -168,15 +167,6 @@ SSL key file. This setting enables serving Parsec over SSL.
SSL certificate file. This setting enables serving Parsec over SSL.
- ``--forward-proto-enforce-https``
- Environ: ``PARSEC_FORWARD_PROTO_ENFORCE_HTTPS``
Enforce HTTPS by redirecting incoming request that do not comply with the provided header.
This is useful when running Parsec behind a forward proxy handing the SSL layer.
You should *only* use this setting if you control your proxy or have some other
guarantee that it sets/strips this header appropriately.
Typical value for this setting should be `X-Forwarded-Proto:https`.
### Logs
- ``--log-level <level>, -l <level>``
Expand Down
3 changes: 0 additions & 3 deletions docs/hosting/deployment/parsec.env
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ PARSEC_HOST=0.0.0.0
# The SSL certificate file.
# PARSEC_SSL_CERTFILE=/run/secrets/parsec-pem-crt

# Enforce HTTPS by redirecting HTTP request.
PARSEC_FORWARD_PROTO_ENFORCE_HTTPS=X-Forwarded-Proto:https

# The granularity of Error log outputs.
PARSEC_LOG_LEVEL=WARNING

Expand Down
8 changes: 8 additions & 0 deletions newsfragments/8626.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Add server option ``--proxy-trusted-addresses`` to enabled parsing of proxy headers from trusted addresses.

.. important::
Breaking change: Previous the server used the default value provided by ``uvicorn`` which always enabled the proxy header for localhost proxy.
If you want to have the same behavior as before, you need to set the ``--proxy-trusted-addresses`` to ``localhost,127.0.0.1,::1``.

.. tip::
The env variable ``PARSEC_PROXY_TRUSTED_ADDRESSES`` instead of the option.
2 changes: 0 additions & 2 deletions server/packaging/server/template-prod.env.list
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ PARSEC_DB_MAX_CONNECTIONS=7
# PARSEC_SSL_KEYFILE
# The SSL certificate file.
# PARSEC_SSL_CERTFILE
# Enforce HTTPS by redirecting HTTP request.
PARSEC_FORWARD_PROTO_ENFORCE_HTTPS=true

# The granularity of Error log outputs.
PARSEC_LOG_LEVEL=WARNING
Expand Down
23 changes: 8 additions & 15 deletions server/parsec/asgi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,25 +60,11 @@ async def page_not_found(scope: Scope, receive: Receive, send: Send) -> None:
app: AsgiApp = app_factory()


# TODO: implement forward_proto_enforce_https
# # Do https redirection if incoming request doesn't follow forward proto rules
# if backend.config.forward_proto_enforce_https:
# header_key, header_expected_value = backend.config.forward_proto_enforce_https

# @app.before_request
# def redirect_unsecure() -> ResponseReturnValue | None:
# header_value = request.headers.get(header_key)
# # If redirection header match and protocol match, then no need for a redirection.
# if header_value is not None and header_value != header_expected_value:
# if request.url.startswith("http://"):
# return quart_redirect(request.url.replace("http://", "https://", 1), code=301)
# return None


async def serve_parsec_asgi_app(
app: AsgiApp,
host: str,
port: int,
proxy_trusted_addresses: list[str],
ssl_certfile: Path | None = None,
ssl_keyfile: Path | None = None,
workers: int | None = None,
Expand All @@ -93,6 +79,7 @@ async def serve_parsec_asgi_app(
v_major, _ = parsec_version.split(".", 1)
# ex: parsec/3
server_header = f"parsec/{v_major}"

# Note: Uvicorn comes with default values for incoming data size to
# avoid DoS abuse, so just trust them on that ;-)
config = uvicorn.Config(
Expand All @@ -106,6 +93,12 @@ async def serve_parsec_asgi_app(
ssl_keyfile=ssl_keyfile, # type: ignore
ssl_certfile=ssl_certfile,
workers=workers,
# Enable/Disable X-Forwarded-Proto, X-Forwarded-For to populate remote address info.
# Currently uvicorn only supports X-Forwarded headers
# An issue exist to support Forwarded headers: https://github.com/encode/uvicorn/issues/2237
proxy_headers=(proxy_trusted_addresses != []),
# Comma separated list of IP Addresses, IP Networks, or literals (e.g. UNIX Socket path) to trust with proxy headers
forwarded_allow_ips=proxy_trusted_addresses,
# TODO: configure access log format:
# Timestamp is added by the log processor configured in `parsec.logging`,
# here we configure peer address + req line + rep status + rep body size + time
Expand Down
38 changes: 11 additions & 27 deletions server/parsec/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,6 @@
DEFAULT_EMAIL_SENDER = "[email protected]"


def _parse_forward_proto_enforce_https_check_param(
raw_param: str | None,
) -> tuple[str, str] | None:
if raw_param is None:
return None
try:
key, value = raw_param.split(":")
except ValueError:
raise click.BadParameter("Invalid format, should be `<header-name>:<header-value>`")
# HTTP header key is case-insensitive unlike the header value
return (key.lower(), value)


def _parse_organization_initial_tos_url(raw_param: str | None) -> dict[TosLocale, TosUrl] | None:
if raw_param is None:
Expand Down Expand Up @@ -292,20 +280,15 @@ def handle_parse_result(
help="Sender address used in sent emails",
)
@click.option(
"--forward-proto-enforce-https",
type=str,
show_default=True,
default=None,
callback=lambda ctx, param, value: _parse_forward_proto_enforce_https_check_param(value),
envvar="PARSEC_FORWARD_PROTO_ENFORCE_HTTPS",
"--proxy-trusted-addresses",
default=[],
envvar="PARSEC_PROXY_TRUSTED_ADDRESSES",
callback=lambda ctx, param, value: str(value).split(","),
show_envvar=True,
help=(
"Enforce HTTPS by redirecting incoming request that do not comply with the provided header."
" This is useful when running Parsec behind a forward proxy handing the SSL layer."
" You should *only* use this setting if you control your proxy or have some other"
" guarantee that it sets/strips this header appropriately."
" Typical value for this setting should be `X-Forwarded-Proto:https`."
),
help="""\b
Comma-separated list of IP Addresses, IP Networks or literals to trust with proxy headers.
Settings this value will allow the server to use the forwarded headers from those clients.
""",
)
@click.option(
"--ssl-keyfile",
Expand Down Expand Up @@ -373,7 +356,7 @@ def run_cmd(
email_use_ssl: bool,
email_use_tls: bool,
email_sender: str | None,
forward_proto_enforce_https: tuple[str, str] | None,
proxy_trusted_addresses: list[str],
ssl_keyfile: Path | None,
ssl_certfile: Path | None,
log_level: LogLevel,
Expand Down Expand Up @@ -418,7 +401,7 @@ def run_cmd(
sse_keepalive=sse_keepalive,
blockstore_config=blockstore,
email_config=email_config,
forward_proto_enforce_https=forward_proto_enforce_https,
proxy_trusted_addresses=proxy_trusted_addresses,
server_addr=server_addr,
debug=debug,
organization_bootstrap_webhook_url=organization_bootstrap_webhook,
Expand Down Expand Up @@ -505,6 +488,7 @@ async def _run_backend(
port=port,
ssl_certfile=ssl_certfile,
ssl_keyfile=ssl_keyfile,
proxy_trusted_addresses=app_config.proxy_trusted_addresses,
)
return

Expand Down
2 changes: 1 addition & 1 deletion server/parsec/cli/testbed.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ async def testbed_backend_factory(
debug=True,
db_config=db_config,
sse_keepalive=30,
forward_proto_enforce_https=None,
proxy_trusted_addresses=[],
server_addr=server_addr,
email_config=MockedEmailConfig("[email protected]", tmpdir),
blockstore_config=blockstore_config,
Expand Down
3 changes: 2 additions & 1 deletion server/parsec/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class LogLevel(Enum):
CRITICAL = logging.CRITICAL



@dataclass(slots=True)
class BackendConfig:
administration_token: str
Expand All @@ -177,7 +178,7 @@ class BackendConfig:
blockstore_config: BaseBlockStoreConfig

email_config: SmtpEmailConfig | MockedEmailConfig
forward_proto_enforce_https: tuple[str, str] | None
proxy_trusted_addresses: list[str]
server_addr: ParsecAddr | None

debug: bool
Expand Down
11 changes: 9 additions & 2 deletions server/tests/common/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@
from parsec.backend import Backend, backend_factory
from parsec.cli.testbed import TestbedBackend, TestbedTemplate
from parsec.components.memory.organization import MemoryOrganization, OrganizationID
from parsec.config import BackendConfig, BaseBlockStoreConfig, BaseDatabaseConfig, MockedEmailConfig
from parsec.config import (
BackendConfig,
BaseBlockStoreConfig,
BaseDatabaseConfig,
MockedEmailConfig,
ProxyHeaderMode,
)
from tests.common.postgresql import reset_postgresql_testbed

SERVER_DOMAIN = "parsec.invalid"
Expand All @@ -33,7 +39,8 @@ def backend_config(
debug=True,
db_config=db_config,
sse_keepalive=30,
forward_proto_enforce_https=None,
proxy_header_mode=ProxyHeaderMode.Disabled,
proxy_trusted_addresses=[],
server_addr=ParsecAddr(hostname=SERVER_DOMAIN, port=None, use_ssl=True),
email_config=MockedEmailConfig("[email protected]", tmpdir),
blockstore_config=blockstore_config,
Expand Down

0 comments on commit ab99e7b

Please sign in to comment.