mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-03 11:11:30 +08:00
Add HTTP/2 dependency and configuration
- Add optional h2 dependency for HTTP/2 support - Add http2 module skeleton with availability check and errors - Add HTTP/2 configuration settings (max_concurrent_streams, initial_window_size, max_frame_size, max_header_list_size) - Add ALPN support to SSL context for HTTP/2 negotiation
This commit is contained in:
parent
1fe9e5816e
commit
c711d9fb6f
@ -2395,6 +2395,180 @@ class Ciphers(Setting):
|
||||
"""
|
||||
|
||||
|
||||
# HTTP/2 Protocol Settings
|
||||
|
||||
# Valid protocol identifiers
|
||||
VALID_HTTP_PROTOCOLS = frozenset(["h1", "h2", "h3"])
|
||||
# Map protocol identifiers to ALPN protocol names
|
||||
ALPN_PROTOCOL_MAP = {
|
||||
"h1": "http/1.1",
|
||||
"h2": "h2",
|
||||
"h3": "h3", # Future: HTTP/3 over QUIC
|
||||
}
|
||||
|
||||
|
||||
def validate_http_protocols(val):
|
||||
"""Validate http_protocols setting.
|
||||
|
||||
Accepts comma-separated list of protocol identifiers.
|
||||
Valid values: h1 (HTTP/1.1), h2 (HTTP/2), h3 (HTTP/3 - future)
|
||||
Order indicates preference (first = most preferred).
|
||||
"""
|
||||
if val is None:
|
||||
return ["h1"]
|
||||
if not isinstance(val, str):
|
||||
raise TypeError("http_protocols must be a string")
|
||||
|
||||
val = val.strip()
|
||||
if not val:
|
||||
return ["h1"]
|
||||
|
||||
protocols = [p.strip().lower() for p in val.split(",") if p.strip()]
|
||||
if not protocols:
|
||||
return ["h1"]
|
||||
|
||||
# Validate each protocol
|
||||
for proto in protocols:
|
||||
if proto not in VALID_HTTP_PROTOCOLS:
|
||||
raise ValueError(
|
||||
f"Invalid protocol '{proto}'. "
|
||||
f"Valid protocols: {', '.join(sorted(VALID_HTTP_PROTOCOLS))}"
|
||||
)
|
||||
|
||||
# Check for duplicates
|
||||
if len(protocols) != len(set(protocols)):
|
||||
raise ValueError("Duplicate protocols specified")
|
||||
|
||||
return protocols
|
||||
|
||||
|
||||
class HTTPProtocols(Setting):
|
||||
name = "http_protocols"
|
||||
section = "HTTP/2"
|
||||
cli = ["--http-protocols"]
|
||||
meta = "STRING"
|
||||
validator = validate_http_protocols
|
||||
default = "h1"
|
||||
desc = """\
|
||||
HTTP protocol versions to support (comma-separated, order = preference).
|
||||
|
||||
Valid protocols:
|
||||
|
||||
* ``h1`` - HTTP/1.1 (default)
|
||||
* ``h2`` - HTTP/2 (requires TLS with ALPN)
|
||||
* ``h3`` - HTTP/3 (future, not yet implemented)
|
||||
|
||||
Examples::
|
||||
|
||||
# HTTP/1.1 only (default, backward compatible)
|
||||
--http-protocols=h1
|
||||
|
||||
# Prefer HTTP/2, fallback to HTTP/1.1
|
||||
--http-protocols=h2,h1
|
||||
|
||||
# HTTP/2 only (reject HTTP/1.1 clients)
|
||||
--http-protocols=h2
|
||||
|
||||
HTTP/2 requires:
|
||||
|
||||
* TLS (--certfile and --keyfile)
|
||||
* The h2 library: ``pip install gunicorn[http2]``
|
||||
* ALPN-capable TLS client
|
||||
|
||||
.. note::
|
||||
HTTP/2 cleartext (h2c) is not supported due to security concerns
|
||||
and lack of browser support.
|
||||
|
||||
.. versionadded:: 24.0.0
|
||||
"""
|
||||
|
||||
|
||||
class HTTP2MaxConcurrentStreams(Setting):
|
||||
name = "http2_max_concurrent_streams"
|
||||
section = "HTTP/2"
|
||||
cli = ["--http2-max-concurrent-streams"]
|
||||
meta = "INT"
|
||||
validator = validate_pos_int
|
||||
type = int
|
||||
default = 100
|
||||
desc = """\
|
||||
Maximum number of concurrent HTTP/2 streams per connection.
|
||||
|
||||
This limits how many requests can be processed simultaneously on a
|
||||
single HTTP/2 connection. Higher values allow more parallelism but
|
||||
use more memory.
|
||||
|
||||
Default is 100, which matches common server configurations.
|
||||
The HTTP/2 specification allows up to 2^31-1.
|
||||
|
||||
.. versionadded:: 24.0.0
|
||||
"""
|
||||
|
||||
|
||||
class HTTP2InitialWindowSize(Setting):
|
||||
name = "http2_initial_window_size"
|
||||
section = "HTTP/2"
|
||||
cli = ["--http2-initial-window-size"]
|
||||
meta = "INT"
|
||||
validator = validate_pos_int
|
||||
type = int
|
||||
default = 65535
|
||||
desc = """\
|
||||
Initial HTTP/2 flow control window size in bytes.
|
||||
|
||||
This controls how much data can be in-flight before the receiver
|
||||
sends WINDOW_UPDATE frames. Larger values can improve throughput
|
||||
for large transfers but use more memory.
|
||||
|
||||
Default is 65535 (64KB - 1), the HTTP/2 specification default.
|
||||
Maximum is 2^31-1 (2147483647).
|
||||
|
||||
.. versionadded:: 24.0.0
|
||||
"""
|
||||
|
||||
|
||||
class HTTP2MaxFrameSize(Setting):
|
||||
name = "http2_max_frame_size"
|
||||
section = "HTTP/2"
|
||||
cli = ["--http2-max-frame-size"]
|
||||
meta = "INT"
|
||||
validator = validate_pos_int
|
||||
type = int
|
||||
default = 16384
|
||||
desc = """\
|
||||
Maximum HTTP/2 frame payload size in bytes.
|
||||
|
||||
This is the largest frame payload the server will accept.
|
||||
Larger frames reduce framing overhead but may increase latency
|
||||
for small messages.
|
||||
|
||||
Default is 16384 (16KB), the HTTP/2 specification minimum.
|
||||
Range is 16384 to 16777215 (16MB - 1).
|
||||
|
||||
.. versionadded:: 24.0.0
|
||||
"""
|
||||
|
||||
|
||||
class HTTP2MaxHeaderListSize(Setting):
|
||||
name = "http2_max_header_list_size"
|
||||
section = "HTTP/2"
|
||||
cli = ["--http2-max-header-list-size"]
|
||||
meta = "INT"
|
||||
validator = validate_pos_int
|
||||
type = int
|
||||
default = 65536
|
||||
desc = """\
|
||||
Maximum size of HTTP/2 header list in bytes (HPACK protection).
|
||||
|
||||
This limits the total size of headers after HPACK decompression.
|
||||
Protects against compression bombs and excessive memory use.
|
||||
|
||||
Default is 65536 (64KB). Set to 0 for unlimited (not recommended).
|
||||
|
||||
.. versionadded:: 24.0.0
|
||||
"""
|
||||
|
||||
|
||||
class PasteGlobalConf(Setting):
|
||||
name = "raw_paste_global_conf"
|
||||
action = "append"
|
||||
|
||||
58
gunicorn/http2/__init__.py
Normal file
58
gunicorn/http2/__init__.py
Normal file
@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
"""
|
||||
HTTP/2 support for Gunicorn.
|
||||
|
||||
This module provides HTTP/2 protocol support using the hyper-h2 library.
|
||||
HTTP/2 requires TLS with ALPN negotiation.
|
||||
"""
|
||||
|
||||
H2_MIN_VERSION = (4, 1, 0)
|
||||
|
||||
_h2_available = None
|
||||
_h2_version = None
|
||||
|
||||
|
||||
def is_http2_available():
|
||||
"""Check if HTTP/2 support is available.
|
||||
|
||||
Returns:
|
||||
bool: True if the h2 library is installed with minimum required version.
|
||||
"""
|
||||
global _h2_available, _h2_version
|
||||
|
||||
if _h2_available is not None:
|
||||
return _h2_available
|
||||
|
||||
try:
|
||||
import h2
|
||||
version_str = getattr(h2, '__version__', '0.0.0')
|
||||
version_parts = tuple(int(x) for x in version_str.split('.')[:3])
|
||||
_h2_version = version_parts
|
||||
_h2_available = version_parts >= H2_MIN_VERSION
|
||||
except ImportError:
|
||||
_h2_available = False
|
||||
_h2_version = None
|
||||
|
||||
return _h2_available
|
||||
|
||||
|
||||
def get_h2_version():
|
||||
"""Get the installed h2 library version.
|
||||
|
||||
Returns:
|
||||
tuple: Version tuple (major, minor, patch) or None if not installed.
|
||||
"""
|
||||
if _h2_version is None:
|
||||
is_http2_available() # Populate _h2_version
|
||||
return _h2_version
|
||||
|
||||
|
||||
__all__ = [
|
||||
'is_http2_available',
|
||||
'get_h2_version',
|
||||
'H2_MIN_VERSION',
|
||||
]
|
||||
153
gunicorn/http2/errors.py
Normal file
153
gunicorn/http2/errors.py
Normal file
@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
"""
|
||||
HTTP/2 specific exceptions.
|
||||
|
||||
These exceptions map to HTTP/2 error codes defined in RFC 7540.
|
||||
"""
|
||||
|
||||
|
||||
class HTTP2Error(Exception):
|
||||
"""Base exception for HTTP/2 errors."""
|
||||
|
||||
error_code = 0x0 # NO_ERROR
|
||||
|
||||
def __init__(self, message=None, error_code=None):
|
||||
self.message = message or self.__class__.__doc__
|
||||
if error_code is not None:
|
||||
self.error_code = error_code
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class HTTP2ProtocolError(HTTP2Error):
|
||||
"""Protocol error detected."""
|
||||
|
||||
error_code = 0x1 # PROTOCOL_ERROR
|
||||
|
||||
|
||||
class HTTP2InternalError(HTTP2Error):
|
||||
"""Internal error occurred."""
|
||||
|
||||
error_code = 0x2 # INTERNAL_ERROR
|
||||
|
||||
|
||||
class HTTP2FlowControlError(HTTP2Error):
|
||||
"""Flow control limits exceeded."""
|
||||
|
||||
error_code = 0x3 # FLOW_CONTROL_ERROR
|
||||
|
||||
|
||||
class HTTP2SettingsTimeout(HTTP2Error):
|
||||
"""Settings acknowledgment timeout."""
|
||||
|
||||
error_code = 0x4 # SETTINGS_TIMEOUT
|
||||
|
||||
|
||||
class HTTP2StreamClosed(HTTP2Error):
|
||||
"""Stream was closed."""
|
||||
|
||||
error_code = 0x5 # STREAM_CLOSED
|
||||
|
||||
|
||||
class HTTP2FrameSizeError(HTTP2Error):
|
||||
"""Frame size is incorrect."""
|
||||
|
||||
error_code = 0x6 # FRAME_SIZE_ERROR
|
||||
|
||||
|
||||
class HTTP2RefusedStream(HTTP2Error):
|
||||
"""Stream was refused."""
|
||||
|
||||
error_code = 0x7 # REFUSED_STREAM
|
||||
|
||||
|
||||
class HTTP2Cancel(HTTP2Error):
|
||||
"""Stream was cancelled."""
|
||||
|
||||
error_code = 0x8 # CANCEL
|
||||
|
||||
|
||||
class HTTP2CompressionError(HTTP2Error):
|
||||
"""Compression state error."""
|
||||
|
||||
error_code = 0x9 # COMPRESSION_ERROR
|
||||
|
||||
|
||||
class HTTP2ConnectError(HTTP2Error):
|
||||
"""Connection error during CONNECT."""
|
||||
|
||||
error_code = 0xa # CONNECT_ERROR
|
||||
|
||||
|
||||
class HTTP2EnhanceYourCalm(HTTP2Error):
|
||||
"""Peer is generating excessive load."""
|
||||
|
||||
error_code = 0xb # ENHANCE_YOUR_CALM
|
||||
|
||||
|
||||
class HTTP2InadequateSecurity(HTTP2Error):
|
||||
"""Transport security is inadequate."""
|
||||
|
||||
error_code = 0xc # INADEQUATE_SECURITY
|
||||
|
||||
|
||||
class HTTP2RequiresHTTP11(HTTP2Error):
|
||||
"""HTTP/1.1 is required for this request."""
|
||||
|
||||
error_code = 0xd # HTTP_1_1_REQUIRED
|
||||
|
||||
|
||||
class HTTP2StreamError(HTTP2Error):
|
||||
"""Error specific to a single stream."""
|
||||
|
||||
def __init__(self, stream_id, message=None, error_code=None):
|
||||
self.stream_id = stream_id
|
||||
super().__init__(message, error_code)
|
||||
|
||||
def __str__(self):
|
||||
return f"Stream {self.stream_id}: {self.message}"
|
||||
|
||||
|
||||
class HTTP2ConnectionError(HTTP2Error):
|
||||
"""Error affecting the entire connection."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HTTP2ConfigurationError(HTTP2Error):
|
||||
"""Invalid HTTP/2 configuration."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HTTP2NotAvailable(HTTP2Error):
|
||||
"""HTTP/2 support is not available (h2 library not installed)."""
|
||||
|
||||
def __init__(self, message=None):
|
||||
message = message or "HTTP/2 requires the h2 library: pip install gunicorn[http2]"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'HTTP2Error',
|
||||
'HTTP2ProtocolError',
|
||||
'HTTP2InternalError',
|
||||
'HTTP2FlowControlError',
|
||||
'HTTP2SettingsTimeout',
|
||||
'HTTP2StreamClosed',
|
||||
'HTTP2FrameSizeError',
|
||||
'HTTP2RefusedStream',
|
||||
'HTTP2Cancel',
|
||||
'HTTP2CompressionError',
|
||||
'HTTP2ConnectError',
|
||||
'HTTP2EnhanceYourCalm',
|
||||
'HTTP2InadequateSecurity',
|
||||
'HTTP2RequiresHTTP11',
|
||||
'HTTP2StreamError',
|
||||
'HTTP2ConnectionError',
|
||||
'HTTP2ConfigurationError',
|
||||
'HTTP2NotAvailable',
|
||||
]
|
||||
@ -235,6 +235,35 @@ def close_sockets(listeners, unlink=True):
|
||||
os.unlink(sock_name)
|
||||
|
||||
|
||||
def _get_alpn_protocols(conf):
|
||||
"""Get ALPN protocol list from configuration.
|
||||
|
||||
Returns list of ALPN protocol identifiers based on http_protocols setting.
|
||||
Returns empty list if HTTP/2 is not configured or available.
|
||||
"""
|
||||
from gunicorn.config import ALPN_PROTOCOL_MAP
|
||||
|
||||
http_protocols = conf.http_protocols
|
||||
if not http_protocols:
|
||||
return []
|
||||
|
||||
# Only configure ALPN if h2 is in the protocol list
|
||||
if "h2" not in http_protocols:
|
||||
return []
|
||||
|
||||
# Check if h2 library is available
|
||||
from gunicorn.http2 import is_http2_available
|
||||
if not is_http2_available():
|
||||
return []
|
||||
|
||||
# Map to ALPN identifiers, maintaining preference order
|
||||
alpn_protocols = []
|
||||
for proto in http_protocols:
|
||||
if proto in ALPN_PROTOCOL_MAP:
|
||||
alpn_protocols.append(ALPN_PROTOCOL_MAP[proto])
|
||||
return alpn_protocols
|
||||
|
||||
|
||||
def ssl_context(conf):
|
||||
def default_ssl_context_factory():
|
||||
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=conf.ca_certs)
|
||||
@ -242,6 +271,12 @@ def ssl_context(conf):
|
||||
context.verify_mode = conf.cert_reqs
|
||||
if conf.ciphers:
|
||||
context.set_ciphers(conf.ciphers)
|
||||
|
||||
# Configure ALPN for HTTP/2 if enabled
|
||||
alpn_protocols = _get_alpn_protocols(conf)
|
||||
if alpn_protocols:
|
||||
context.set_alpn_protocols(alpn_protocols)
|
||||
|
||||
return context
|
||||
|
||||
return conf.ssl_context(conf, default_ssl_context_factory)
|
||||
@ -252,3 +287,29 @@ def ssl_wrap_socket(sock, conf):
|
||||
server_side=True,
|
||||
suppress_ragged_eofs=conf.suppress_ragged_eofs,
|
||||
do_handshake_on_connect=conf.do_handshake_on_connect)
|
||||
|
||||
|
||||
def get_negotiated_protocol(ssl_socket):
|
||||
"""Get the negotiated ALPN protocol from an SSL socket.
|
||||
|
||||
Returns:
|
||||
str: The negotiated protocol name ('h2', 'http/1.1', etc.)
|
||||
or None if no protocol was negotiated.
|
||||
"""
|
||||
if not isinstance(ssl_socket, ssl.SSLSocket):
|
||||
return None
|
||||
|
||||
try:
|
||||
return ssl_socket.selected_alpn_protocol()
|
||||
except (AttributeError, ssl.SSLError):
|
||||
return None
|
||||
|
||||
|
||||
def is_http2_negotiated(ssl_socket):
|
||||
"""Check if HTTP/2 was negotiated on an SSL socket.
|
||||
|
||||
Returns:
|
||||
bool: True if HTTP/2 was negotiated via ALPN.
|
||||
"""
|
||||
protocol = get_negotiated_protocol(ssl_socket)
|
||||
return protocol == "h2"
|
||||
|
||||
@ -52,9 +52,11 @@ eventlet = ["eventlet>=0.40.3"]
|
||||
tornado = ["tornado>=6.5.0"]
|
||||
gthread = []
|
||||
setproctitle = ["setproctitle"]
|
||||
http2 = ["h2>=4.1.0"]
|
||||
testing = [
|
||||
"gevent>=24.10.1",
|
||||
"eventlet>=0.40.3",
|
||||
"h2>=4.1.0",
|
||||
"coverage",
|
||||
"pytest",
|
||||
"pytest-cov",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user