summaryrefslogtreecommitdiff
path: root/tests
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 /tests
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
Diffstat (limited to 'tests')
-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
20 files changed, 1515 insertions, 15 deletions
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