mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-07 13: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):
|
class PasteGlobalConf(Setting):
|
||||||
name = "raw_paste_global_conf"
|
name = "raw_paste_global_conf"
|
||||||
action = "append"
|
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)
|
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 ssl_context(conf):
|
||||||
def default_ssl_context_factory():
|
def default_ssl_context_factory():
|
||||||
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=conf.ca_certs)
|
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
|
context.verify_mode = conf.cert_reqs
|
||||||
if conf.ciphers:
|
if conf.ciphers:
|
||||||
context.set_ciphers(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 context
|
||||||
|
|
||||||
return conf.ssl_context(conf, default_ssl_context_factory)
|
return conf.ssl_context(conf, default_ssl_context_factory)
|
||||||
@ -252,3 +287,29 @@ def ssl_wrap_socket(sock, conf):
|
|||||||
server_side=True,
|
server_side=True,
|
||||||
suppress_ragged_eofs=conf.suppress_ragged_eofs,
|
suppress_ragged_eofs=conf.suppress_ragged_eofs,
|
||||||
do_handshake_on_connect=conf.do_handshake_on_connect)
|
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"]
|
tornado = ["tornado>=6.5.0"]
|
||||||
gthread = []
|
gthread = []
|
||||||
setproctitle = ["setproctitle"]
|
setproctitle = ["setproctitle"]
|
||||||
|
http2 = ["h2>=4.1.0"]
|
||||||
testing = [
|
testing = [
|
||||||
"gevent>=24.10.1",
|
"gevent>=24.10.1",
|
||||||
"eventlet>=0.40.3",
|
"eventlet>=0.40.3",
|
||||||
|
"h2>=4.1.0",
|
||||||
"coverage",
|
"coverage",
|
||||||
"pytest",
|
"pytest",
|
||||||
"pytest-cov",
|
"pytest-cov",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user