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