diff options
author | Stefan Eissing <stefan@eissing.org> | 2023-04-26 12:38:22 +0200 |
---|---|---|
committer | Daniel Stenberg <daniel@haxx.se> | 2023-04-26 23:24:46 +0200 |
commit | acd82c8bfd743d0f743a1c1296890738832ac83e (patch) | |
tree | fc4da775326efd7562f6217f41929b703902431c /tests | |
parent | 21575b26fe33099e087b8beaac9bc43fa8597874 (diff) | |
download | curl-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.am | 1 | ||||
-rw-r--r-- | tests/http/Makefile.am | 10 | ||||
-rw-r--r-- | tests/http/clients/.gitignore | 8 | ||||
-rw-r--r-- | tests/http/clients/Makefile.am | 71 | ||||
-rw-r--r-- | tests/http/clients/Makefile.inc | 30 | ||||
-rw-r--r-- | tests/http/clients/h2-download.c | 275 | ||||
-rw-r--r-- | tests/http/clients/h2-serverpush.c | 271 | ||||
-rw-r--r-- | tests/http/clients/ws-data.c | 263 | ||||
-rw-r--r-- | tests/http/clients/ws-pingpong.c | 158 | ||||
-rw-r--r-- | tests/http/conftest.py | 2 | ||||
-rw-r--r-- | tests/http/requirements.txt | 29 | ||||
-rw-r--r-- | tests/http/test_02_download.py | 39 | ||||
-rw-r--r-- | tests/http/test_09_push.py | 21 | ||||
-rw-r--r-- | tests/http/test_20_websockets.py | 131 | ||||
-rw-r--r-- | tests/http/testenv/__init__.py | 1 | ||||
-rw-r--r-- | tests/http/testenv/client.py | 105 | ||||
-rw-r--r-- | tests/http/testenv/env.py | 22 | ||||
-rw-r--r-- | tests/http/testenv/httpd.py | 1 | ||||
-rwxr-xr-x | tests/http/testenv/ws_echo_server.py | 66 | ||||
-rw-r--r-- | tests/requirements.txt | 26 |
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 |