Skip to content

Commit 3c8bb1b

Browse files
committed
mod_http2: Send HTTP/2 error codes in GOAWAY frames
h2_session_shutdown() passed its reason, an APR or nghttp2 status, as the error code of the GOAWAY frame. A connection timeout was sent as 70007 (APR_TIMEUP), an inadequate TLS connection as 22 (APR_EINVAL). Give h2_session_shutdown() the HTTP/2 error code as a separate argument. Timeouts and graceful shutdowns send NO_ERROR, failures INTERNAL_ERROR and inadequate TLS parameters INADEQUATE_SECURITY. PR: 70212 GitHub: closes #799 git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1939156 13f79535-47bb-0310-9956-ffa450edef68
1 parent 261058f commit 3c8bb1b

3 files changed

Lines changed: 124 additions & 12 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*) mod_http2: Send only HTTP/2 error codes in GOAWAY frames. The APR status
2+
of a connection timeout or error was sent instead.
3+
PR 70212. [Arturo Bernal]

‎modules/http2/h2_session.c‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,16 @@ static void on_stream_state_enter(void *ctx, h2_stream *stream);
6262
static void on_stream_state_event(void *ctx, h2_stream *stream, h2_stream_event_t ev);
6363
static void on_stream_event(void *ctx, h2_stream *stream, h2_stream_event_t ev);
6464
static apr_status_t h2_session_shutdown(h2_session *session, int error,
65-
const char *msg, int force_close);
65+
uint32_t h2_error, const char *msg,
66+
int force_close);
67+
68+
/* The error code of a GOAWAY frame is one of RFC 9113, section 7. The
69+
* reasons for a shutdown are APR or nghttp2 status values that must not
70+
* end up on the wire. */
71+
static uint32_t h2_error_from_status(int status)
72+
{
73+
return status? H2_ERR_INTERNAL_ERROR : H2_ERR_NO_ERROR;
74+
}
6675

6776
static int h2_session_status_from_apr_status(apr_status_t rv)
6877
{
@@ -789,7 +798,8 @@ static apr_status_t h2_session_shutdown_notice(h2_session *session)
789798
}
790799

791800
static apr_status_t h2_session_shutdown(h2_session *session, int error,
792-
const char *msg, int force_close)
801+
uint32_t h2_error, const char *msg,
802+
int force_close)
793803
{
794804
apr_status_t status = APR_SUCCESS;
795805

@@ -827,14 +837,15 @@ static apr_status_t h2_session_shutdown(h2_session *session, int error,
827837
if (!session->c1->aborted) {
828838
nghttp2_submit_goaway(session->ngh2, NGHTTP2_FLAG_NONE,
829839
session->local.accepted_max,
830-
error, (uint8_t*)msg, msg? strlen(msg):0);
840+
h2_error, (uint8_t*)msg, msg? strlen(msg):0);
831841
status = nghttp2_session_send(session->ngh2);
832842
if (status == APR_SUCCESS) {
833843
status = h2_c1_io_assure_flushed(&session->io);
834844
}
835845
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1,
836846
H2_SSSN_LOG(APLOGNO(03069), session,
837-
"sent GOAWAY, err=%d, msg=%s"), error, msg? msg : "");
847+
"sent GOAWAY, err=%u, status=%d, msg=%s"),
848+
(unsigned int)h2_error, error, msg? msg : "");
838849
}
839850
h2_session_dispatch_event(session, H2_SESSION_EV_LOCAL_GOAWAY, error, msg);
840851
return status;
@@ -1592,7 +1603,7 @@ static void h2_session_ev_conn_error(h2_session *session, int arg, const char *m
15921603
H2_SSSN_LOG(APLOGNO(03401), session,
15931604
"conn error -> shutdown, remote.emitted=%d"),
15941605
(int)session->remote.emitted_count);
1595-
h2_session_shutdown(session, arg, msg, 0);
1606+
h2_session_shutdown(session, arg, h2_error_from_status(arg), msg, 0);
15961607
break;
15971608
}
15981609
}
@@ -1603,15 +1614,15 @@ static void h2_session_ev_proto_error(h2_session *session, int arg, const char *
16031614
ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, session->c1,
16041615
H2_SSSN_LOG(APLOGNO(03402), session,
16051616
"proto error -> shutdown"));
1606-
h2_session_shutdown(session, arg, msg, 0);
1617+
h2_session_shutdown(session, arg, h2_error_from_status(arg), msg, 0);
16071618
}
16081619
}
16091620

16101621
static void h2_session_ev_conn_timeout(h2_session *session, int arg, const char *msg)
16111622
{
16121623
transit(session, msg, H2_SESSION_ST_DONE);
16131624
if (!session->local.shutdown) {
1614-
h2_session_shutdown(session, arg, msg, 1);
1625+
h2_session_shutdown(session, arg, H2_ERR_NO_ERROR, msg, 1);
16151626
}
16161627
}
16171628

