You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by kg...@apache.org on 2020/10/06 13:24:59 UTC
[qpid-dispatch] branch dev-protocol-adaptors updated:
DISPATCH-1788: fixes and testcases for HTTP/1.0 support
This is an automated email from the ASF dual-hosted git repository.
kgiusti pushed a commit to branch dev-protocol-adaptors
in repository https://gitbox.apache.org/repos/asf/qpid-dispatch.git
The following commit(s) were added to refs/heads/dev-protocol-adaptors by this push:
new afaf5f4 DISPATCH-1788: fixes and testcases for HTTP/1.0 support
afaf5f4 is described below
commit afaf5f4305ba348ffab20172a5923852bb215c2c
Author: Kenneth Giusti <kg...@apache.org>
AuthorDate: Thu Oct 1 11:17:16 2020 -0400
DISPATCH-1788: fixes and testcases for HTTP/1.0 support
This closes #859
---
include/qpid/dispatch/http1_codec.h | 10 +
src/adaptors/http1/http1_adaptor.c | 28 --
src/adaptors/http1/http1_client.c | 30 +-
src/adaptors/http1/http1_codec.c | 96 ++++-
src/adaptors/http1/http1_private.h | 10 -
tests/system_tests_http1_adaptor.py | 674 +++++++++++++++++++++++-------------
6 files changed, 541 insertions(+), 307 deletions(-)
diff --git a/include/qpid/dispatch/http1_codec.h b/include/qpid/dispatch/http1_codec.h
index 3682dd1..62e70fb 100644
--- a/include/qpid/dispatch/http1_codec.h
+++ b/include/qpid/dispatch/http1_codec.h
@@ -187,6 +187,16 @@ bool h1_codec_request_complete(const h1_codec_request_state_t *hrs);
// true when codec has encoded/decoded a complete response message
bool h1_codec_response_complete(const h1_codec_request_state_t *hrs);
+// Utility for iterating over a list of HTTP tokens.
+//
+// start - begin search
+// len - (output) length of token if non-null returned
+// next - (output) address past token - for start of next search
+// Returns a pointer to the first byte of the token, or 0 if no token found
+//
+const char *h1_codec_token_list_next(const char *start, size_t *len, const char **next);
+
+
//
// API for sending HTTP/1.x messages
//
diff --git a/src/adaptors/http1/http1_adaptor.c b/src/adaptors/http1/http1_adaptor.c
index d9d4e4c..48e9326 100644
--- a/src/adaptors/http1/http1_adaptor.c
+++ b/src/adaptors/http1/http1_adaptor.c
@@ -235,34 +235,6 @@ void qdr_http1_error_response(qdr_http1_request_base_t *hreq,
}
-const char *qdr_http1_token_list_next(const char *start, size_t *len, const char **next)
-{
- static const char *SKIPME = ", \t";
-
- *len = 0;
- *next = 0;
-
- if (!start) return 0;
-
- while (*start && strchr(SKIPME, *start))
- ++start;
-
- if (!*start) return 0;
-
- const char *end = start;
- while (*end && !strchr(SKIPME, *end))
- ++end;
-
- *len = end - start;
- *next = end;
-
- while (**next && strchr(SKIPME, **next))
- ++(*next);
-
- return start;
-}
-
-
//
// Raw Connection Write Buffer Management
//
diff --git a/src/adaptors/http1/http1_client.c b/src/adaptors/http1/http1_client.c
index 0a06787..0a81df4 100644
--- a/src/adaptors/http1/http1_client.c
+++ b/src/adaptors/http1/http1_client.c
@@ -78,6 +78,7 @@ typedef struct _client_request_t {
bool codec_completed; // encoder/decoder done
bool cancelled;
bool close_on_complete; // close the conn when this request is complete
+ bool conn_close_hdr; // add Connection: close to response msg
} _client_request_t;
ALLOC_DECLARE(_client_request_t);
@@ -607,6 +608,7 @@ static int _client_rx_request_cb(h1_codec_request_state_t *hrs,
creq->base.msg_id = hconn->client.next_msg_id++;
creq->base.lib_rs = hrs;
creq->base.hconn = hconn;
+ creq->close_on_complete = (version_minor == 0);
DEQ_INIT(creq->responses);
creq->request_props = qd_compose(QD_PERFORMATIVE_APPLICATION_PROPERTIES, 0);
@@ -656,21 +658,23 @@ static int _client_rx_header_cb(h1_codec_request_state_t *hrs, const char *key,
"[C%"PRIu64"][L%"PRIu64"] HTTP request header received: key='%s' value='%s'",
hconn->conn_id, hconn->in_link_id, key, value);
- if (strcasecmp(key, "connection") == 0) {
- // We need to filter the connection header out. See if client
- // requested 'close' - this means it expects us to close the connection
- // when the response has been sent
+ if (strcasecmp(key, "Connection") == 0) {
+ // We need to filter the connection header out. But first see if
+ // client requested that the connection be closed after the response
+ // arrives.
//
// @TODO(kgiusti): also have to remove other headers given in value!
+ // @TODO(kgiusti): do we need to support keep-alive on 1.0 connections?
//
size_t len;
- const char *token = qdr_http1_token_list_next(value, &len, &value);
+ const char *token = h1_codec_token_list_next(value, &len, &value);
while (token) {
if (len == 5 && strncasecmp(token, "close", 5) == 0) {
hreq->close_on_complete = true;
+ hreq->conn_close_hdr = true;
break;
}
- token = qdr_http1_token_list_next(value, &len, &value);
+ token = h1_codec_token_list_next(value, &len, &value);
}
} else {
@@ -1007,7 +1011,7 @@ static bool _encode_response_headers(_client_request_t *hreq,
// the value for RESPONSE_HEADER_KEY is optional and is set
// to a string representation of the version of the server
- // (e.g. "1.1"
+ // (e.g. "1.1")
uint32_t major = 1;
uint32_t minor = 1;
tmp = qd_parse_value_by_key(app_props, RESPONSE_HEADER_KEY);
@@ -1052,7 +1056,7 @@ static bool _encode_response_headers(_client_request_t *hreq,
char *header_key = (char*) qd_iterator_copy(i_key);
char *header_value = (char*) qd_iterator_copy(i_value);
-
+ // @TODO(kgiusti): remove me (sensitive content)
qd_log(hreq->base.hconn->adaptor->log, QD_LOG_TRACE,
"[C%"PRIu64"][L%"PRIu64"] Encoding response header %s:%s",
hreq->base.hconn->conn_id, hreq->base.hconn->out_link_id,
@@ -1066,6 +1070,16 @@ static bool _encode_response_headers(_client_request_t *hreq,
key = qd_field_next_child(value);
}
+
+ // If the client has requested Connection: close respond
+ // accordingly IF this is the terminal response (not
+ // INFORMATIONAL)
+ if (ok && (status_code / 100) == 1) {
+ if (hreq->conn_close_hdr) {
+ ok = !h1_codec_tx_add_header(hreq->base.lib_rs,
+ "Connection", "close");
+ }
+ }
}
}
}
diff --git a/src/adaptors/http1/http1_codec.c b/src/adaptors/http1/http1_codec.c
index bec96fa..f91b1f2 100644
--- a/src/adaptors/http1/http1_codec.c
+++ b/src/adaptors/http1/http1_codec.c
@@ -149,10 +149,13 @@ struct h1_codec_connection_t {
bool is_request;
bool is_chunked;
+ bool is_http10;
// decoded headers
bool hdr_transfer_encoding;
bool hdr_content_length;
+ bool hdr_conn_close; // Connection: close
+ bool hdr_conn_keep_alive; // Connection: keep-alive
} decoder;
// Encoder for current outgoing msg.
@@ -233,21 +236,21 @@ h1_codec_connection_t *h1_codec_connection(h1_codec_config_t *config, void *cont
// The connection has closed. If this is a connection to a server this may
-// simply be the end of the response message. If so mark it complete.
+// simply be the end of the response message. Mark the in-flight response as
+// completed.
//
void h1_codec_connection_closed(h1_codec_connection_t *conn)
{
if (conn) {
if (conn->config.type == HTTP1_CONN_SERVER) {
struct decoder_t *decoder = &conn->decoder;
- if (decoder->hrs &&
- decoder->hrs->request_complete &&
- decoder->state == HTTP1_MSG_STATE_BODY) {
-
- h1_codec_request_state_t *hrs = decoder->hrs;
+ h1_codec_request_state_t *hrs = decoder->hrs;
+ if (hrs && hrs->request_complete) {
decoder_reset(decoder);
- hrs->response_complete = true;
- conn->config.rx_done(hrs);
+ if (!hrs->response_complete) {
+ hrs->response_complete = true;
+ conn->config.rx_done(hrs);
+ }
conn->config.request_complete(hrs, false);
h1_codec_request_state_free(hrs);
}
@@ -291,8 +294,11 @@ static void decoder_reset(struct decoder_t *decoder)
decoder->error_msg = 0;
decoder->is_request = false;
decoder->is_chunked = false;
+ decoder->is_http10 = false;
decoder->hdr_transfer_encoding = false;
decoder->hdr_content_length = false;
+ decoder->hdr_conn_close = false;
+ decoder->hdr_conn_keep_alive = false;
}
@@ -628,6 +634,8 @@ static bool parse_request_line(h1_codec_connection_t *conn, struct decoder_t *de
return decoder->error;
}
+ decoder->is_http10 = minor == 0;
+
h1_codec_request_state_t *hrs = h1_codec_request_state(conn);
// check for methods that do not support body content in the response:
@@ -718,6 +726,7 @@ static int parse_response_line(h1_codec_connection_t *conn, struct decoder_t *de
}
decoder->is_request = false;
+ decoder->is_http10 = minor == 0;
decoder->error = conn->config.rx_response(decoder->hrs,
hrs->response_code,
@@ -852,6 +861,19 @@ static int process_header(h1_codec_connection_t *conn, struct decoder_t *decoder
} else if (strcasecmp("Transfer-Encoding", (char*) key) == 0) {
decoder->is_chunked = _is_transfer_chunked((char*) value);
decoder->hdr_transfer_encoding = true;
+
+ } else if (strcasecmp("Connection", (char*) key) == 0) {
+ // parse out connection lifecycle options
+ const char *token = 0;
+ size_t len = 0;
+ const char *next = (const char*) value;
+ while (*next && (token = h1_codec_token_list_next(next, &len, &next)) != 0) {
+ if (len == 5 && strncmp("close", token, 5) == 0) {
+ decoder->hdr_conn_close = true;
+ } else if (len == 10 && strncmp("keep-alive", token, 10) == 0) {
+ decoder->hdr_conn_keep_alive = true;
+ }
+ }
}
return 0;
@@ -1169,25 +1191,40 @@ static bool parse_done(h1_codec_connection_t *conn, struct decoder_t *decoder)
// signal the message receive is complete
conn->config.rx_done(hrs);
+ bool close_expected = false;
if (is_response) {
// Informational 1xx response codes are NOT teriminal - further responses are allowed!
if (IS_INFO_RESPONSE(hrs->response_code)) {
hrs->response_code = 0;
} else {
hrs->response_complete = true;
+
+ // In certain scenarios an HTTP server will close the connection to
+ // indicate the end of a response message. This may happen even if
+ // the request message has a known length (Content-Length or
+ // Transfer-Encoding). In these circumstances do NOT signal that
+ // the request is complete (call request_complete() callback) until
+ // the connection closes. Otherwise the user may start sending the
+ // next request message before the HTTP server closes the TCP
+ // connection. (see RFC7230, section Persistence)
+
+ close_expected = decoder->hdr_conn_close
+ || (decoder->is_http10 && !decoder->hdr_conn_keep_alive);
}
} else {
hrs->request_complete = true;
}
- if (hrs->request_complete && hrs->response_complete) {
- conn->config.request_complete(hrs, false);
- decoder->hrs = 0;
- h1_codec_request_state_free(hrs);
- }
-
- decoder_reset(decoder);
- return !!decoder->read_ptr.remaining;
+ if (!close_expected) {
+ if (hrs->request_complete && hrs->response_complete) {
+ conn->config.request_complete(hrs, false);
+ decoder->hrs = 0;
+ h1_codec_request_state_free(hrs);
+ }
+ decoder_reset(decoder);
+ return !!decoder->read_ptr.remaining;
+ } else
+ return false; // stop parsing input, wait for close
}
@@ -1501,3 +1538,30 @@ bool h1_codec_response_complete(const h1_codec_request_state_t *hrs)
{
return hrs && hrs->response_complete;
}
+
+
+const char *h1_codec_token_list_next(const char *start, size_t *len, const char **next)
+{
+ static const char *SKIPME = ", \t";
+
+ *len = 0;
+ *next = 0;
+
+ if (!start) return 0;
+
+ while (*start && strchr(SKIPME, *start))
+ ++start;
+
+ if (!*start) return 0;
+
+ const char *end = start;
+ while (*end && !strchr(SKIPME, *end))
+ ++end;
+
+ *len = end - start;
+ while (*end && strchr(SKIPME, *end))
+ ++end;
+
+ *next = end;
+ return start;
+}
diff --git a/src/adaptors/http1/http1_private.h b/src/adaptors/http1/http1_private.h
index 5533aee..d82e421 100644
--- a/src/adaptors/http1/http1_private.h
+++ b/src/adaptors/http1/http1_private.h
@@ -217,16 +217,6 @@ void qdr_http1_rejected_response(qdr_http1_request_base_t *hreq,
const qdr_error_t *error);
-// return the next HTTP token in a comma separated list of tokens
-//
-// start - search for token start pointer
-// len - length of token if non-null returned
-// next - address of start of next token
-//
-const char *qdr_http1_token_list_next(const char *start, size_t *len, const char **next);
-
-
-
// http1_client.c protocol adaptor callbacks
//
void qdr_http1_client_core_link_flow(qdr_http1_adaptor_t *adaptor,
diff --git a/tests/system_tests_http1_adaptor.py b/tests/system_tests_http1_adaptor.py
index 036b8ac..dbe1627 100644
--- a/tests/system_tests_http1_adaptor.py
+++ b/tests/system_tests_http1_adaptor.py
@@ -27,6 +27,7 @@ from __future__ import absolute_import
from __future__ import print_function
+import socket
import sys
from threading import Thread
try:
@@ -69,17 +70,15 @@ class ResponseMsg(object):
message when called by the HTTPServer via the BaseHTTPRequestHandler
"""
def __init__(self, status, version=None, reason=None,
- headers=None, body=None, eom_close=False, error=False):
+ headers=None, body=None, error=False):
self.status = status
self.version = version or "HTTP/1.1"
self.reason = reason
self.headers = headers or []
self.body = body
- self.eom_close = eom_close
self.error = error
def send_response(self, handler):
- handler.protocol_version = self.version
if self.error:
handler.send_error(self.status,
message=self.reason)
@@ -94,8 +93,6 @@ class ResponseMsg(object):
handler.wfile.write(self.body)
handler.wfile.flush()
- return self.eom_close
-
class ResponseValidator(object):
"""
@@ -124,208 +121,12 @@ class ResponseValidator(object):
return body
-DEFAULT_TEST_SCENARIOS = {
-
- #
- # GET
- #
-
- "GET": [
- (RequestMsg("GET", "/GET/error",
- headers={"Content-Length": 0}),
- ResponseMsg(400, reason="Bad breath", error=True),
- ResponseValidator(status=400)),
-
- (RequestMsg("GET", "/GET/content_len",
- headers={"Content-Length": "00"}),
- ResponseMsg(200, reason="OK",
- headers={"Content-Length": 1,
- "Content-Type": "text/plain;charset=utf-8"},
- body=b'?'),
- ResponseValidator(expect_headers={'Content-Length': '1'},
- expect_body=b'?')),
-
- (RequestMsg("GET", "/GET/content_len_511",
- headers={"Content-Length": 0}),
- ResponseMsg(200, reason="OK",
- headers={"Content-Length": 511,
- "Content-Type": "text/plain;charset=utf-8"},
- body=b'X' * 511),
- ResponseValidator(expect_headers={'Content-Length': '511'},
- expect_body=b'X' * 511)),
-
- (RequestMsg("GET", "/GET/content_len_4096",
- headers={"Content-Length": 0}),
- ResponseMsg(200, reason="OK",
- headers={"Content-Length": 4096,
- "Content-Type": "text/plain;charset=utf-8"},
- body=b'X' * 4096),
- ResponseValidator(expect_headers={'Content-Length': '4096'},
- expect_body=b'X' * 4096)),
-
- (RequestMsg("GET", "/GET/chunked",
- headers={"Content-Length": 0}),
- ResponseMsg(200, reason="OK",
- headers={"transfer-encoding": "chunked",
- "Content-Type": "text/plain;charset=utf-8"},
- # note: the chunk length does not count the trailing CRLF
- body=b'16\r\n'
- + b'Mary had a little pug \r\n'
- + b'1b\r\n'
- + b'Its name was "Skupper-Jack"\r\n'
- + b'0\r\n'
- + b'Optional: Trailer\r\n'
- + b'Optional: Trailer\r\n'
- + b'\r\n'),
- ResponseValidator(expect_headers={'transfer-encoding': 'chunked'},
- expect_body=b'Mary had a little pug Its name was "Skupper-Jack"')),
-
- (RequestMsg("GET", "/GET/chunked_large",
- headers={"Content-Length": 0}),
- ResponseMsg(200, reason="OK",
- headers={"transfer-encoding": "chunked",
- "Content-Type": "text/plain;charset=utf-8"},
- # note: the chunk length does not count the trailing CRLF
- body=b'1\r\n'
- + b'?\r\n'
- + b'800\r\n'
- + b'X' * 0x800 + b'\r\n'
- + b'13\r\n'
- + b'Y' * 0x13 + b'\r\n'
- + b'0\r\n'
- + b'Optional: Trailer\r\n'
- + b'Optional: Trailer\r\n'
- + b'\r\n'),
- ResponseValidator(expect_headers={'transfer-encoding': 'chunked'},
- expect_body=b'?' + b'X' * 0x800 + b'Y' * 0x13)),
-
- (RequestMsg("GET", "/GET/info_content_len",
- headers={"Content-Length": 0}),
- [ResponseMsg(100, reason="Continue",
- headers={"Blab": 1, "Blob": "?"}),
- ResponseMsg(200, reason="OK",
- headers={"Content-Length": 1,
- "Content-Type": "text/plain;charset=utf-8"},
- body=b'?')],
- ResponseValidator(expect_headers={'Content-Type': "text/plain;charset=utf-8"},
- expect_body=b'?')),
-
- (RequestMsg("GET", "/GET/no_length",
- headers={"Content-Length": "0"}),
- ResponseMsg(200, reason="OK",
- headers={"Content-Type": "text/plain;charset=utf-8",
- # ("connection", "close")
- },
- body=b'Hi! ' * 1024 + b'X',
- eom_close=True),
- ResponseValidator(expect_body=b'Hi! ' * 1024 + b'X')),
- ],
-
- #
- # HEAD
- #
-
- "HEAD": [
- (RequestMsg("HEAD", "/HEAD/test_01",
- headers={"Content-Length": "0"}),
- ResponseMsg(200, headers={"App-Header-1": "Value 01",
- "Content-Length": "10",
- "App-Header-2": "Value 02"},
- body=None),
- ResponseValidator(expect_headers={"App-Header-1": "Value 01",
- "Content-Length": "10",
- "App-Header-2": "Value 02"})
- ),
- (RequestMsg("HEAD", "/HEAD/test_02",
- headers={"Content-Length": "0"}),
- ResponseMsg(200, headers={"App-Header-1": "Value 01",
- "Transfer-Encoding": "chunked",
- "App-Header-2": "Value 02"}),
- ResponseValidator(expect_headers={"App-Header-1": "Value 01",
- "Transfer-Encoding": "chunked",
- "App-Header-2": "Value 02"})),
-
- (RequestMsg("HEAD", "/HEAD/test_03",
- headers={"Content-Length": "0"}),
- ResponseMsg(200, headers={"App-Header-3": "Value 03"}, eom_close=True),
- ResponseValidator(expect_headers={"App-Header-3": "Value 03"})),
- ],
-
- #
- # POST
- #
-
- "POST": [
- (RequestMsg("POST", "/POST/test_01",
- headers={"App-Header-1": "Value 01",
- "Content-Length": "18",
- "Content-Type": "application/x-www-form-urlencoded"},
- body=b'one=1&two=2&three=3'),
- ResponseMsg(200, reason="OK",
- headers={"Response-Header": "whatever",
- "Transfer-Encoding": "chunked"},
- body=b'8\r\n'
- + b'12345678\r\n'
- + b'f\r\n'
- + b'abcdefghijklmno\r\n'
- + b'000\r\n'
- + b'\r\n'),
- ResponseValidator(expect_body=b'12345678abcdefghijklmno')
- ),
- (RequestMsg("POST", "/POST/test_02",
- headers={"App-Header-1": "Value 01",
- "Transfer-Encoding": "chunked"},
- body=b'01\r\n'
- + b'!\r\n'
- + b'0\r\n\r\n'),
- ResponseMsg(200, reason="OK",
- headers={"Response-Header": "whatever",
- "Content-Length": "9"},
- body=b'Hi There!',
- eom_close=True),
- ResponseValidator(expect_body=b'Hi There!')
- ),
- ],
-
- #
- # PUT
- #
-
- "PUT": [
- (RequestMsg("PUT", "/PUT/test_01",
- headers={"Put-Header-1": "Value 01",
- "Transfer-Encoding": "chunked",
- "Content-Type": "text/plain;charset=utf-8"},
- body=b'80\r\n'
- + b'$' * 0x80 + b'\r\n'
- + b'0\r\n\r\n'),
- ResponseMsg(201, reason="Created",
- headers={"Response-Header": "whatever",
- "Content-length": "3"},
- body=b'ABC'),
- ResponseValidator(status=201, expect_body=b'ABC')
- ),
-
- (RequestMsg("PUT", "/PUT/test_02",
- headers={"Put-Header-1": "Value 01",
- "Content-length": "0",
- "Content-Type": "text/plain;charset=utf-8"}),
- ResponseMsg(201, reason="Created",
- headers={"Response-Header": "whatever",
- "Transfer-Encoding": "chunked"},
- body=b'1\r\n$\r\n0\r\n\r\n',
- eom_close=True),
- ResponseValidator(status=201, expect_body=b'$')
- ),
- ]
-}
-
-
class RequestHandler(BaseHTTPRequestHandler):
"""
Dispatches requests received by the HTTPServer based on the method
"""
protocol_version = 'HTTP/1.1'
+
def _execute_request(self, tests):
for req, resp, val in tests:
if req.target == self.path:
@@ -334,9 +135,6 @@ class RequestHandler(BaseHTTPRequestHandler):
resp = [resp]
for r in resp:
r.send_response(self)
- if r.eom_close:
- self.close_connection = 1
- self.server.system_test_server_done = True
return
self.send_error(404, "Not Found")
@@ -348,13 +146,13 @@ class RequestHandler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path == "/SHUTDOWN":
- self.close_connection = True
- self.server.system_test_server_done = True
self.send_response(200, "OK")
self.send_header("Content-Length", "13")
self.end_headers()
self.wfile.write(b'Server Closed')
self.wfile.flush()
+ self.close_connection = True
+ self.server.server_killed = True
return
self._execute_request(self.server.system_tests["POST"])
@@ -397,14 +195,18 @@ class RequestHandler(BaseHTTPRequestHandler):
return self.rfile.read()
+class RequestHandler10(RequestHandler):
+ """
+ RequestHandler that forces the server to use HTTP version 1.0 semantics
+ """
+ protocol_version = 'HTTP/1.0'
+
+
class MyHTTPServer(HTTPServer):
"""
Adds a switch to the HTTPServer to allow it to exit gracefully
"""
- def __init__(self, addr, handler_cls, testcases=None):
- if testcases is None:
- testcases = DEFAULT_TEST_SCENARIOS
- self.system_test_server_done = False
+ def __init__(self, addr, handler_cls, testcases):
self.system_tests = testcases
HTTPServer.__init__(self, addr, handler_cls)
@@ -413,10 +215,13 @@ class TestServer(object):
"""
A HTTPServer running in a separate thread
"""
- def __init__(self, port=8080, tests=None):
+ def __init__(self, server_port, client_port, tests, handler_cls=None):
self._logger = Logger(title="TestServer", print_to_console=False)
- self._server_addr = ("", port)
- self._server = MyHTTPServer(self._server_addr, RequestHandler, tests)
+ self._client_port = client_port
+ self._server_addr = ("", server_port)
+ self._server = MyHTTPServer(self._server_addr,
+ handler_cls or RequestHandler,
+ tests)
self._server.allow_reuse_address = True
self._thread = Thread(target=self._run)
self._thread.daemon = True
@@ -425,17 +230,26 @@ class TestServer(object):
def _run(self):
self._logger.log("TestServer listening on %s:%s" % self._server_addr)
try:
- while not self._server.system_test_server_done:
+ self._server.server_killed = False
+ while not self._server.server_killed:
self._server.handle_request()
except Exception as exc:
- self._logger.log("TestServer %s:%s crash: %s" %
+ self._logger.log("TestServer %s crash: %s" %
(self._server_addr, exc))
raise
self._logger.log("TestServer %s:%s closed" % self._server_addr)
def wait(self, timeout=TIMEOUT):
self._logger.log("TestServer %s:%s shutting down" % self._server_addr)
- self._thread.join(timeout=TIMEOUT)
+ if self._thread.is_alive():
+ client = HTTPConnection("127.0.0.1:%s" % self._client_port,
+ timeout=TIMEOUT)
+ client.putrequest("POST", "/SHUTDOWN")
+ client.putheader("Content-Length", "0")
+ client.endheaders()
+ client.getresponse()
+ client.close()
+ self._thread.join(timeout=TIMEOUT)
if self._server:
self._server.server_close()
@@ -490,8 +304,316 @@ class ThreadedTestClient(object):
class Http1AdaptorOneRouterTest(TestCase):
"""
- Test an HTTP server and client attached to a standalone router
+ Test HTTP servers and clients attached to a standalone router
"""
+
+ # HTTP/1.1 compliant test cases
+ TESTS_11 = {
+ #
+ # GET
+ #
+ "GET": [
+ (RequestMsg("GET", "/GET/error",
+ headers={"Content-Length": 0}),
+ ResponseMsg(400, reason="Bad breath", error=True),
+ ResponseValidator(status=400)),
+
+ (RequestMsg("GET", "/GET/content_len",
+ headers={"Content-Length": "00"}),
+ ResponseMsg(200, reason="OK",
+ headers={"Content-Length": 1,
+ "Content-Type": "text/plain;charset=utf-8"},
+ body=b'?'),
+ ResponseValidator(expect_headers={'Content-Length': '1'},
+ expect_body=b'?')),
+
+ (RequestMsg("GET", "/GET/content_len_511",
+ headers={"Content-Length": 0}),
+ ResponseMsg(200, reason="OK",
+ headers={"Content-Length": 511,
+ "Content-Type": "text/plain;charset=utf-8"},
+ body=b'X' * 511),
+ ResponseValidator(expect_headers={'Content-Length': '511'},
+ expect_body=b'X' * 511)),
+
+ (RequestMsg("GET", "/GET/content_len_4096",
+ headers={"Content-Length": 0}),
+ ResponseMsg(200, reason="OK",
+ headers={"Content-Length": 4096,
+ "Content-Type": "text/plain;charset=utf-8"},
+ body=b'X' * 4096),
+ ResponseValidator(expect_headers={'Content-Length': '4096'},
+ expect_body=b'X' * 4096)),
+
+ (RequestMsg("GET", "/GET/chunked",
+ headers={"Content-Length": 0}),
+ ResponseMsg(200, reason="OK",
+ headers={"transfer-encoding": "chunked",
+ "Content-Type": "text/plain;charset=utf-8"},
+ # note: the chunk length does not count the trailing CRLF
+ body=b'16\r\n'
+ + b'Mary had a little pug \r\n'
+ + b'1b\r\n'
+ + b'Its name was "Skupper-Jack"\r\n'
+ + b'0\r\n'
+ + b'Optional: Trailer\r\n'
+ + b'Optional: Trailer\r\n'
+ + b'\r\n'),
+ ResponseValidator(expect_headers={'transfer-encoding': 'chunked'},
+ expect_body=b'Mary had a little pug Its name was "Skupper-Jack"')),
+
+ (RequestMsg("GET", "/GET/chunked_large",
+ headers={"Content-Length": 0}),
+ ResponseMsg(200, reason="OK",
+ headers={"transfer-encoding": "chunked",
+ "Content-Type": "text/plain;charset=utf-8"},
+ # note: the chunk length does not count the trailing CRLF
+ body=b'1\r\n'
+ + b'?\r\n'
+ + b'800\r\n'
+ + b'X' * 0x800 + b'\r\n'
+ + b'13\r\n'
+ + b'Y' * 0x13 + b'\r\n'
+ + b'0\r\n'
+ + b'Optional: Trailer\r\n'
+ + b'Optional: Trailer\r\n'
+ + b'\r\n'),
+ ResponseValidator(expect_headers={'transfer-encoding': 'chunked'},
+ expect_body=b'?' + b'X' * 0x800 + b'Y' * 0x13)),
+
+ (RequestMsg("GET", "/GET/info_content_len",
+ headers={"Content-Length": 0}),
+ [ResponseMsg(100, reason="Continue",
+ headers={"Blab": 1, "Blob": "?"}),
+ ResponseMsg(200, reason="OK",
+ headers={"Content-Length": 1,
+ "Content-Type": "text/plain;charset=utf-8"},
+ body=b'?')],
+ ResponseValidator(expect_headers={'Content-Type': "text/plain;charset=utf-8"},
+ expect_body=b'?')),
+
+ # (RequestMsg("GET", "/GET/no_length",
+ # headers={"Content-Length": "0"}),
+ # ResponseMsg(200, reason="OK",
+ # headers={"Content-Type": "text/plain;charset=utf-8",
+ # "connection": "close"
+ # },
+ # body=b'Hi! ' * 1024 + b'X'),
+ # ResponseValidator(expect_body=b'Hi! ' * 1024 + b'X')),
+ ],
+ #
+ # HEAD
+ #
+ "HEAD": [
+ (RequestMsg("HEAD", "/HEAD/test_01",
+ headers={"Content-Length": "0"}),
+ ResponseMsg(200, headers={"App-Header-1": "Value 01",
+ "Content-Length": "10",
+ "App-Header-2": "Value 02"},
+ body=None),
+ ResponseValidator(expect_headers={"App-Header-1": "Value 01",
+ "Content-Length": "10",
+ "App-Header-2": "Value 02"})
+ ),
+ (RequestMsg("HEAD", "/HEAD/test_02",
+ headers={"Content-Length": "0"}),
+ ResponseMsg(200, headers={"App-Header-1": "Value 01",
+ "Transfer-Encoding": "chunked",
+ "App-Header-2": "Value 02"}),
+ ResponseValidator(expect_headers={"App-Header-1": "Value 01",
+ "Transfer-Encoding": "chunked",
+ "App-Header-2": "Value 02"})),
+
+ (RequestMsg("HEAD", "/HEAD/test_03",
+ headers={"Content-Length": "0"}),
+ ResponseMsg(200, headers={"App-Header-3": "Value 03"}),
+ ResponseValidator(expect_headers={"App-Header-3": "Value 03"})),
+ ],
+ #
+ # POST
+ #
+ "POST": [
+ (RequestMsg("POST", "/POST/test_01",
+ headers={"App-Header-1": "Value 01",
+ "Content-Length": "19",
+ "Content-Type": "application/x-www-form-urlencoded"},
+ body=b'one=1&two=2&three=3'),
+ ResponseMsg(200, reason="OK",
+ headers={"Response-Header": "whatever",
+ "Transfer-Encoding": "chunked"},
+ body=b'8\r\n'
+ + b'12345678\r\n'
+ + b'f\r\n'
+ + b'abcdefghijklmno\r\n'
+ + b'000\r\n'
+ + b'\r\n'),
+ ResponseValidator(expect_body=b'12345678abcdefghijklmno')
+ ),
+ (RequestMsg("POST", "/POST/test_02",
+ headers={"App-Header-1": "Value 01",
+ "Transfer-Encoding": "chunked"},
+ body=b'01\r\n'
+ + b'!\r\n'
+ + b'0\r\n\r\n'),
+ ResponseMsg(200, reason="OK",
+ headers={"Response-Header": "whatever",
+ "Content-Length": "9"},
+ body=b'Hi There!'),
+ ResponseValidator(expect_body=b'Hi There!')
+ ),
+ ],
+ #
+ # PUT
+ #
+ "PUT": [
+ (RequestMsg("PUT", "/PUT/test_01",
+ headers={"Put-Header-1": "Value 01",
+ "Transfer-Encoding": "chunked",
+ "Content-Type": "text/plain;charset=utf-8"},
+ body=b'80\r\n'
+ + b'$' * 0x80 + b'\r\n'
+ + b'0\r\n\r\n'),
+ ResponseMsg(201, reason="Created",
+ headers={"Response-Header": "whatever",
+ "Content-length": "3"},
+ body=b'ABC'),
+ ResponseValidator(status=201, expect_body=b'ABC')
+ ),
+
+ (RequestMsg("PUT", "/PUT/test_02",
+ headers={"Put-Header-1": "Value 01",
+ "Content-length": "0",
+ "Content-Type": "text/plain;charset=utf-8"}),
+ ResponseMsg(201, reason="Created",
+ headers={"Response-Header": "whatever",
+ "Transfer-Encoding": "chunked"},
+ body=b'1\r\n$\r\n0\r\n\r\n'),
+ ResponseValidator(status=201, expect_body=b'$')
+ ),
+ ]
+ }
+
+ # HTTP/1.0 compliant test cases (no chunked, response length unspecified)
+ TESTS_10 = {
+ #
+ # GET
+ #
+ "GET": [
+ (RequestMsg("GET", "/GET/error",
+ headers={"Content-Length": 0}),
+ ResponseMsg(400, reason="Bad breath", error=True),
+ ResponseValidator(status=400)),
+
+ (RequestMsg("GET", "/GET/content_len_511",
+ headers={"Content-Length": 0}),
+ ResponseMsg(200, reason="OK",
+ headers={"Content-Length": 511,
+ "Content-Type": "text/plain;charset=utf-8"},
+ body=b'X' * 511),
+ ResponseValidator(expect_headers={'Content-Length': '511'},
+ expect_body=b'X' * 511)),
+
+ (RequestMsg("GET", "/GET/content_len_4096",
+ headers={"Content-Length": 0}),
+ ResponseMsg(200, reason="OK",
+ headers={"Content-Type": "text/plain;charset=utf-8"},
+ body=b'X' * 4096),
+ ResponseValidator(expect_headers={"Content-Type": "text/plain;charset=utf-8"},
+ expect_body=b'X' * 4096)),
+
+ (RequestMsg("GET", "/GET/info_content_len",
+ headers={"Content-Length": 0}),
+ [ResponseMsg(100, reason="Continue",
+ headers={"Blab": 1, "Blob": "?"}),
+ ResponseMsg(200, reason="OK",
+ headers={"Content-Type": "text/plain;charset=utf-8"},
+ body=b'?')],
+ ResponseValidator(expect_headers={'Content-Type': "text/plain;charset=utf-8"},
+ expect_body=b'?')),
+
+ # (RequestMsg("GET", "/GET/no_length",
+ # headers={"Content-Length": "0"}),
+ # ResponseMsg(200, reason="OK",
+ # headers={"Content-Type": "text/plain;charset=utf-8",
+ # "connection": "close"
+ # },
+ # body=b'Hi! ' * 1024 + b'X'),
+ # ResponseValidator(expect_body=b'Hi! ' * 1024 + b'X')),
+ ],
+ #
+ # HEAD
+ #
+ "HEAD": [
+ (RequestMsg("HEAD", "/HEAD/test_01",
+ headers={"Content-Length": "0"}),
+ ResponseMsg(200, headers={"App-Header-1": "Value 01",
+ "Content-Length": "10",
+ "App-Header-2": "Value 02"},
+ body=None),
+ ResponseValidator(expect_headers={"App-Header-1": "Value 01",
+ "Content-Length": "10",
+ "App-Header-2": "Value 02"})
+ ),
+
+ (RequestMsg("HEAD", "/HEAD/test_03",
+ headers={"Content-Length": "0"}),
+ ResponseMsg(200, headers={"App-Header-3": "Value 03"}),
+ ResponseValidator(expect_headers={"App-Header-3": "Value 03"})),
+ ],
+ #
+ # POST
+ #
+ "POST": [
+ (RequestMsg("POST", "/POST/test_01",
+ headers={"App-Header-1": "Value 01",
+ "Content-Length": "19",
+ "Content-Type": "application/x-www-form-urlencoded"},
+ body=b'one=1&two=2&three=3'),
+ ResponseMsg(200, reason="OK",
+ headers={"Response-Header": "whatever"},
+ body=b'12345678abcdefghijklmno'),
+ ResponseValidator(expect_body=b'12345678abcdefghijklmno')
+ ),
+ (RequestMsg("POST", "/POST/test_02",
+ headers={"App-Header-1": "Value 01",
+ "Content-Length": "5"},
+ body=b'01234'),
+ ResponseMsg(200, reason="OK",
+ headers={"Response-Header": "whatever",
+ "Content-Length": "9"},
+ body=b'Hi There!'),
+ ResponseValidator(expect_body=b'Hi There!')
+ ),
+ ],
+ #
+ # PUT
+ #
+ "PUT": [
+ (RequestMsg("PUT", "/PUT/test_01",
+ headers={"Put-Header-1": "Value 01",
+ "Content-Length": "513",
+ "Content-Type": "text/plain;charset=utf-8"},
+ body=b'$' * 513),
+ ResponseMsg(201, reason="Created",
+ headers={"Response-Header": "whatever",
+ "Content-length": "3"},
+ body=b'ABC'),
+ ResponseValidator(status=201, expect_body=b'ABC')
+ ),
+
+ (RequestMsg("PUT", "/PUT/test_02",
+ headers={"Put-Header-1": "Value 01",
+ "Content-length": "0",
+ "Content-Type": "text/plain;charset=utf-8"}),
+ ResponseMsg(201, reason="Created",
+ headers={"Response-Header": "whatever"},
+ body=b'No Content Length'),
+ ResponseValidator(status=201, expect_body=b'No Content Length')
+ ),
+ ]
+ }
+
+
@classmethod
def setUpClass(cls):
"""Start a router"""
@@ -515,7 +637,7 @@ class Http1AdaptorOneRouterTest(TestCase):
return cls.routers[-1]
# configuration:
- # interior
+ # One interior router, two servers (one running as HTTP/1.0)
#
# +----------------+
# | INT.A |
@@ -523,28 +645,55 @@ class Http1AdaptorOneRouterTest(TestCase):
# ^ ^
# | |
# V V
- # <client> <server>
+ # <clients> <servers>
cls.routers = []
- cls.http_server_port = cls.tester.get_port()
- cls.http_listener_port = cls.tester.get_port()
+ #cls.http_server11_port = cls.tester.get_port()
+ #cls.http_server10_port = cls.tester.get_port()
+ #cls.http_listener11_port = cls.tester.get_port()
+ #cls.http_listener10_port = cls.tester.get_port()
+ cls.http_server11_port = 9090
+ cls.http_listener11_port = 8080
+ cls.http_server10_port = 9091
+ cls.http_listener10_port = 8081
router('INT.A', 'standalone',
- [('httpConnector', {'port': cls.http_server_port,
+ [('httpConnector', {'port': cls.http_server11_port,
'protocolVersion': 'HTTP1',
- 'address': 'testServer'}),
- ('httpListener', {'port': cls.http_listener_port,
+ 'address': 'testServer11'}),
+ ('httpConnector', {'port': cls.http_server10_port,
+ 'protocolVersion': 'HTTP1',
+ 'address': 'testServer10'}),
+ ('httpListener', {'port': cls.http_listener11_port,
'protocolVersion': 'HTTP1',
- 'address': 'testServer'})
+ 'address': 'testServer11'}),
+ ('httpListener', {'port': cls.http_listener10_port,
+ 'protocolVersion': 'HTTP1',
+ 'address': 'testServer10'})
])
+
cls.INT_A = cls.routers[0]
cls.INT_A.listener = cls.INT_A.addresses[0]
- def _do_request(self, tests):
- server = TestServer(port=self.http_server_port)
+ cls.http11_server = TestServer(server_port=cls.http_server11_port,
+ client_port=cls.http_listener11_port,
+ tests=cls.TESTS_11)
+ cls.http10_server = TestServer(server_port=cls.http_server10_port,
+ client_port=cls.http_listener10_port,
+ tests=cls.TESTS_10,
+ handler_cls=RequestHandler10)
+ cls.INT_A.wait_connectors()
+
+ @classmethod
+ def tearDownClass(cls):
+ if cls.http11_server:
+ cls.http11_server.wait()
+ if cls.http10_server:
+ cls.http10_server.wait()
+ super(Http1AdaptorOneRouterTest, cls).tearDownClass()
+
+ def _do_request(self, client, tests):
for req, _, val in tests:
- client = HTTPConnection("127.0.0.1:%s" % self.http_listener_port,
- timeout=TIMEOUT)
req.send_request(client)
rsp = client.getresponse()
try:
@@ -555,20 +704,53 @@ class Http1AdaptorOneRouterTest(TestCase):
if req.method is "BODY":
self.assertEqual(b'', body)
- client.close()
- server.wait()
-
def test_001_get(self):
- self._do_request(DEFAULT_TEST_SCENARIOS["GET"])
+ client = HTTPConnection("127.0.0.1:%s" % self.http_listener11_port,
+ timeout=TIMEOUT)
+ self._do_request(client, self.TESTS_11["GET"])
+ client.close()
def test_002_head(self):
- self._do_request(DEFAULT_TEST_SCENARIOS["HEAD"])
+ client = HTTPConnection("127.0.0.1:%s" % self.http_listener11_port,
+ timeout=TIMEOUT)
+ self._do_request(client, self.TESTS_11["HEAD"])
+ client.close()
def test_003_post(self):
- self._do_request(DEFAULT_TEST_SCENARIOS["POST"])
+ client = HTTPConnection("127.0.0.1:%s" % self.http_listener11_port,
+ timeout=TIMEOUT)
+ self._do_request(client, self.TESTS_11["POST"])
+ client.close()
def test_004_put(self):
- self._do_request(DEFAULT_TEST_SCENARIOS["PUT"])
+ client = HTTPConnection("127.0.0.1:%s" % self.http_listener11_port,
+ timeout=TIMEOUT)
+ self._do_request(client, self.TESTS_11["PUT"])
+ client.close()
+
+ def test_005_get_10(self):
+ client = HTTPConnection("127.0.0.1:%s" % self.http_listener10_port,
+ timeout=TIMEOUT)
+ self._do_request(client, self.TESTS_10["GET"])
+ client.close()
+
+ def test_006_head_10(self):
+ client = HTTPConnection("127.0.0.1:%s" % self.http_listener10_port,
+ timeout=TIMEOUT)
+ self._do_request(client, self.TESTS_10["HEAD"])
+ client.close()
+
+ def test_007_post_10(self):
+ client = HTTPConnection("127.0.0.1:%s" % self.http_listener10_port,
+ timeout=TIMEOUT)
+ self._do_request(client, self.TESTS_10["POST"])
+ client.close()
+
+ def test_008_put_10(self):
+ client = HTTPConnection("127.0.0.1:%s" % self.http_listener10_port,
+ timeout=TIMEOUT)
+ self._do_request(client, self.TESTS_10["PUT"])
+ client.close()
class Http1AdaptorInteriorTest(TestCase):
@@ -661,8 +843,10 @@ class Http1AdaptorInteriorTest(TestCase):
cls.routers = []
cls.INTA_edge_port = cls.tester.get_port()
- cls.http_server_port = cls.tester.get_port()
- cls.http_listener_port = cls.tester.get_port()
+ #cls.http_server_port = cls.tester.get_port()
+ #cls.http_listener_port = cls.tester.get_port()
+ cls.http_server_port = 9090
+ cls.http_listener_port = 8080
router('INT.A', 'interior',
[('listener', {'role': 'edge', 'port': cls.INTA_edge_port}),
@@ -691,7 +875,9 @@ class Http1AdaptorInteriorTest(TestCase):
"""
Test multiple clients running as fast as possible
"""
- server = TestServer(port=self.http_server_port, tests=self.TESTS)
+ server = TestServer(server_port=self.http_server_port,
+ client_port=self.http_listener_port,
+ tests=self.TESTS)
clients = []
for _ in range(5):
@@ -702,14 +888,12 @@ class Http1AdaptorInteriorTest(TestCase):
client.wait()
self.assertIsNone(client.error)
- # terminate the server thread by sending a request
- # with eom_close set
-
+ # send command to stop the server thread
client = ThreadedTestClient({"POST": [(RequestMsg("POST",
"/SHUTDOWN",
{"Content-Length": "0"}),
- None,
- None)]},
+ None,
+ None)]},
self.http_listener_port)
client.wait()
self.assertIsNone(client.error)
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org