diff options
author | Dr. David von Oheimb <David.von.Oheimb@siemens.com> | 2021-04-28 00:26:14 +0200 |
---|---|---|
committer | Dr. David von Oheimb <dev@ddvo.net> | 2021-05-14 19:24:42 +0200 |
commit | 19f97fe6f10bf0d1daec26a9ae2ad919127c67d5 (patch) | |
tree | 4734918abbc423e7c2153b59bb4420fe4022205a /test | |
parent | 19a39b29e846e465ee97e7519acf14ddc9302198 (diff) |
HTTP: Implement persistent connections (keep-alive)
Both at API and at CLI level (for the CMP app only, so far)
there is a new parameter/option: keep_alive.
* 0 means HTTP connections are not kept open after
receiving a response, which is the default behavior for HTTP 1.0.
* 1 means that persistent connections are requested.
* 2 means that persistent connections are required, i.e.,
in case the server does not grant them an error occurs.
For the CMP app the default value is 1, which means preferring to keep
the connection open. For all other internal uses of the HTTP client
(fetching an OCSP response, a cert, or a CRL) it does not matter
because these operations just take one round trip.
If the client application requested or required a persistent connection
and this was granted by the server, it can keep the OSSL_HTTP_REQ_CTX *
as long as it wants to send further requests and OSSL_HTTP_is_alive()
returns nonzero,
else it should call OSSL_HTTP_REQ_CTX_free() or OSSL_HTTP_close().
In case the client application keeps the OSSL_HTTP_REQ_CTX *
but the connection then dies for any reason at the server side, it will
notice this obtaining an I/O error when trying to send the next request.
This requires extending the HTTP header parsing and
rearranging the high-level HTTP client API. In particular:
* Split the monolithic OSSL_HTTP_transfer() into OSSL_HTTP_open(),
OSSL_HTTP_set_request(), a lean OSSL_HTTP_transfer(), and OSSL_HTTP_close().
* Split the timeout functionality accordingly and improve default behavior.
* Extract part of OSSL_HTTP_REQ_CTX_new() to OSSL_HTTP_REQ_CTX_set_expected().
* Extend struct ossl_http_req_ctx_st accordingly.
Use the new feature for the CMP client, which requires extending
related transaction management of CMP client and test server.
Update the documentation and extend the tests accordingly.
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/15053)
Diffstat (limited to 'test')
-rw-r--r-- | test/http_test.c | 183 | ||||
-rw-r--r-- | test/recipes/80-test_cmp_http_data/test_connection.csv | 92 |
2 files changed, 196 insertions, 79 deletions
diff --git a/test/http_test.c b/test/http_test.c index e4209a37c0..907650453d 100644 --- a/test/http_test.c +++ b/test/http_test.c @@ -17,26 +17,30 @@ static const ASN1_ITEM *x509_it = NULL; static X509 *x509 = NULL; -#define SERVER "mock.server" -#define PORT "81" -#define RPATH "path/any.crt" -static const char *rpath; - -/* - * pretty trivial HTTP mock server: - * for POST, copy request headers+body from mem BIO 'in' as response to 'out' - * for GET, first redirect the request then respond with 'rsp' of ASN1 type 'it' +#define RPATH "/path/result.crt" + +typedef struct { + BIO *out; + char version; + int keep_alive; +} server_args; + +/*- + * Pretty trivial HTTP mock server: + * For POST, copy request headers+body from mem BIO 'in' as response to 'out'. + * For GET, redirect to RPATH, else respond with 'rsp' of ASN1 type 'it'. + * Respond with HTTP version 1.'version' and 'keep_alive' (unless implicit). */ -static int mock_http_server(BIO *in, BIO *out, +static int mock_http_server(BIO *in, BIO *out, char version, int keep_alive, ASN1_VALUE *rsp, const ASN1_ITEM *it) { - const char *req; + const char *req, *path; long count = BIO_get_mem_data(in, (unsigned char **)&req); const char *hdr = (char *)req; int is_get = count >= 4 && strncmp(hdr, "GET ", 4) == 0; int len; - /* first line should contain "<GET or POST> <rpath> HTTP/1.x" */ + /* first line should contain "<GET or POST> <path> HTTP/1.x" */ if (is_get) hdr += 4; else if (TEST_true(count >= 5 && strncmp(hdr, "POST ", 5) == 0)) @@ -44,16 +48,12 @@ static int mock_http_server(BIO *in, BIO *out, else return 0; - while (*rpath == '/') - rpath++; - while (*hdr == '/') - hdr++; - len = strlen(rpath); - if (!TEST_strn_eq(hdr, rpath, len) || !TEST_char_eq(hdr++[len], ' ')) + path = hdr; + hdr = strchr(hdr, ' '); + if (hdr == NULL) return 0; - hdr += len; len = strlen("HTTP/1."); - if (!TEST_strn_eq(hdr, "HTTP/1.", len)) + if (!TEST_strn_eq(++hdr, "HTTP/1.", len)) return 0; hdr += len; /* check for HTTP version 1.0 .. 1.1 */ @@ -62,16 +62,22 @@ static int mock_http_server(BIO *in, BIO *out, if (!TEST_char_eq(*hdr++, '\r') || !TEST_char_eq(*hdr++, '\n')) return 0; count -= (hdr - req); - if (count <= 0 || out == NULL) + if (count < 0 || out == NULL) return 0; - if (is_get && strcmp(rpath, RPATH) == 0) { - rpath = "path/new.crt"; - return BIO_printf(out, "HTTP/1.1 301 Moved Permanently\r\n" - "Location: /%s\r\n\r\n", rpath) > 0; /* same server */ + if (strncmp(path, RPATH, strlen(RPATH)) != 0) { + if (!is_get) + return 0; + return BIO_printf(out, "HTTP/1.%c 301 Moved Permanently\r\n" + "Location: %s\r\n\r\n", + version, RPATH) > 0; /* same server */ } - if (BIO_printf(out, "HTTP/1.1 200 OK\r\n") <= 0) + if (BIO_printf(out, "HTTP/1.%c 200 OK\r\n", version) <= 0) return 0; + if ((version == '0') == keep_alive) /* otherwise, default */ + if (BIO_printf(out, "Connection: %s\r\n", + version == '0' ? "keep-alive" : "close") <= 0) + return 0; if (is_get) { /* construct new header and body */ if ((len = ASN1_item_i2d(rsp, NULL, it)) <= 0) return 0; @@ -80,16 +86,26 @@ static int mock_http_server(BIO *in, BIO *out, return 0; return ASN1_item_i2d_bio(it, out, rsp); } else { - return BIO_write(out, hdr, count) == count; /* echo header and body */ + len = strlen("Connection: "); + if (strncmp(hdr, "Connection: ", len) == 0) { + /* skip req Connection header */ + hdr = strstr(hdr + len, "\r\n"); + if (hdr == NULL) + return 0; + hdr += 2; + } + /* echo remaining request header and body */ + return BIO_write(out, hdr, count) == count; } } static long http_bio_cb_ex(BIO *bio, int oper, const char *argp, size_t len, int cmd, long argl, int ret, size_t *processed) { + server_args *args = (server_args *)BIO_get_callback_arg(bio); if (oper == (BIO_CB_CTRL | BIO_CB_RETURN) && cmd == BIO_CTRL_FLUSH) - ret = mock_http_server(bio, (BIO *)BIO_get_callback_arg(bio), + ret = mock_http_server(bio, args->out, args->version, args->keep_alive, (ASN1_VALUE *)x509, x509_it); return ret; } @@ -99,6 +115,7 @@ static int test_http_x509(int do_get) X509 *rcert = NULL; BIO *wbio = BIO_new(BIO_s_mem()); BIO *rbio = BIO_new(BIO_s_mem()); + server_args mock_args = { NULL, '0', 0 }; BIO *rsp, *req = ASN1_item_i2d_mem_bio(x509_it, (ASN1_VALUE *)x509); STACK_OF(CONF_VALUE) *headers = NULL; const char content_type[] = "application/x-x509-ca-cert"; @@ -106,14 +123,14 @@ static int test_http_x509(int do_get) if (wbio == NULL || rbio == NULL || req == NULL) goto err; + mock_args.out = rbio; BIO_set_callback_ex(wbio, http_bio_cb_ex); - BIO_set_callback_arg(wbio, (char *)rbio); + BIO_set_callback_arg(wbio, (char *)&mock_args); - rpath = RPATH; rsp = do_get ? - OSSL_HTTP_get("http://"SERVER":"PORT"/"RPATH, + OSSL_HTTP_get("/will-be-redirected", NULL /* proxy */, NULL /* no_proxy */, - wbio, rbio, NULL /* bio_fn */, NULL /* arg */, + wbio, rbio, NULL /* bio_update_fn */, NULL /* arg */, 0 /* buf_size */, headers, content_type, 1 /* expect_asn1 */, HTTP_DEFAULT_MAX_RESP_LEN, 0 /* timeout */) @@ -137,6 +154,52 @@ static int test_http_x509(int do_get) return res; } +static int test_http_keep_alive(char version, int keep_alive, int kept_alive) +{ + BIO *wbio = BIO_new(BIO_s_mem()); + BIO *rbio = BIO_new(BIO_s_mem()); + BIO *rsp; + server_args mock_args = { NULL, '0', 0 }; + const char *const content_type = "application/x-x509-ca-cert"; + OSSL_HTTP_REQ_CTX *rctx = NULL; + int i, res = 0; + + if (wbio == NULL || rbio == NULL) + goto err; + mock_args.out = rbio; + mock_args.version = version; + mock_args.keep_alive = kept_alive; + BIO_set_callback_ex(wbio, http_bio_cb_ex); + BIO_set_callback_arg(wbio, (char *)&mock_args); + + for (res = 1, i = 1; res && i <= 2; i++) { + rsp = OSSL_HTTP_transfer(&rctx, NULL /* server */, NULL /* port */, + RPATH, 0 /* use_ssl */, + NULL /* proxy */, NULL /* no_proxy */, + wbio, rbio, NULL /* bio_update_fn */, NULL, + 0 /* buf_size */, NULL /* headers */, + NULL /* content_type */, NULL /* req => GET */, + content_type, 0 /* ASN.1 not expected */, + 0 /* max_resp_len */, 0 /* timeout */, + keep_alive); + if (keep_alive == 2 && kept_alive == 0) + res = res && TEST_ptr_null(rsp) + && TEST_int_eq(OSSL_HTTP_is_alive(rctx), 0); + else + res = res && TEST_ptr(rsp) + && TEST_int_eq(OSSL_HTTP_is_alive(rctx), keep_alive > 0); + BIO_free(rsp); + (void)BIO_reset(rbio); /* discard response contents */ + keep_alive = 0; + } + OSSL_HTTP_close(rctx, res); + + err: + BIO_free(wbio); + BIO_free(rbio); + return res; +} + static int test_http_url_ok(const char *url, int exp_ssl, const char *exp_host, const char *exp_port, const char *exp_path) { @@ -253,21 +316,61 @@ static int test_http_post_x509(void) return test_http_x509(0); } +static int test_http_keep_alive_0_no_no(void) +{ + return test_http_keep_alive('0', 0, 0); +} + +static int test_http_keep_alive_1_no_no(void) +{ + return test_http_keep_alive('1', 0, 0); +} + +static int test_http_keep_alive_0_prefer_yes(void) +{ + return test_http_keep_alive('0', 1, 1); +} + +static int test_http_keep_alive_1_prefer_yes(void) +{ + return test_http_keep_alive('1', 1, 1); +} + +static int test_http_keep_alive_0_require_yes(void) +{ + return test_http_keep_alive('0', 2, 1); +} + +static int test_http_keep_alive_1_require_yes(void) +{ + return test_http_keep_alive('1', 2, 1); +} + +static int test_http_keep_alive_0_require_no(void) +{ + return test_http_keep_alive('0', 2, 0); +} + +static int test_http_keep_alive_1_require_no(void) +{ + return test_http_keep_alive('1', 2, 0); +} + void cleanup_tests(void) { X509_free(x509); } +OPT_TEST_DECLARE_USAGE("cert.pem\n") + int setup_tests(void) { - if (!test_skip_common_options()) { - TEST_error("Error parsing test options\n"); + if (!test_skip_common_options()) return 0; - } x509_it = ASN1_ITEM_rptr(X509); if (!TEST_ptr((x509 = load_cert_pem(test_get_argument(0), NULL)))) - return 1; + return 0; ADD_TEST(test_http_url_dns); ADD_TEST(test_http_url_path_query); @@ -279,5 +382,13 @@ int setup_tests(void) ADD_TEST(test_http_url_invalid_path); ADD_TEST(test_http_get_x509); ADD_TEST(test_http_post_x509); + ADD_TEST(test_http_keep_alive_0_no_no); + ADD_TEST(test_http_keep_alive_1_no_no); + ADD_TEST(test_http_keep_alive_0_prefer_yes); + ADD_TEST(test_http_keep_alive_1_prefer_yes); + ADD_TEST(test_http_keep_alive_0_require_yes); + ADD_TEST(test_http_keep_alive_1_require_yes); + ADD_TEST(test_http_keep_alive_0_require_no); + ADD_TEST(test_http_keep_alive_1_require_no); return 1; } diff --git a/test/recipes/80-test_cmp_http_data/test_connection.csv b/test/recipes/80-test_cmp_http_data/test_connection.csv index 5d1700fa21..3276eb5fb3 100644 --- a/test/recipes/80-test_cmp_http_data/test_connection.csv +++ b/test/recipes/80-test_cmp_http_data/test_connection.csv @@ -1,43 +1,49 @@ -expected,description, -section,val, -server,val, -proxy,val, -path,val, -msg_timeout,int, -total_timeout,int, -tls_used,noarg, -no_proxy,val -,Message transfer options:,,,,,,,,,,,, -,,,,,,,,,,,,,,,,,,,,,,,,, -0,default config, -section,,,,,,,,BLANK,,BLANK,,BLANK,,BLANK, -TBD,Domain name, -section,, -server,_SERVER_CN:_SERVER_PORT,,,, -TBD,IP address, -section,, -server,_SERVER_IP:_SERVER_PORT,,,, -,,,,,,,,,,,,,,,,,,,,,,,,, -1,wrong server, -section,, -server,example.com:_SERVER_PORT,,,,, -msg_timeout,1,BLANK,,BLANK,,BLANK, -1,wrong server port, -section,, -server,_SERVER_HOST:99,,,,, -msg_timeout,1,BLANK,,BLANK,,BLANK, -1,server default port, -section,, -server,_SERVER_HOST,,,,, -msg_timeout,1,BLANK,,BLANK,,BLANK, -1,server port out of range, -section,, -server,_SERVER_HOST:65536,,,,,BLANK,,BLANK,,BLANK,,BLANK, -1,server port negative, -section,, -server,_SERVER_HOST:-10,,,,,BLANK,,BLANK,,BLANK,,BLANK, -1,server missing argument, -section,, -server,,,,,,BLANK,,BLANK,,BLANK,,BLANK, -1,server with default port, -section,, -server,_SERVER_HOST,,,,,BLANK,,BLANK,,BLANK,,BLANK, -1,server port bad syntax: leading garbage, -section,, -server,_SERVER_HOST:x/+80,,,,,BLANK,,BLANK,,BLANK,,BLANK, -1,server port bad synatx: trailing garbage, -section,, -server,_SERVER_HOST:_SERVER_PORT+/x.,,,,,BLANK,,BLANK,,BLANK,,BLANK, -1,server with TLS port, -section,, -server,_SERVER_HOST:_SERVER_TLS,,,,,BLANK,,BLANK,,BLANK,,BLANK, -TBD,server IP address with TLS port, -section,, -server,_SERVER_IP:_SERVER_TLS,,,,,BLANK,,BLANK,,BLANK,,BLANK, -,,,,,,,,,,,,,,,,,,,,,,,,, -1,proxy port bad syntax: leading garbage, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1:x*/8888,,,BLANK,,BLANK,,BLANK,,BLANK, -no_proxy,nonmatch.com,-msg_timeout,1 -1,proxy port out of range, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1:65536,,,BLANK,,BLANK,,BLANK,,BLANK, -no_proxy,nonmatch.com,-msg_timeout,1 -1,proxy default port, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1,,,BLANK,,BLANK,,BLANK,,BLANK, -no_proxy,nonmatch.com,-msg_timeout,1 -1,proxy missing argument, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,,,,BLANK,,BLANK,,BLANK,,BLANK, -no_proxy,nonmatch.com -,,,,,,,,,,,,,,,,,,,,,,,,, -0,path explicit, -section,, -server,_SERVER_HOST:_SERVER_PORT,,, -path,_SERVER_PATH,BLANK,,BLANK,,BLANK,,BLANK, -0,path overrides -server path, -section,, -server,_SERVER_HOST:_SERVER_PORT/ignored,,, -path,_SERVER_PATH,BLANK,,BLANK,,BLANK,,BLANK, -0,path default -server path, -section,, -server,_SERVER_HOST:_SERVER_PORT/_SERVER_PATH,,, -path,"""",BLANK,,BLANK,,BLANK,,BLANK, -1,path missing argument, -section,,,,,, -path,,BLANK,,BLANK,,BLANK,,BLANK, -1,path wrong, -section,,,,,, -path,/publicweb/cmp/example,BLANK,,BLANK,,BLANK,,BLANK, -0,path with additional '/'s fine according to RFC 3986, -section,,,,,, -path,/_SERVER_PATH////,BLANK,,BLANK,,BLANK,,BLANK -1,path mixed case, -section,,,,,, -path,pKiX/,BLANK,,BLANK,,BLANK,,BLANK, -1,path upper case, -section,,,,,, -path,PKIX/,BLANK,,BLANK,,BLANK,,BLANK, -,,,,,,,,,,,,,,,,,,,,,,,,, -1,msg_timeout missing argument, -section,,,,,,,, -msg_timeout,,BLANK,,BLANK,,BLANK, -1,msg_timeout negative, -section,,,,,,,, -msg_timeout,-5,BLANK,,BLANK,,BLANK, -0,msg_timeout 5, -section,,,,,,,, -msg_timeout,5,BLANK,,BLANK,,BLANK, -0,msg_timeout 0, -section,,,,,,,, -msg_timeout,0,BLANK,,BLANK,,BLANK, -,,,,,,,,,,,,,,,,,,,,,,,,, -1,total_timeout missing argument, -section,,,,,,,,BLANK,, -total_timeout,,BLANK,,BLANK, -1,total_timeout negative, -section,,,,,,,,BLANK,, -total_timeout,-5,BLANK,,BLANK, -0,total_timeout 10, -section,,,,,,,,BLANK,, -total_timeout,10,BLANK,,BLANK, -0,total_timeout 0, -section,,,,,,,,BLANK,, -total_timeout,0,BLANK,,BLANK, -,,,,,,,,,,,,,,,,,,,,,,,,, +expected,description, -section,val, -server,val, -proxy,val, -no_proxy,val, -tls_used,noarg, -path,val, -msg_timeout,int, -total_timeout,int, -keep_alive,val +,Message transfer options:,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,, +0,default config, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,,BLANK, +TBD,Domain name, -section,, -server,_SERVER_CN:_SERVER_PORT,,,,,,,,,,,,,, +TBD,IP address, -section,, -server,_SERVER_IP:_SERVER_PORT,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,, +1,wrong server, -section,, -server,example.com:_SERVER_PORT,,,,,BLANK,,,, -msg_timeout,1,BLANK,,BLANK, +1,wrong server port, -section,, -server,_SERVER_HOST:99,,,,,BLANK,,,, -msg_timeout,1,BLANK,,BLANK, +1,server default port, -section,, -server,_SERVER_HOST,,,,,BLANK,,,, -msg_timeout,1,BLANK,,BLANK, +1,server port out of range, -section,, -server,_SERVER_HOST:65536,,,,,BLANK,,,,BLANK,,BLANK,,BLANK, +1,server port negative, -section,, -server,_SERVER_HOST:-10,,,,,BLANK,,,,BLANK,,BLANK,,BLANK, +1,server missing argument, -section,, -server,,,,,,BLANK,,,,BLANK,,BLANK,,BLANK, +1,server with default port, -section,, -server,_SERVER_HOST,,,,,BLANK,,,,BLANK,,BLANK,,BLANK, +1,server port bad syntax: leading garbage, -section,, -server,_SERVER_HOST:x/+80,,,,,BLANK,,,,BLANK,,BLANK,,BLANK, +1,server port bad synatx: trailing garbage, -section,, -server,_SERVER_HOST:_SERVER_PORT+/x.,,,,,BLANK,,,,BLANK,,BLANK,,BLANK, +1,server with TLS port, -section,, -server,_SERVER_HOST:_SERVER_TLS,,,,,BLANK,,,,BLANK,,BLANK,,BLANK, +TBD,server IP address with TLS port, -section,, -server,_SERVER_IP:_SERVER_TLS,,,,,BLANK,,,,BLANK,,BLANK,,BLANK, +,,,,,,,,,,,,,,,,,,, +1,proxy port bad syntax: leading garbage, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1:x*/8888, -no_proxy,nonmatch.com,BLANK,,,,-msg_timeout,1,BLANK,,BLANK, +1,proxy port out of range, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1:65536, -no_proxy,nonmatch.com,BLANK,,,,-msg_timeout,1,BLANK,,BLANK, +1,proxy default port, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,127.0.0.1, -no_proxy,nonmatch.com,BLANK,,,,-msg_timeout,1,BLANK,,BLANK, +1,proxy missing argument, -section,, -server,_SERVER_HOST:_SERVER_PORT, -proxy,, -no_proxy,nonmatch.com,BLANK,,,,BLANK,,BLANK,,BLANK, +,,,,,,,,,,,,,,,,,,, +0,path explicit, -section,, -server,_SERVER_HOST:_SERVER_PORT,,,,,BLANK,, -path,_SERVER_PATH,BLANK,,BLANK,,BLANK, +0,path overrides -server path, -section,, -server,_SERVER_HOST:_SERVER_PORT/ignored,,,,,BLANK,, -path,_SERVER_PATH,BLANK,,BLANK,,BLANK, +0,path default -server path, -section,, -server,_SERVER_HOST:_SERVER_PORT/_SERVER_PATH,,,,,BLANK,, -path,"""",BLANK,,BLANK,,BLANK, +1,path missing argument, -section,,,,,,,,BLANK,, -path,,BLANK,,BLANK,,BLANK, +1,path wrong, -section,,,,,,,,BLANK,, -path,/publicweb/cmp/example,BLANK,,BLANK,,BLANK, +0,path with additional '/'s fine according to RFC 3986, -section,,,,,,,,BLANK,, -path,/_SERVER_PATH////,BLANK,,BLANK,,BLANK, +1,path mixed case, -section,,,,,,,,BLANK,, -path,pKiX/,BLANK,,BLANK,,BLANK, +1,path upper case, -section,,,,,,,,BLANK,, -path,PKIX/,BLANK,,BLANK,,BLANK, +,,,,,,,,,,,,,,,,,,, +1,msg_timeout missing argument, -section,,,,,,,,BLANK,,,, -msg_timeout,,BLANK,,BLANK, +1,msg_timeout negative, -section,,,,,,,,BLANK,,,, -msg_timeout,-5,BLANK,,BLANK, +0,msg_timeout 5, -section,,,,,,,,BLANK,,,, -msg_timeout,5,BLANK,,BLANK, +0,msg_timeout 0, -section,,,,,,,,BLANK,,,, -msg_timeout,0,BLANK,,BLANK, +,,,,,,,,,,,,,,,,,,, +1,total_timeout missing argument, -section,,,,,,,,BLANK,,,,BLANK,, -total_timeout,,BLANK, +1,total_timeout negative, -section,,,,,,,,BLANK,,,,BLANK,, -total_timeout,-5,BLANK, +0,total_timeout 10, -section,,,,,,,,BLANK,,,,BLANK,, -total_timeout,10,BLANK, +0,total_timeout 0, -section,,,,,,,,BLANK,,,,BLANK,, -total_timeout,0,BLANK, +,,,,,,,,,,,,,,,,,,, +1,keep_alive missing argument, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive, +1,keep_alive negative, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,-1 +0,keep_alive 0, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,0 +0,keep_alive 1, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,1 +0,keep_alive 2, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,2 +1,keep_alive 3, -section,,,,,,,,BLANK,,,,BLANK,,BLANK,, -keep_alive,3 |