@@ -1646,13 +1657,13 @@ static void h2_session_ev_bad_client(h2_session *session, int arg, const char *m
16461657
{
16471658
transit(session, msg, H2_SESSION_ST_DONE);
16481659
if (!session->local.shutdown) {
1649-
h2_session_shutdown(session, arg, msg, 1);
1660+
h2_session_shutdown(session, arg, (uint32_t)arg, msg, 1);
16501661
}
16511662
}
16521663

16531664
static void h2_session_ev_pre_close(h2_session *session, int arg, const char *msg)
16541665
{
1655-
h2_session_shutdown(session, arg, msg, 1);
1666+
h2_session_shutdown(session, arg, H2_ERR_NO_ERROR, msg, 1);
16561667
}
16571668

16581669
static void h2_session_ev_no_more_streams(h2_session *session)
@@ -1670,7 +1681,7 @@ static void h2_session_ev_no_more_streams(h2_session *session)
16701681
else {
16711682
/* We are no longer accepting new streams.
16721683
* Time to leave. */
1673-
h2_session_shutdown(session, 0, "done", 0);
1684+
h2_session_shutdown(session, 0, H2_ERR_NO_ERROR, "done", 0);
16741685
transit(session, "c1 done after goaway", H2_SESSION_ST_DONE);
16751686
}
16761687
}
@@ -1895,7 +1906,8 @@ apr_status_t h2_session_process(h2_session *session, int async,
18951906
if (!h2_protocol_is_acceptable_c1(c, session->r, 1)) {
18961907
const char *msg = nghttp2_strerror(NGHTTP2_INADEQUATE_SECURITY);
18971908
update_child_status(session, SERVER_BUSY_READ, msg, NULL);
1898-
h2_session_shutdown(session, APR_EINVAL, msg, 1);
1909+
h2_session_shutdown(session, APR_EINVAL, H2_ERR_INADEQUATE_SECURITY,
1910+
msg, 1);
18991911
}
19001912
else {
19011913
update_child_status(session, SERVER_BUSY_READ, "init", NULL);
@@ -1999,7 +2011,7 @@ apr_status_t h2_session_process(h2_session *session, int async,
19992011
* (Checked before the want_read assert below: after receiving a
20002012
* GOAWAY nghttp2 may no longer want to read.) */
20012013
if (!session->local.shutdown) {
2002-
h2_session_shutdown(session, 0, "done", 0);
2014+
h2_session_shutdown(session, 0, H2_ERR_NO_ERROR, "done", 0);
20032015
}
20042016
transit(session, "remote goaway, streams drained",
20052017
H2_SESSION_ST_DONE);
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#
2+
# mod-h2 test suite
3+
# check the error code of GOAWAY frames sent by the server
4+
#
5+
import socket
6+
import ssl
7+
import struct
8+
9+
import pytest
10+
11+
from .env import H2Conf, H2TestEnv
12+
13+
14+
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
15+
class TestGoaway:
16+
17+
PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'
18+
FRAME_SETTINGS = 0x04
19+
FRAME_GOAWAY = 0x07
20+
# error codes of RFC 9113, section 7
21+
ERR_NO_ERROR = 0x00
22+
ERR_INADEQUATE_SECURITY = 0x0c
23+
ERR_HTTP_1_1_REQUIRED = 0x0d
24+
25+
@pytest.fixture(autouse=True, scope='class')
26+
def _class_scope(self, env):
27+
conf = H2Conf(env, extras={
28+
'base': [
29+
"AcceptFilter http none",
30+
"Timeout 1",
31+
"KeepAliveTimeout 1",
32+
"SSLCipherSuite HIGH:!aNULL",
33+
]
34+
})
35+
conf.add_vhost_cgi()
36+
conf.install()
37+
assert env.apache_restart() == 0
38+
39+
@staticmethod
40+
def _frame(ftype, payload=b''):
41+
# length, type, flags and the stream id 0
42+
return len(payload).to_bytes(3, 'big') + bytes([ftype, 0]) + bytes(4) + payload
43+
44+
@staticmethod
45+
def _recv_exact(sock, n):
46+
data = b''
47+
while len(data) < n:
48+
chunk = sock.recv(n - len(data))
49+
if not chunk:
50+
break
51+
data += chunk
52+
return data
53+
54+
def _goaway_error(self, env, ctx):
55+
"""Open a connection, send the client preface and return the error
56+
code of the first GOAWAY frame the server sends, None if there is none."""
57+
with socket.create_connection(('localhost', int(env.https_port)), timeout=10) as raw:
58+
with ctx.wrap_socket(raw, server_hostname='localhost') as sock:
59+
assert sock.selected_alpn_protocol() == 'h2'
60+
sock.sendall(self.PREFACE + self._frame(self.FRAME_SETTINGS))
61+
while True:
62+
head = self._recv_exact(sock, 9)
63+
if len(head) < 9:
64+
return None
65+
length = int.from_bytes(head[0:3], 'big')
66+
payload = self._recv_exact(sock, length)
67+
if head[3] == self.FRAME_GOAWAY:
68+
return struct.unpack('>II', payload[:8])[1]
69+
70+
@staticmethod
71+
def _tls_context(ciphers=None, tls12_only=False):
72+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
73+
ctx.check_hostname = False
74+
ctx.verify_mode = ssl.CERT_NONE
75+
ctx.set_alpn_protocols(['h2'])
76+
if tls12_only:
77+
ctx.maximum_version = ssl.TLSVersion.TLSv1_2
78+
if ciphers:
79+
ctx.set_ciphers(ciphers)
80+
return ctx
81+
82+
# An idle connection that runs into the server timeout is closed
83+
# gracefully. The GOAWAY must carry NO_ERROR and not the APR status
84+
# of the timeout.
85+
def test_h2_108_01_idle_timeout(self, env):
86+
error = self._goaway_error(env, self._tls_context())
87+
assert error == self.ERR_NO_ERROR, f"GOAWAY error code {error}"
88+
89+
# A TLS connection that is not acceptable for HTTP/2 is refused with
90+
# INADEQUATE_SECURITY (RFC 9113, section 9.2.1).
91+
def test_h2_108_02_inadequate_security(self, env):
92+
try:
93+
ctx = self._tls_context(ciphers='AES128-SHA256', tls12_only=True)
94+
error = self._goaway_error(env, ctx)
95+
except ssl.SSLError as ex:
96+
pytest.skip(f"cipher not available: {ex}")
97+
assert error == self.ERR_INADEQUATE_SECURITY, f"GOAWAY error code {error}"

0 commit comments

Comments
 (0)