Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to enable proxy header #8675

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
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
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_SSL_KEYFILE=/run/secrets/parsec-pem-key
# 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
FirelightFlagboy marked this conversation as resolved.
Show resolved Hide resolved

# The granularity of Error log outputs.
PARSEC_LOG_LEVEL=WARNING

Expand Down
1 change: 1 addition & 0 deletions newsfragments/8626.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add server option ``--proxy-trusted-addresses`` to enable parsing of proxy headers from trusted addresses. By default, the server will trust the proxy headers from localhost.
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
24 changes: 9 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,13 @@ 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.
# When enabled, is restricted to only trusting connecting IPs in forwarded-allow-ips.
# See: https://www.uvicorn.org/settings/#http
# Currently uvicorn only supports X-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,
FirelightFlagboy marked this conversation as resolved.
Show resolved Hide resolved
# 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
39 changes: 11 additions & 28 deletions server/parsec/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +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:
return None
Expand Down Expand Up @@ -292,20 +279,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=["localhost", "127.0.0.1", "::1"],
envvar="PARSEC_PROXY_TRUSTED_ADDRESSES",
callback=lambda ctx, param, value: [item.strip() for item in 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.
Set this value to allow the server to use the forwarded headers from those clients.
""",
)
@click.option(
"--ssl-keyfile",
Expand Down Expand Up @@ -373,7 +355,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 +400,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 +487,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
6 changes: 4 additions & 2 deletions 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 Expand Up @@ -343,7 +343,9 @@ async def _watch_and_stop_after_process(pid: int, cancel_scope: anyio.CancelScop

app.state.testbed = testbed
app.state.backend = testbed.backend
await serve_parsec_asgi_app(host=host, port=port, app=app)
await serve_parsec_asgi_app(
host=host, port=port, app=app, proxy_trusted_addresses=[]
)

click.echo("bye ;-)")

Expand Down
2 changes: 1 addition & 1 deletion server/parsec/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,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
9 changes: 7 additions & 2 deletions server/tests/common/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
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,
)
from tests.common.postgresql import reset_postgresql_testbed

SERVER_DOMAIN = "parsec.invalid"
Expand All @@ -33,7 +38,7 @@ def backend_config(
debug=True,
db_config=db_config,
sse_keepalive=30,
forward_proto_enforce_https=None,
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
Loading