summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Eissing <stefan@eissing.org>2023-04-26 12:38:22 +0200
committerDaniel Stenberg <daniel@haxx.se>2023-04-26 23:24:46 +0200
commitacd82c8bfd743d0f743a1c1296890738832ac83e (patch)
treefc4da775326efd7562f6217f41929b703902431c
parent21575b26fe33099e087b8beaac9bc43fa8597874 (diff)
downloadcurl-acd82c8bfd743d0f743a1c1296890738832ac83e.tar.gz
tests/http: more tests with specific clients
- Makefile support for building test specific clients in tests/http/clients - auto-make of clients when invoking pytest - added test_09_02 for server PUSH_PROMISEs using clients/h2-serverpush - added test_02_21 for lib based downloads and pausing/unpausing transfers curl url parser: - added internal method `curl_url_set_authority()` for setting the authority part of a url (used for PUSH_PROMISE) http2: - made logging of PUSH_PROMISE handling nicer Placing python test requirements in requirements.txt files - separate files to base test suite and http tests since use and module lists differ - using the files in the gh workflows websocket test cases, fixes for we and bufq - bufq: account for spare chunks in space calculation - bufq: reset chunks that are skipped empty - ws: correctly encode frames with 126 bytes payload - ws: update frame meta information on first call of collect callback that fills user buffer - test client ws-data: some test/reporting improvements Closes #11006
-rw-r--r--.github/workflows/linux.yml2
-rw-r--r--.github/workflows/ngtcp2-gnutls.yml5
-rw-r--r--.github/workflows/ngtcp2-quictls.yml5
-rw-r--r--.github/workflows/ngtcp2-wolfssl.yml7
-rw-r--r--.lift/config.toml1
-rw-r--r--configure.ac1
-rw-r--r--docs/examples/http2-serverpush.c12
-rw-r--r--lib/bufq.c11
-rw-r--r--lib/http2.c59
-rw-r--r--lib/urlapi-int.h3
-rw-r--r--lib/urlapi.c116
-rw-r--r--lib/ws.c28
-rw-r--r--tests/Makefile.am1
-rw-r--r--tests/http/Makefile.am10
-rw-r--r--tests/http/clients/.gitignore8
-rw-r--r--tests/http/clients/Makefile.am71
-rw-r--r--tests/http/clients/Makefile.inc30
-rw-r--r--tests/http/clients/h2-download.c275
-rw-r--r--tests/http/clients/h2-serverpush.c271
-rw-r--r--tests/http/clients/ws-data.c263
-rw-r--r--tests/http/clients/ws-pingpong.c158
-rw-r--r--tests/http/conftest.py2
-rw-r--r--tests/http/requirements.txt29
-rw-r--r--tests/http/test_02_download.py39
-rw-r--r--tests/http/test_09_push.py21
-rw-r--r--tests/http/test_20_websockets.py131
-rw-r--r--tests/http/testenv/__init__.py1
-rw-r--r--tests/http/testenv/client.py105
-rw-r--r--tests/http/testenv/env.py22
-rw-r--r--tests/http/testenv/httpd.py1
-rwxr-xr-xtests/http/testenv/ws_echo_server.py66
-rw-r--r--tests/requirements.txt26
32 files changed, 1678 insertions, 102 deletions
diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index a29774871..f8ef60089 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -288,7 +288,7 @@ jobs:
- if: ${{ contains(matrix.build.install_steps, 'pytest') }}
run: |
sudo apt-get install apache2 apache2-dev libnghttp2-dev
- sudo python3 -m pip install impacket pytest cryptography multipart
+ sudo python3 -m pip install -r tests/http/requirements.txt
git clone --quiet --depth=1 -b master https://github.com/icing/mod_h2
cd mod_h2
autoreconf -fi
diff --git a/.github/workflows/ngtcp2-gnutls.yml b/.github/workflows/ngtcp2-gnutls.yml
index 943dc6642..686013534 100644
--- a/.github/workflows/ngtcp2-gnutls.yml
+++ b/.github/workflows/ngtcp2-gnutls.yml
@@ -75,7 +75,6 @@ jobs:
sudo apt-get update
sudo apt-get install libtool autoconf automake pkg-config stunnel4 ${{ matrix.build.install }}
sudo apt-get install apache2 apache2-dev
- sudo python3 -m pip install impacket pytest cryptography multipart
name: 'install prereqs and impacket, pytest, crypto'
- run: |
@@ -136,6 +135,10 @@ jobs:
- uses: actions/checkout@v3
+ - run: |
+ sudo python3 -m pip install -r tests/requirements.txt -r tests/http/requirements.txt
+ name: 'install python test prereqs'
+
- run: autoreconf -fi
name: 'autoreconf'
diff --git a/.github/workflows/ngtcp2-quictls.yml b/.github/workflows/ngtcp2-quictls.yml
index 0cce56cf8..b5b5c64ae 100644
--- a/.github/workflows/ngtcp2-quictls.yml
+++ b/.github/workflows/ngtcp2-quictls.yml
@@ -66,7 +66,6 @@ jobs:
sudo apt-get update
sudo apt-get install libtool autoconf automake pkg-config stunnel4 ${{ matrix.build.install }}
sudo apt-get install apache2 apache2-dev
- sudo python3 -m pip install impacket pytest cryptography multipart
name: 'install prereqs and impacket, pytest, crypto'
- run: |
@@ -111,6 +110,10 @@ jobs:
- uses: actions/checkout@v3
+ - run: |
+ sudo python3 -m pip install -r tests/requirements.txt -r tests/http/requirements.txt
+ name: 'install python test prereqs'
+
- run: autoreconf -fi
name: 'autoreconf'
diff --git a/.github/workflows/ngtcp2-wolfssl.yml b/.github/workflows/ngtcp2-wolfssl.yml
index b5aa42128..8fdca6726 100644
--- a/.github/workflows/ngtcp2-wolfssl.yml
+++ b/.github/workflows/ngtcp2-wolfssl.yml
@@ -70,8 +70,7 @@ jobs:
sudo apt-get update
sudo apt-get install libtool autoconf automake pkg-config stunnel4 ${{ matrix.build.install }}
sudo apt-get install apache2 apache2-dev
- sudo python3 -m pip install impacket pytest cryptography multipart
- name: 'install prereqs and impacket, pytest, crypto'
+ name: 'install prereqs'
- run: |
git clone --quiet --depth=1 https://github.com/wolfSSL/wolfssl.git
@@ -123,6 +122,10 @@ jobs:
- uses: actions/checkout@v3
+ - run: |
+ sudo python3 -m pip install -r tests/requirements.txt -r tests/http/requirements.txt
+ name: 'install python test prereqs'
+
- run: autoreconf -fi
name: 'autoreconf'
diff --git a/.lift/config.toml b/.lift/config.toml
index e9dc0332e..a57f3bad6 100644
--- a/.lift/config.toml
+++ b/.lift/config.toml
@@ -3,5 +3,6 @@
# SPDX-License-Identifier: curl
ignoreRules = [ "DEAD_STORE", "subprocess_without_shell_equals_true" ]
+ignoreFiles = [ "tests/http/**" ]
build = "make"
setup = ".lift/setup.sh"
diff --git a/configure.ac b/configure.ac
index d54d8b264..1461e031a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4693,6 +4693,7 @@ AC_CONFIG_FILES([Makefile \
tests/unit/Makefile \
tests/http/config.ini \
tests/http/Makefile \
+ tests/http/clients/Makefile \
packages/Makefile \
packages/vms/Makefile \
curl-config \
diff --git a/docs/examples/http2-serverpush.c b/docs/examples/http2-serverpush.c
index 31a34715d..7524c6289 100644
--- a/docs/examples/http2-serverpush.c
+++ b/docs/examples/http2-serverpush.c
@@ -130,7 +130,7 @@ int my_trace(CURL *handle, curl_infotype type,
#define OUTPUTFILE "dl"
-static int setup(CURL *hnd)
+static int setup(CURL *hnd, const char *url)
{
FILE *out = fopen(OUTPUTFILE, "wb");
if(!out)
@@ -141,7 +141,7 @@ static int setup(CURL *hnd)
curl_easy_setopt(hnd, CURLOPT_WRITEDATA, out);
/* set the same URL */
- curl_easy_setopt(hnd, CURLOPT_URL, "https://localhost:8443/index.html");
+ curl_easy_setopt(hnd, CURLOPT_URL, url);
/* please be verbose */
curl_easy_setopt(hnd, CURLOPT_VERBOSE, 1L);
@@ -211,12 +211,16 @@ static int server_push_callback(CURL *parent,
/*
* Download a file over HTTP/2, take care of server push.
*/
-int main(void)
+int main(int argc, char *argv[])
{
CURL *easy;
CURLM *multi_handle;
int transfers = 1; /* we start with one */
struct CURLMsg *m;
+ const char *url = "https://localhost:8443/index.html";
+
+ if(argc == 2)
+ url = argv[1];
/* init a multi stack */
multi_handle = curl_multi_init();
@@ -224,7 +228,7 @@ int main(void)
easy = curl_easy_init();
/* set options */
- if(setup(easy)) {
+ if(setup(easy, url)) {
fprintf(stderr, "failed\n");
return 1;
}
diff --git a/lib/bufq.c b/lib/bufq.c
index 8702cabe3..e80419322 100644
--- a/lib/bufq.c
+++ b/lib/bufq.c
@@ -138,6 +138,8 @@ static size_t chunk_skip(struct buf_chunk *chunk, size_t amount)
if(n) {
n = CURLMIN(n, amount);
chunk->r_offset += n;
+ if(chunk->r_offset == chunk->w_offset)
+ chunk->r_offset = chunk->w_offset = 0;
}
return n;
}
@@ -288,6 +290,13 @@ size_t Curl_bufq_space(const struct bufq *q)
size_t space = 0;
if(q->tail)
space += chunk_space(q->tail);
+ if(q->spare) {
+ struct buf_chunk *chunk = q->spare;
+ while(chunk) {
+ space += chunk->dlen;
+ chunk = chunk->next;
+ }
+ }
if(q->chunk_count < q->max_chunks) {
space += (q->max_chunks - q->chunk_count) * q->chunk_size;
}
@@ -611,6 +620,8 @@ ssize_t Curl_bufq_slurpn(struct bufq *q, size_t max_len,
/* blocked on first read or real error, fail */
nread = -1;
}
+ else
+ *err = CURLE_OK;
break;
}
else if(n == 0) {
diff --git a/lib/http2.c b/lib/http2.c
index 726f5a64b..62eed3c35 100644
--- a/lib/http2.c
+++ b/lib/http2.c
@@ -38,6 +38,7 @@
#include "strcase.h"
#include "multiif.h"
#include "url.h"
+#include "urlapi-int.h"
#include "cfilters.h"
#include "connect.h"
#include "strtoofft.h"
@@ -768,7 +769,7 @@ static int set_transfer_url(struct Curl_easy *data,
v = curl_pushheader_byname(hp, HTTP_PSEUDO_AUTHORITY);
if(v) {
- uc = curl_url_set(u, CURLUPART_HOST, v, 0);
+ uc = curl_url_set_authority(u, v, CURLU_DISALLOW_USER);
if(uc) {
rc = 2;
goto fail;
@@ -950,8 +951,15 @@ static CURLcode on_stream_frame(struct Curl_cfilter *cf,
switch(frame->hd.type) {
case NGHTTP2_DATA:
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%d] FRAME[DATA len=%zu pad=%zu], "
+ "buffered=%zu, window=%d/%d",
+ stream_id, frame->hd.length, frame->data.padlen,
+ Curl_bufq_len(&stream->recvbuf),
+ nghttp2_session_get_stream_effective_recv_data_length(
+ ctx->h2, stream->id),
+ nghttp2_session_get_stream_effective_local_window_size(
+ ctx->h2, stream->id)));
/* If !body started on this stream, then receiving DATA is illegal. */
- DEBUGF(LOG_CF(data, cf, "[h2sid=%d] recv frame DATA", stream_id));
if(!stream->bodystarted) {
rv = nghttp2_submit_rst_stream(ctx->h2, NGHTTP2_FLAG_NONE,
stream_id, NGHTTP2_PROTOCOL_ERROR);
@@ -965,7 +973,7 @@ static CURLcode on_stream_frame(struct Curl_cfilter *cf,
}
break;
case NGHTTP2_HEADERS:
- DEBUGF(LOG_CF(data, cf, "[h2sid=%d] recv frame HEADERS", stream_id));
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%d] FRAME[HEADERS]", stream_id));
if(stream->bodystarted) {
/* Only valid HEADERS after body started is trailer HEADERS. We
buffer them in on_header callback. */
@@ -993,7 +1001,7 @@ static CURLcode on_stream_frame(struct Curl_cfilter *cf,
drain_stream(cf, data, stream);
break;
case NGHTTP2_PUSH_PROMISE:
- DEBUGF(LOG_CF(data, cf, "[h2sid=%d] recv PUSH_PROMISE", stream_id));
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%d] FRAME[PUSH_PROMISE]", stream_id));
rv = push_promise(cf, data, &frame->push_promise);
if(rv) { /* deny! */
DEBUGASSERT((rv > CURL_PUSH_OK) && (rv <= CURL_PUSH_ERROROUT));
@@ -1010,13 +1018,13 @@ static CURLcode on_stream_frame(struct Curl_cfilter *cf,
}
break;
case NGHTTP2_RST_STREAM:
- DEBUGF(LOG_CF(data, cf, "[h2sid=%d] recv RST", stream_id));
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%d] FARME[RST]", stream_id));
stream->closed = TRUE;
stream->reset = TRUE;
drain_stream(cf, data, stream);
break;
case NGHTTP2_WINDOW_UPDATE:
- DEBUGF(LOG_CF(data, cf, "[h2sid=%d] recv WINDOW_UPDATE", stream_id));
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%d] FRAME[WINDOW_UPDATE]", stream_id));
if((data->req.keepon & KEEP_SEND_HOLD) &&
(data->req.keepon & KEEP_SEND)) {
data->req.keepon &= ~KEEP_SEND_HOLD;
@@ -1026,7 +1034,7 @@ static CURLcode on_stream_frame(struct Curl_cfilter *cf,
}
break;
default:
- DEBUGF(LOG_CF(data, cf, "[h2sid=%d] recv frame %x",
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%d] FRAME[%x]",
stream_id, frame->hd.type));
break;
}
@@ -1048,7 +1056,7 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame,
switch(frame->hd.type) {
case NGHTTP2_SETTINGS: {
uint32_t max_conn = ctx->max_concurrent_streams;
- DEBUGF(LOG_CF(data, cf, "recv frame SETTINGS"));
+ DEBUGF(LOG_CF(data, cf, "FRAME[SETTINGS]"));
ctx->max_concurrent_streams = nghttp2_session_get_remote_settings(
session, NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS);
ctx->enable_push = nghttp2_session_get_remote_settings(
@@ -1070,7 +1078,7 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame,
ctx->goaway_error = frame->goaway.error_code;
ctx->last_stream_id = frame->goaway.last_stream_id;
if(data) {
- DEBUGF(LOG_CF(data, cf, "recv GOAWAY, error=%d, last_stream=%u",
+ DEBUGF(LOG_CF(data, cf, "FRAME[GOAWAY, error=%d, last_stream=%u]",
ctx->goaway_error, ctx->last_stream_id));
infof(data, "received GOAWAY, error=%d, last_stream=%u",
ctx->goaway_error, ctx->last_stream_id);
@@ -1078,7 +1086,7 @@ static int on_frame_recv(nghttp2_session *session, const nghttp2_frame *frame,
}
break;
case NGHTTP2_WINDOW_UPDATE:
- DEBUGF(LOG_CF(data, cf, "recv frame WINDOW_UPDATE"));
+ DEBUGF(LOG_CF(data, cf, "FRAME[WINDOW_UPDATE]"));
break;
default:
DEBUGF(LOG_CF(data, cf, "recv frame %x on 0", frame->hd.type));
@@ -1139,10 +1147,6 @@ static int on_data_chunk_recv(nghttp2_session *session, uint8_t flags,
drain_stream(cf, data_s, stream);
DEBUGASSERT((size_t)nwritten == len);
- DEBUGF(LOG_CF(data_s, cf, "[h2sid=%d] %zd/%zu DATA recvd, "
- "(buffer now holds %zu)",
- stream_id, nwritten, len, Curl_bufq_len(&stream->recvbuf)));
-
return 0;
}
@@ -1675,11 +1679,13 @@ static ssize_t stream_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
if(nread < 0) {
if(stream->closed) {
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%d] returning CLOSE", stream->id));
nread = http2_handle_stream_close(cf, data, stream, err);
}
else if(stream->reset ||
(ctx->conn_closed && Curl_bufq_is_empty(&ctx->inbufq)) ||
(ctx->goaway && ctx->last_stream_id < stream->id)) {
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%d] returning ERR", stream->id));
*err = stream->bodystarted? CURLE_PARTIAL_FILE : CURLE_RECV_ERROR;
nread = -1;
}
@@ -1730,7 +1736,8 @@ static CURLcode h2_progress_ingress(struct Curl_cfilter *cf,
Curl_bufq_len(&ctx->inbufq), nread, result)); */
if(nread < 0) {
if(result != CURLE_AGAIN) {
- failf(data, "Failed receiving HTTP2 data");
+ failf(data, "Failed receiving HTTP2 data: %d(%s)", result,
+ curl_easy_strerror(result));
return result;
}
break;
@@ -1762,12 +1769,6 @@ static ssize_t cf_h2_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
CF_DATA_SAVE(save, cf, data);
- DEBUGF(LOG_CF(data, cf, "[h2sid=%d] cf_recv: win %u/%u",
- stream->id,
- nghttp2_session_get_local_window_size(ctx->h2),
- nghttp2_session_get_stream_local_window_size(ctx->h2,
- stream->id)
- ));
nread = stream_recv(cf, data, buf, len, err);
if(nread < 0 && *err != CURLE_AGAIN)
goto out;
@@ -1794,8 +1795,6 @@ static ssize_t cf_h2_recv(struct Curl_cfilter *cf, struct Curl_easy *data,
stream->resp_hds_len = 0;
}
if(data_consumed) {
- DEBUGF(LOG_CF(data, cf, "[h2sid=%d] increase window by %zu",
- stream->id, data_consumed));
nghttp2_session_consume(ctx->h2, stream->id, data_consumed);
}
}
@@ -1813,8 +1812,15 @@ out:
*err = result;
nread = -1;
}
- DEBUGF(LOG_CF(data, cf, "[h2sid=%d] cf_recv(len=%zu) -> %zd %d",
- stream->id, len, nread, *err));
+ DEBUGF(LOG_CF(data, cf, "[h2sid=%d] cf_recv(len=%zu) -> %zd %d, "
+ "buffered=%zu, window=%d/%d",
+ stream->id, len, nread, *err,
+ Curl_bufq_len(&stream->recvbuf),
+ nghttp2_session_get_stream_effective_recv_data_length(
+ ctx->h2, stream->id),
+ nghttp2_session_get_stream_effective_local_window_size(
+ ctx->h2, stream->id)));
+
CF_DATA_RESTORE(cf, save);
return nread;
}
@@ -2205,6 +2211,9 @@ static CURLcode http2_data_pause(struct Curl_cfilter *cf,
return CURLE_HTTP2;
}
+ if(!pause)
+ drain_stream(cf, data, stream);
+
/* make sure the window update gets sent */
result = h2_progress_egress(cf, data);
if(result)
diff --git a/lib/urlapi-int.h b/lib/urlapi-int.h
index 28e5dd7ea..ff9f79907 100644
--- a/lib/urlapi-int.h
+++ b/lib/urlapi-int.h
@@ -28,6 +28,9 @@
size_t Curl_is_absolute_url(const char *url, char *buf, size_t buflen,
bool guess_scheme);
+CURLUcode curl_url_set_authority(CURLU *u, const char *authority,
+ unsigned int flags);
+
#ifdef DEBUGBUILD
CURLUcode Curl_parse_port(struct Curl_URL *u, struct dynbuf *host,
bool has_scheme);
diff --git a/lib/urlapi.c b/lib/urlapi.c
index 15e31ec6d..bdbe6c8a7 100644
--- a/lib/urlapi.c
+++ b/lib/urlapi.c
@@ -432,6 +432,7 @@ static CURLUcode parse_hostname_login(struct Curl_URL *u,
DEBUGASSERT(login);
+ *offset = 0;
ptr = memchr(login, '@', len);
if(!ptr)
goto out;
@@ -462,14 +463,17 @@ static CURLUcode parse_hostname_login(struct Curl_URL *u,
result = CURLUE_USER_NOT_ALLOWED;
goto out;
}
+ free(u->user);
u->user = userp;
}
if(passwdp) {
+ free(u->password);
u->password = passwdp;
}
if(optionsp) {
+ free(u->options);
u->options = optionsp;
}
@@ -543,6 +547,7 @@ UNITTEST CURLUcode Curl_parse_port(struct Curl_URL *u, struct dynbuf *host,
u->portnum = port;
/* generate a new port number string to get rid of leading zeroes etc */
+ free(u->port);
u->port = aprintf("%ld", port);
if(!u->port)
return CURLUE_OUT_OF_MEMORY;
@@ -763,6 +768,75 @@ static CURLUcode urldecode_host(struct dynbuf *host)
return CURLUE_OK;
}
+static CURLUcode parse_authority(struct Curl_URL *u,
+ const char *auth, size_t authlen,
+ unsigned int flags,
+ struct dynbuf *host,
+ bool has_scheme)
+{
+ size_t offset;
+ CURLUcode result;
+
+ /*
+ * Parse the login details and strip them out of the host name.
+ */
+ result = parse_hostname_login(u, auth, authlen, flags, &offset);
+ if(result)
+ goto out;
+
+ if(Curl_dyn_addn(host, auth + offset, authlen - offset)) {
+ result = CURLUE_OUT_OF_MEMORY;
+ goto out;
+ }
+
+ result = Curl_parse_port(u, host, has_scheme);
+ if(result)
+ goto out;
+
+ switch(ipv4_normalize(host)) {
+ case HOST_IPV4:
+ break;
+ case HOST_IPV6:
+ result = ipv6_parse(u, Curl_dyn_ptr(host), Curl_dyn_len(host));
+ break;
+ case HOST_NAME:
+ result = urldecode_host(host);
+ if(!result)
+ result = hostname_check(u, Curl_dyn_ptr(host), Curl_dyn_len(host));
+ break;
+ case HOST_ERROR:
+ result = CURLUE_OUT_OF_MEMORY;
+ break;
+ case HOST_BAD:
+ default:
+ result = CURLUE_BAD_HOSTNAME; /* Bad IPv4 address even */
+ break;
+ }
+
+out:
+ return result;
+}
+
+CURLUcode curl_url_set_authority(CURLU *u, const char *authority,
+ unsigned int flags)
+{
+ CURLUcode result;
+ struct dynbuf host;
+
+ DEBUGASSERT(authority);
+ Curl_dyn_init(&host, CURL_MAX_INPUT_LENGTH);
+
+ result = parse_authority(u, authority, strlen(authority), flags,
+ &host, !!u->scheme);
+ if(result)
+ Curl_dyn_free(&host);
+ else {
+ free(u->host);
+ u->host = Curl_dyn_ptr(&host);
+ }
+ return result;
+}
+
/*
* "Remove Dot Segments"
* https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
@@ -1091,48 +1165,8 @@ static CURLUcode parseurl(const char *url, CURLU *u, unsigned int flags)
/* this pathlen also contains the query and the fragment */
pathlen = urllen - (path - url);
if(hostlen) {
- /* number of bytes into the string the host name starts: */
- size_t offset = 0;
- /*
- * Parse the login details and strip them out of the host name.
- */
- result = parse_hostname_login(u, hostp, hostlen, flags, &offset);
- if(!result) {
- hostp += offset;
- hostlen -= offset;
- if(Curl_dyn_addn(&host, hostp, hostlen))
- result = CURLUE_OUT_OF_MEMORY;
- else
- result = Curl_parse_port(u, &host, schemelen);
- }
- if(!result) {
- int norm = ipv4_normalize(&host);
- switch(norm) {
- case HOST_IPV4:
- break;
-
- case HOST_IPV6:
- result = ipv6_parse(u, Curl_dyn_ptr(&host), Curl_dyn_len(&host));
- break;
-
- case HOST_NAME:
- result = urldecode_host(&host);
- if(!result)
- result = hostname_check(u, Curl_dyn_ptr(&host),
- Curl_dyn_len(&host));
- break;
-
- case HOST_ERROR:
- result = CURLUE_OUT_OF_MEMORY;
- break;
-
- case HOST_BAD:
- default:
- result = CURLUE_BAD_HOSTNAME; /* Bad IPv4 address even */
- break;
- }
- }
+ result = parse_authority(u, hostp, hostlen, flags, &host, schemelen);
if(result)
goto fail;
diff --git a/lib/ws.c b/lib/ws.c
index 7b28f5794..71ad2ccb8 100644
--- a/lib/ws.c
+++ b/lib/ws.c
@@ -455,7 +455,7 @@ static ssize_t ws_enc_write_head(struct Curl_easy *data,
head[9] = (unsigned char)(payload_len & 0xff);
hlen = 10;
}
- else if(payload_len > 126) {
+ else if(payload_len >= 126) {
head[1] = 126 | WSBIT_MASK;
head[2] = (unsigned char)((payload_len >> 8) & 0xff);
head[3] = (unsigned char)(payload_len & 0xff);
@@ -786,6 +786,14 @@ static ssize_t ws_client_collect(const unsigned char *buf, size_t buflen,
size_t nwritten;
curl_off_t remain = (payload_len - (payload_offset + buflen));
+ if(!ctx->bufidx) {
+ /* first write */
+ ctx->frame_age = frame_age;
+ ctx->frame_flags = frame_flags;
+ ctx->payload_offset = payload_offset;
+ ctx->payload_len = payload_len;
+ }
+
if((frame_flags & CURLWS_PING) && !remain) {
/* auto-respond to PINGs, only works for single-frame payloads atm */
size_t bytes;
@@ -810,13 +818,6 @@ static ssize_t ws_client_collect(const unsigned char *buf, size_t buflen,
}
*err = CURLE_OK;
memcpy(ctx->buffer, buf, nwritten);
- if(!ctx->bufidx) {
- /* first write */
- ctx->frame_age = frame_age;
- ctx->frame_flags = frame_flags;
- ctx->payload_offset = payload_offset;
- ctx->payload_len = payload_len;
- }
ctx->bufidx += nwritten;
}
return nwritten;
@@ -831,7 +832,7 @@ static ssize_t nw_in_recv(void *reader_ctx,
*err = curl_easy_recv(data, buf, buflen, &nread);
if(*err)
- return *err;
+ return -1;
return (ssize_t)nread;
}
@@ -888,6 +889,8 @@ CURL_EXTERN CURLcode curl_ws_recv(struct Curl_easy *data, void *buffer,
infof(data, "connection expectedly closed?");
return CURLE_GOT_NOTHING;
}
+ DEBUGF(infof(data, "curl_ws_recv, added %zu bytes from network",
+ Curl_bufq_len(&ws->recvbuf)));
}
result = ws_dec_pass(&ws->dec, data, &ws->recvbuf,
@@ -1015,12 +1018,13 @@ CURL_EXTERN CURLcode curl_ws_send(struct Curl_easy *data, const void *buffer,
if(result)
return result;
- /* Limit what we are willing to buffer */
+ /* TODO: the current design does not allow partial writes, afaict.
+ * It is not clear who the application is supposed to react. */
space = Curl_bufq_space(&ws->sendbuf);
+ DEBUGF(infof(data, "curl_ws_send(len=%zu), sendbuf len=%zu space %zu",
+ buflen, Curl_bufq_len(&ws->sendbuf), space));
if(space < 14)
return CURLE_AGAIN;
- if(buflen > space)
- buflen = space;
if(sendflags & CURLWS_OFFSET) {
if(totalsize) {
diff --git a/tests/Makefile.am b/tests/Makefile.am
index ae6b5ca33..7708132ae 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -120,6 +120,7 @@ checksrc:
cd libtest && $(MAKE) checksrc
cd unit && $(MAKE) checksrc
cd server && $(MAKE) checksrc
+ cd http && $(MAKE) checksrc
if CURLDEBUG
# for debug builds, we scan the sources on all regular make invokes
diff --git a/tests/http/Makefile.am b/tests/http/Makefile.am
index 6a4cc831a..cce66daf8 100644
--- a/tests/http/Makefile.am
+++ b/tests/http/Makefile.am
@@ -22,6 +22,16 @@
#
###########################################################################
+SUBDIRS = clients
+
clean-local:
rm -rf *.pyc __pycache__
rm -rf gen
+
+check: clients
+
+clients:
+ @(cd clients; $(MAKE) check)
+
+checksrc:
+ cd clients && $(MAKE) checksrc
diff --git a/tests/http/clients/.gitignore b/tests/http/clients/.gitignore
new file mode 100644
index 000000000..02084e18a
--- /dev/null
+++ b/tests/http/clients/.gitignore
@@ -0,0 +1,8 @@
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# SPDX-License-Identifier: curl
+
+h2-serverpush
+h2-download
+ws-data
+ws-pingpong \ No newline at end of file
diff --git a/tests/http/clients/Makefile.am b/tests/http/clients/Makefile.am
new file mode 100644
index 000000000..386da5a22
--- /dev/null
+++ b/tests/http/clients/Makefile.am
@@ -0,0 +1,71 @@
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+
+AUTOMAKE_OPTIONS = foreign nostdinc
+
+
+# Specify our include paths here, and do it relative to $(top_srcdir) and
+# $(top_builddir), to ensure that these paths which belong to the library
+# being currently built and tested are searched before the library which
+# might possibly already be installed in the system.
+#
+# $(top_srcdir)/include is for libcurl's external include files
+
+AM_CPPFLAGS = -I$(top_srcdir)/include \
+ -DCURL_DISABLE_DEPRECATION
+
+LIBDIR = $(top_builddir)/lib
+
+# Avoid libcurl obsolete stuff
+AM_CPPFLAGS += -DCURL_NO_OLDIES
+
+if USE_CPPFLAG_CURL_STATICLIB
+AM_CPPFLAGS += -DCURL_STATICLIB
+endif
+
+# Prevent LIBS from being used for all link targets
+LIBS = $(BLANK_AT_MAKETIME)
+
+# Dependencies
+if USE_EXPLICIT_LIB_DEPS
+LDADD = $(LIBDIR)/libcurl.la @LIBCURL_LIBS@
+else
+LDADD = $(LIBDIR)/libcurl.la
+endif
+
+# This might hold -Werror
+CFLAGS += @CURL_CFLAG_EXTRAS@
+
+# Makefile.inc provides the check_PROGRAMS and COMPLICATED_EXAMPLES defines
+include Makefile.inc
+
+all: $(check_PROGRAMS)
+
+CHECKSRC = $(CS_$(V))
+CS_0 = @echo " RUN " $@;
+CS_1 =
+CS_ = $(CS_0)
+
+checksrc:
+ $(CHECKSRC)(@PERL@ $(top_srcdir)/scripts/checksrc.pl -D$(srcdir) $(srcdir)/*.c)
diff --git a/tests/http/clients/Makefile.inc b/tests/http/clients/Makefile.inc
new file mode 100644
index 000000000..e0abf0a33
--- /dev/null
+++ b/tests/http/clients/Makefile.inc
@@ -0,0 +1,30 @@
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+
+# These are all libcurl example programs to be test compiled
+check_PROGRAMS = \
+ h2-serverpush \
+ h2-download \
+ ws-data \
+ ws-pingpong
diff --git a/tests/http/clients/h2-download.c b/tests/http/clients/h2-download.c
new file mode 100644
index 000000000..d87ee01e1
--- /dev/null
+++ b/tests/http/clients/h2-download.c
@@ -0,0 +1,275 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+/* <DESC>
+ * HTTP/2 server push
+ * </DESC>
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* somewhat unix-specific */
+#include <sys/time.h>
+#include <unistd.h>
+
+/* curl stuff */
+#include <curl/curl.h>
+#include <curl/mprintf.h>
+
+#ifndef CURLPIPE_MULTIPLEX
+#error "too old libcurl, cannot do HTTP/2 server push!"
+#endif
+
+static int verbose = 1;
+
+static
+int my_trace(CURL *handle, curl_infotype type,
+ char *data, size_t size,
+ void *userp)
+{
+ const char *text;
+ (void)handle; /* prevent compiler warning */
+ (void)userp;
+
+ switch(type) {
+ case CURLINFO_TEXT:
+ fprintf(stderr, "== Info: %s", data);
+ /* FALLTHROUGH */
+ default: /* in case a new one is introduced to shock us */
+ return 0;
+
+ case CURLINFO_HEADER_OUT:
+ text = "=> Send header";
+ break;
+ case CURLINFO_DATA_OUT:
+ if(verbose <= 1)
+ return 0;
+ text = "=> Send data";
+ break;
+ case CURLINFO_HEADER_IN:
+ text = "<= Recv header";
+ break;
+ case CURLINFO_DATA_IN:
+ if(verbose <= 1)
+ return 0;
+ text = "<= Recv data";
+ break;
+ }
+
+ fprintf(stderr, "%s, %lu bytes (0x%lx)\n",
+ text, (unsigned long)size, (unsigned long)size);
+ return 0;
+}
+
+struct transfer {
+ int idx;
+ CURL *easy;
+ char filename[128];
+ FILE *out;
+ curl_off_t recv_size;
+ curl_off_t pause_at;
+ int paused;
+ int resumed;
+ int done;
+};
+
+static size_t transfer_count;
+static struct transfer *transfers;
+
+static struct transfer *get_transfer_for_easy(CURL *easy)
+{
+ size_t i;
+ for(i = 0; i < transfer_count; ++i) {
+ if(easy == transfers[i].easy)
+ return &transfers[i];
+ }
+ return NULL;
+}
+
+static size_t my_write_cb(char *buf, size_t nitems, size_t buflen,
+ void *userdata)
+{
+ struct transfer *t = userdata;
+ ssize_t nwritten;
+
+ if(!t->resumed &&
+ t->recv_size < t->pause_at &&
+ ((curl_off_t)(t->recv_size + (nitems * buflen)) >= t->pause_at)) {
+ fprintf(stderr, "transfer %d: PAUSE\n", t->idx);
+ t->paused = 1;
+ return CURL_WRITEFUNC_PAUSE;
+ }
+
+ if(!t->out) {
+ curl_msnprintf(t->filename, sizeof(t->filename)-1, "download_%u.data",
+ t->idx);
+ t->out = fopen(t->filename, "wb");
+ if(!t->out)
+ return 0;
+ }
+
+ nwritten = fwrite(buf, nitems, buflen, t->out);
+ if(nwritten < 0) {
+ fprintf(stderr, "transfer %d: write failure\n", t->idx);
+ return 0;
+ }
+ t->recv_size += nwritten;
+ return (size_t)nwritten;
+}
+
+static int setup(CURL *hnd, const char *url, struct transfer *t)
+{
+ curl_easy_setopt(hnd, CURLOPT_URL, url);
+ curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
+ curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
+ curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYHOST, 0L);
+
+ curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, my_write_cb);
+ curl_easy_setopt(hnd, CURLOPT_WRITEDATA, t);
+
+ /* please be verbose */
+ if(verbose) {
+ curl_easy_setopt(hnd, CURLOPT_VERBOSE, 1L);
+ curl_easy_setopt(hnd, CURLOPT_DEBUGFUNCTION, my_trace);
+ }
+
+#if (CURLPIPE_MULTIPLEX > 0)
+ /* wait for pipe connection to confirm */
+ curl_easy_setopt(hnd, CURLOPT_PIPEWAIT, 1L);
+#endif
+ return 0; /* all is good */
+}
+
+/*
+ * Download a file over HTTP/2, take care of server push.
+ */
+int main(int argc, char *argv[])
+{
+ CURLM *multi_handle;
+ int active_transfers;
+ struct CURLMsg *m;
+ const char *url;
+ size_t i;
+ long pause_offset;
+ struct transfer *t;
+
+ if(argc != 4) {
+ fprintf(stderr, "usage: h2-download count pause-offset url\n");
+ return 2;
+ }
+
+ transfer_count = (size_t)strtol(argv[1], NULL, 10);
+ pause_offset = strtol(argv[2], NULL, 10);
+ url = argv[3];
+
+ transfers = calloc(transfer_count, sizeof(*transfers));
+ if(!transfers) {
+ fprintf(stderr, "error allocating transfer structs\n");
+ return 1;
+ }
+
+ multi_handle = curl_multi_init();
+ curl_multi_setopt(multi_handle, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
+
+ active_transfers = 0;
+ for(i = 0; i < transfer_count; ++i) {
+ t = &transfers[i];
+ t->idx = (int)i;
+ t->pause_at = (curl_off_t)pause_offset * i;
+ t->easy = curl_easy_init();
+ if(!t->easy || setup(t->easy, url, t)) {
+ fprintf(stderr, "setup of transfer #%d failed\n", (int)i);
+ return 1;
+ }
+ curl_multi_add_handle(multi_handle, t->easy);
+ ++active_transfers;
+ }
+
+ do {
+ int still_running; /* keep number of running handles */
+ CURLMcode mc = curl_multi_perform(multi_handle, &still_running);
+
+ if(still_running) {
+ /* wait for activity, timeout or "nothing" */
+ mc = curl_multi_poll(multi_handle, NULL, 0, 1000, NULL);
+ fprintf(stderr, "curl_multi_poll() -> %d\n", mc);
+ }
+
+ if(mc)
+ break;
+
+ /*
+ * A little caution when doing server push is that libcurl itself has
+ * created and added one or more easy handles but we need to clean them up
+ * when we are done.
+ */
+ do {
+ int msgq = 0;
+ m = curl_multi_info_read(multi_handle, &msgq);
+ if(m && (m->msg == CURLMSG_DONE)) {
+ CURL *e = m->easy_handle;
+ active_transfers--;
+ curl_multi_remove_handle(multi_handle, e);
+ t = get_transfer_for_easy(e);
+ if(t) {
+ t->done = 1;
+ }
+ else
+ curl_easy_cleanup(e);
+ }
+ else {
+ /* nothing happending, resume one paused transfer if there is one */
+ for(i = 0; i < transfer_count; ++i) {
+ t = &transfers[i];
+ if(!t->done && t->paused) {
+ t->resumed = 1;
+ t->paused = 0;
+ curl_easy_pause(t->easy, CURLPAUSE_CONT);
+ fprintf(stderr, "transfer %d: RESUME\n", t->idx);
+ break;
+ }
+ }
+
+ }
+ } while(m);
+
+ } while(active_transfers); /* as long as we have transfers going */
+
+ for(i = 0; i < transfer_count; ++i) {
+ t = &transfers[i];
+ if(t->out) {
+ fclose(t->out);
+ t->out = NULL;
+ }
+ if(t->easy) {
+ curl_easy_cleanup(t->easy);
+ t->easy = NULL;
+ }
+ }
+ free(transfers);
+
+ curl_multi_cleanup(multi_handle);
+
+ return 0;
+}
diff --git a/tests/http/clients/h2-serverpush.c b/tests/http/clients/h2-serverpush.c
new file mode 100644
index 000000000..5a1eb3c91
--- /dev/null
+++ b/tests/http/clients/h2-serverpush.c
@@ -0,0 +1,271 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+/* <DESC>
+ * HTTP/2 server push
+ * </DESC>
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* somewhat unix-specific */
+#include <sys/time.h>
+#include <unistd.h>
+
+/* curl stuff */
+#include <curl/curl.h>
+#include <curl/mprintf.h>
+
+#ifndef CURLPIPE_MULTIPLEX
+#error "too old libcurl, cannot do HTTP/2 server push!"
+#endif
+
+static
+void dump(const char *text, unsigned char *ptr, size_t size,
+ char nohex)
+{
+ size_t i;
+ size_t c;
+
+ unsigned int width = 0x10;
+
+ if(nohex)
+ /* without the hex output, we can fit more on screen */
+ width = 0x40;
+
+ fprintf(stderr, "%s, %lu bytes (0x%lx)\n",
+ text, (unsigned long)size, (unsigned long)size);
+
+ for(i = 0; i<size; i += width) {
+
+ fprintf(stderr, "%4.4lx: ", (unsigned long)i);
+
+ if(!nohex) {
+ /* hex not disabled, show it */
+ for(c = 0; c < width; c++)
+ if(i + c < size)
+ fprintf(stderr, "%02x ", ptr[i + c]);
+ else
+ fputs(" ", stderr);
+ }
+
+ for(c = 0; (c < width) && (i + c < size); c++) {
+ /* check for 0D0A; if found, skip past and start a new line of output */
+ if(nohex && (i + c + 1 < size) && ptr[i + c] == 0x0D &&
+ ptr[i + c + 1] == 0x0A) {
+ i += (c + 2 - width);
+ break;
+ }
+ fprintf(stderr, "%c",
+ (ptr[i + c] >= 0x20) && (ptr[i + c]<0x80)?ptr[i + c]:'.');
+ /* check again for 0D0A, to avoid an extra \n if it's at width */
+ if(nohex && (i + c + 2 < size) && ptr[i + c + 1] == 0x0D &&
+ ptr[i + c + 2] == 0x0A) {
+ i += (c + 3 - width);
+ break;
+ }
+ }
+ fputc('\n', stderr); /* newline */
+ }
+}
+
+static
+int my_trace(CURL *handle, curl_infotype type,
+ char *data, size_t size,
+ void *userp)
+{
+ const char *text;
+ (void)handle; /* prevent compiler warning */
+ (void)userp;
+ switch(type) {
+ case CURLINFO_TEXT:
+ fprintf(stderr, "== Info: %s", data);
+ /* FALLTHROUGH */
+ default: /* in case a new one is introduced to shock us */
+ return 0;
+
+ case CURLINFO_HEADER_OUT:
+ text = "=> Send header";
+ break;
+ case CURLINFO_DATA_OUT:
+ text = "=> Send data";
+ break;
+ case CURLINFO_SSL_DATA_OUT:
+ text = "=> Send SSL data";
+ break;
+ case CURLINFO_HEADER_IN:
+ text = "<= Recv header";
+ break;
+ case CURLINFO_DATA_IN:
+ text = "<= Recv data";
+ break;
+ case CURLINFO_SSL_DATA_IN:
+ text = "<= Recv SSL data";
+ break;
+ }
+
+ dump(text, (unsigned char *)data, size, 1);
+ return 0;
+}
+
+#define OUTPUTFILE "download_0.data"
+
+static int setup(CURL *hnd, const char *url)
+{
+ FILE *out = fopen(OUTPUTFILE, "wb");
+ if(!out)
+ /* failed */
+ return 1;
+
+ curl_easy_setopt(hnd, CURLOPT_URL, url);
+ curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
+ curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYPEER, 0L);
+ curl_easy_setopt(hnd, CURLOPT_SSL_VERIFYHOST, 0L);
+
+ curl_easy_setopt(hnd, CURLOPT_WRITEDATA, out);
+
+ /* please be verbose */
+ curl_easy_setopt(hnd, CURLOPT_VERBOSE, 1L);
+ curl_easy_setopt(hnd, CURLOPT_DEBUGFUNCTION, my_trace);
+
+#if (CURLPIPE_MULTIPLEX > 0)
+ /* wait for pipe connection to confirm */
+ curl_easy_setopt(hnd, CURLOPT_PIPEWAIT, 1L);
+#endif
+ return 0; /* all is good */
+}
+
+/* called when there's an incoming push */
+static int server_push_callback(CURL *parent,
+ CURL *easy,
+ size_t num_headers,
+ struct curl_pushheaders *headers,
+ void *userp)
+{
+ char *headp;
+ size_t i;
+ int *transfers = (int *)userp;
+ char filename[128];
+ FILE *out;
+ static unsigned int count = 0;
+ int rv;
+
+ (void)parent; /* we have no use for this */
+ curl_msnprintf(filename, sizeof(filename)-1, "push%u", count++);
+
+ /* here's a new stream, save it in a new file for each new push */
+ out = fopen(filename, "wb");
+ if(!out) {
+ /* if we cannot save it, deny it */
+ fprintf(stderr, "Failed to create output file for push\n");
+ rv = CURL_PUSH_DENY;
+ goto out;
+ }
+
+ /* write to this file */
+ curl_easy_setopt(easy, CURLOPT_WRITEDATA, out);
+
+ fprintf(stderr, "**** push callback approves stream %u, got %lu headers!\n",
+ count, (unsigned long)num_headers);
+
+ for(i = 0; i<num_headers; i++) {
+ headp = curl_pushheader_bynum(headers, i);
+ fprintf(stderr, "**** header %lu: %s\n", (unsigned long)i, headp);
+ }
+
+ headp = curl_pushheader_byname(headers, ":path");
+ if(headp) {
+ fprintf(stderr, "**** The PATH is %s\n", headp /* skip :path + colon */);
+ }
+
+ (*transfers)++; /* one more */
+ rv = CURL_PUSH_OK;
+
+out:
+ return rv;
+}
+
+
+/*
+ * Download a file over HTTP/2, take care of server push.
+ */
+int main(int argc, char *argv[])
+{
+ CURL *easy;
+ CURLM *multi_handle;
+ int transfers = 1; /* we start with one */
+ struct CURLMsg *m;
+ const char *url;
+
+ if(argc != 2) {
+ fprintf(stderr, "need URL as argument\n");
+ return 2;
+ }
+ url = argv[1];
+
+ multi_handle = curl_multi_init();
+ curl_multi_setopt(multi_handle, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
+ curl_multi_setopt(multi_handle, CURLMOPT_PUSHFUNCTION, server_push_callback);
+ curl_multi_setopt(multi_handle, CURLMOPT_PUSHDATA, &transfers);
+
+ easy = curl_easy_init();
+ if(setup(easy, url)) {
+ fprintf(stderr, "failed\n");
+ return 1;
+ }
+
+ curl_multi_add_handle(multi_handle, easy);
+ do {
+ int still_running; /* keep number of running handles */
+ CURLMcode mc = curl_multi_perform(multi_handle, &still_running);
+
+ if(still_running)
+ /* wait for activity, timeout or "nothing" */
+ mc = curl_multi_poll(multi_handle, NULL, 0, 1000, NULL);
+
+ if(mc)
+ break;
+
+ /*
+ * A little caution when doing server push is that libcurl itself has
+ * created and added one or more easy handles but we need to clean them up
+ * when we are done.
+ */
+ do {
+ int msgq = 0;
+ m = curl_multi_info_read(multi_handle, &msgq);
+ if(m && (m->msg == CURLMSG_DONE)) {
+ CURL *e = m->easy_handle;
+ transfers--;
+ curl_multi_remove_handle(multi_handle, e);
+ curl_easy_cleanup(e);
+ }
+ } while(m);
+
+ } while(transfers); /* as long as we have transfers going */
+
+ curl_multi_cleanup(multi_handle);
+
+ return 0;
+}
diff --git a/tests/http/clients/ws-data.c b/tests/http/clients/ws-data.c
new file mode 100644
index 000000000..1c61d59f7
--- /dev/null
+++ b/tests/http/clients/ws-data.c
@@ -0,0 +1,263 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+/* <DESC>
+ * Websockets data echos
+ * </DESC>
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* somewhat unix-specific */
+#include <sys/time.h>
+#include <unistd.h>
+
+
+/* curl stuff */
+#include <curl/curl.h>
+#include "../../../lib/curl_setup.h"
+
+#ifdef USE_WEBSOCKETS
+
+static
+void dump(const char *text, unsigned char *ptr, size_t size,
+ char nohex)
+{
+ size_t i;
+ size_t c;
+
+ unsigned int width = 0x10;
+
+ if(nohex)
+ /* without the hex output, we can fit more on screen */
+ width = 0x40;
+
+ fprintf(stderr, "%s, %lu bytes (0x%lx)\n",
+ text, (unsigned long)size, (unsigned long)size);
+
+ for(i = 0; i<size; i += width) {
+
+ fprintf(stderr, "%4.4lx: ", (unsigned long)i);
+
+ if(!nohex) {
+ /* hex not disabled, show it */
+ for(c = 0; c < width; c++)
+ if(i + c < size)
+ fprintf(stderr, "%02x ", ptr[i + c]);
+ else
+ fputs(" ", stderr);
+ }
+
+ for(c = 0; (c < width) && (i + c < size); c++) {
+ /* check for 0D0A; if found, skip past and start a new line of output */
+ if(nohex && (i + c + 1 < size) && ptr[i + c] == 0x0D &&
+ ptr[i + c + 1] == 0x0A) {
+ i += (c + 2 - width);
+ break;
+ }
+ fprintf(stderr, "%c",
+ (ptr[i + c] >= 0x20) && (ptr[i + c]<0x80)?ptr[i + c]:'.');
+ /* check again for 0D0A, to avoid an extra \n if it's at width */
+ if(nohex && (i + c + 2 < size) && ptr[i + c + 1] == 0x0D &&
+ ptr[i + c + 2] == 0x0A) {
+ i += (c + 3 - width);
+ break;
+ }
+ }
+ fputc('\n', stderr); /* newline */
+ }
+}
+
+static CURLcode send_binary(CURL *curl, char *buf, size_t buflen)
+{
+ size_t nwritten;
+ CURLcode result =
+ curl_ws_send(curl, buf, buflen, &nwritten, 0, CURLWS_BINARY);
+ fprintf(stderr, "ws: send_binary(len=%ld) -> %d, %ld\n",
+ (long)buflen, result, (long)nwritten);
+ return result;
+}
+
+static CURLcode recv_binary(CURL *curl, char *exp_data, size_t exp_len)
+{
+ struct curl_ws_frame *frame;
+ char recvbuf[256];
+ size_t r_offset, nread;
+ CURLcode result;
+
+ fprintf(stderr, "recv_binary: expected payload %ld bytes\n", (long)exp_len);
+ r_offset = 0;
+ while(1) {
+ result = curl_ws_recv(curl, recvbuf, sizeof(recvbuf), &nread, &frame);
+ if(result == CURLE_AGAIN) {
+ fprintf(stderr, "EAGAIN, sleep, try again\n");
+ usleep(100*1000);
+ continue;
+ }
+ fprintf(stderr, "ws: curl_ws_recv(offset=%ld, len=%ld) -> %d, %ld\n",
+ (long)r_offset, (long)sizeof(recvbuf), result, (long)nread);
+ if(result) {
+ return result;
+ }
+ if(!(frame->flags & CURLWS_BINARY)) {
+ fprintf(stderr, "recv_data: wrong frame, got %ld bytes rflags %x\n",
+ (long)nread, frame->flags);
+ return CURLE_RECV_ERROR;
+ }
+ if(frame->offset != (curl_off_t)r_offset) {
+ fprintf(stderr, "recv_data: frame offset, expected %ld, got %ld\n",
+ (long)r_offset, (long)frame->offset);
+ return CURLE_RECV_ERROR;
+ }
+ if(frame->bytesleft != (curl_off_t)(exp_len - r_offset - nread)) {
+ fprintf(stderr, "recv_data: frame bytesleft, expected %ld, got %ld\n",
+ (long)(exp_len - r_offset - nread), (long)frame->bytesleft);
+ return CURLE_RECV_ERROR;
+ }
+ if(r_offset + nread > exp_len) {
+ fprintf(stderr, "recv_data: data length, expected %ld, now at %ld\n",
+ (long)exp_len, (long)(r_offset + nread));
+ return CURLE_RECV_ERROR;
+ }
+ if(memcmp(exp_data + r_offset, recvbuf, nread)) {
+ fprintf(stderr, "recv_data: data differs, offset=%ld, len=%ld\n",
+ (long)r_offset, (long)nread);
+ dump("expected:", (unsigned char *)exp_data + r_offset, nread, 0);
+ dump("received:", (unsigned char *)recvbuf, nread, 0);
+ return CURLE_RECV_ERROR;
+ }
+ r_offset += nread;
+ if(r_offset >= exp_len) {
+ fprintf(stderr, "recv_data: frame complete\n");
+ break;
+ }
+ }
+ return CURLE_OK;
+}
+
+/* just close the connection */
+static void websocket_close(CURL *curl)
+{
+ size_t sent;
+ CURLcode result =
+ curl_ws_send(curl, "", 0, &sent, 0, CURLWS_CLOSE);
+ fprintf(stderr,
+ "ws: curl_ws_send returned %u, sent %u\n", (int)result, (int)sent);
+}
+
+static CURLcode data_echo(CURL *curl, size_t plen_min, size_t plen_max)
+{
+ CURLcode res;
+ size_t len;
+ char *send_buf;
+ size_t i;
+
+ send_buf = calloc(1, plen_max);
+ if(!send_buf)
+ return CURLE_OUT_OF_MEMORY;
+ for(i = 0; i < plen_max; ++i) {
+ send_buf[i] = (char)('0' + ((int)i % 10));
+ }
+
+ for(len = plen_min; len <= plen_max; ++len) {
+ res = send_binary(curl, send_buf, len);
+ if(res)
+ goto out;
+ res = recv_binary(curl, send_buf, len);
+ if(res) {
+ fprintf(stderr, "recv_data(len=%ld) -> %d\n", (long)len, res);
+ goto out;
+ }
+ }
+
+out:
+ if(!res)
+ websocket_close(curl);
+ free(send_buf);
+ return res;
+}
+
+#endif
+
+int main(int argc, char *argv[])
+{
+#ifdef USE_WEBSOCKETS
+ CURL *curl;
+ CURLcode res = CURLE_OK;
+ const char *url;
+ curl_off_t l1, l2;
+ size_t plen_min, plen_max;
+
+
+ if(argc != 4) {
+ fprintf(stderr, "usage: ws-data url minlen maxlen\n");
+ return 2;
+ }
+ url = argv[1];
+ l1 = strtol(argv[2], NULL, 10);
+ if(l1 < 0) {
+ fprintf(stderr, "minlen must be >= 0, got %ld\n", (long)l1);
+ return 2;
+ }
+ l2 = strtol(argv[3], NULL, 10);
+ if(l2 < 0) {
+ fprintf(stderr, "maxlen must be >= 0, got %ld\n", (long)l2);
+ return 2;
+ }
+ plen_min = l1;
+ plen_max = l2;
+ if(plen_max < plen_min) {
+ fprintf(stderr, "maxlen must be >= minlen, got %ld-%ld\n",
+ (long)plen_min, (long)plen_max);
+ return 2;
+ }
+
+ curl_global_init(CURL_GLOBAL_ALL);
+
+ curl = curl_easy_init();
+ if(curl) {
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+
+ /* use the callback style */
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, "ws-data");
+ curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
+ curl_easy_setopt(curl, CURLOPT_CONNECT_ONLY, 2L); /* websocket style */
+ res = curl_easy_perform(curl);
+ fprintf(stderr, "curl_easy_perform() returned %u\n", (int)res);
+ if(res == CURLE_OK)
+ res = data_echo(curl, plen_min, plen_max);
+
+ /* always cleanup */
+ curl_easy_cleanup(curl);
+ }
+ curl_global_cleanup();
+ return (int)res;
+
+#else /* USE_WEBSOCKETS */
+ (void)argc;
+ (void)argv;
+ fprintf(stderr, "websockets not enabled in libcurl\n");
+ return 1;
+#endif /* !USE_WEBSOCKETS */
+}
diff --git a/tests/http/clients/ws-pingpong.c b/tests/http/clients/ws-pingpong.c
new file mode 100644
index 000000000..4307dea73
--- /dev/null
+++ b/tests/http/clients/ws-pingpong.c
@@ -0,0 +1,158 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution. The terms
+ * are also available at https://curl.se/docs/copyright.html.
+ *
+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+ * copies of the Software, and permit persons to whom the Software is
+ * furnished to do so, under the terms of the COPYING file.
+ *
+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ * KIND, either express or implied.
+ *
+ * SPDX-License-Identifier: curl
+ *
+ ***************************************************************************/
+/* <DESC>
+ * Websockets pingpong
+ * </DESC>
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* somewhat unix-specific */
+#include <sys/time.h>
+#include <unistd.h>
+
+
+/* curl stuff */
+#include <curl/curl.h>
+#include "../../../lib/curl_setup.h"
+
+#ifdef USE_WEBSOCKETS
+
+static CURLcode ping(CURL *curl, const char *send_payload)
+{
+ size_t sent;
+ CURLcode result =
+ curl_ws_send(curl, send_payload, strlen(send_payload), &sent, 0,
+ CURLWS_PING);
+ fprintf(stderr,
+ "ws: curl_ws_send returned %u, sent %u\n", (int)result, (int)sent);
+
+ return result;
+}
+
+static CURLcode recv_pong(CURL *curl, const char *exected_payload)
+{
+ size_t rlen;
+ struct curl_ws_frame *meta;
+ char buffer[256];
+ CURLcode result = curl_ws_recv(curl, buffer, sizeof(buffer), &rlen, &meta);
+ if(result) {
+ fprintf(stderr, "ws: curl_ws_recv returned %u, received %ld\n",
+ (int)result, (long)rlen);
+ return result;
+ }
+
+ if(!(meta->flags & CURLWS_PONG)) {
+ fprintf(stderr, "recv_pong: wrong frame, got %d bytes rflags %x\n",
+ (int)rlen, meta->flags);
+ return CURLE_RECV_ERROR;
+ }
+
+ fprintf(stderr, "ws: got PONG back\n");
+ if(rlen == strlen(exected_payload) &&
+ !memcmp(exected_payload, buffer, rlen)) {
+ fprintf(stderr, "ws: got the same payload back\n");
+ return CURLE_OK;
+ }
+ fprintf(stderr, "ws: did NOT get the same payload back\n");
+ return CURLE_RECV_ERROR;
+}
+
+/* just close the connection */
+static void websocket_close(CURL *curl)
+{
+ size_t sent;
+ CURLcode result =
+ curl_ws_send(curl, "", 0, &sent, 0, CURLWS_CLOSE);
+ fprintf(stderr,
+ "ws: curl_ws_send returned %u, sent %u\n", (int)result, (int)sent);
+}
+
+static CURLcode pingpong(CURL *curl, const char *payload)
+{
+ CURLcode res;
+ int i;
+
+ res = ping(curl, payload);
+ if(res)
+ return res;
+ for(i = 0; i < 10; ++i) {
+ fprintf(stderr, "Receive pong\n");
+ res = recv_pong(curl, payload);
+ if(res == CURLE_AGAIN) {
+ usleep(100*1000);
+ continue;
+ }
+ websocket_close(curl);
+ return res;
+ }
+ websocket_close(curl);
+ return CURLE_RECV_ERROR;
+}
+
+#endif
+
+int main(int argc, char *argv[])
+{
+#ifdef USE_WEBSOCKETS
+ CURL *curl;
+ CURLcode res = CURLE_OK;
+ const char *url, *payload;
+
+ if(argc != 3) {
+ fprintf(stderr, "usage: ws-pingpong url payload\n");
+ return 2;
+ }
+ url = argv[1];
+ payload = argv[2];
+
+ curl_global_init(CURL_GLOBAL_ALL);
+
+ curl = curl_easy_init();
+ if(curl) {
+ curl_easy_setopt(curl, CURLOPT_URL, url);
+
+ /* use the callback style */
+ curl_easy_setopt(curl, CURLOPT_USERAGENT, "ws-pingpong");
+ curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
+ curl_easy_setopt(curl, CURLOPT_CONNECT_ONLY, 2L); /* websocket style */
+ res = curl_easy_perform(curl);
+ fprintf(stderr, "curl_easy_perform() returned %u\n", (int)res);
+ if(res == CURLE_OK)
+ res = pingpong(curl, payload);
+
+ /* always cleanup */
+ curl_easy_cleanup(curl);
+ }
+ curl_global_cleanup();
+ return (int)res;
+
+#else /* USE_WEBSOCKETS */
+ (void)argc;
+ (void)argv;
+ fprintf(stderr, "websockets not enabled in libcurl\n");
+ return 1;
+#endif /* !USE_WEBSOCKETS */
+}
diff --git a/tests/http/conftest.py b/tests/http/conftest.py
index bd97baa8b..09e52e63a 100644
--- a/tests/http/conftest.py
+++ b/tests/http/conftest.py
@@ -46,6 +46,8 @@ def env(pytestconfig) -> Env:
pytest.skip(env.incomplete_reason())
env.setup()
+ if not env.make_clients():
+ pytest.exit(1)
return env
@pytest.fixture(scope="package", autouse=True)
diff --git a/tests/http/requirements.txt b/tests/http/requirements.txt
new file mode 100644
index 000000000..efc3de73c
--- /dev/null
+++ b/tests/http/requirements.txt
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+pytest
+cryptography
+multipart
+websockets \ No newline at end of file
diff --git a/tests/http/test_02_download.py b/tests/http/test_02_download.py
index f718c8b98..bd99d2a20 100644
--- a/tests/http/test_02_download.py
+++ b/tests/http/test_02_download.py
@@ -30,7 +30,7 @@ import logging
import os
import pytest
-from testenv import Env, CurlClient
+from testenv import Env, CurlClient, LocalClient
log = logging.getLogger(__name__)
@@ -47,9 +47,12 @@ class TestDownload:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env, httpd):
- env.make_data_file(indir=httpd.docs_dir, fname="data-100k", fsize=100*1024)
- env.make_data_file(indir=httpd.docs_dir, fname="data-1m", fsize=1024*1024)
- env.make_data_file(indir=httpd.docs_dir, fname="data-10m", fsize=10*1024*1024)
+ indir = httpd.docs_dir
+ env.make_data_file(indir=indir, fname="data-10k", fsize=10*1024)
+ env.make_data_file(indir=indir, fname="data-100k", fsize=100*1024)
+ env.make_data_file(indir=indir, fname="data-1m", fsize=1024*1024)
+ env.make_data_file(indir=indir, fname="data-10m", fsize=10*1024*1024)
+ env.make_data_file(indir=indir, fname="data-50m", fsize=50*1024*1024)
# download 1 file
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
@@ -272,8 +275,29 @@ class TestDownload:
])
r.check_response(count=count, http_status=200)
srcfile = os.path.join(httpd.docs_dir, 'data-1m')
+ self.check_downloads(curl, srcfile, count)
+ # restore httpd defaults
+ httpd.set_extra_config(env.domain1, lines=None)
+ assert httpd.stop()
+ assert httpd.start()
+
+ # download via lib client, pause/resume at different offsets
+ @pytest.mark.parametrize("pause_offset", [0, 10*1024, 100*1023, 640000])
+ def test_02_21_h2_lib_download(self, env: Env, httpd, nghttpx, pause_offset, repeat):
+ count = 10
+ docname = 'data-10m'
+ url = f'https://localhost:{env.https_port}/{docname}'
+ client = LocalClient(name='h2-download', env=env)
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ r = client.run(args=[str(count), str(pause_offset), url])
+ r.check_exit_code(0)
+ srcfile = os.path.join(httpd.docs_dir, docname)
+ self.check_downloads(client, srcfile, count)
+
+ def check_downloads(self, client, srcfile: str, count: int):
for i in range(count):
- dfile = curl.download_file(i)
+ dfile = client.download_file(i)
assert os.path.exists(dfile)
if not filecmp.cmp(srcfile, dfile, shallow=False):
diff = "".join(difflib.unified_diff(a=open(srcfile).readlines(),
@@ -282,8 +306,3 @@ class TestDownload:
tofile=dfile,
n=1))
assert False, f'download {dfile} differs:\n{diff}'
- # restore httpd defaults
- httpd.set_extra_config(env.domain1, lines=None)
- assert httpd.stop()
- assert httpd.start()
-
diff --git a/tests/http/test_09_push.py b/tests/http/test_09_push.py
index be1b73a25..92d4a5990 100644
--- a/tests/http/test_09_push.py
+++ b/tests/http/test_09_push.py
@@ -28,7 +28,7 @@ import logging
import os
import pytest
-from testenv import Env, CurlClient
+from testenv import Env, CurlClient, LocalClient
log = logging.getLogger(__name__)
@@ -41,9 +41,9 @@ class TestPush:
push_dir = os.path.join(httpd.docs_dir, 'push')
if not os.path.exists(push_dir):
os.makedirs(push_dir)
- env.make_data_file(indir=push_dir, fname="data1", fsize=100*1024)
- env.make_data_file(indir=push_dir, fname="data2", fsize=100*1024)
- env.make_data_file(indir=push_dir, fname="data3", fsize=100*1024)
+ env.make_data_file(indir=push_dir, fname="data1", fsize=1*1024)
+ env.make_data_file(indir=push_dir, fname="data2", fsize=1*1024)
+ env.make_data_file(indir=push_dir, fname="data3", fsize=1*1024)
httpd.set_extra_config(env.domain1, [
f'H2EarlyHints on',
f'<Location /push/data1>',
@@ -61,7 +61,7 @@ class TestPush:
httpd.reload()
# download a file that triggers a "103 Early Hints" response
- def test_09_01_early_hints(self, env: Env, httpd, repeat):
+ def test_09_01_h2_early_hints(self, env: Env, httpd, repeat):
curl = CurlClient(env=env)
url = f'https://{env.domain1}:{env.https_port}/push/data1'
r = curl.http_download(urls=[url], alpn_proto='h2', with_stats=False,
@@ -71,3 +71,14 @@ class TestPush:
assert r.responses[0]['status'] == 103, f'{r.responses}'
assert 'link' in r.responses[0]['header'], f'{r.responses[0]}'
assert r.responses[0]['header']['link'] == '</push/data2>; rel=preload', f'{r.responses[0]}'
+
+ def test_09_02_h2_push(self, env: Env, httpd, repeat):
+ # use localhost as we do not have resolve support in local client
+ url = f'https://localhost:{env.https_port}/push/data1'
+ client = LocalClient(name='h2-serverpush', env=env)
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ r = client.run(args=[url])
+ r.check_exit_code(0)
+ assert os.path.exists(client.download_file(0))
+ assert os.path.exists(os.path.join(client.run_dir, 'push0')), r.dump_logs()
diff --git a/tests/http/test_20_websockets.py b/tests/http/test_20_websockets.py
new file mode 100644
index 000000000..4e70dcef0
--- /dev/null
+++ b/tests/http/test_20_websockets.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+import logging
+import os
+import shutil
+import subprocess
+import time
+from datetime import datetime, timedelta
+import pytest
+
+from testenv import Env, CurlClient, LocalClient
+
+
+log = logging.getLogger(__name__)
+
+
+@pytest.mark.skipif(condition=not Env.curl_has_protocol('ws'),
+ reason='curl lacks ws protocol support')
+class TestWebsockets:
+
+ def check_alive(self, env, timeout=5):
+ curl = CurlClient(env=env)
+ url = f'http://localhost:{env.ws_port}/'
+ end = datetime.now() + timedelta(seconds=timeout)
+ while datetime.now() < end:
+ r = curl.http_download(urls=[url])
+ if r.exit_code == 0:
+ return True
+ time.sleep(.1)
+ return False
+
+ def _mkpath(self, path):
+ if not os.path.exists(path):
+ return os.makedirs(path)
+
+ def _rmrf(self, path):
+ if os.path.exists(path):
+ return shutil.rmtree(path)
+
+ @pytest.fixture(autouse=True, scope='class')
+ def ws_echo(self, env):
+ run_dir = os.path.join(env.gen_dir, 'ws-echo-server')
+ err_file = os.path.join(run_dir, 'stderr')
+ self._rmrf(run_dir)
+ self._mkpath(run_dir)
+
+ with open(err_file, 'w') as cerr:
+ cmd = os.path.join(env.project_dir,
+ 'tests/http/testenv/ws_echo_server.py')
+ args = [cmd, '--port', str(env.ws_port)]
+ p = subprocess.Popen(args=args, cwd=run_dir, stderr=cerr,
+ stdout=cerr)
+ assert self.check_alive(env)
+ yield
+ p.terminate()
+
+ def test_20_01_basic(self, env: Env, ws_echo, repeat):
+ curl = CurlClient(env=env)
+ url = f'http://localhost:{env.ws_port}/'
+ r = curl.http_download(urls=[url])
+ r.check_response(http_status=426)
+
+ def test_20_02_pingpong_small(self, env: Env, ws_echo, repeat):
+ payload = 125 * "x"
+ client = LocalClient(env=env, name='ws-pingpong')
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ url = f'ws://localhost:{env.ws_port}/'
+ r = client.run(args=[url, payload])
+ r.check_exit_code(0)
+
+ # the python websocket server does not like 'large' control frames
+ def test_20_03_pingpong_too_large(self, env: Env, ws_echo, repeat):
+ payload = 127 * "x"
+ client = LocalClient(env=env, name='ws-pingpong')
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ url = f'ws://localhost:{env.ws_port}/'
+ r = client.run(args=[url, payload])
+ r.check_exit_code(56)
+
+ # the python websocket server does not like 'large' control frames
+ def test_20_04_data_small(self, env: Env, ws_echo, repeat):
+ client = LocalClient(env=env, name='ws-data')
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ url = f'ws://localhost:{env.ws_port}/'
+ r = client.run(args=[url, str(0), str(10)])
+ r.check_exit_code(0)
+
+ # the python websocket server does not like 'large' control frames
+ def test_20_05_data_med(self, env: Env, ws_echo, repeat):
+ client = LocalClient(env=env, name='ws-data')
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ url = f'ws://localhost:{env.ws_port}/'
+ r = client.run(args=[url, str(120), str(130)])
+ r.check_exit_code(0)
+
+ # the python websocket server does not like 'large' control frames
+ def test_20_06_data_large(self, env: Env, ws_echo, repeat):
+ client = LocalClient(env=env, name='ws-data')
+ if not client.exists():
+ pytest.skip(f'example client not built: {client.name}')
+ url = f'ws://localhost:{env.ws_port}/'
+ r = client.run(args=[url, str(65535 - 5), str(65535 + 5)])
+ r.check_exit_code(0)
diff --git a/tests/http/testenv/__init__.py b/tests/http/testenv/__init__.py
index 3da668570..2195634fd 100644
--- a/tests/http/testenv/__init__.py
+++ b/tests/http/testenv/__init__.py
@@ -33,5 +33,6 @@ from .certs import TestCA, Credentials
from .caddy import Caddy
from .httpd import Httpd
from .curl import CurlClient, ExecResult
+from .client import LocalClient
from .nghttpx import Nghttpx
from .nghttpx import Nghttpx, NghttpxQuic, NghttpxFwd
diff --git a/tests/http/testenv/client.py b/tests/http/testenv/client.py
new file mode 100644
index 000000000..3b7ea0fc6
--- /dev/null
+++ b/tests/http/testenv/client.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+import pytest
+import json
+import logging
+import os
+import re
+import shutil
+import subprocess
+from datetime import timedelta, datetime
+from typing import List, Optional, Dict, Union
+from urllib.parse import urlparse
+
+from . import ExecResult
+from .env import Env
+
+
+log = logging.getLogger(__name__)
+
+
+class LocalClient:
+
+ def __init__(self, name: str, env: Env, run_dir: Optional[str] = None,
+ timeout: Optional[float] = None):
+ self.name = name
+ self.path = os.path.join(env.project_dir, f'tests/http/clients/{name}')
+ self.env = env
+ self._timeout = timeout if timeout else env.test_timeout
+ self._curl = os.environ['CURL'] if 'CURL' in os.environ else env.curl
+ self._run_dir = run_dir if run_dir else os.path.join(env.gen_dir, name)
+ self._stdoutfile = f'{self._run_dir}/stdout'
+ self._stderrfile = f'{self._run_dir}/stderr'
+ self._rmrf(self._run_dir)
+ self._mkpath(self._run_dir)
+
+ @property
+ def run_dir(self) -> str:
+ return self._run_dir
+
+ def exists(self) -> bool:
+ return os.path.exists(self.path)
+
+ def download_file(self, i: int) -> str:
+ return os.path.join(self._run_dir, f'download_{i}.data')
+
+ def _rmf(self, path):
+ if os.path.exists(path):
+ return os.remove(path)
+
+ def _rmrf(self, path):
+ if os.path.exists(path):
+ return shutil.rmtree(path)
+
+ def _mkpath(self, path):
+ if not os.path.exists(path):
+ return os.makedirs(path)
+
+ def run(self, args):
+ self._rmf(self._stdoutfile)
+ self._rmf(self._stderrfile)
+ start = datetime.now()
+ exception = None
+ myargs = [self.path]
+ myargs.extend(args)
+ try:
+ with open(self._stdoutfile, 'w') as cout:
+ with open(self._stderrfile, 'w') as cerr:
+ p = subprocess.run(myargs, stderr=cerr, stdout=cout,
+ cwd=self._run_dir, shell=False,
+ input=None,
+ timeout=self._timeout)
+ exitcode = p.returncode
+ except subprocess.TimeoutExpired:
+ log.warning(f'Timeout after {self._timeout}s: {args}')
+ exitcode = -1
+ exception = 'TimeoutExpired'
+ coutput = open(self._stdoutfile).readlines()
+ cerrput = open(self._stderrfile).readlines()
+ return ExecResult(args=myargs, exit_code=exitcode, exception=exception,
+ stdout=coutput, stderr=cerrput,
+ duration=datetime.now() - start)
diff --git a/tests/http/testenv/env.py b/tests/http/testenv/env.py
index 1e175e4a9..b2ed4e60b 100644
--- a/tests/http/testenv/env.py
+++ b/tests/http/testenv/env.py
@@ -33,6 +33,8 @@ import sys
from configparser import ConfigParser, ExtendedInterpolation
from typing import Optional
+import pytest
+
from .certs import CertificateSpec, TestCA, Credentials
from .ports import alloc_ports
@@ -60,6 +62,7 @@ class EnvConfig:
def __init__(self):
self.tests_dir = TESTS_HTTPD_PATH
self.gen_dir = os.path.join(self.tests_dir, 'gen')
+ self.project_dir = os.path.dirname(os.path.dirname(self.tests_dir))
self.config = DEF_CONFIG
# check cur and its features
self.curl = CURL
@@ -109,6 +112,7 @@ class EnvConfig:
'h2proxys': socket.SOCK_STREAM,
'caddy': socket.SOCK_STREAM,
'caddys': socket.SOCK_STREAM,
+ 'ws': socket.SOCK_STREAM,
})
self.httpd = self.config['httpd']['httpd']
self.apachectl = self.config['httpd']['apachectl']
@@ -348,6 +352,10 @@ class Env:
return self.CONFIG.gen_dir
@property
+ def project_dir(self) -> str:
+ return self.CONFIG.project_dir
+
+ @property
def ca(self):
return self._ca
@@ -408,6 +416,10 @@ class Env:
return self.CONFIG.ports['caddy']
@property
+ def ws_port(self) -> int:
+ return self.CONFIG.ports['ws']
+
+ @property
def curl(self) -> str:
return self.CONFIG.curl
@@ -448,3 +460,13 @@ class Env:
s = f"{i:09d}-{s}\n"
fd.write(s[0:remain])
return fpath
+
+ def make_clients(self):
+ client_dir = os.path.join(self.project_dir, 'tests/http/clients')
+ p = subprocess.run(['make'], capture_output=True, text=True,
+ cwd=client_dir)
+ if p.returncode != 0:
+ pytest.exit(f"`make`in {client_dir} failed:\n{p.stderr}")
+ return False
+ return True
+
diff --git a/tests/http/testenv/httpd.py b/tests/http/testenv/httpd.py
index 612da1006..d3b2de4cd 100644
--- a/tests/http/testenv/httpd.py
+++ b/tests/http/testenv/httpd.py
@@ -260,6 +260,7 @@ class Httpd:
conf.extend([ # https host for domain1, h1 + h2
f'<VirtualHost *:{self.env.https_port}>',
f' ServerName {domain1}',
+ f' ServerAlias localhost',
f' Protocols h2 http/1.1',
f' SSLEngine on',
f' SSLCertificateFile {creds1.cert_file}',
diff --git a/tests/http/testenv/ws_echo_server.py b/tests/http/testenv/ws_echo_server.py
new file mode 100755
index 000000000..d13f757c7
--- /dev/null
+++ b/tests/http/testenv/ws_echo_server.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+import argparse
+import asyncio
+import logging
+from asyncio import IncompleteReadError
+
+from websockets import server
+from websockets.exceptions import ConnectionClosedError
+
+
+async def echo(websocket):
+ try:
+ async for message in websocket:
+ await websocket.send(message)
+ except ConnectionClosedError:
+ pass
+
+
+async def run_server(port):
+ async with server.serve(echo, "localhost", port):
+ await asyncio.Future() # run forever
+
+
+def main():
+ parser = argparse.ArgumentParser(prog='scorecard', description="""
+ Run a websocket echo server.
+ """)
+ parser.add_argument("--port", type=int,
+ default=9876, help="port to listen on")
+ args = parser.parse_args()
+
+ logging.basicConfig(
+ format="%(asctime)s %(message)s",
+ level=logging.DEBUG,
+ )
+
+ asyncio.run(run_server(args.port))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/requirements.txt b/tests/requirements.txt
new file mode 100644
index 000000000..706043ac3
--- /dev/null
+++ b/tests/requirements.txt
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+# SPDX-License-Identifier: curl
+#
+###########################################################################
+#
+impacket