diff options
Diffstat (limited to '')
28 files changed, 10977 insertions, 0 deletions
diff --git a/src/lib/http/tests/Makefile.am b/src/lib/http/tests/Makefile.am new file mode 100644 index 0000000..5b2b2a0 --- /dev/null +++ b/src/lib/http/tests/Makefile.am @@ -0,0 +1,76 @@ +SUBDIRS = . + +# Add to the tarball: +EXTRA_DIST = testdata/empty +EXTRA_DIST += testdata/hiddenp +EXTRA_DIST += testdata/hiddens +EXTRA_DIST += testdata/hiddenu + +AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) +TEST_CA_DIR = $(srcdir)/../../asiolink/testutils/ca +AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(TEST_CA_DIR)\" +AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" +AM_CPPFLAGS += -DDATA_DIR=\"$(abs_srcdir)/testdata\" + +AM_CXXFLAGS = $(KEA_CXXFLAGS) + +if USE_STATIC_LINK +AM_LDFLAGS = -static +endif + +CLEANFILES = *.gcno *.gcda + +TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) + +TESTS = +if HAVE_GTEST +TESTS += libhttp_unittests + +libhttp_unittests_SOURCES = basic_auth_unittests.cc +libhttp_unittests_SOURCES += basic_auth_config_unittests.cc +libhttp_unittests_SOURCES += connection_pool_unittests.cc +libhttp_unittests_SOURCES += date_time_unittests.cc +libhttp_unittests_SOURCES += http_header_unittests.cc +libhttp_unittests_SOURCES += post_request_unittests.cc +libhttp_unittests_SOURCES += post_request_json_unittests.cc +libhttp_unittests_SOURCES += request_parser_unittests.cc +libhttp_unittests_SOURCES += request_test.h +libhttp_unittests_SOURCES += response_creator_unittests.cc +libhttp_unittests_SOURCES += response_parser_unittests.cc +libhttp_unittests_SOURCES += response_test.h +libhttp_unittests_SOURCES += request_unittests.cc +libhttp_unittests_SOURCES += response_unittests.cc +libhttp_unittests_SOURCES += response_json_unittests.cc +libhttp_unittests_SOURCES += run_unittests.cc +libhttp_unittests_SOURCES += server_client_unittests.cc +if HAVE_OPENSSL +libhttp_unittests_SOURCES += tls_server_unittests.cc +libhttp_unittests_SOURCES += tls_client_unittests.cc +endif +if HAVE_BOTAN_BOOST +libhttp_unittests_SOURCES += tls_server_unittests.cc +libhttp_unittests_SOURCES += tls_client_unittests.cc +endif +libhttp_unittests_SOURCES += url_unittests.cc +libhttp_unittests_SOURCES += test_http_client.h +libhttp_unittests_SOURCES += client_mt_unittests.cc + +libhttp_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) +libhttp_unittests_CXXFLAGS = $(AM_CXXFLAGS) +libhttp_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) + +libhttp_unittests_LDADD = $(top_builddir)/src/lib/http/libkea-http.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/hooks/libkea-hooks.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/testutils/libkea-testutils.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/asiolink/testutils/libasiolinktest.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la +libhttp_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +libhttp_unittests_LDADD += $(LOG4CPLUS_LIBS) +libhttp_unittests_LDADD += $(BOOST_LIBS) $(CRYPTO_LIBS) $(GTEST_LDADD) +endif + +noinst_PROGRAMS = $(TESTS) diff --git a/src/lib/http/tests/Makefile.in b/src/lib/http/tests/Makefile.in new file mode 100644 index 0000000..5c5bc7a --- /dev/null +++ b/src/lib/http/tests/Makefile.in @@ -0,0 +1,1392 @@ +# Makefile.in generated by automake 1.16.1 from Makefile.am. +# @configure_input@ + +# Copyright (C) 1994-2018 Free Software Foundation, Inc. + +# This Makefile.in is free software; the Free Software Foundation +# gives unlimited permission to copy and/or distribute it, +# with or without modifications, as long as this notice is preserved. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY, to the extent permitted by law; without +# even the implied warranty of MERCHANTABILITY or FITNESS FOR A +# PARTICULAR PURPOSE. + +@SET_MAKE@ + +VPATH = @srcdir@ +am__is_gnu_make = { \ + if test -z '$(MAKELEVEL)'; then \ + false; \ + elif test -n '$(MAKE_HOST)'; then \ + true; \ + elif test -n '$(MAKE_VERSION)' && test -n '$(CURDIR)'; then \ + true; \ + else \ + false; \ + fi; \ +} +am__make_running_with_option = \ + case $${target_option-} in \ + ?) ;; \ + *) echo "am__make_running_with_option: internal error: invalid" \ + "target option '$${target_option-}' specified" >&2; \ + exit 1;; \ + esac; \ + has_opt=no; \ + sane_makeflags=$$MAKEFLAGS; \ + if $(am__is_gnu_make); then \ + sane_makeflags=$$MFLAGS; \ + else \ + case $$MAKEFLAGS in \ + *\\[\ \ ]*) \ + bs=\\; \ + sane_makeflags=`printf '%s\n' "$$MAKEFLAGS" \ + | sed "s/$$bs$$bs[$$bs $$bs ]*//g"`;; \ + esac; \ + fi; \ + skip_next=no; \ + strip_trailopt () \ + { \ + flg=`printf '%s\n' "$$flg" | sed "s/$$1.*$$//"`; \ + }; \ + for flg in $$sane_makeflags; do \ + test $$skip_next = yes && { skip_next=no; continue; }; \ + case $$flg in \ + *=*|--*) continue;; \ + -*I) strip_trailopt 'I'; skip_next=yes;; \ + -*I?*) strip_trailopt 'I';; \ + -*O) strip_trailopt 'O'; skip_next=yes;; \ + -*O?*) strip_trailopt 'O';; \ + -*l) strip_trailopt 'l'; skip_next=yes;; \ + -*l?*) strip_trailopt 'l';; \ + -[dEDm]) skip_next=yes;; \ + -[JT]) skip_next=yes;; \ + esac; \ + case $$flg in \ + *$$target_option*) has_opt=yes; break;; \ + esac; \ + done; \ + test $$has_opt = yes +am__make_dryrun = (target_option=n; $(am__make_running_with_option)) +am__make_keepgoing = (target_option=k; $(am__make_running_with_option)) +pkgdatadir = $(datadir)/@PACKAGE@ +pkgincludedir = $(includedir)/@PACKAGE@ +pkglibdir = $(libdir)/@PACKAGE@ +pkglibexecdir = $(libexecdir)/@PACKAGE@ +am__cd = CDPATH="$${ZSH_VERSION+.}$(PATH_SEPARATOR)" && cd +install_sh_DATA = $(install_sh) -c -m 644 +install_sh_PROGRAM = $(install_sh) -c +install_sh_SCRIPT = $(install_sh) -c +INSTALL_HEADER = $(INSTALL_DATA) +transform = $(program_transform_name) +NORMAL_INSTALL = : +PRE_INSTALL = : +POST_INSTALL = : +NORMAL_UNINSTALL = : +PRE_UNINSTALL = : +POST_UNINSTALL = : +build_triplet = @build@ +host_triplet = @host@ +TESTS = $(am__EXEEXT_1) +@HAVE_GTEST_TRUE@am__append_1 = libhttp_unittests +@HAVE_GTEST_TRUE@@HAVE_OPENSSL_TRUE@am__append_2 = \ +@HAVE_GTEST_TRUE@@HAVE_OPENSSL_TRUE@ tls_server_unittests.cc \ +@HAVE_GTEST_TRUE@@HAVE_OPENSSL_TRUE@ tls_client_unittests.cc +@HAVE_BOTAN_BOOST_TRUE@@HAVE_GTEST_TRUE@am__append_3 = tls_server_unittests.cc \ +@HAVE_BOTAN_BOOST_TRUE@@HAVE_GTEST_TRUE@ tls_client_unittests.cc +noinst_PROGRAMS = $(am__EXEEXT_2) +subdir = src/lib/http/tests +ACLOCAL_M4 = $(top_srcdir)/aclocal.m4 +am__aclocal_m4_deps = $(top_srcdir)/m4macros/ax_boost_for_kea.m4 \ + $(top_srcdir)/m4macros/ax_cpp11.m4 \ + $(top_srcdir)/m4macros/ax_cpp20.m4 \ + $(top_srcdir)/m4macros/ax_crypto.m4 \ + $(top_srcdir)/m4macros/ax_find_library.m4 \ + $(top_srcdir)/m4macros/ax_gssapi.m4 \ + $(top_srcdir)/m4macros/ax_gtest.m4 \ + $(top_srcdir)/m4macros/ax_isc_rpath.m4 \ + $(top_srcdir)/m4macros/ax_netconf.m4 \ + $(top_srcdir)/m4macros/libtool.m4 \ + $(top_srcdir)/m4macros/ltoptions.m4 \ + $(top_srcdir)/m4macros/ltsugar.m4 \ + $(top_srcdir)/m4macros/ltversion.m4 \ + $(top_srcdir)/m4macros/lt~obsolete.m4 \ + $(top_srcdir)/configure.ac +am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \ + $(ACLOCAL_M4) +DIST_COMMON = $(srcdir)/Makefile.am $(am__DIST_COMMON) +mkinstalldirs = $(install_sh) -d +CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +@HAVE_GTEST_TRUE@am__EXEEXT_1 = libhttp_unittests$(EXEEXT) +am__EXEEXT_2 = $(am__EXEEXT_1) +PROGRAMS = $(noinst_PROGRAMS) +am__libhttp_unittests_SOURCES_DIST = basic_auth_unittests.cc \ + basic_auth_config_unittests.cc connection_pool_unittests.cc \ + date_time_unittests.cc http_header_unittests.cc \ + post_request_unittests.cc post_request_json_unittests.cc \ + request_parser_unittests.cc request_test.h \ + response_creator_unittests.cc response_parser_unittests.cc \ + response_test.h request_unittests.cc response_unittests.cc \ + response_json_unittests.cc run_unittests.cc \ + server_client_unittests.cc tls_server_unittests.cc \ + tls_client_unittests.cc url_unittests.cc test_http_client.h \ + client_mt_unittests.cc +@HAVE_GTEST_TRUE@@HAVE_OPENSSL_TRUE@am__objects_1 = libhttp_unittests-tls_server_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@@HAVE_OPENSSL_TRUE@ libhttp_unittests-tls_client_unittests.$(OBJEXT) +@HAVE_BOTAN_BOOST_TRUE@@HAVE_GTEST_TRUE@am__objects_2 = libhttp_unittests-tls_server_unittests.$(OBJEXT) \ +@HAVE_BOTAN_BOOST_TRUE@@HAVE_GTEST_TRUE@ libhttp_unittests-tls_client_unittests.$(OBJEXT) +@HAVE_GTEST_TRUE@am_libhttp_unittests_OBJECTS = libhttp_unittests-basic_auth_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-basic_auth_config_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-connection_pool_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-date_time_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-http_header_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-post_request_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-post_request_json_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-request_parser_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-response_creator_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-response_parser_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-request_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-response_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-response_json_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-run_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-server_client_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ $(am__objects_1) $(am__objects_2) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-url_unittests.$(OBJEXT) \ +@HAVE_GTEST_TRUE@ libhttp_unittests-client_mt_unittests.$(OBJEXT) +libhttp_unittests_OBJECTS = $(am_libhttp_unittests_OBJECTS) +am__DEPENDENCIES_1 = +@HAVE_GTEST_TRUE@libhttp_unittests_DEPENDENCIES = \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/http/libkea-http.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/hooks/libkea-hooks.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/testutils/libkea-testutils.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cc/libkea-cc.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/asiolink/testutils/libasiolinktest.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/asiolink/libkea-asiolink.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/log/libkea-log.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/util/libkea-util.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/exceptions/libkea-exceptions.la \ +@HAVE_GTEST_TRUE@ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \ +@HAVE_GTEST_TRUE@ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) +AM_V_lt = $(am__v_lt_@AM_V@) +am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@) +am__v_lt_0 = --silent +am__v_lt_1 = +libhttp_unittests_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \ + $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) \ + $(libhttp_unittests_LDFLAGS) $(LDFLAGS) -o $@ +AM_V_P = $(am__v_P_@AM_V@) +am__v_P_ = $(am__v_P_@AM_DEFAULT_V@) +am__v_P_0 = false +am__v_P_1 = : +AM_V_GEN = $(am__v_GEN_@AM_V@) +am__v_GEN_ = $(am__v_GEN_@AM_DEFAULT_V@) +am__v_GEN_0 = @echo " GEN " $@; +am__v_GEN_1 = +AM_V_at = $(am__v_at_@AM_V@) +am__v_at_ = $(am__v_at_@AM_DEFAULT_V@) +am__v_at_0 = @ +am__v_at_1 = +DEFAULT_INCLUDES = -I.@am__isrc@ -I$(top_builddir) +depcomp = $(SHELL) $(top_srcdir)/depcomp +am__maybe_remake_depfiles = depfiles +am__depfiles_remade = \ + ./$(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-basic_auth_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-client_mt_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-connection_pool_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-date_time_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-http_header_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-post_request_json_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-post_request_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-request_parser_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-request_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-response_creator_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-response_json_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-response_parser_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-response_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-run_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-server_client_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-tls_client_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-tls_server_unittests.Po \ + ./$(DEPDIR)/libhttp_unittests-url_unittests.Po +am__mv = mv -f +CXXCOMPILE = $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) \ + $(AM_CPPFLAGS) $(CPPFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS) +LTCXXCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) \ + $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \ + $(AM_CXXFLAGS) $(CXXFLAGS) +AM_V_CXX = $(am__v_CXX_@AM_V@) +am__v_CXX_ = $(am__v_CXX_@AM_DEFAULT_V@) +am__v_CXX_0 = @echo " CXX " $@; +am__v_CXX_1 = +CXXLD = $(CXX) +CXXLINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=link $(CXXLD) $(AM_CXXFLAGS) \ + $(CXXFLAGS) $(AM_LDFLAGS) $(LDFLAGS) -o $@ +AM_V_CXXLD = $(am__v_CXXLD_@AM_V@) +am__v_CXXLD_ = $(am__v_CXXLD_@AM_DEFAULT_V@) +am__v_CXXLD_0 = @echo " CXXLD " $@; +am__v_CXXLD_1 = +COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \ + $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) +LTCOMPILE = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) \ + $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) $(CPPFLAGS) \ + $(AM_CFLAGS) $(CFLAGS) +AM_V_CC = $(am__v_CC_@AM_V@) +am__v_CC_ = $(am__v_CC_@AM_DEFAULT_V@) +am__v_CC_0 = @echo " CC " $@; +am__v_CC_1 = +CCLD = $(CC) +LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \ + $(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \ + $(AM_LDFLAGS) $(LDFLAGS) -o $@ +AM_V_CCLD = $(am__v_CCLD_@AM_V@) +am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@) +am__v_CCLD_0 = @echo " CCLD " $@; +am__v_CCLD_1 = +SOURCES = $(libhttp_unittests_SOURCES) +DIST_SOURCES = $(am__libhttp_unittests_SOURCES_DIST) +RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \ + ctags-recursive dvi-recursive html-recursive info-recursive \ + install-data-recursive install-dvi-recursive \ + install-exec-recursive install-html-recursive \ + install-info-recursive install-pdf-recursive \ + install-ps-recursive install-recursive installcheck-recursive \ + installdirs-recursive pdf-recursive ps-recursive \ + tags-recursive uninstall-recursive +am__can_run_installinfo = \ + case $$AM_UPDATE_INFO_DIR in \ + n|no|NO) false;; \ + *) (install-info --version) >/dev/null 2>&1;; \ + esac +RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \ + distclean-recursive maintainer-clean-recursive +am__recursive_targets = \ + $(RECURSIVE_TARGETS) \ + $(RECURSIVE_CLEAN_TARGETS) \ + $(am__extra_recursive_targets) +AM_RECURSIVE_TARGETS = $(am__recursive_targets:-recursive=) TAGS CTAGS \ + distdir distdir-am +am__tagged_files = $(HEADERS) $(SOURCES) $(TAGS_FILES) $(LISP) +# Read a list of newline-separated strings from the standard input, +# and print each of them once, without duplicates. Input order is +# *not* preserved. +am__uniquify_input = $(AWK) '\ + BEGIN { nonempty = 0; } \ + { items[$$0] = 1; nonempty = 1; } \ + END { if (nonempty) { for (i in items) print i; }; } \ +' +# Make sure the list of sources is unique. This is necessary because, +# e.g., the same source file might be shared among _SOURCES variables +# for different programs/libraries. +am__define_uniq_tagged_files = \ + list='$(am__tagged_files)'; \ + unique=`for i in $$list; do \ + if test -f "$$i"; then echo $$i; else echo $(srcdir)/$$i; fi; \ + done | $(am__uniquify_input)` +ETAGS = etags +CTAGS = ctags +am__tty_colors_dummy = \ + mgn= red= grn= lgn= blu= brg= std=; \ + am__color_tests=no +am__tty_colors = { \ + $(am__tty_colors_dummy); \ + if test "X$(AM_COLOR_TESTS)" = Xno; then \ + am__color_tests=no; \ + elif test "X$(AM_COLOR_TESTS)" = Xalways; then \ + am__color_tests=yes; \ + elif test "X$$TERM" != Xdumb && { test -t 1; } 2>/dev/null; then \ + am__color_tests=yes; \ + fi; \ + if test $$am__color_tests = yes; then \ + red='[0;31m'; \ + grn='[0;32m'; \ + lgn='[1;32m'; \ + blu='[1;34m'; \ + mgn='[0;35m'; \ + brg='[1m'; \ + std='[m'; \ + fi; \ +} +DIST_SUBDIRS = $(SUBDIRS) +am__DIST_COMMON = $(srcdir)/Makefile.in $(top_srcdir)/depcomp +DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) +am__relativize = \ + dir0=`pwd`; \ + sed_first='s,^\([^/]*\)/.*$$,\1,'; \ + sed_rest='s,^[^/]*/*,,'; \ + sed_last='s,^.*/\([^/]*\)$$,\1,'; \ + sed_butlast='s,/*[^/]*$$,,'; \ + while test -n "$$dir1"; do \ + first=`echo "$$dir1" | sed -e "$$sed_first"`; \ + if test "$$first" != "."; then \ + if test "$$first" = ".."; then \ + dir2=`echo "$$dir0" | sed -e "$$sed_last"`/"$$dir2"; \ + dir0=`echo "$$dir0" | sed -e "$$sed_butlast"`; \ + else \ + first2=`echo "$$dir2" | sed -e "$$sed_first"`; \ + if test "$$first2" = "$$first"; then \ + dir2=`echo "$$dir2" | sed -e "$$sed_rest"`; \ + else \ + dir2="../$$dir2"; \ + fi; \ + dir0="$$dir0"/"$$first"; \ + fi; \ + fi; \ + dir1=`echo "$$dir1" | sed -e "$$sed_rest"`; \ + done; \ + reldir="$$dir2" +ACLOCAL = @ACLOCAL@ +AMTAR = @AMTAR@ +AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +AR = @AR@ +ASCIIDOC = @ASCIIDOC@ +AUTOCONF = @AUTOCONF@ +AUTOHEADER = @AUTOHEADER@ +AUTOMAKE = @AUTOMAKE@ +AWK = @AWK@ +BOOST_INCLUDES = @BOOST_INCLUDES@ +BOOST_LIBS = @BOOST_LIBS@ +BOTAN_TOOL = @BOTAN_TOOL@ +CC = @CC@ +CCDEPMODE = @CCDEPMODE@ +CFLAGS = @CFLAGS@ +CONTRIB_DIR = @CONTRIB_DIR@ +CPP = @CPP@ +CPPFLAGS = @CPPFLAGS@ +CRYPTO_CFLAGS = @CRYPTO_CFLAGS@ +CRYPTO_INCLUDES = @CRYPTO_INCLUDES@ +CRYPTO_LDFLAGS = @CRYPTO_LDFLAGS@ +CRYPTO_LIBS = @CRYPTO_LIBS@ +CRYPTO_PACKAGE = @CRYPTO_PACKAGE@ +CRYPTO_RPATH = @CRYPTO_RPATH@ +CXX = @CXX@ +CXXCPP = @CXXCPP@ +CXXDEPMODE = @CXXDEPMODE@ +CXXFLAGS = @CXXFLAGS@ +CYGPATH_W = @CYGPATH_W@ +DEFS = @DEFS@ +DEPDIR = @DEPDIR@ +DISTCHECK_BOOST_CONFIGURE_FLAG = @DISTCHECK_BOOST_CONFIGURE_FLAG@ +DISTCHECK_CONTRIB_CONFIGURE_FLAG = @DISTCHECK_CONTRIB_CONFIGURE_FLAG@ +DISTCHECK_CRYPTO_CONFIGURE_FLAG = @DISTCHECK_CRYPTO_CONFIGURE_FLAG@ +DISTCHECK_GSSAPI_CONFIGURE_FLAG = @DISTCHECK_GSSAPI_CONFIGURE_FLAG@ +DISTCHECK_GTEST_CONFIGURE_FLAG = @DISTCHECK_GTEST_CONFIGURE_FLAG@ +DISTCHECK_KEA_SHELL_CONFIGURE_FLAG = @DISTCHECK_KEA_SHELL_CONFIGURE_FLAG@ +DISTCHECK_LIBYANGCPP_CONFIGURE_FLAG = @DISTCHECK_LIBYANGCPP_CONFIGURE_FLAG@ +DISTCHECK_LIBYANG_CONFIGURE_FLAG = @DISTCHECK_LIBYANG_CONFIGURE_FLAG@ +DISTCHECK_LOG4CPLUS_CONFIGURE_FLAG = @DISTCHECK_LOG4CPLUS_CONFIGURE_FLAG@ +DISTCHECK_MYSQL_CONFIGURE_FLAG = @DISTCHECK_MYSQL_CONFIGURE_FLAG@ +DISTCHECK_PERFDHCP_CONFIGURE_FLAG = @DISTCHECK_PERFDHCP_CONFIGURE_FLAG@ +DISTCHECK_PGSQL_CONFIGURE_FLAG = @DISTCHECK_PGSQL_CONFIGURE_FLAG@ +DISTCHECK_PREMIUM_CONFIGURE_FLAG = @DISTCHECK_PREMIUM_CONFIGURE_FLAG@ +DISTCHECK_SYSREPOCPP_CONFIGURE_FLAG = @DISTCHECK_SYSREPOCPP_CONFIGURE_FLAG@ +DISTCHECK_SYSREPO_CONFIGURE_FLAG = @DISTCHECK_SYSREPO_CONFIGURE_FLAG@ +DLLTOOL = @DLLTOOL@ +DSYMUTIL = @DSYMUTIL@ +DUMPBIN = @DUMPBIN@ +ECHO_C = @ECHO_C@ +ECHO_N = @ECHO_N@ +ECHO_T = @ECHO_T@ +EGREP = @EGREP@ +EXEEXT = @EXEEXT@ +FGREP = @FGREP@ +GENHTML = @GENHTML@ +GREP = @GREP@ +GSSAPI_CFLAGS = @GSSAPI_CFLAGS@ +GSSAPI_LIBS = @GSSAPI_LIBS@ +GTEST_CONFIG = @GTEST_CONFIG@ +GTEST_INCLUDES = @GTEST_INCLUDES@ +GTEST_LDADD = @GTEST_LDADD@ +GTEST_LDFLAGS = @GTEST_LDFLAGS@ +GTEST_SOURCE = @GTEST_SOURCE@ +HAVE_NETCONF = @HAVE_NETCONF@ +INSTALL = @INSTALL@ +INSTALL_DATA = @INSTALL_DATA@ +INSTALL_PROGRAM = @INSTALL_PROGRAM@ +INSTALL_SCRIPT = @INSTALL_SCRIPT@ +INSTALL_STRIP_PROGRAM = @INSTALL_STRIP_PROGRAM@ +KEA_CXXFLAGS = @KEA_CXXFLAGS@ +KEA_SRCID = @KEA_SRCID@ +KRB5_CONFIG = @KRB5_CONFIG@ +LCOV = @LCOV@ +LD = @LD@ +LDFLAGS = @LDFLAGS@ +LEX = @LEX@ +LEXLIB = @LEXLIB@ +LEX_OUTPUT_ROOT = @LEX_OUTPUT_ROOT@ +LIBOBJS = @LIBOBJS@ +LIBS = @LIBS@ +LIBTOOL = @LIBTOOL@ +LIBYANGCPP_CPPFLAGS = @LIBYANGCPP_CPPFLAGS@ +LIBYANGCPP_INCLUDEDIR = @LIBYANGCPP_INCLUDEDIR@ +LIBYANGCPP_LIBS = @LIBYANGCPP_LIBS@ +LIBYANGCPP_PREFIX = @LIBYANGCPP_PREFIX@ +LIBYANGCPP_VERSION = @LIBYANGCPP_VERSION@ +LIBYANG_CPPFLAGS = @LIBYANG_CPPFLAGS@ +LIBYANG_INCLUDEDIR = @LIBYANG_INCLUDEDIR@ +LIBYANG_LIBS = @LIBYANG_LIBS@ +LIBYANG_PREFIX = @LIBYANG_PREFIX@ +LIBYANG_VERSION = @LIBYANG_VERSION@ +LIPO = @LIPO@ +LN_S = @LN_S@ +LOG4CPLUS_INCLUDES = @LOG4CPLUS_INCLUDES@ +LOG4CPLUS_LIBS = @LOG4CPLUS_LIBS@ +LTLIBOBJS = @LTLIBOBJS@ +LT_SYS_LIBRARY_PATH = @LT_SYS_LIBRARY_PATH@ +MAKEINFO = @MAKEINFO@ +MANIFEST_TOOL = @MANIFEST_TOOL@ +MKDIR_P = @MKDIR_P@ +MYSQL_CPPFLAGS = @MYSQL_CPPFLAGS@ +MYSQL_LIBS = @MYSQL_LIBS@ +NM = @NM@ +NMEDIT = @NMEDIT@ +OBJDUMP = @OBJDUMP@ +OBJEXT = @OBJEXT@ +OTOOL = @OTOOL@ +OTOOL64 = @OTOOL64@ +PACKAGE = @PACKAGE@ +PACKAGE_BUGREPORT = @PACKAGE_BUGREPORT@ +PACKAGE_NAME = @PACKAGE_NAME@ +PACKAGE_STRING = @PACKAGE_STRING@ +PACKAGE_TARNAME = @PACKAGE_TARNAME@ +PACKAGE_URL = @PACKAGE_URL@ +PACKAGE_VERSION = @PACKAGE_VERSION@ +PACKAGE_VERSION_TYPE = @PACKAGE_VERSION_TYPE@ +PATH_SEPARATOR = @PATH_SEPARATOR@ +PDFLATEX = @PDFLATEX@ +PERL = @PERL@ +PGSQL_CPPFLAGS = @PGSQL_CPPFLAGS@ +PGSQL_LIBS = @PGSQL_LIBS@ +PKGPYTHONDIR = @PKGPYTHONDIR@ +PKG_CONFIG = @PKG_CONFIG@ +PLANTUML = @PLANTUML@ +PREMIUM_DIR = @PREMIUM_DIR@ +PYTHON = @PYTHON@ +PYTHON_EXEC_PREFIX = @PYTHON_EXEC_PREFIX@ +PYTHON_PLATFORM = @PYTHON_PLATFORM@ +PYTHON_PREFIX = @PYTHON_PREFIX@ +PYTHON_VERSION = @PYTHON_VERSION@ +RANLIB = @RANLIB@ +SED = @SED@ +SEP = @SEP@ +SET_MAKE = @SET_MAKE@ +SHELL = @SHELL@ +SPHINXBUILD = @SPHINXBUILD@ +SRPD_PLUGINS_PATH = @SRPD_PLUGINS_PATH@ +SR_PLUGINS_PATH = @SR_PLUGINS_PATH@ +SR_REPO_PATH = @SR_REPO_PATH@ +STRIP = @STRIP@ +SYSREPOCPP_CPPFLAGS = @SYSREPOCPP_CPPFLAGS@ +SYSREPOCPP_INCLUDEDIR = @SYSREPOCPP_INCLUDEDIR@ +SYSREPOCPP_LIBS = @SYSREPOCPP_LIBS@ +SYSREPOCPP_PREFIX = @SYSREPOCPP_PREFIX@ +SYSREPOCPP_VERSION = @SYSREPOCPP_VERSION@ +SYSREPO_CPPFLAGS = @SYSREPO_CPPFLAGS@ +SYSREPO_INCLUDEDIR = @SYSREPO_INCLUDEDIR@ +SYSREPO_LIBS = @SYSREPO_LIBS@ +SYSREPO_PREFIX = @SYSREPO_PREFIX@ +SYSREPO_VERSION = @SYSREPO_VERSION@ +USE_LCOV = @USE_LCOV@ +VALGRIND = @VALGRIND@ +VERSION = @VERSION@ +WARNING_GCC_44_STRICT_ALIASING_CFLAG = @WARNING_GCC_44_STRICT_ALIASING_CFLAG@ +YACC = @YACC@ +abs_builddir = @abs_builddir@ +abs_srcdir = @abs_srcdir@ +abs_top_builddir = @abs_top_builddir@ +abs_top_srcdir = @abs_top_srcdir@ +ac_ct_AR = @ac_ct_AR@ +ac_ct_CC = @ac_ct_CC@ +ac_ct_CXX = @ac_ct_CXX@ +ac_ct_DUMPBIN = @ac_ct_DUMPBIN@ +am__include = @am__include@ +am__leading_dot = @am__leading_dot@ +am__quote = @am__quote@ +am__tar = @am__tar@ +am__untar = @am__untar@ +bindir = @bindir@ +build = @build@ +build_alias = @build_alias@ +build_cpu = @build_cpu@ +build_os = @build_os@ +build_vendor = @build_vendor@ +builddir = @builddir@ +datadir = @datadir@ +datarootdir = @datarootdir@ +docdir = @docdir@ +dvidir = @dvidir@ +exec_prefix = @exec_prefix@ +host = @host@ +host_alias = @host_alias@ +host_cpu = @host_cpu@ +host_os = @host_os@ +host_vendor = @host_vendor@ +htmldir = @htmldir@ +includedir = @includedir@ +infodir = @infodir@ +install_sh = @install_sh@ +libdir = @libdir@ +libexecdir = @libexecdir@ +localedir = @localedir@ +localstatedir = @localstatedir@ +mandir = @mandir@ +mkdir_p = @mkdir_p@ +oldincludedir = @oldincludedir@ +pdfdir = @pdfdir@ +pkgpyexecdir = @pkgpyexecdir@ +pkgpythondir = @pkgpythondir@ +prefix = @prefix@ +program_transform_name = @program_transform_name@ +psdir = @psdir@ +pyexecdir = @pyexecdir@ +pythondir = @pythondir@ +runstatedir = @runstatedir@ +sbindir = @sbindir@ +sharedstatedir = @sharedstatedir@ +srcdir = @srcdir@ +sysconfdir = @sysconfdir@ +target_alias = @target_alias@ +top_build_prefix = @top_build_prefix@ +top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +SUBDIRS = . + +# Add to the tarball: +EXTRA_DIST = testdata/empty testdata/hiddenp testdata/hiddens \ + testdata/hiddenu +AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib \ + $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) \ + -DTEST_DATA_BUILDDIR=\"$(TEST_CA_DIR)\" \ + -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\" \ + -DDATA_DIR=\"$(abs_srcdir)/testdata\" +TEST_CA_DIR = $(srcdir)/../../asiolink/testutils/ca +AM_CXXFLAGS = $(KEA_CXXFLAGS) +@USE_STATIC_LINK_TRUE@AM_LDFLAGS = -static +CLEANFILES = *.gcno *.gcda +TESTS_ENVIRONMENT = $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) +@HAVE_GTEST_TRUE@libhttp_unittests_SOURCES = basic_auth_unittests.cc \ +@HAVE_GTEST_TRUE@ basic_auth_config_unittests.cc \ +@HAVE_GTEST_TRUE@ connection_pool_unittests.cc \ +@HAVE_GTEST_TRUE@ date_time_unittests.cc \ +@HAVE_GTEST_TRUE@ http_header_unittests.cc \ +@HAVE_GTEST_TRUE@ post_request_unittests.cc \ +@HAVE_GTEST_TRUE@ post_request_json_unittests.cc \ +@HAVE_GTEST_TRUE@ request_parser_unittests.cc request_test.h \ +@HAVE_GTEST_TRUE@ response_creator_unittests.cc \ +@HAVE_GTEST_TRUE@ response_parser_unittests.cc response_test.h \ +@HAVE_GTEST_TRUE@ request_unittests.cc response_unittests.cc \ +@HAVE_GTEST_TRUE@ response_json_unittests.cc run_unittests.cc \ +@HAVE_GTEST_TRUE@ server_client_unittests.cc $(am__append_2) \ +@HAVE_GTEST_TRUE@ $(am__append_3) url_unittests.cc \ +@HAVE_GTEST_TRUE@ test_http_client.h client_mt_unittests.cc +@HAVE_GTEST_TRUE@libhttp_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) +@HAVE_GTEST_TRUE@libhttp_unittests_CXXFLAGS = $(AM_CXXFLAGS) +@HAVE_GTEST_TRUE@libhttp_unittests_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) $(GTEST_LDFLAGS) +@HAVE_GTEST_TRUE@libhttp_unittests_LDADD = \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/http/libkea-http.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/hooks/libkea-hooks.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/testutils/libkea-testutils.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/cc/libkea-cc.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/asiolink/testutils/libasiolinktest.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/asiolink/libkea-asiolink.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/log/libkea-log.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/util/libkea-util.la \ +@HAVE_GTEST_TRUE@ $(top_builddir)/src/lib/exceptions/libkea-exceptions.la \ +@HAVE_GTEST_TRUE@ $(LOG4CPLUS_LIBS) $(BOOST_LIBS) \ +@HAVE_GTEST_TRUE@ $(CRYPTO_LIBS) $(GTEST_LDADD) +all: all-recursive + +.SUFFIXES: +.SUFFIXES: .cc .lo .o .obj +$(srcdir)/Makefile.in: $(srcdir)/Makefile.am $(am__configure_deps) + @for dep in $?; do \ + case '$(am__configure_deps)' in \ + *$$dep*) \ + ( cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh ) \ + && { if test -f $@; then exit 0; else break; fi; }; \ + exit 1;; \ + esac; \ + done; \ + echo ' cd $(top_srcdir) && $(AUTOMAKE) --foreign src/lib/http/tests/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/lib/http/tests/Makefile +Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status + @case '$?' in \ + *config.status*) \ + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh;; \ + *) \ + echo ' cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles)'; \ + cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ $(am__maybe_remake_depfiles);; \ + esac; + +$(top_builddir)/config.status: $(top_srcdir)/configure $(CONFIG_STATUS_DEPENDENCIES) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh + +$(top_srcdir)/configure: $(am__configure_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(ACLOCAL_M4): $(am__aclocal_m4_deps) + cd $(top_builddir) && $(MAKE) $(AM_MAKEFLAGS) am--refresh +$(am__aclocal_m4_deps): + +clean-noinstPROGRAMS: + @list='$(noinst_PROGRAMS)'; test -n "$$list" || exit 0; \ + echo " rm -f" $$list; \ + rm -f $$list || exit $$?; \ + test -n "$(EXEEXT)" || exit 0; \ + list=`for p in $$list; do echo "$$p"; done | sed 's/$(EXEEXT)$$//'`; \ + echo " rm -f" $$list; \ + rm -f $$list + +libhttp_unittests$(EXEEXT): $(libhttp_unittests_OBJECTS) $(libhttp_unittests_DEPENDENCIES) $(EXTRA_libhttp_unittests_DEPENDENCIES) + @rm -f libhttp_unittests$(EXEEXT) + $(AM_V_CXXLD)$(libhttp_unittests_LINK) $(libhttp_unittests_OBJECTS) $(libhttp_unittests_LDADD) $(LIBS) + +mostlyclean-compile: + -rm -f *.$(OBJEXT) + +distclean-compile: + -rm -f *.tab.c + +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-basic_auth_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-client_mt_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-connection_pool_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-date_time_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-http_header_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-post_request_json_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-post_request_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-request_parser_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-request_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-response_creator_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-response_json_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-response_parser_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-response_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-run_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-server_client_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-tls_client_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-tls_server_unittests.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libhttp_unittests-url_unittests.Po@am__quote@ # am--include-marker + +$(am__depfiles_remade): + @$(MKDIR_P) $(@D) + @echo '# dummy' >$@-t && $(am__mv) $@-t $@ + +am--depfiles: $(am__depfiles_remade) + +.cc.o: +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $< +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ $< + +.cc.obj: +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ `$(CYGPATH_W) '$<'` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXXCOMPILE) -c -o $@ `$(CYGPATH_W) '$<'` + +.cc.lo: +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LTCXXCOMPILE) -MT $@ -MD -MP -MF $(DEPDIR)/$*.Tpo -c -o $@ $< +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/$*.Tpo $(DEPDIR)/$*.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='$<' object='$@' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LTCXXCOMPILE) -c -o $@ $< + +libhttp_unittests-basic_auth_unittests.o: basic_auth_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-basic_auth_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-basic_auth_unittests.Tpo -c -o libhttp_unittests-basic_auth_unittests.o `test -f 'basic_auth_unittests.cc' || echo '$(srcdir)/'`basic_auth_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-basic_auth_unittests.Tpo $(DEPDIR)/libhttp_unittests-basic_auth_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='basic_auth_unittests.cc' object='libhttp_unittests-basic_auth_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-basic_auth_unittests.o `test -f 'basic_auth_unittests.cc' || echo '$(srcdir)/'`basic_auth_unittests.cc + +libhttp_unittests-basic_auth_unittests.obj: basic_auth_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-basic_auth_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-basic_auth_unittests.Tpo -c -o libhttp_unittests-basic_auth_unittests.obj `if test -f 'basic_auth_unittests.cc'; then $(CYGPATH_W) 'basic_auth_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/basic_auth_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-basic_auth_unittests.Tpo $(DEPDIR)/libhttp_unittests-basic_auth_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='basic_auth_unittests.cc' object='libhttp_unittests-basic_auth_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-basic_auth_unittests.obj `if test -f 'basic_auth_unittests.cc'; then $(CYGPATH_W) 'basic_auth_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/basic_auth_unittests.cc'; fi` + +libhttp_unittests-basic_auth_config_unittests.o: basic_auth_config_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-basic_auth_config_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Tpo -c -o libhttp_unittests-basic_auth_config_unittests.o `test -f 'basic_auth_config_unittests.cc' || echo '$(srcdir)/'`basic_auth_config_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Tpo $(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='basic_auth_config_unittests.cc' object='libhttp_unittests-basic_auth_config_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-basic_auth_config_unittests.o `test -f 'basic_auth_config_unittests.cc' || echo '$(srcdir)/'`basic_auth_config_unittests.cc + +libhttp_unittests-basic_auth_config_unittests.obj: basic_auth_config_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-basic_auth_config_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Tpo -c -o libhttp_unittests-basic_auth_config_unittests.obj `if test -f 'basic_auth_config_unittests.cc'; then $(CYGPATH_W) 'basic_auth_config_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/basic_auth_config_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Tpo $(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='basic_auth_config_unittests.cc' object='libhttp_unittests-basic_auth_config_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-basic_auth_config_unittests.obj `if test -f 'basic_auth_config_unittests.cc'; then $(CYGPATH_W) 'basic_auth_config_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/basic_auth_config_unittests.cc'; fi` + +libhttp_unittests-connection_pool_unittests.o: connection_pool_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-connection_pool_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-connection_pool_unittests.Tpo -c -o libhttp_unittests-connection_pool_unittests.o `test -f 'connection_pool_unittests.cc' || echo '$(srcdir)/'`connection_pool_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-connection_pool_unittests.Tpo $(DEPDIR)/libhttp_unittests-connection_pool_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='connection_pool_unittests.cc' object='libhttp_unittests-connection_pool_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-connection_pool_unittests.o `test -f 'connection_pool_unittests.cc' || echo '$(srcdir)/'`connection_pool_unittests.cc + +libhttp_unittests-connection_pool_unittests.obj: connection_pool_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-connection_pool_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-connection_pool_unittests.Tpo -c -o libhttp_unittests-connection_pool_unittests.obj `if test -f 'connection_pool_unittests.cc'; then $(CYGPATH_W) 'connection_pool_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/connection_pool_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-connection_pool_unittests.Tpo $(DEPDIR)/libhttp_unittests-connection_pool_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='connection_pool_unittests.cc' object='libhttp_unittests-connection_pool_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-connection_pool_unittests.obj `if test -f 'connection_pool_unittests.cc'; then $(CYGPATH_W) 'connection_pool_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/connection_pool_unittests.cc'; fi` + +libhttp_unittests-date_time_unittests.o: date_time_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-date_time_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-date_time_unittests.Tpo -c -o libhttp_unittests-date_time_unittests.o `test -f 'date_time_unittests.cc' || echo '$(srcdir)/'`date_time_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-date_time_unittests.Tpo $(DEPDIR)/libhttp_unittests-date_time_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='date_time_unittests.cc' object='libhttp_unittests-date_time_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-date_time_unittests.o `test -f 'date_time_unittests.cc' || echo '$(srcdir)/'`date_time_unittests.cc + +libhttp_unittests-date_time_unittests.obj: date_time_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-date_time_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-date_time_unittests.Tpo -c -o libhttp_unittests-date_time_unittests.obj `if test -f 'date_time_unittests.cc'; then $(CYGPATH_W) 'date_time_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/date_time_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-date_time_unittests.Tpo $(DEPDIR)/libhttp_unittests-date_time_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='date_time_unittests.cc' object='libhttp_unittests-date_time_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-date_time_unittests.obj `if test -f 'date_time_unittests.cc'; then $(CYGPATH_W) 'date_time_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/date_time_unittests.cc'; fi` + +libhttp_unittests-http_header_unittests.o: http_header_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-http_header_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-http_header_unittests.Tpo -c -o libhttp_unittests-http_header_unittests.o `test -f 'http_header_unittests.cc' || echo '$(srcdir)/'`http_header_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-http_header_unittests.Tpo $(DEPDIR)/libhttp_unittests-http_header_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='http_header_unittests.cc' object='libhttp_unittests-http_header_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-http_header_unittests.o `test -f 'http_header_unittests.cc' || echo '$(srcdir)/'`http_header_unittests.cc + +libhttp_unittests-http_header_unittests.obj: http_header_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-http_header_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-http_header_unittests.Tpo -c -o libhttp_unittests-http_header_unittests.obj `if test -f 'http_header_unittests.cc'; then $(CYGPATH_W) 'http_header_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/http_header_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-http_header_unittests.Tpo $(DEPDIR)/libhttp_unittests-http_header_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='http_header_unittests.cc' object='libhttp_unittests-http_header_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-http_header_unittests.obj `if test -f 'http_header_unittests.cc'; then $(CYGPATH_W) 'http_header_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/http_header_unittests.cc'; fi` + +libhttp_unittests-post_request_unittests.o: post_request_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-post_request_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-post_request_unittests.Tpo -c -o libhttp_unittests-post_request_unittests.o `test -f 'post_request_unittests.cc' || echo '$(srcdir)/'`post_request_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-post_request_unittests.Tpo $(DEPDIR)/libhttp_unittests-post_request_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='post_request_unittests.cc' object='libhttp_unittests-post_request_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-post_request_unittests.o `test -f 'post_request_unittests.cc' || echo '$(srcdir)/'`post_request_unittests.cc + +libhttp_unittests-post_request_unittests.obj: post_request_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-post_request_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-post_request_unittests.Tpo -c -o libhttp_unittests-post_request_unittests.obj `if test -f 'post_request_unittests.cc'; then $(CYGPATH_W) 'post_request_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/post_request_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-post_request_unittests.Tpo $(DEPDIR)/libhttp_unittests-post_request_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='post_request_unittests.cc' object='libhttp_unittests-post_request_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-post_request_unittests.obj `if test -f 'post_request_unittests.cc'; then $(CYGPATH_W) 'post_request_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/post_request_unittests.cc'; fi` + +libhttp_unittests-post_request_json_unittests.o: post_request_json_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-post_request_json_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-post_request_json_unittests.Tpo -c -o libhttp_unittests-post_request_json_unittests.o `test -f 'post_request_json_unittests.cc' || echo '$(srcdir)/'`post_request_json_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-post_request_json_unittests.Tpo $(DEPDIR)/libhttp_unittests-post_request_json_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='post_request_json_unittests.cc' object='libhttp_unittests-post_request_json_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-post_request_json_unittests.o `test -f 'post_request_json_unittests.cc' || echo '$(srcdir)/'`post_request_json_unittests.cc + +libhttp_unittests-post_request_json_unittests.obj: post_request_json_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-post_request_json_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-post_request_json_unittests.Tpo -c -o libhttp_unittests-post_request_json_unittests.obj `if test -f 'post_request_json_unittests.cc'; then $(CYGPATH_W) 'post_request_json_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/post_request_json_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-post_request_json_unittests.Tpo $(DEPDIR)/libhttp_unittests-post_request_json_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='post_request_json_unittests.cc' object='libhttp_unittests-post_request_json_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-post_request_json_unittests.obj `if test -f 'post_request_json_unittests.cc'; then $(CYGPATH_W) 'post_request_json_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/post_request_json_unittests.cc'; fi` + +libhttp_unittests-request_parser_unittests.o: request_parser_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-request_parser_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-request_parser_unittests.Tpo -c -o libhttp_unittests-request_parser_unittests.o `test -f 'request_parser_unittests.cc' || echo '$(srcdir)/'`request_parser_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-request_parser_unittests.Tpo $(DEPDIR)/libhttp_unittests-request_parser_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='request_parser_unittests.cc' object='libhttp_unittests-request_parser_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-request_parser_unittests.o `test -f 'request_parser_unittests.cc' || echo '$(srcdir)/'`request_parser_unittests.cc + +libhttp_unittests-request_parser_unittests.obj: request_parser_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-request_parser_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-request_parser_unittests.Tpo -c -o libhttp_unittests-request_parser_unittests.obj `if test -f 'request_parser_unittests.cc'; then $(CYGPATH_W) 'request_parser_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/request_parser_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-request_parser_unittests.Tpo $(DEPDIR)/libhttp_unittests-request_parser_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='request_parser_unittests.cc' object='libhttp_unittests-request_parser_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-request_parser_unittests.obj `if test -f 'request_parser_unittests.cc'; then $(CYGPATH_W) 'request_parser_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/request_parser_unittests.cc'; fi` + +libhttp_unittests-response_creator_unittests.o: response_creator_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-response_creator_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-response_creator_unittests.Tpo -c -o libhttp_unittests-response_creator_unittests.o `test -f 'response_creator_unittests.cc' || echo '$(srcdir)/'`response_creator_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-response_creator_unittests.Tpo $(DEPDIR)/libhttp_unittests-response_creator_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_creator_unittests.cc' object='libhttp_unittests-response_creator_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-response_creator_unittests.o `test -f 'response_creator_unittests.cc' || echo '$(srcdir)/'`response_creator_unittests.cc + +libhttp_unittests-response_creator_unittests.obj: response_creator_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-response_creator_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-response_creator_unittests.Tpo -c -o libhttp_unittests-response_creator_unittests.obj `if test -f 'response_creator_unittests.cc'; then $(CYGPATH_W) 'response_creator_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/response_creator_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-response_creator_unittests.Tpo $(DEPDIR)/libhttp_unittests-response_creator_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_creator_unittests.cc' object='libhttp_unittests-response_creator_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-response_creator_unittests.obj `if test -f 'response_creator_unittests.cc'; then $(CYGPATH_W) 'response_creator_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/response_creator_unittests.cc'; fi` + +libhttp_unittests-response_parser_unittests.o: response_parser_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-response_parser_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-response_parser_unittests.Tpo -c -o libhttp_unittests-response_parser_unittests.o `test -f 'response_parser_unittests.cc' || echo '$(srcdir)/'`response_parser_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-response_parser_unittests.Tpo $(DEPDIR)/libhttp_unittests-response_parser_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_parser_unittests.cc' object='libhttp_unittests-response_parser_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-response_parser_unittests.o `test -f 'response_parser_unittests.cc' || echo '$(srcdir)/'`response_parser_unittests.cc + +libhttp_unittests-response_parser_unittests.obj: response_parser_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-response_parser_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-response_parser_unittests.Tpo -c -o libhttp_unittests-response_parser_unittests.obj `if test -f 'response_parser_unittests.cc'; then $(CYGPATH_W) 'response_parser_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/response_parser_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-response_parser_unittests.Tpo $(DEPDIR)/libhttp_unittests-response_parser_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_parser_unittests.cc' object='libhttp_unittests-response_parser_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-response_parser_unittests.obj `if test -f 'response_parser_unittests.cc'; then $(CYGPATH_W) 'response_parser_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/response_parser_unittests.cc'; fi` + +libhttp_unittests-request_unittests.o: request_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-request_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-request_unittests.Tpo -c -o libhttp_unittests-request_unittests.o `test -f 'request_unittests.cc' || echo '$(srcdir)/'`request_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-request_unittests.Tpo $(DEPDIR)/libhttp_unittests-request_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='request_unittests.cc' object='libhttp_unittests-request_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-request_unittests.o `test -f 'request_unittests.cc' || echo '$(srcdir)/'`request_unittests.cc + +libhttp_unittests-request_unittests.obj: request_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-request_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-request_unittests.Tpo -c -o libhttp_unittests-request_unittests.obj `if test -f 'request_unittests.cc'; then $(CYGPATH_W) 'request_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/request_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-request_unittests.Tpo $(DEPDIR)/libhttp_unittests-request_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='request_unittests.cc' object='libhttp_unittests-request_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-request_unittests.obj `if test -f 'request_unittests.cc'; then $(CYGPATH_W) 'request_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/request_unittests.cc'; fi` + +libhttp_unittests-response_unittests.o: response_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-response_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-response_unittests.Tpo -c -o libhttp_unittests-response_unittests.o `test -f 'response_unittests.cc' || echo '$(srcdir)/'`response_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-response_unittests.Tpo $(DEPDIR)/libhttp_unittests-response_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_unittests.cc' object='libhttp_unittests-response_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-response_unittests.o `test -f 'response_unittests.cc' || echo '$(srcdir)/'`response_unittests.cc + +libhttp_unittests-response_unittests.obj: response_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-response_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-response_unittests.Tpo -c -o libhttp_unittests-response_unittests.obj `if test -f 'response_unittests.cc'; then $(CYGPATH_W) 'response_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/response_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-response_unittests.Tpo $(DEPDIR)/libhttp_unittests-response_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_unittests.cc' object='libhttp_unittests-response_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-response_unittests.obj `if test -f 'response_unittests.cc'; then $(CYGPATH_W) 'response_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/response_unittests.cc'; fi` + +libhttp_unittests-response_json_unittests.o: response_json_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-response_json_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-response_json_unittests.Tpo -c -o libhttp_unittests-response_json_unittests.o `test -f 'response_json_unittests.cc' || echo '$(srcdir)/'`response_json_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-response_json_unittests.Tpo $(DEPDIR)/libhttp_unittests-response_json_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_json_unittests.cc' object='libhttp_unittests-response_json_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-response_json_unittests.o `test -f 'response_json_unittests.cc' || echo '$(srcdir)/'`response_json_unittests.cc + +libhttp_unittests-response_json_unittests.obj: response_json_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-response_json_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-response_json_unittests.Tpo -c -o libhttp_unittests-response_json_unittests.obj `if test -f 'response_json_unittests.cc'; then $(CYGPATH_W) 'response_json_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/response_json_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-response_json_unittests.Tpo $(DEPDIR)/libhttp_unittests-response_json_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_json_unittests.cc' object='libhttp_unittests-response_json_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-response_json_unittests.obj `if test -f 'response_json_unittests.cc'; then $(CYGPATH_W) 'response_json_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/response_json_unittests.cc'; fi` + +libhttp_unittests-run_unittests.o: run_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-run_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-run_unittests.Tpo -c -o libhttp_unittests-run_unittests.o `test -f 'run_unittests.cc' || echo '$(srcdir)/'`run_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-run_unittests.Tpo $(DEPDIR)/libhttp_unittests-run_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='run_unittests.cc' object='libhttp_unittests-run_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-run_unittests.o `test -f 'run_unittests.cc' || echo '$(srcdir)/'`run_unittests.cc + +libhttp_unittests-run_unittests.obj: run_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-run_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-run_unittests.Tpo -c -o libhttp_unittests-run_unittests.obj `if test -f 'run_unittests.cc'; then $(CYGPATH_W) 'run_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/run_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-run_unittests.Tpo $(DEPDIR)/libhttp_unittests-run_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='run_unittests.cc' object='libhttp_unittests-run_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-run_unittests.obj `if test -f 'run_unittests.cc'; then $(CYGPATH_W) 'run_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/run_unittests.cc'; fi` + +libhttp_unittests-server_client_unittests.o: server_client_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-server_client_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-server_client_unittests.Tpo -c -o libhttp_unittests-server_client_unittests.o `test -f 'server_client_unittests.cc' || echo '$(srcdir)/'`server_client_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-server_client_unittests.Tpo $(DEPDIR)/libhttp_unittests-server_client_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='server_client_unittests.cc' object='libhttp_unittests-server_client_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-server_client_unittests.o `test -f 'server_client_unittests.cc' || echo '$(srcdir)/'`server_client_unittests.cc + +libhttp_unittests-server_client_unittests.obj: server_client_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-server_client_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-server_client_unittests.Tpo -c -o libhttp_unittests-server_client_unittests.obj `if test -f 'server_client_unittests.cc'; then $(CYGPATH_W) 'server_client_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/server_client_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-server_client_unittests.Tpo $(DEPDIR)/libhttp_unittests-server_client_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='server_client_unittests.cc' object='libhttp_unittests-server_client_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-server_client_unittests.obj `if test -f 'server_client_unittests.cc'; then $(CYGPATH_W) 'server_client_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/server_client_unittests.cc'; fi` + +libhttp_unittests-tls_server_unittests.o: tls_server_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-tls_server_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-tls_server_unittests.Tpo -c -o libhttp_unittests-tls_server_unittests.o `test -f 'tls_server_unittests.cc' || echo '$(srcdir)/'`tls_server_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-tls_server_unittests.Tpo $(DEPDIR)/libhttp_unittests-tls_server_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='tls_server_unittests.cc' object='libhttp_unittests-tls_server_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-tls_server_unittests.o `test -f 'tls_server_unittests.cc' || echo '$(srcdir)/'`tls_server_unittests.cc + +libhttp_unittests-tls_server_unittests.obj: tls_server_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-tls_server_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-tls_server_unittests.Tpo -c -o libhttp_unittests-tls_server_unittests.obj `if test -f 'tls_server_unittests.cc'; then $(CYGPATH_W) 'tls_server_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/tls_server_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-tls_server_unittests.Tpo $(DEPDIR)/libhttp_unittests-tls_server_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='tls_server_unittests.cc' object='libhttp_unittests-tls_server_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-tls_server_unittests.obj `if test -f 'tls_server_unittests.cc'; then $(CYGPATH_W) 'tls_server_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/tls_server_unittests.cc'; fi` + +libhttp_unittests-tls_client_unittests.o: tls_client_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-tls_client_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-tls_client_unittests.Tpo -c -o libhttp_unittests-tls_client_unittests.o `test -f 'tls_client_unittests.cc' || echo '$(srcdir)/'`tls_client_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-tls_client_unittests.Tpo $(DEPDIR)/libhttp_unittests-tls_client_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='tls_client_unittests.cc' object='libhttp_unittests-tls_client_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-tls_client_unittests.o `test -f 'tls_client_unittests.cc' || echo '$(srcdir)/'`tls_client_unittests.cc + +libhttp_unittests-tls_client_unittests.obj: tls_client_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-tls_client_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-tls_client_unittests.Tpo -c -o libhttp_unittests-tls_client_unittests.obj `if test -f 'tls_client_unittests.cc'; then $(CYGPATH_W) 'tls_client_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/tls_client_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-tls_client_unittests.Tpo $(DEPDIR)/libhttp_unittests-tls_client_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='tls_client_unittests.cc' object='libhttp_unittests-tls_client_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-tls_client_unittests.obj `if test -f 'tls_client_unittests.cc'; then $(CYGPATH_W) 'tls_client_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/tls_client_unittests.cc'; fi` + +libhttp_unittests-url_unittests.o: url_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-url_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-url_unittests.Tpo -c -o libhttp_unittests-url_unittests.o `test -f 'url_unittests.cc' || echo '$(srcdir)/'`url_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-url_unittests.Tpo $(DEPDIR)/libhttp_unittests-url_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='url_unittests.cc' object='libhttp_unittests-url_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-url_unittests.o `test -f 'url_unittests.cc' || echo '$(srcdir)/'`url_unittests.cc + +libhttp_unittests-url_unittests.obj: url_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-url_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-url_unittests.Tpo -c -o libhttp_unittests-url_unittests.obj `if test -f 'url_unittests.cc'; then $(CYGPATH_W) 'url_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/url_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-url_unittests.Tpo $(DEPDIR)/libhttp_unittests-url_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='url_unittests.cc' object='libhttp_unittests-url_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-url_unittests.obj `if test -f 'url_unittests.cc'; then $(CYGPATH_W) 'url_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/url_unittests.cc'; fi` + +libhttp_unittests-client_mt_unittests.o: client_mt_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-client_mt_unittests.o -MD -MP -MF $(DEPDIR)/libhttp_unittests-client_mt_unittests.Tpo -c -o libhttp_unittests-client_mt_unittests.o `test -f 'client_mt_unittests.cc' || echo '$(srcdir)/'`client_mt_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-client_mt_unittests.Tpo $(DEPDIR)/libhttp_unittests-client_mt_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='client_mt_unittests.cc' object='libhttp_unittests-client_mt_unittests.o' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-client_mt_unittests.o `test -f 'client_mt_unittests.cc' || echo '$(srcdir)/'`client_mt_unittests.cc + +libhttp_unittests-client_mt_unittests.obj: client_mt_unittests.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -MT libhttp_unittests-client_mt_unittests.obj -MD -MP -MF $(DEPDIR)/libhttp_unittests-client_mt_unittests.Tpo -c -o libhttp_unittests-client_mt_unittests.obj `if test -f 'client_mt_unittests.cc'; then $(CYGPATH_W) 'client_mt_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/client_mt_unittests.cc'; fi` +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libhttp_unittests-client_mt_unittests.Tpo $(DEPDIR)/libhttp_unittests-client_mt_unittests.Po +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='client_mt_unittests.cc' object='libhttp_unittests-client_mt_unittests.obj' libtool=no @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libhttp_unittests_CPPFLAGS) $(CPPFLAGS) $(libhttp_unittests_CXXFLAGS) $(CXXFLAGS) -c -o libhttp_unittests-client_mt_unittests.obj `if test -f 'client_mt_unittests.cc'; then $(CYGPATH_W) 'client_mt_unittests.cc'; else $(CYGPATH_W) '$(srcdir)/client_mt_unittests.cc'; fi` + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs + +# This directory's subdirectories are mostly independent; you can cd +# into them and run 'make' without going through this Makefile. +# To change the values of 'make' variables: instead of editing Makefiles, +# (1) if the variable is set in 'config.status', edit 'config.status' +# (which will cause the Makefiles to be regenerated when you run 'make'); +# (2) otherwise, pass the desired values on the 'make' command line. +$(am__recursive_targets): + @fail=; \ + if $(am__make_keepgoing); then \ + failcom='fail=yes'; \ + else \ + failcom='exit 1'; \ + fi; \ + dot_seen=no; \ + target=`echo $@ | sed s/-recursive//`; \ + case "$@" in \ + distclean-* | maintainer-clean-*) list='$(DIST_SUBDIRS)' ;; \ + *) list='$(SUBDIRS)' ;; \ + esac; \ + for subdir in $$list; do \ + echo "Making $$target in $$subdir"; \ + if test "$$subdir" = "."; then \ + dot_seen=yes; \ + local_target="$$target-am"; \ + else \ + local_target="$$target"; \ + fi; \ + ($(am__cd) $$subdir && $(MAKE) $(AM_MAKEFLAGS) $$local_target) \ + || eval $$failcom; \ + done; \ + if test "$$dot_seen" = "no"; then \ + $(MAKE) $(AM_MAKEFLAGS) "$$target-am" || exit 1; \ + fi; test -z "$$fail" + +ID: $(am__tagged_files) + $(am__define_uniq_tagged_files); mkid -fID $$unique +tags: tags-recursive +TAGS: tags + +tags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + set x; \ + here=`pwd`; \ + if ($(ETAGS) --etags-include --version) >/dev/null 2>&1; then \ + include_option=--etags-include; \ + empty_fix=.; \ + else \ + include_option=--include; \ + empty_fix=; \ + fi; \ + list='$(SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + test ! -f $$subdir/TAGS || \ + set "$$@" "$$include_option=$$here/$$subdir/TAGS"; \ + fi; \ + done; \ + $(am__define_uniq_tagged_files); \ + shift; \ + if test -z "$(ETAGS_ARGS)$$*$$unique"; then :; else \ + test -n "$$unique" || unique=$$empty_fix; \ + if test $$# -gt 0; then \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + "$$@" $$unique; \ + else \ + $(ETAGS) $(ETAGSFLAGS) $(AM_ETAGSFLAGS) $(ETAGS_ARGS) \ + $$unique; \ + fi; \ + fi +ctags: ctags-recursive + +CTAGS: ctags +ctags-am: $(TAGS_DEPENDENCIES) $(am__tagged_files) + $(am__define_uniq_tagged_files); \ + test -z "$(CTAGS_ARGS)$$unique" \ + || $(CTAGS) $(CTAGSFLAGS) $(AM_CTAGSFLAGS) $(CTAGS_ARGS) \ + $$unique + +GTAGS: + here=`$(am__cd) $(top_builddir) && pwd` \ + && $(am__cd) $(top_srcdir) \ + && gtags -i $(GTAGS_ARGS) "$$here" +cscopelist: cscopelist-recursive + +cscopelist-am: $(am__tagged_files) + list='$(am__tagged_files)'; \ + case "$(srcdir)" in \ + [\\/]* | ?:[\\/]*) sdir="$(srcdir)" ;; \ + *) sdir=$(subdir)/$(srcdir) ;; \ + esac; \ + for i in $$list; do \ + if test -f "$$i"; then \ + echo "$(subdir)/$$i"; \ + else \ + echo "$$sdir/$$i"; \ + fi; \ + done >> $(top_builddir)/cscope.files + +distclean-tags: + -rm -f TAGS ID GTAGS GRTAGS GSYMS GPATH tags + +check-TESTS: $(TESTS) + @failed=0; all=0; xfail=0; xpass=0; skip=0; \ + srcdir=$(srcdir); export srcdir; \ + list=' $(TESTS) '; \ + $(am__tty_colors); \ + if test -n "$$list"; then \ + for tst in $$list; do \ + if test -f ./$$tst; then dir=./; \ + elif test -f $$tst; then dir=; \ + else dir="$(srcdir)/"; fi; \ + if $(TESTS_ENVIRONMENT) $${dir}$$tst $(AM_TESTS_FD_REDIRECT); then \ + all=`expr $$all + 1`; \ + case " $(XFAIL_TESTS) " in \ + *[\ \ ]$$tst[\ \ ]*) \ + xpass=`expr $$xpass + 1`; \ + failed=`expr $$failed + 1`; \ + col=$$red; res=XPASS; \ + ;; \ + *) \ + col=$$grn; res=PASS; \ + ;; \ + esac; \ + elif test $$? -ne 77; then \ + all=`expr $$all + 1`; \ + case " $(XFAIL_TESTS) " in \ + *[\ \ ]$$tst[\ \ ]*) \ + xfail=`expr $$xfail + 1`; \ + col=$$lgn; res=XFAIL; \ + ;; \ + *) \ + failed=`expr $$failed + 1`; \ + col=$$red; res=FAIL; \ + ;; \ + esac; \ + else \ + skip=`expr $$skip + 1`; \ + col=$$blu; res=SKIP; \ + fi; \ + echo "$${col}$$res$${std}: $$tst"; \ + done; \ + if test "$$all" -eq 1; then \ + tests="test"; \ + All=""; \ + else \ + tests="tests"; \ + All="All "; \ + fi; \ + if test "$$failed" -eq 0; then \ + if test "$$xfail" -eq 0; then \ + banner="$$All$$all $$tests passed"; \ + else \ + if test "$$xfail" -eq 1; then failures=failure; else failures=failures; fi; \ + banner="$$All$$all $$tests behaved as expected ($$xfail expected $$failures)"; \ + fi; \ + else \ + if test "$$xpass" -eq 0; then \ + banner="$$failed of $$all $$tests failed"; \ + else \ + if test "$$xpass" -eq 1; then passes=pass; else passes=passes; fi; \ + banner="$$failed of $$all $$tests did not behave as expected ($$xpass unexpected $$passes)"; \ + fi; \ + fi; \ + dashes="$$banner"; \ + skipped=""; \ + if test "$$skip" -ne 0; then \ + if test "$$skip" -eq 1; then \ + skipped="($$skip test was not run)"; \ + else \ + skipped="($$skip tests were not run)"; \ + fi; \ + test `echo "$$skipped" | wc -c` -le `echo "$$banner" | wc -c` || \ + dashes="$$skipped"; \ + fi; \ + report=""; \ + if test "$$failed" -ne 0 && test -n "$(PACKAGE_BUGREPORT)"; then \ + report="Please report to $(PACKAGE_BUGREPORT)"; \ + test `echo "$$report" | wc -c` -le `echo "$$banner" | wc -c` || \ + dashes="$$report"; \ + fi; \ + dashes=`echo "$$dashes" | sed s/./=/g`; \ + if test "$$failed" -eq 0; then \ + col="$$grn"; \ + else \ + col="$$red"; \ + fi; \ + echo "$${col}$$dashes$${std}"; \ + echo "$${col}$$banner$${std}"; \ + test -z "$$skipped" || echo "$${col}$$skipped$${std}"; \ + test -z "$$report" || echo "$${col}$$report$${std}"; \ + echo "$${col}$$dashes$${std}"; \ + test "$$failed" -eq 0; \ + else :; fi + +distdir: $(BUILT_SOURCES) + $(MAKE) $(AM_MAKEFLAGS) distdir-am + +distdir-am: $(DISTFILES) + @srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + topsrcdirstrip=`echo "$(top_srcdir)" | sed 's/[].[^$$\\*]/\\\\&/g'`; \ + list='$(DISTFILES)'; \ + dist_files=`for file in $$list; do echo $$file; done | \ + sed -e "s|^$$srcdirstrip/||;t" \ + -e "s|^$$topsrcdirstrip/|$(top_builddir)/|;t"`; \ + case $$dist_files in \ + */*) $(MKDIR_P) `echo "$$dist_files" | \ + sed '/\//!d;s|^|$(distdir)/|;s,/[^/]*$$,,' | \ + sort -u` ;; \ + esac; \ + for file in $$dist_files; do \ + if test -f $$file || test -d $$file; then d=.; else d=$(srcdir); fi; \ + if test -d $$d/$$file; then \ + dir=`echo "/$$file" | sed -e 's,/[^/]*$$,,'`; \ + if test -d "$(distdir)/$$file"; then \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + if test -d $(srcdir)/$$file && test $$d != $(srcdir); then \ + cp -fpR $(srcdir)/$$file "$(distdir)$$dir" || exit 1; \ + find "$(distdir)/$$file" -type d ! -perm -700 -exec chmod u+rwx {} \;; \ + fi; \ + cp -fpR $$d/$$file "$(distdir)$$dir" || exit 1; \ + else \ + test -f "$(distdir)/$$file" \ + || cp -p $$d/$$file "$(distdir)/$$file" \ + || exit 1; \ + fi; \ + done + @list='$(DIST_SUBDIRS)'; for subdir in $$list; do \ + if test "$$subdir" = .; then :; else \ + $(am__make_dryrun) \ + || test -d "$(distdir)/$$subdir" \ + || $(MKDIR_P) "$(distdir)/$$subdir" \ + || exit 1; \ + dir1=$$subdir; dir2="$(distdir)/$$subdir"; \ + $(am__relativize); \ + new_distdir=$$reldir; \ + dir1=$$subdir; dir2="$(top_distdir)"; \ + $(am__relativize); \ + new_top_distdir=$$reldir; \ + echo " (cd $$subdir && $(MAKE) $(AM_MAKEFLAGS) top_distdir="$$new_top_distdir" distdir="$$new_distdir" \\"; \ + echo " am__remove_distdir=: am__skip_length_check=: am__skip_mode_fix=: distdir)"; \ + ($(am__cd) $$subdir && \ + $(MAKE) $(AM_MAKEFLAGS) \ + top_distdir="$$new_top_distdir" \ + distdir="$$new_distdir" \ + am__remove_distdir=: \ + am__skip_length_check=: \ + am__skip_mode_fix=: \ + distdir) \ + || exit 1; \ + fi; \ + done +check-am: all-am + $(MAKE) $(AM_MAKEFLAGS) check-TESTS +check: check-recursive +all-am: Makefile $(PROGRAMS) +installdirs: installdirs-recursive +installdirs-am: +install: install-recursive +install-exec: install-exec-recursive +install-data: install-data-recursive +uninstall: uninstall-recursive + +install-am: all-am + @$(MAKE) $(AM_MAKEFLAGS) install-exec-am install-data-am + +installcheck: installcheck-recursive +install-strip: + if test -z '$(STRIP)'; then \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + install; \ + else \ + $(MAKE) $(AM_MAKEFLAGS) INSTALL_PROGRAM="$(INSTALL_STRIP_PROGRAM)" \ + install_sh_PROGRAM="$(INSTALL_STRIP_PROGRAM)" INSTALL_STRIP_FLAG=-s \ + "INSTALL_PROGRAM_ENV=STRIPPROG='$(STRIP)'" install; \ + fi +mostlyclean-generic: + +clean-generic: + -test -z "$(CLEANFILES)" || rm -f $(CLEANFILES) + +distclean-generic: + -test -z "$(CONFIG_CLEAN_FILES)" || rm -f $(CONFIG_CLEAN_FILES) + -test . = "$(srcdir)" || test -z "$(CONFIG_CLEAN_VPATH_FILES)" || rm -f $(CONFIG_CLEAN_VPATH_FILES) + +maintainer-clean-generic: + @echo "This command is intended for maintainers to use" + @echo "it deletes files that may require special tools to rebuild." +clean: clean-recursive + +clean-am: clean-generic clean-libtool clean-noinstPROGRAMS \ + mostlyclean-am + +distclean: distclean-recursive + -rm -f ./$(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-basic_auth_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-client_mt_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-connection_pool_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-date_time_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-http_header_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-post_request_json_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-post_request_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-request_parser_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-request_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-response_creator_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-response_json_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-response_parser_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-response_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-run_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-server_client_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-tls_client_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-tls_server_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-url_unittests.Po + -rm -f Makefile +distclean-am: clean-am distclean-compile distclean-generic \ + distclean-tags + +dvi: dvi-recursive + +dvi-am: + +html: html-recursive + +html-am: + +info: info-recursive + +info-am: + +install-data-am: + +install-dvi: install-dvi-recursive + +install-dvi-am: + +install-exec-am: + +install-html: install-html-recursive + +install-html-am: + +install-info: install-info-recursive + +install-info-am: + +install-man: + +install-pdf: install-pdf-recursive + +install-pdf-am: + +install-ps: install-ps-recursive + +install-ps-am: + +installcheck-am: + +maintainer-clean: maintainer-clean-recursive + -rm -f ./$(DEPDIR)/libhttp_unittests-basic_auth_config_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-basic_auth_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-client_mt_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-connection_pool_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-date_time_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-http_header_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-post_request_json_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-post_request_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-request_parser_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-request_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-response_creator_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-response_json_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-response_parser_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-response_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-run_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-server_client_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-tls_client_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-tls_server_unittests.Po + -rm -f ./$(DEPDIR)/libhttp_unittests-url_unittests.Po + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic + +mostlyclean: mostlyclean-recursive + +mostlyclean-am: mostlyclean-compile mostlyclean-generic \ + mostlyclean-libtool + +pdf: pdf-recursive + +pdf-am: + +ps: ps-recursive + +ps-am: + +uninstall-am: + +.MAKE: $(am__recursive_targets) check-am install-am install-strip + +.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am \ + am--depfiles check check-TESTS check-am clean clean-generic \ + clean-libtool clean-noinstPROGRAMS cscopelist-am ctags \ + ctags-am distclean distclean-compile distclean-generic \ + distclean-libtool distclean-tags distdir dvi dvi-am html \ + html-am info info-am install install-am install-data \ + install-data-am install-dvi install-dvi-am install-exec \ + install-exec-am install-html install-html-am install-info \ + install-info-am install-man install-pdf install-pdf-am \ + install-ps install-ps-am install-strip installcheck \ + installcheck-am installdirs installdirs-am maintainer-clean \ + maintainer-clean-generic mostlyclean mostlyclean-compile \ + mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \ + tags tags-am uninstall uninstall-am + +.PRECIOUS: Makefile + + +# Tell versions [3.59,3.63) of GNU make to not export all variables. +# Otherwise a system limit (for SysV at least) may be exceeded. +.NOEXPORT: diff --git a/src/lib/http/tests/basic_auth_config_unittests.cc b/src/lib/http/tests/basic_auth_config_unittests.cc new file mode 100644 index 0000000..5df2170 --- /dev/null +++ b/src/lib/http/tests/basic_auth_config_unittests.cc @@ -0,0 +1,540 @@ +// Copyright (C) 2020-2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <http/basic_auth_config.h> +#include <testutils/gtest_utils.h> +#include <testutils/test_to_element.h> +#include <gtest/gtest.h> + +using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::http; +using namespace isc::test; +using namespace std; + +namespace { + +string data_dir(DATA_DIR); + +// Test that basic auth client works as expected. +TEST(BasicHttpAuthClientTest, basic) { + // Create a client. + ConstElementPtr ctx = Element::fromJSON("{ \"foo\": \"bar\" }"); + BasicHttpAuthClient client("foo", "bar", ctx); + + // Check it. + EXPECT_EQ("foo", client.getUser()); + EXPECT_EQ("", client.getUserFile()); + EXPECT_EQ("bar", client.getPassword()); + EXPECT_EQ("", client.getPasswordFile()); + EXPECT_FALSE(client.getPasswordFileOnly()); + EXPECT_TRUE(ctx->equals(*client.getContext())); + + // Check toElement. + ElementPtr expected = Element::createMap(); + expected->set("user", Element::create(string("foo"))); + expected->set("password", Element::create(string("bar"))); + expected->set("user-context", ctx); + runToElementTest<BasicHttpAuthClient>(expected, client); +} + +// Test that basic auth client with files works as expected. +TEST(BasicHttpAuthClientTest, basicFiles) { + // Create a client. + ConstElementPtr ctx = Element::fromJSON("{ \"foo\": \"bar\" }"); + BasicHttpAuthClient client("", "foo", "", "bar", false, ctx); + + // Check it. + EXPECT_EQ("", client.getUser()); + EXPECT_EQ("foo", client.getUserFile()); + EXPECT_EQ("", client.getPassword()); + EXPECT_EQ("bar", client.getPasswordFile()); + EXPECT_FALSE(client.getPasswordFileOnly()); + EXPECT_TRUE(ctx->equals(*client.getContext())); + + // Check toElement. + ElementPtr expected = Element::createMap(); + expected->set("user-file", Element::create(string("foo"))); + expected->set("password-file", Element::create(string("bar"))); + expected->set("user-context", ctx); + runToElementTest<BasicHttpAuthClient>(expected, client); +} + +// Test that basic auth client with one file works as expected. +TEST(BasicHttpAuthClientTest, basicOneFile) { + // Create a client. + ConstElementPtr ctx = Element::fromJSON("{ \"foo\": \"bar\" }"); + BasicHttpAuthClient client("", "", "", "foobar", true, ctx); + + // Check it. + EXPECT_EQ("", client.getUser()); + EXPECT_EQ("", client.getUserFile()); + EXPECT_EQ("", client.getPassword()); + EXPECT_EQ("foobar", client.getPasswordFile()); + EXPECT_TRUE(client.getPasswordFileOnly()); + EXPECT_TRUE(ctx->equals(*client.getContext())); + + // Check toElement. + ElementPtr expected = Element::createMap(); + expected->set("password-file", Element::create(string("foobar"))); + expected->set("user-context", ctx); + runToElementTest<BasicHttpAuthClient>(expected, client); +} + +// Test that basic auth configuration works as expected. +TEST(BasicHttpAuthConfigTest, basic) { + // Create a configuration. + BasicHttpAuthConfig config; + + // Initial configuration is empty. + EXPECT_TRUE(config.empty()); + EXPECT_TRUE(config.getRealm().empty()); + EXPECT_TRUE(config.getDirectory().empty()); + EXPECT_TRUE(config.getClientList().empty()); + EXPECT_TRUE(config.getCredentialMap().empty()); + + // Set the realm, directory and user context. + EXPECT_NO_THROW(config.setRealm("my-realm")); + EXPECT_EQ("my-realm", config.getRealm()); + EXPECT_NO_THROW(config.setDirectory("/tmp")); + EXPECT_EQ("/tmp", config.getDirectory()); + ConstElementPtr horse = Element::fromJSON("{ \"value\": \"a horse\" }"); + EXPECT_NO_THROW(config.setContext(horse)); + EXPECT_TRUE(horse->equals(*config.getContext())); + + // Add rejects user id with embedded ':'. + EXPECT_THROW(config.add("foo:", "", "bar", ""), BadValue); + + // Add a client. + EXPECT_TRUE(config.empty()); + ConstElementPtr ctx = Element::fromJSON("{ \"foo\": \"bar\" }"); + EXPECT_NO_THROW(config.add("foo", "", "bar", "", false, ctx)); + EXPECT_FALSE(config.empty()); + + // Check the client. + ASSERT_EQ(1, config.getClientList().size()); + const BasicHttpAuthClient& client = config.getClientList().front(); + EXPECT_EQ("foo", client.getUser()); + EXPECT_EQ("bar", client.getPassword()); + EXPECT_TRUE(ctx->equals(*client.getContext())); + + // Check the credential. + ASSERT_NE(0, config.getCredentialMap().count("Zm9vOmJhcg==")); + string user; + EXPECT_NO_THROW(user = config.getCredentialMap().at("Zm9vOmJhcg==")); + EXPECT_EQ("foo", user); + + // Check toElement. + ElementPtr expected = Element::createMap(); + ElementPtr clients = Element::createList(); + ElementPtr elem = Element::createMap(); + elem->set("user", Element::create(string("foo"))); + elem->set("password", Element::create(string("bar"))); + elem->set("user-context", ctx); + clients->add(elem); + expected->set("type", Element::create(string("basic"))); + expected->set("realm", Element::create(string("my-realm"))); + expected->set("directory", Element::create(string("/tmp"))); + expected->set("user-context", horse); + expected->set("clients", clients); + runToElementTest<BasicHttpAuthConfig>(expected, config); + + // Add a second client and test it. + EXPECT_NO_THROW(config.add("test", "", "123\xa3", "")); + ASSERT_EQ(2, config.getClientList().size()); + EXPECT_EQ("foo", config.getClientList().front().getUser()); + EXPECT_EQ("test", config.getClientList().back().getUser()); + ASSERT_NE(0, config.getCredentialMap().count("dGVzdDoxMjPCow==")); + + // Check clear. + config.clear(); + EXPECT_TRUE(config.empty()); + expected->set("clients", Element::createList()); + runToElementTest<BasicHttpAuthConfig>(expected, config); + + // Add clients again. + EXPECT_NO_THROW(config.add("test", "", "123\xa3", "")); + EXPECT_NO_THROW(config.add("foo", "", "bar", "", false, ctx)); + + // Check that toElement keeps add order. + ElementPtr elem0 = Element::createMap(); + elem0->set("user", Element::create(string("test"))); + elem0->set("password", Element::create(string("123\xa3"))); + clients = Element::createList(); + clients->add(elem0); + clients->add(elem); + expected->set("clients", clients); + runToElementTest<BasicHttpAuthConfig>(expected, config); +} + +// Test that basic auth configuration with files works as expected. +TEST(BasicHttpAuthConfigTest, basicFiles) { + // Create a configuration. + BasicHttpAuthConfig config; + + // Set the realm, directory and user context. + EXPECT_NO_THROW(config.setRealm("my-realm")); + EXPECT_EQ("my-realm", config.getRealm()); + EXPECT_NO_THROW(config.setDirectory(data_dir)); + EXPECT_EQ(data_dir, config.getDirectory()); + ConstElementPtr horse = Element::fromJSON("{ \"value\": \"a horse\" }"); + EXPECT_NO_THROW(config.setContext(horse)); + EXPECT_TRUE(horse->equals(*config.getContext())); + + // ':' in user id check is done during parsing + + // Add a client. + EXPECT_TRUE(config.empty()); + ConstElementPtr ctx = Element::fromJSON("{ \"foo\": \"bar\" }"); + EXPECT_NO_THROW(config.add("foo", "", "", "hiddenp", false, ctx)); + EXPECT_FALSE(config.empty()); + + // Check the client. + ASSERT_EQ(1, config.getClientList().size()); + const BasicHttpAuthClient& client = config.getClientList().front(); + EXPECT_EQ("foo", client.getUser()); + EXPECT_EQ("", client.getUserFile()); + EXPECT_EQ("", client.getPassword()); + EXPECT_EQ("hiddenp", client.getPasswordFile()); + EXPECT_FALSE(client.getPasswordFileOnly()); + EXPECT_TRUE(ctx->equals(*client.getContext())); + + // Check toElement. + ElementPtr expected = Element::createMap(); + ElementPtr clients = Element::createList(); + ElementPtr elem = Element::createMap(); + elem->set("user", Element::create(string("foo"))); + elem->set("password-file", Element::create(string("hiddenp"))); + elem->set("user-context", ctx); + clients->add(elem); + expected->set("type", Element::create(string("basic"))); + expected->set("realm", Element::create(string("my-realm"))); + expected->set("directory", Element::create(data_dir)); + expected->set("user-context", horse); + expected->set("clients", clients); + runToElementTest<BasicHttpAuthConfig>(expected, config); + + // Add a second client and test it. + EXPECT_NO_THROW(config.add("", "hiddenu", "", "hiddenp")); + ASSERT_EQ(2, config.getClientList().size()); + EXPECT_EQ("foo", config.getClientList().front().getUser()); + EXPECT_EQ("hiddenu", config.getClientList().back().getUserFile()); + + // Check clear. + config.clear(); + EXPECT_TRUE(config.empty()); + expected->set("clients", Element::createList()); + runToElementTest<BasicHttpAuthConfig>(expected, config); + + // Add clients again. + EXPECT_NO_THROW(config.add("", "hiddenu", "", "hiddenp")); + EXPECT_NO_THROW(config.add("foo", "", "", "hiddenp", false, ctx)); + + // Check that toElement keeps add order. + ElementPtr elem0 = Element::createMap(); + elem0->set("user-file", Element::create(string("hiddenu"))); + elem0->set("password-file", Element::create(string("hiddenp"))); + clients = Element::createList(); + clients->add(elem0); + clients->add(elem); + expected->set("clients", clients); + runToElementTest<BasicHttpAuthConfig>(expected, config); +} + +// Test that basic auth configuration parses. +TEST(BasicHttpAuthConfigTest, parse) { + BasicHttpAuthConfig config; + ElementPtr cfg; + + // No config is accepted. + EXPECT_NO_THROW(config.parse(cfg)); + EXPECT_TRUE(config.empty()); + EXPECT_TRUE(config.getClientList().empty()); + EXPECT_TRUE(config.getCredentialMap().empty()); + ElementPtr expected = Element::createMap(); + expected->set("type", Element::create(string("basic"))); + expected->set("realm", Element::create(string(""))); + expected->set("directory", Element::create(string(""))); + expected->set("clients", Element::createList()); + runToElementTest<BasicHttpAuthConfig>(expected, config); + + // The config must be a map. + cfg = Element::createList(); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "authentication must be a map (:0:0)"); + + // The type must be present. + cfg = Element::createMap(); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "type is required in authentication (:0:0)"); + + // The type must be a string. + cfg->set("type", Element::create(true)); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "type must be a string (:0:0)"); + + // The type must be basic. + cfg->set("type", Element::create(string("foobar"))); + string errmsg = "only basic HTTP authentication is supported: type is "; + errmsg += "'foobar' not 'basic' (:0:0)"; + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, errmsg); + cfg->set("type", Element::create(string("basic"))); + EXPECT_NO_THROW(config.parse(cfg)); + + // The realm must be a string. + cfg->set("realm", Element::createList()); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "realm must be a string (:0:0)"); + cfg->set("realm", Element::create(string("my-realm"))); + EXPECT_NO_THROW(config.parse(cfg)); + + // The directory must be a string. + cfg->set("directory", Element::createMap()); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "directory must be a string (:0:0)"); + cfg->set("directory", Element::create(data_dir)); + EXPECT_NO_THROW(config.parse(cfg)); + + // The user context must be a map. + ElementPtr ctx = Element::createList(); + cfg->set("user-context", ctx); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user-context must be a map (:0:0)"); + ctx = Element::fromJSON("{ \"value\": \"a horse\" }"); + cfg->set("user-context", ctx); + EXPECT_NO_THROW(config.parse(cfg)); + + // Clients must be a list. + ElementPtr clients_cfg = Element::createMap(); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "clients must be a list (:0:0)"); + + // The client config must be a map. + clients_cfg = Element::createList(); + ElementPtr client_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "clients items must be maps (:0:0)"); + + // The user parameter is mandatory in client config + // without a password file. + client_cfg = Element::createMap(); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user is required in clients items (:0:0)"); + + // The user parameter must be a string. + ElementPtr user_cfg = Element::create(1); + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user must be a string (:0:0)"); + + // The user parameter must not be empty. + user_cfg = Element::create(string("")); + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user must not be empty (:0:0)"); + + // The user parameter must not contain ':'. + user_cfg = Element::create(string("foo:bar")); + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user must not contain a ':': 'foo:bar' (:0:0)"); + + // The user-file parameter must be a string. + ElementPtr user_file_cfg = Element::create(1); + client_cfg = Element::createMap(); + client_cfg->set("user-file", user_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user-file must be a string (:0:0)"); + + // The user and user-file parameters are incompatible. + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + client_cfg->set("user-file", user_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user (:0:0) and user-file (:0:0) are " + "mutually exclusive"); + + // The user-file parameter must not be empty. + user_file_cfg = Element::create(string("empty")); + client_cfg = Element::createMap(); + client_cfg->set("user-file", user_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user must not be empty from user-file " + "'empty' (:0:0)"); + + // The user-file parameter must not contain ':'. + user_file_cfg = Element::create(string("hiddens")); + client_cfg = Element::createMap(); + client_cfg->set("user-file", user_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user must not contain a ':' from user-file " + "'hiddens' (:0:0)"); + + // Password is not required. + user_cfg = Element::create(string("foo")); + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_NO_THROW(config.parse(cfg)); + ASSERT_EQ(1, config.getClientList().size()); + EXPECT_EQ("", config.getClientList().front().getPassword()); + config.clear(); + + // The password parameter must be a string. + ElementPtr password_cfg = Element::create(1); + client_cfg = Element::createMap(); + client_cfg->set("password", password_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "password must be a string (:0:0)"); + + // Empty password is accepted. + password_cfg = Element::create(string("")); + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + client_cfg->set("password", password_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_NO_THROW(config.parse(cfg)); + ASSERT_EQ(1, config.getClientList().size()); + EXPECT_EQ("", config.getClientList().front().getPassword()); + config.clear(); + + // The password-file parameter must be a string. + ElementPtr password_file_cfg = Element::create(1); + client_cfg = Element::createMap(); + // user is not required when password-file is here. + client_cfg->set("password-file", password_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "password-file must be a string (:0:0)"); + + // The password and password-file parameters are incompatible. + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + client_cfg->set("password", password_cfg); + client_cfg->set("password-file", password_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "password (:0:0) and password-file (:0:0) are " + "mutually exclusive"); + + // Empty password-file is accepted. + password_file_cfg = Element::create(string("empty")); + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + client_cfg->set("password-file", password_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_NO_THROW(config.parse(cfg)); + ASSERT_EQ(1, config.getClientList().size()); + EXPECT_EQ("", config.getClientList().front().getPassword()); + config.clear(); + + // password-file is enough. + password_file_cfg = Element::create(string("hiddens")); + client_cfg = Element::createMap(); + client_cfg->set("password-file", password_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_NO_THROW(config.parse(cfg)); + ASSERT_EQ(1, config.getClientList().size()); + EXPECT_EQ("test", config.getClientList().front().getPassword()); + config.clear(); + + // password-file only requires a ':' in the content. + password_file_cfg = Element::create(string("hiddenp")); + client_cfg = Element::createMap(); + client_cfg->set("password-file", password_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "can't find the user id part in password-file " + "'hiddenp' (:0:0)"); + + // User context must be a map. + password_cfg = Element::create(string("bar")); + ctx = Element::createList(); + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + client_cfg->set("password", password_cfg); + client_cfg->set("user-context", ctx); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_THROW_MSG(config.parse(cfg), DhcpConfigError, + "user-context must be a map (:0:0)"); + + // Check a working not empty config. + ctx = Element::fromJSON("{ \"foo\": \"bar\" }"); + client_cfg = Element::createMap(); + client_cfg->set("user", user_cfg); + client_cfg->set("password", password_cfg); + client_cfg->set("user-context", ctx); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_NO_THROW(config.parse(cfg)); + runToElementTest<BasicHttpAuthConfig>(cfg, config); + + // Check a working not empty config with files. + config.clear(); + client_cfg = Element::createMap(); + user_file_cfg = Element::create(string("hiddenu")); + client_cfg->set("user-file", user_file_cfg); + client_cfg->set("password-file", password_file_cfg); + clients_cfg = Element::createList(); + clients_cfg->add(client_cfg); + cfg->set("clients", clients_cfg); + EXPECT_NO_THROW(config.parse(cfg)); + runToElementTest<BasicHttpAuthConfig>(cfg, config); +} + +} // end of anonymous namespace diff --git a/src/lib/http/tests/basic_auth_unittests.cc b/src/lib/http/tests/basic_auth_unittests.cc new file mode 100644 index 0000000..1c2693d --- /dev/null +++ b/src/lib/http/tests/basic_auth_unittests.cc @@ -0,0 +1,65 @@ +// Copyright (C) 2020 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <http/basic_auth.h> +#include <gtest/gtest.h> + +using namespace isc; +using namespace isc::http; + +namespace { + +// Test that user name with a colon is rejected. +TEST(BasicHttpAuthTest, userColon) { + BasicHttpAuthPtr basic_auth; + EXPECT_THROW(basic_auth.reset(new BasicHttpAuth("foo:bar", "")), BadValue); +} + +// Test that secret without a colon is rejected. +TEST(BasicHttpAuthTest, secretNoColon) { + BasicHttpAuthPtr basic_auth; + EXPECT_THROW(basic_auth.reset(new BasicHttpAuth("foo-bar")), BadValue); +} + +// Test that valid user and password work. +TEST(BasicHttpAuthTest, user) { + BasicHttpAuthPtr basic_auth; + EXPECT_NO_THROW(basic_auth.reset(new BasicHttpAuth("foo", "bar"))); + ASSERT_TRUE(basic_auth); + EXPECT_EQ("foo:bar", basic_auth->getSecret()); + EXPECT_EQ("Zm9vOmJhcg==", basic_auth->getCredential()); +} + +// Test that valid secret work. +TEST(BasicHttpAuthTest, secret) { + BasicHttpAuthPtr basic_auth; + EXPECT_NO_THROW(basic_auth.reset(new BasicHttpAuth("foo:bar"))); + ASSERT_TRUE(basic_auth); + EXPECT_EQ("foo:bar", basic_auth->getSecret()); + EXPECT_EQ("Zm9vOmJhcg==", basic_auth->getCredential()); +} + +// Test that secret is encoded in UTF-8. +TEST(BasicHttpAuthTest, utf8) { + BasicHttpAuthPtr basic_auth; + EXPECT_NO_THROW(basic_auth.reset(new BasicHttpAuth("foo\n", "b\ar"))); + ASSERT_TRUE(basic_auth); + EXPECT_EQ("foo\n:b\ar", basic_auth->getSecret()); + EXPECT_EQ("Zm9vCjpiB3I=", basic_auth->getCredential()); +} + +// Test that a header context for basic HTTP authentication can be created. +TEST(BasicHttpAuthTest, headerContext) { + BasicHttpAuthPtr basic_auth; + EXPECT_NO_THROW(basic_auth.reset(new BasicHttpAuth("foo", "bar"))); + ASSERT_TRUE(basic_auth); + BasicAuthHttpHeaderContext ctx(*basic_auth); + EXPECT_EQ("Authorization", ctx.name_); + EXPECT_EQ("Basic Zm9vOmJhcg==", ctx.value_); +} + +} // end of anonymous namespace diff --git a/src/lib/http/tests/client_mt_unittests.cc b/src/lib/http/tests/client_mt_unittests.cc new file mode 100644 index 0000000..ef04c28 --- /dev/null +++ b/src/lib/http/tests/client_mt_unittests.cc @@ -0,0 +1,1042 @@ +// Copyright (C) 2021-2023 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <asiolink/asio_wrapper.h> +#include <asiolink/interval_timer.h> +#include <cc/data.h> +#include <http/client.h> +#include <http/listener.h> +#include <http/post_request_json.h> +#include <http/response_creator.h> +#include <http/response_creator_factory.h> +#include <http/response_json.h> +#include <http/url.h> +#include <util/multi_threading_mgr.h> +#include <testutils/gtest_utils.h> + +#include <boost/asio/ip/tcp.hpp> +#include <boost/pointer_cast.hpp> +#include <gtest/gtest.h> + +#include <functional> +#include <sstream> +#include <string> + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::data; +using namespace isc::http; +using namespace isc::util; +namespace ph = std::placeholders; + +namespace { + +/// @brief IP address to which HTTP service is bound. +const std::string SERVER_ADDRESS = "127.0.0.1"; + +/// @brief Port number to which HTTP service is bound. +const unsigned short SERVER_PORT = 18123; + +/// @brief Test timeout (ms). +const long TEST_TIMEOUT = 10000; + +/// @brief Container request/response pair handled by a given thread. +struct ClientRR { + /// @brief Thread id of the client thread handling the request as a string. + std::string thread_id_; + + /// @brief HTTP request submitted by the client thread. + PostHttpRequestJsonPtr request_; + + /// @brief HTTP response received by the client thread. + HttpResponseJsonPtr response_; +}; + +/// @brief Pointer to a ClientRR instance. +typedef boost::shared_ptr<ClientRR> ClientRRPtr; + +/// @brief Implementation of the @ref HttpResponseCreator. +/// +/// Creates a response to a request containing body content +/// as follows: +/// +/// ``` +/// { "sequence" : nnnn } +/// ``` +/// +/// The response will include the sequence number of the request +/// as well as the server port passed into the creator's constructor: +/// +/// ``` +/// { "sequence": nnnn, "server-port": xxxx } +/// ``` +class TestHttpResponseCreator : public HttpResponseCreator { +public: + /// @brief Constructor + /// + /// @param server_port integer value the server listens upon, it is + /// echoed back in responses as "server-port". + TestHttpResponseCreator(uint16_t server_port) + : server_port_(server_port) { } + + /// @brief Create a new request. + /// + /// @return Pointer to the new instance of the @ref HttpRequest. + virtual HttpRequestPtr + createNewHttpRequest() const { + return (HttpRequestPtr(new PostHttpRequestJson())); + } + +private: + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @param status_code status code to include in the response. + /// + /// @return Pointer to the generated HTTP response. + virtual HttpResponsePtr + createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const { + // The request hasn't been finalized so the request object + // doesn't contain any information about the HTTP version number + // used. But, the context should have this data (assuming the + // HTTP version is parsed OK). + HttpVersion http_version(request->context()->http_version_major_, + request->context()->http_version_minor_); + // This will generate the response holding JSON content. + HttpResponseJsonPtr response(new HttpResponseJson(http_version, status_code)); + response->finalize(); + return (response); + } + + /// @brief Creates HTTP response. + /// + /// Generates a response which echoes the requests sequence + /// number as well as the creator's server port value. Responses + /// should appear as follows: + /// + /// ``` + /// { "sequence" : nnnn } + /// ``` + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP OK response with no content. + virtual HttpResponsePtr + createDynamicHttpResponse(HttpRequestPtr request) { + // Request must always be JSON. + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast<PostHttpRequestJson>(request); + if (!request_json) { + return (createStockHttpResponse(request, HttpStatusCode::BAD_REQUEST)); + } + + // Extract the sequence from the request. + ConstElementPtr sequence = request_json->getJsonElement("sequence"); + if (!sequence) { + return (createStockHttpResponse(request, HttpStatusCode::BAD_REQUEST)); + } + + // Create the response. + HttpResponseJsonPtr response(new HttpResponseJson(request->getHttpVersion(), + HttpStatusCode::OK)); + // Construct the body. + ElementPtr body = Element::createMap(); + body->set("server-port", Element::create(server_port_)); + body->set("sequence", sequence); + + // Echo request body back in the response. + response->setBodyAsJson(body); + + response->finalize(); + return (response); + } + + /// @brief Port upon which this creator's server is listening. + /// + /// The intent is to use the value to determine which server generated + /// a given response. + uint16_t server_port_; +}; + +/// @brief Implementation of the test @ref HttpResponseCreatorFactory. +/// +/// This factory class creates @ref TestHttpResponseCreator instances. +class TestHttpResponseCreatorFactory : public HttpResponseCreatorFactory { +public: + + /// @brief Constructor + /// + /// @param server_port port upon with the server is listening. This + /// value will be included in responses such that each response + /// can be attributed to a specific server. + TestHttpResponseCreatorFactory(uint16_t server_port) + : server_port_(server_port) {}; + + /// @brief Creates @ref TestHttpResponseCreator instance. + virtual HttpResponseCreatorPtr create() const { + HttpResponseCreatorPtr response_creator(new TestHttpResponseCreator(server_port_)); + return (response_creator); + } + + /// @brief Port upon which this factory's server is listening. + /// + /// The intent is to use the value to determine which server generated + /// a given response. + uint16_t server_port_; +}; + +/// @brief Test fixture class for testing threading modes of HTTP client. +class MultiThreadingHttpClientTest : public ::testing::Test { +public: + + /// @brief Constructor. + MultiThreadingHttpClientTest() + : io_service_(), client_(), listener_(), factory_(), listeners_(), factories_(), + test_timer_(io_service_), num_threads_(0), num_batches_(0), num_listeners_(0), + expected_requests_(0), num_in_progress_(0), num_finished_(0), paused_(false), + pause_cnt_(0) { + test_timer_.setup(std::bind(&MultiThreadingHttpClientTest::timeoutHandler, this, true), + TEST_TIMEOUT, IntervalTimer::ONE_SHOT); + MultiThreadingMgr::instance().setMode(true); + } + + /// @brief Destructor. + ~MultiThreadingHttpClientTest() { + // Stop the client. + if (client_) { + client_->stop(); + } + + // Stop all listeners. + for (const auto& listener : listeners_) { + listener->stop(); + } + + MultiThreadingMgr::instance().setMode(false); + } + + /// @brief Callback function to invoke upon test timeout. + /// + /// It stops the IO service and reports test timeout. + /// + /// @param fail_on_timeout Specifies if test failure should be reported. + void timeoutHandler(const bool fail_on_timeout) { + if (fail_on_timeout) { + ADD_FAILURE() << "Timeout occurred while running the test!"; + } + io_service_.stop(); + } + + /// @brief Runs the test's IOService until the desired number of requests + /// have been carried out or the test fails. + void runIOService(size_t request_limit) { + while (getRRCount() < request_limit) { + // Always call reset() before we call run(); + io_service_.restart(); + + // Run until a client stops the service. + io_service_.run(); + } + } + + /// @brief Creates an HTTP request with JSON body. + /// + /// It includes a JSON parameter with a specified value. + /// + /// @param parameter_name JSON parameter to be included. + /// @param value JSON parameter value. + /// @param version HTTP version to be used. Default is HTTP/1.1. + template<typename ValueType> + PostHttpRequestJsonPtr createRequest(const std::string& parameter_name, + const ValueType& value, + const HttpVersion& version = HttpVersion(1, 1)) { + // Create POST request with JSON body. + PostHttpRequestJsonPtr request(new PostHttpRequestJson(HttpRequest::Method::HTTP_POST, + "/boo", version)); + // Body is a map with a specified parameter included. + ElementPtr body = Element::createMap(); + body->set(parameter_name, Element::create(value)); + request->setBodyAsJson(body); + try { + request->finalize(); + } catch (const std::exception& ex) { + ADD_FAILURE() << "failed to create request: " << ex.what(); + } + + return (request); + } + + /// @brief Test that worker threads are not permitted to change thread pool + /// state. + void testIllegalThreadPoolActions() { + ASSERT_THROW(client_->start(), MultiThreadingInvalidOperation); + ASSERT_THROW(client_->pause(), MultiThreadingInvalidOperation); + ASSERT_THROW(client_->resume(), MultiThreadingInvalidOperation); + } + + /// @brief Initiates a single HTTP request. + /// + /// Constructs an HTTP post whose body is a JSON map containing a + /// single integer element, "sequence". + /// + /// The request completion handler will block each requesting thread + /// until the number of in-progress threads reaches the number of + /// threads in the pool. At that point, the handler will unblock + /// until all threads have finished preparing their response and are + /// ready to return. The handler will then notify all pending threads + /// and invoke stop() on the test's main IO service thread. + /// + /// @param sequence value for the integer element, "sequence", + /// to send in the request. + void startRequest(int sequence, int port_offset = 0) { + // Create the URL on which the server can be reached. + std::stringstream ss; + ss << "http://" << SERVER_ADDRESS << ":" << (SERVER_PORT + port_offset); + Url url(ss.str()); + + // Initiate request to the server. + PostHttpRequestJsonPtr request_json = createRequest("sequence", sequence); + HttpResponseJsonPtr response_json = boost::make_shared<HttpResponseJson>(); + ASSERT_NO_THROW(client_->asyncSendRequest(url, TlsContextPtr(), + request_json, response_json, + [this, request_json, response_json](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + // Bail on an error. + ASSERT_FALSE(ec) << "asyncSendRequest failed, ec: " << ec; + + // Wait here until we have as many in progress as we have threads. + { + std::unique_lock<std::mutex> lck(test_mutex_); + ++num_in_progress_; + if (num_threads_ == 0 || num_in_progress_ == num_threads_) { + // Everybody has one, let's go. + num_finished_ = 0; + test_cv_.notify_all(); + } else { + // I'm ready but others aren't wait here. + bool ret = test_cv_.wait_for(lck, std::chrono::seconds(10), + [&]() { return (num_in_progress_ == num_threads_); }); + if (!ret) { + ADD_FAILURE() << "clients failed to start work"; + } + } + } + + // If running on multiple threads, threads should be prohibited from + // changing the thread pool state. + if (num_threads_) { + testIllegalThreadPoolActions(); + } + + // Get stringified thread-id. + std::stringstream ss; + ss << std::this_thread::get_id(); + + // Create the ClientRR. + ClientRRPtr clientRR(new ClientRR()); + clientRR->thread_id_ = ss.str(); + clientRR->request_ = request_json; + clientRR->response_ = response_json; + + // Wait here until we have as many ready to finish as we have threads. + { + std::unique_lock<std::mutex> lck(test_mutex_); + ++num_finished_; + clientRRs_.push_back(clientRR); + if (num_threads_ == 0 || num_finished_ == num_threads_) { + // We're all done, notify the others and finish. + num_in_progress_ = 0; + test_cv_.notify_all(); + // Stop the test's IOService. + io_service_.stop(); + } else { + // I'm done but others aren't wait here. + bool ret = test_cv_.wait_for(lck, std::chrono::seconds(10), + [&]() { return (num_finished_ == num_threads_); }); + if (!ret) { + ADD_FAILURE() << "clients failed to finish work"; + } + } + } + })); + } + + /// @brief Initiates a single HTTP request. + /// + /// Constructs an HTTP post whose body is a JSON map containing a + /// single integer element, "sequence". + /// + /// The request completion handler simply constructs the response, + /// and adds it the list of completed request/responses. If the + /// number of completed requests has reached the expected number + /// it stops the test IOService. + /// + /// @param sequence value for the integer element, "sequence", + /// to send in the request. + void startRequestSimple(int sequence, int port_offset = 0) { + // Create the URL on which the server can be reached. + std::stringstream ss; + ss << "http://" << SERVER_ADDRESS << ":" << (SERVER_PORT + port_offset); + Url url(ss.str()); + + // Initiate request to the server. + PostHttpRequestJsonPtr request_json = createRequest("sequence", sequence); + HttpResponseJsonPtr response_json = boost::make_shared<HttpResponseJson>(); + ASSERT_NO_THROW(client_->asyncSendRequest(url, TlsContextPtr(), + request_json, response_json, + [this, request_json, response_json](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + // Bail on an error. + ASSERT_FALSE(ec) << "asyncSendRequest failed, ec: " << ec; + + // Get stringified thread-id. + std::stringstream ss; + ss << std::this_thread::get_id(); + + // Create the ClientRR. + ClientRRPtr clientRR(new ClientRR()); + clientRR->thread_id_ = ss.str(); + clientRR->request_ = request_json; + clientRR->response_ = response_json; + + { + std::unique_lock<std::mutex> lck(test_mutex_); + clientRRs_.push_back(clientRR); + ++num_finished_; + if ((num_finished_ >= expected_requests_) && !io_service_.stopped()) { + io_service_.stop(); + } + } + + })); + } + + /// @brief Carries out HTTP requests via HttpClient to HTTP listener(s). + /// + /// This function creates one HttpClient with the given number + /// of threads and then the given number of HttpListeners. It then + /// initiates the given number of request batches where each batch + /// contains one request per thread per listener. + /// + /// Then it iteratively runs the test's IOService until all + /// the requests have been responded to, an error occurs, or the + /// test times out. + /// + /// Each request carries a single integer element, "sequence", which + /// uniquely identifies the request. Each response is expected to + /// contain this value echoed back along with the listener's server + /// port number. Thus each response can be matched to it's request + /// and to the listener that handled the request. + /// + /// After all requests have been conducted, the function verifies + /// that: + /// + /// 1. The number of requests conducted is correct + /// 2. The sequence numbers in request-response pairs match + /// 3. Each client thread handled the same number of requests + /// 4. Each listener handled the same number of requests + /// + /// @param num_threads number of threads the HttpClient should use. + /// A value of 0 puts the HttpClient in single-threaded mode. + /// @param num_batches number of batches of requests that should be + /// conducted. + /// @param num_listeners number of HttpListeners to create. Defaults + /// to 1. + void threadRequestAndReceive(size_t num_threads, size_t num_batches, + size_t num_listeners = 1) { + ASSERT_TRUE(num_batches); + ASSERT_TRUE(num_listeners); + num_threads_ = num_threads; + num_batches_ = num_batches; + num_listeners_ = num_listeners; + + // Client in ST is, in effect, 1 thread. + size_t effective_threads = (num_threads_ == 0 ? 1 : num_threads_); + + // Calculate the expected number of requests. + expected_requests_ = (num_batches_ * num_listeners_ * effective_threads); + + for (auto i = 0; i < num_listeners_; ++i) { + // Make a factory + HttpResponseCreatorFactoryPtr factory(new TestHttpResponseCreatorFactory(SERVER_PORT + i)); + factories_.push_back(factory); + + // Need to create a Listener on + HttpListenerPtr listener(new HttpListener(io_service_, + IOAddress(SERVER_ADDRESS), (SERVER_PORT + i), + TlsContextPtr(), factory, + HttpListener::RequestTimeout(10000), + HttpListener::IdleTimeout(10000))); + listeners_.push_back(listener); + + // Start the server. + ASSERT_NO_THROW(listener->start()); + } + + // Create an MT client with num_threads + ASSERT_NO_THROW_LOG(client_.reset(new HttpClient(io_service_, + num_threads ? true : false, + num_threads, true))); + ASSERT_TRUE(client_); + + if (num_threads_ == 0) { + // If we single-threaded client should not have it's own IOService. + ASSERT_FALSE(client_->getThreadIOService()); + } else { + // If we multi-threaded client should have it's own IOService. + ASSERT_TRUE(client_->getThreadIOService()); + } + + // Start the requisite number of requests: + // batch * listeners * threads. + int sequence = 0; + for (auto b = 0; b < num_batches; ++b) { + for (auto l = 0; l < num_listeners_; ++l) { + for (auto t = 0; t < effective_threads; ++t) { + startRequest(++sequence, l); + } + } + } + + client_->start(); + + // Verify the pool size and number of threads are as expected. + ASSERT_EQ(client_->getThreadPoolSize(), num_threads); + ASSERT_EQ(client_->getThreadCount(), num_threads); + + // Loop until the clients are done, an error occurs, or the time runs out. + runIOService(expected_requests_); + + // Client should stop without issue. + ASSERT_NO_THROW(client_->stop()); + + // Listeners should stop without issue. + for (const auto& listener : listeners_) { + ASSERT_NO_THROW(listener->stop()); + } + + // We should have a response for each request. + ASSERT_EQ(getRRCount(), expected_requests_); + + // Create a map to track number of responses for each client thread. + std::map<std::string, int> responses_per_thread; + + // Create a map to track number of responses for each listener port. + std::map<uint16_t, int> responses_per_listener; + + // Get the stringified thread-id of the test's main thread. + std::stringstream ss; + ss << std::this_thread::get_id(); + std::string main_thread_id = ss.str(); + + // Iterate over the client request/response pairs. + for (auto const& clientRR : clientRRs_) { + // Make sure it's whole. + ASSERT_FALSE(clientRR->thread_id_.empty()); + ASSERT_TRUE(clientRR->request_); + ASSERT_TRUE(clientRR->response_); + + // Request should contain an integer sequence number. + int request_sequence; + ConstElementPtr sequence = clientRR->request_->getJsonElement("sequence"); + ASSERT_TRUE(sequence); + ASSERT_NO_THROW(request_sequence = sequence->intValue()); + + // Response should contain an integer sequence number. + int response_sequence; + sequence = clientRR->response_->getJsonElement("sequence"); + ASSERT_TRUE(sequence); + ASSERT_NO_THROW(response_sequence = sequence->intValue()); + + // Request and Response sequence numbers should match. + ASSERT_EQ(request_sequence, response_sequence); + + ConstElementPtr server_port_elem = clientRR->response_->getJsonElement("server-port"); + ASSERT_TRUE(server_port_elem); + uint16_t server_port = server_port_elem->intValue(); + + if (num_threads_ == 0) { + // For ST mode thread id should always be the main thread. + ASSERT_EQ(clientRR->thread_id_, main_thread_id); + } else { + // For MT mode the thread id should never be the main thread. + ASSERT_NE(clientRR->thread_id_, main_thread_id); + } + + // Bump the response count for the given client thread-id. + auto rit = responses_per_thread.find(clientRR->thread_id_); + if (rit != responses_per_thread.end()) { + responses_per_thread[clientRR->thread_id_] = rit->second + 1; + } else { + responses_per_thread[clientRR->thread_id_] = 1; + } + + // Bump the response count for the given server port. + auto lit = responses_per_listener.find(server_port); + if (lit != responses_per_listener.end()) { + responses_per_listener[server_port] = lit->second + 1; + } else { + responses_per_listener[server_port] = 1; + } + } + + // Make sure that all client threads received responses. + ASSERT_EQ(responses_per_thread.size(), effective_threads); + + // Make sure that each client thread received the same number of responses. + for (auto const& it : responses_per_thread) { + EXPECT_EQ(it.second, (num_batches_ * num_listeners_)) + << "thread-id: " << it.first + << ", responses: " << it.second << std::endl; + } + + // Make sure that all listeners generated responses. + ASSERT_EQ(responses_per_listener.size(), num_listeners_); + + // Make sure Each listener generated the same number of responses. + for (auto const& it : responses_per_listener) { + EXPECT_EQ(it.second, (num_batches_ * effective_threads)) + << "server-port: " << it.first + << ", responses: " << it.second << std::endl; + } + } + + /// @brief Verifies the client can be paused and resumed repeatedly + /// while doing multi-threaded work. + /// + /// @param num_threads number of threads the HttpClient should use. + /// Must be greater than zero, this test does not make sense for a + /// single threaded client. + /// @param num_batches number of batches of requests that should be + /// conducted. + /// @param num_listeners number of HttpListeners to create. + /// @param num_pauses number of pauses to conduct. + void workPauseResumeShutdown(size_t num_threads, size_t num_batches, + size_t num_listeners, size_t num_pauses) { + ASSERT_TRUE(num_threads); + ASSERT_TRUE(num_batches); + ASSERT_TRUE(num_listeners); + num_threads_ = num_threads; + num_batches_ = num_batches; + num_listeners_ = num_listeners; + + // Calculate the total expected number of requests. + size_t total_requests = (num_batches_ * num_listeners_ * num_threads_); + + // Create the listeners. + for (auto i = 0; i < num_listeners_; ++i) { + // Make a factory + HttpResponseCreatorFactoryPtr factory(new TestHttpResponseCreatorFactory(SERVER_PORT + i)); + factories_.push_back(factory); + + // Need to create a Listener on + HttpListenerPtr listener(new HttpListener(io_service_, + IOAddress(SERVER_ADDRESS), (SERVER_PORT + i), + TlsContextPtr(), factory, + HttpListener::RequestTimeout(10000), + HttpListener::IdleTimeout(10000))); + listeners_.push_back(listener); + + // Start the server. + ASSERT_NO_THROW(listener->start()); + } + + // Create an instant start, MT client with num_threads + ASSERT_NO_THROW_LOG(client_.reset(new HttpClient(io_service_, true, num_threads, true))); + ASSERT_TRUE(client_); + + // Start the requisite number of requests: + // batch * listeners * threads. + int sequence = 0; + for (auto b = 0; b < num_batches; ++b) { + for (auto l = 0; l < num_listeners_; ++l) { + for (auto t = 0; t < num_threads_; ++t) { + startRequestSimple(++sequence, l); + } + } + } + + client_->start(); + + // Client should be running. Check convenience functions. + ASSERT_TRUE(client_->isRunning()); + ASSERT_FALSE(client_->isPaused()); + ASSERT_FALSE(client_->isStopped()); + + // Verify the pool size and number of threads are as expected. + ASSERT_EQ(client_->getThreadPoolSize(), num_threads); + ASSERT_EQ(client_->getThreadCount(), num_threads); + + size_t rr_count = 0; + while (rr_count < total_requests) { + size_t request_limit = (pause_cnt_ < num_pauses ? + (rr_count + ((total_requests - rr_count) / num_pauses)) + : total_requests); + + // Run test IOService until we hit the limit. + runIOService(request_limit); + + // If we've done all our pauses we should be through. + if (pause_cnt_ == num_pauses) { + break; + } + + // Pause the client. + ASSERT_NO_THROW(client_->pause()); + ASSERT_TRUE(client_->isPaused()); + ++pause_cnt_; + + // Check our progress. + rr_count = getRRCount(); + ASSERT_GE(rr_count, request_limit); + + // Resume the client. + ASSERT_NO_THROW(client_->resume()); + ASSERT_TRUE(client_->isRunning()); + } + + // Client should stop without issue. + ASSERT_NO_THROW(client_->stop()); + ASSERT_TRUE(client_->isStopped()); + + // We should have finished all our requests. + ASSERT_EQ(getRRCount(), total_requests); + + // Stopping again should be harmless. + ASSERT_NO_THROW(client_->stop()); + + // Listeners should stop without issue. + for (const auto& listener : listeners_) { + ASSERT_NO_THROW(listener->stop()); + } + + // Get the stringified thread-id of the test's main thread. + std::stringstream ss; + ss << std::this_thread::get_id(); + std::string main_thread_id = ss.str(); + + // Tracks the number for requests fulfilled by main thread. + size_t worked_by_main = 0; + + // Iterate over the client request/response pairs. + for (auto const& clientRR : clientRRs_) { + // Make sure it's whole. + ASSERT_FALSE(clientRR->thread_id_.empty()); + ASSERT_TRUE(clientRR->request_); + ASSERT_TRUE(clientRR->response_); + + // Request should contain an integer sequence number. + int request_sequence; + ConstElementPtr sequence = clientRR->request_->getJsonElement("sequence"); + ASSERT_TRUE(sequence); + ASSERT_NO_THROW(request_sequence = sequence->intValue()); + + // Response should contain an integer sequence number. + int response_sequence; + sequence = clientRR->response_->getJsonElement("sequence"); + ASSERT_TRUE(sequence); + ASSERT_NO_THROW(response_sequence = sequence->intValue()); + + // Request and Response sequence numbers should match. + ASSERT_EQ(request_sequence, response_sequence); + + ConstElementPtr server_port_elem = clientRR->response_->getJsonElement("server-port"); + ASSERT_TRUE(server_port_elem); + + // Track how many requests were completed by the main thread. + // These can occur when pausing calls IOService::poll. + if (clientRR->thread_id_ == main_thread_id) { + ++worked_by_main; + } + } + + // Make sure the majority of the requests were worked by + // worker threads. In theory, the number of calls to poll + // times the number of threads is the limit for responses + // built by the main thread. + ASSERT_LE(worked_by_main, num_pauses * num_threads); + } + + /// @brief Fetch the number of completed requests. + /// + /// @return number of completed requests. + size_t getRRCount() { + std::lock_guard<std::mutex> lck(test_mutex_); + return (clientRRs_.size()); + } + + /// @brief IO service used in the tests. + IOService io_service_; + + /// @brief Instance of the client used in the tests. + HttpClientPtr client_; + + /// @brief Instance of the listener used in the tests. + HttpListenerPtr listener_; + + /// @brief Pointer to the response creator factory. + HttpResponseCreatorFactoryPtr factory_; + + /// @brief List of listeners. + std::vector<HttpListenerPtr> listeners_; + + /// @brief List of response factories. + std::vector<HttpResponseCreatorFactoryPtr> factories_; + + /// @brief Asynchronous timer service to detect timeouts. + IntervalTimer test_timer_; + + /// @brief Number of threads HttpClient should use. + size_t num_threads_; + + /// @brief Number of request batches to conduct. + size_t num_batches_; + + /// @brief Number of listeners to start. + size_t num_listeners_; + + /// @brief Number of expected requests to carry out. + size_t expected_requests_; + + /// @brief Number of requests that are in progress. + size_t num_in_progress_; + + /// @brief Number of requests that have been completed. + size_t num_finished_; + + /// @brief a List of client request-response pairs. + std::vector<ClientRRPtr> clientRRs_; + + /// @brief Mutex for locking. + std::mutex test_mutex_; + + /// @brief Condition variable used to make client threads wait + /// until number of in-progress requests reaches the number + /// of client requests. + std::condition_variable test_cv_; + + /// @brief Indicates if client threads are currently "paused". + bool paused_; + + /// @brief Number of times client has been paused during the test. + size_t pause_cnt_; +}; + +// Verifies we can construct and destruct, in both single +// and multi-threaded modes. +TEST_F(MultiThreadingHttpClientTest, basics) { + HttpClientPtr client; + + // Value of 0 for thread_pool_size means single-threaded. + ASSERT_NO_THROW_LOG(client.reset(new HttpClient(io_service_, false))); + ASSERT_TRUE(client); + + ASSERT_FALSE(client->getThreadIOService()); + ASSERT_EQ(client->getThreadPoolSize(), 0); + ASSERT_EQ(client->getThreadCount(), 0); + + // Make sure destruction doesn't throw. + ASSERT_NO_THROW_LOG(client.reset()); + + // Non-zero thread-pool-size means multi-threaded mode, should throw. + ASSERT_THROW_MSG(client.reset(new HttpClient(io_service_, false, 1)), InvalidOperation, + "HttpClient thread_pool_size must be zero " + "when Kea core multi-threading is disabled"); + ASSERT_FALSE(client); + + // Multi-threaded construction should work now. + ASSERT_NO_THROW_LOG(client.reset(new HttpClient(io_service_, true, 3))); + ASSERT_TRUE(client); + + // Verify that it has an internal IOService and that thread pool size + // and thread count match. + ASSERT_TRUE(client->getThreadIOService()); + EXPECT_FALSE(client->getThreadIOService()->stopped()); + ASSERT_EQ(client->getThreadPoolSize(), 3); + ASSERT_EQ(client->getThreadCount(), 3); + + // Check convenience functions. + ASSERT_TRUE(client->isRunning()); + ASSERT_FALSE(client->isPaused()); + ASSERT_FALSE(client->isStopped()); + + // Verify stop doesn't throw. + ASSERT_NO_THROW_LOG(client->stop()); + + // Verify we're stopped. + ASSERT_TRUE(client->getThreadIOService()); + EXPECT_TRUE(client->getThreadIOService()->stopped()); + ASSERT_EQ(client->getThreadPoolSize(), 3); + ASSERT_EQ(client->getThreadCount(), 0); + + // Check convenience functions. + ASSERT_FALSE(client->isRunning()); + ASSERT_FALSE(client->isPaused()); + ASSERT_TRUE(client->isStopped()); + + // Verify a second call to stop() doesn't throw. + ASSERT_NO_THROW_LOG(client->stop()); + + // Make sure destruction doesn't throw. + ASSERT_NO_THROW_LOG(client.reset()); + + // Create another multi-threaded instance. + ASSERT_NO_THROW_LOG(client.reset(new HttpClient(io_service_, true, 3))); + + // Make sure destruction doesn't throw. + ASSERT_NO_THROW_LOG(client.reset()); +} + +// Verifies we can construct with deferred start. +TEST_F(MultiThreadingHttpClientTest, deferredStart) { + HttpClientPtr client; + size_t thread_pool_size = 3; + + // Create MT client with deferred start. + ASSERT_NO_THROW_LOG(client.reset(new HttpClient(io_service_, true, thread_pool_size, true))); + ASSERT_TRUE(client); + + // Client should be STOPPED, with no threads. + ASSERT_TRUE(client->getThreadIOService()); + ASSERT_EQ(client->getThreadPoolSize(), thread_pool_size); + ASSERT_EQ(client->getThreadCount(), 0); + + // Check convenience functions. + ASSERT_FALSE(client->isRunning()); + ASSERT_FALSE(client->isPaused()); + ASSERT_TRUE(client->isStopped()); + + // We should be able to start it. + ASSERT_NO_THROW(client->start()); + + // Verify we have threads and run state is RUNNING. + ASSERT_EQ(client->getThreadCount(), 3); + ASSERT_TRUE(client->getThreadIOService()); + ASSERT_FALSE(client->getThreadIOService()->stopped()); + + // Check convenience functions. + ASSERT_TRUE(client->isRunning()); + ASSERT_FALSE(client->isPaused()); + ASSERT_FALSE(client->isStopped()); + + // Second call to start should be harmless. + ASSERT_NO_THROW_LOG(client->start()); + + // Verify we didn't break it. + ASSERT_EQ(client->getThreadCount(), 3); + ASSERT_TRUE(client->isRunning()); + + // Make sure destruction doesn't throw. + ASSERT_NO_THROW_LOG(client.reset()); +} + +// Verifies we can restart after stop. +TEST_F(MultiThreadingHttpClientTest, restartAfterStop) { + HttpClientPtr client; + size_t thread_pool_size = 3; + + // Create MT client with instant start. + ASSERT_NO_THROW_LOG(client.reset(new HttpClient(io_service_, true, thread_pool_size))); + ASSERT_TRUE(client); + + // Verify we're started. + ASSERT_EQ(client->getThreadCount(), 3); + ASSERT_TRUE(client->getThreadIOService()); + ASSERT_FALSE(client->getThreadIOService()->stopped()); + ASSERT_TRUE(client->isRunning()); + + // Stop should succeed. + ASSERT_NO_THROW_LOG(client->stop()); + + // Verify we're stopped. + ASSERT_EQ(client->getThreadCount(), 0); + ASSERT_TRUE(client->getThreadIOService()); + ASSERT_TRUE(client->getThreadIOService()->stopped()); + ASSERT_TRUE(client->isStopped()); + + // Starting again should succeed. + ASSERT_NO_THROW_LOG(client->start()); + + // Verify we didn't break it. + ASSERT_EQ(client->getThreadCount(), 3); + ASSERT_TRUE(client->getThreadIOService()); + ASSERT_FALSE(client->getThreadIOService()->stopped()); + ASSERT_TRUE(client->isRunning()); + + // Make sure destruction doesn't throw. + ASSERT_NO_THROW_LOG(client.reset()); +} + +// Now we'll run some permutations of the number of client threads, +// requests, and listeners. + +// Single-threaded, three batches, one listener. +TEST_F(MultiThreadingHttpClientTest, zeroByThreeByOne) { + size_t num_threads = 0; // Zero threads = ST mode. + size_t num_batches = 3; + threadRequestAndReceive(num_threads, num_batches); +} + +// Single-threaded, three batches, three listeners. +TEST_F(MultiThreadingHttpClientTest, zeroByThreeByThree) { + size_t num_threads = 0; // Zero threads = ST mode. + size_t num_batches = 3; + size_t num_listeners = 3; + threadRequestAndReceive(num_threads, num_batches, num_listeners); +} + +// Multi-threaded with one thread, three batches, one listener +TEST_F(MultiThreadingHttpClientTest, oneByThreeByOne) { + size_t num_threads = 1; + size_t num_batches = 3; + threadRequestAndReceive(num_threads, num_batches); +} + +// Multi-threaded with three threads, three batches, one listener +TEST_F(MultiThreadingHttpClientTest, threeByThreeByOne) { + size_t num_threads = 3; + size_t num_batches = 3; + threadRequestAndReceive(num_threads, num_batches); +} + +// Multi-threaded with three threads, nine batches, one listener +TEST_F(MultiThreadingHttpClientTest, threeByNineByOne) { + size_t num_threads = 3; + size_t num_batches = 9; + threadRequestAndReceive(num_threads, num_batches); +} + +// Multi-threaded with two threads, four batches, two listeners +TEST_F(MultiThreadingHttpClientTest, twoByFourByTwo) { + size_t num_threads = 2; + size_t num_batches = 4; + size_t num_listeners = 2; + threadRequestAndReceive(num_threads, num_batches, num_listeners); +} + +// Multi-threaded with four threads, four batches, two listeners +TEST_F(MultiThreadingHttpClientTest, fourByFourByTwo) { + size_t num_threads = 4; + size_t num_batches = 4; + size_t num_listeners = 2; + threadRequestAndReceive(num_threads, num_batches, num_listeners); +} + +// Verifies that we can cleanly pause, resume, and shutdown while doing +// multi-threaded work. +TEST_F(MultiThreadingHttpClientTest, workPauseResumeShutdown) { + size_t num_threads = 4; + size_t num_batches = 4; + size_t num_listeners = 4; + size_t num_pauses = 3; + workPauseResumeShutdown(num_threads, num_batches, num_listeners, num_pauses); +} + +} // end of anonymous namespace diff --git a/src/lib/http/tests/connection_pool_unittests.cc b/src/lib/http/tests/connection_pool_unittests.cc new file mode 100644 index 0000000..b8337c3 --- /dev/null +++ b/src/lib/http/tests/connection_pool_unittests.cc @@ -0,0 +1,270 @@ +// Copyright (C) 2017-2021 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <asiolink/asio_wrapper.h> +#include <asiolink/io_service.h> +#include <http/http_acceptor.h> +#include <http/connection.h> +#include <http/connection_pool.h> +#include <http/post_request_json.h> +#include <http/response_creator.h> +#include <http/response_json.h> +#include <http/tests/response_test.h> +#include <util/multi_threading_mgr.h> + +#include <boost/shared_ptr.hpp> +#include <gtest/gtest.h> +#include <algorithm> + +using namespace isc::asiolink; +using namespace isc::http; +using namespace isc::http::test; +using namespace isc::util; + +namespace { + +/// @brief Test HTTP response. +typedef TestHttpResponseBase<HttpResponseJson> Response; + +/// @brief Pointer to test HTTP response. +typedef boost::shared_ptr<Response> ResponsePtr; + +/// @brief Request timeout used in tests. +const long CONN_REQUEST_TIMEOUT = 1000; + +/// @brief Idle connection timeout used in tests. +const long CONN_IDLE_TIMEOUT = 1000; + +/// @brief Implementation of the @ref HttpResponseCreator. +class TestHttpResponseCreator : public HttpResponseCreator { +public: + + /// @brief Create a new request. + /// + /// @return Pointer to the new instance of the @ref HttpRequest. + virtual HttpRequestPtr + createNewHttpRequest() const { + return (HttpRequestPtr(new PostHttpRequestJson())); + } + +private: + + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP response. + virtual HttpResponsePtr + createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const { + // The request hasn't been finalized so the request object + // doesn't contain any information about the HTTP version number + // used. But, the context should have this data (assuming the + // HTTP version is parsed ok). + HttpVersion http_version(request->context()->http_version_major_, + request->context()->http_version_minor_); + // This will generate the response holding JSON content. + ResponsePtr response(new Response(http_version, status_code)); + return (response); + } + + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP OK response with no content. + virtual HttpResponsePtr + createDynamicHttpResponse(HttpRequestPtr request) { + // The simplest thing is to create a response with no content. + // We don't need content to test our class. + ResponsePtr response(new Response(request->getHttpVersion(), + HttpStatusCode::OK)); + return (response); + } +}; + +/// @brief Derivation of @ref HttpConnectionPool exposing protected member. +class TestHttpConnectionPool : public HttpConnectionPool { +public: + + using HttpConnectionPool::connections_; + + /// @brief Checks if specified connection belongs to the pool. + bool hasConnection(const HttpConnectionPtr& conn) const { + return (std::find(connections_.begin(), connections_.end(), conn) + != connections_.end()); + } + +}; + +/// @brief Test fixture class for @ref HttpConnectionPool. +class HttpConnectionPoolTest : public ::testing::Test { +public: + + /// @brief Constructor. + HttpConnectionPoolTest() + : io_service_(), + acceptor_(new HttpAcceptor(io_service_)), + connection_pool_(), + response_creator_(new TestHttpResponseCreator()) { + MultiThreadingMgr::instance().setMode(false); + } + + /// @brief Destructor. + ~HttpConnectionPoolTest() { + MultiThreadingMgr::instance().setMode(false); + } + + /// @brief Verifies that connections can be added to the pool and removed. + void startStopTest() { + // Create two distinct connections. + HttpConnectionPtr conn1(new HttpConnection(io_service_, acceptor_, + TlsContextPtr(), + connection_pool_, + response_creator_, + HttpAcceptorCallback(), + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); + + HttpConnectionPtr conn2(new HttpConnection(io_service_, acceptor_, + TlsContextPtr(), + connection_pool_, + response_creator_, + HttpAcceptorCallback(), + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); + // The pool should be initially empty. + TestHttpConnectionPool pool; + ASSERT_TRUE(pool.connections_.empty()); + + // Start first connection and check that it has been added to the pool. + ASSERT_NO_THROW(pool.start(conn1)); + ASSERT_EQ(1, pool.connections_.size()); + ASSERT_EQ(1, pool.hasConnection(conn1)); + + // Start second connection and check that it also has been added. + ASSERT_NO_THROW(pool.start(conn2)); + ASSERT_EQ(2, pool.connections_.size()); + ASSERT_EQ(1, pool.hasConnection(conn2)); + + // Stop first connection. + ASSERT_NO_THROW(pool.stop(conn1)); + ASSERT_EQ(1, pool.connections_.size()); + // Check that it has been removed but the second connection is still + // there. + ASSERT_EQ(0, pool.hasConnection(conn1)); + ASSERT_EQ(1, pool.hasConnection(conn2)); + + // Remove second connection and verify. + ASSERT_NO_THROW(pool.stop(conn2)); + EXPECT_TRUE(pool.connections_.empty()); + } + + /// @brief Verifies that all connections can be remove with a single call. + void stopAllTest() { + // Create two distinct connections. + HttpConnectionPtr conn1(new HttpConnection(io_service_, acceptor_, + TlsContextPtr(), + connection_pool_, + response_creator_, + HttpAcceptorCallback(), + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); + + HttpConnectionPtr conn2(new HttpConnection(io_service_, acceptor_, + TlsContextPtr(), + connection_pool_, + response_creator_, + HttpAcceptorCallback(), + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); + TestHttpConnectionPool pool; + ASSERT_NO_THROW(pool.start(conn1)); + ASSERT_NO_THROW(pool.start(conn2)); + + // There are two distinct connections in the pool. + ASSERT_EQ(2, pool.connections_.size()); + + // This should remove all connections. + ASSERT_NO_THROW(pool.stopAll()); + EXPECT_TRUE(pool.connections_.empty()); + } + + /// @brief Verifies that stopping a non-existing connection is no-op. + void stopInvalidTest() { + HttpConnectionPtr conn1(new HttpConnection(io_service_, acceptor_, + TlsContextPtr(), + connection_pool_, + response_creator_, + HttpAcceptorCallback(), + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); + + HttpConnectionPtr conn2(new HttpConnection(io_service_, acceptor_, + TlsContextPtr(), + connection_pool_, + response_creator_, + HttpAcceptorCallback(), + CONN_REQUEST_TIMEOUT, + CONN_IDLE_TIMEOUT)); + TestHttpConnectionPool pool; + ASSERT_NO_THROW(pool.start(conn1)); + ASSERT_NO_THROW(pool.stop(conn2)); + ASSERT_EQ(1, pool.connections_.size()); + ASSERT_EQ(1, pool.hasConnection(conn1)); + } + + IOService io_service_; ///< IO service. + HttpAcceptorPtr acceptor_; ///< Test acceptor. + HttpConnectionPool connection_pool_; ///< Test connection pool. + HttpResponseCreatorPtr response_creator_; ///< Test response creator. + +}; + +// Verifies that connections can be added to the pool and removed. +// with MultiThreading disabled. +TEST_F(HttpConnectionPoolTest, startStopTest) { + ASSERT_FALSE(MultiThreadingMgr::instance().getMode()); + startStopTest(); +} + +// Verifies that connections can be added to the pool and removed +// with MultiThreading enabled. +TEST_F(HttpConnectionPoolTest, startStopTestMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + startStopTest(); +} + +// Check that all connections can be remove with a single call. +// with MultiThreading disabled. +TEST_F(HttpConnectionPoolTest, stopAll) { + ASSERT_FALSE(MultiThreadingMgr::instance().getMode()); + stopAllTest(); +} + +// Check that all connections can be remove with a single call +// with MultiThreading enabled. +TEST_F(HttpConnectionPoolTest, stopAllMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_TRUE(MultiThreadingMgr::instance().getMode()); + stopAllTest(); +} + +// Check that stopping non-existing connection is no-op. +// with MultiThreading disabled. +TEST_F(HttpConnectionPoolTest, stopInvalid) { + ASSERT_FALSE(MultiThreadingMgr::instance().getMode()); + stopInvalidTest(); +} + +// Check that stopping non-existing connection is no-op. +// with MultiThreading enabled. +TEST_F(HttpConnectionPoolTest, stopInvalidMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_TRUE(MultiThreadingMgr::instance().getMode()); + stopInvalidTest(); +} + +} diff --git a/src/lib/http/tests/date_time_unittests.cc b/src/lib/http/tests/date_time_unittests.cc new file mode 100644 index 0000000..59d75b1 --- /dev/null +++ b/src/lib/http/tests/date_time_unittests.cc @@ -0,0 +1,190 @@ +// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <http/date_time.h> +#include <boost/date_time/gregorian/gregorian.hpp> +#include <boost/date_time/posix_time/posix_time.hpp> +#include <gtest/gtest.h> + +using namespace boost::gregorian; +using namespace boost::posix_time; +using namespace isc::http; + +namespace { + +/// @brief Test fixture class for @ref HttpDateTime. +class HttpDateTimeTest : public ::testing::Test { +public: + + /// @brief Checks time value against expected values. + /// + /// This method uses value of @ref date_time_ for the test. + /// + /// @param exp_day_of_week Expected day of week. + /// @param exp_day Expected day of month. + /// @param exp_month Expected month. + /// @param exp_year Expected year. + /// @param exp_hours Expected hour value. + /// @param exp_minutes Expected minutes value. + /// @param exp_seconds Expected seconds value. + void testDateTime(const unsigned short exp_day_of_week, + const unsigned short exp_day, + const unsigned short exp_month, + const unsigned short exp_year, + const long exp_hours, + const long exp_minutes, + const long exp_seconds) { + // Retrieve @c boost::posix_time::ptime value. + ptime as_ptime = date_time_.getPtime(); + // Date is contained within this object. + date date_part = as_ptime.date(); + + // Verify weekday. + greg_weekday day_of_week = date_part.day_of_week(); + EXPECT_EQ(exp_day_of_week, day_of_week.as_number()); + + // Verify day of month. + greg_day day = date_part.day(); + EXPECT_EQ(exp_day, day.as_number()); + + // Verify month. + greg_month month = date_part.month(); + EXPECT_EQ(exp_month, month.as_number()); + + // Verify year. + greg_year year = date_part.year(); + EXPECT_EQ(exp_year, static_cast<unsigned short>(year)); + + // Retrieve time of the day and verify hour, minute and second. + time_duration time_of_day = as_ptime.time_of_day(); + EXPECT_EQ(exp_hours, time_of_day.hours()); + EXPECT_EQ(exp_minutes, time_of_day.minutes()); + EXPECT_EQ(exp_seconds, time_of_day.seconds()); + } + + /// @brief Date/time value which should be set by the tests. + HttpDateTime date_time_; + +}; + +// Test formatting as specified in RFC 1123. +TEST_F(HttpDateTimeTest, rfc1123Format) { + date gdate(greg_year(2002), greg_month(1), greg_day(20)); + time_duration tm(23, 59, 59, 0); + ptime t = ptime(gdate, tm); + HttpDateTime date_time(t); + std::string formatted; + ASSERT_NO_THROW(formatted = date_time.rfc1123Format()); + EXPECT_EQ("Sun, 20 Jan 2002 23:59:59 GMT", formatted); +} + +// Test formatting as specified in RFC 850. +TEST_F(HttpDateTimeTest, rfc850Format) { + date gdate(greg_year(1994), greg_month(8), greg_day(6)); + time_duration tm(11, 12, 13, 0); + ptime t = ptime(gdate, tm); + + HttpDateTime date_time(t); + std::string formatted; + ASSERT_NO_THROW(formatted = date_time.rfc850Format()); + EXPECT_EQ("Saturday, 06-Aug-94 11:12:13 GMT", formatted); +} + +// Test formatting as output of asctime(). +TEST_F(HttpDateTimeTest, asctimeFormat) { + date gdate(greg_year(1999), greg_month(11), greg_day(2)); + time_duration tm(03, 57, 12, 0); + ptime t = ptime(gdate, tm); + + HttpDateTime date_time(t); + std::string formatted; + ASSERT_NO_THROW(formatted = date_time.asctimeFormat()); + EXPECT_EQ("Tue Nov 2 03:57:12 1999", formatted); +} + +// Test parsing time in RFC 1123 format. +TEST_F(HttpDateTimeTest, fromRfc1123) { + ASSERT_NO_THROW( + date_time_ = HttpDateTime::fromRfc1123("Wed, 21 Dec 2016 18:53:45 GMT") + ); + testDateTime(3, 21, 12, 2016, 18, 53, 45); + EXPECT_THROW(HttpDateTime::fromRfc1123("Wed, 21 Dex 2016 18:53:45 GMT"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromRfc1123("Wed, 43 Dec 2016 18:53:45 GMT"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromRfc1123("Wed, 21 Dec 16 18:53:45 GMT"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromRfc1123("Wed, 21 Dec 2016 18:53:45"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromRfc1123("Wed, 21 Dec 2016 1853:45 GMT"), + HttpTimeConversionError); +} + +// Test parsing time in RFC 850 format. +TEST_F(HttpDateTimeTest, fromRfc850) { + ASSERT_NO_THROW( + date_time_ = HttpDateTime::fromRfc850("Wednesday, 21-Dec-16 18:53:45 GMT"); + ); + testDateTime(3, 21, 12, 2016, 18, 53, 45); + EXPECT_THROW(HttpDateTime::fromRfc850("Wednesday, 55-Dec-16 18:53:45 GMT"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromRfc850("Wednesday, 21-Dex-16 18:53:45 GMT"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromRfc850("Wednesday, 21-Dec-2016 18:53:45 GMT"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromRfc850("Wednesday, 21-Dec-16 1853:45 GMT"), + HttpTimeConversionError); +} + +// Test parsing time in asctime() format. +TEST_F(HttpDateTimeTest, fromRfcAsctime) { + ASSERT_NO_THROW( + date_time_ = HttpDateTime::fromAsctime("Wed Dec 21 08:49:37 2016"); + ); + testDateTime(3, 21, 12, 2016, 8, 49, 37); + EXPECT_THROW(HttpDateTime::fromAsctime("Wed Dex 21 08:49:37 2016"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromAsctime("Wed Dec 55 08:49:37 2016"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromAsctime("Wed Dec 21 08:49:37 16"), + HttpTimeConversionError); + EXPECT_THROW(HttpDateTime::fromAsctime("Wed Dec 21 08:4937 2016"), + HttpTimeConversionError); +} + +// Test parsing time in RFC 1123 format using HttpDateTime::fromAny(). +TEST_F(HttpDateTimeTest, fromAnyRfc1123) { + ASSERT_NO_THROW( + date_time_ = HttpDateTime::fromAny("Thu, 05 Jan 2017 09:15:06 GMT"); + ); + testDateTime(4, 5, 1, 2017, 9, 15, 06); +} + +// Test parsing time in RFC 850 format using HttpDateTime::fromAny(). +TEST_F(HttpDateTimeTest, fromAnyRfc850) { + ASSERT_NO_THROW( + date_time_ = HttpDateTime::fromAny("Saturday, 18-Feb-17 01:02:10 GMT"); + ); + testDateTime(6, 18, 2, 2017, 1, 2, 10); +} + +// Test parsing time in asctime() format using HttpDateTime::fromAny(). +TEST_F(HttpDateTimeTest, fromAnyAsctime) { + ASSERT_NO_THROW( + date_time_ = HttpDateTime::fromAny("Wed Mar 1 15:45:07 2017 GMT"); + ); + testDateTime(3, 1, 3, 2017, 15, 45, 7); +} + +// Test that HttpDateTime::fromAny throws exception if unsupported format is +// used. +TEST_F(HttpDateTimeTest, fromAnyInvalidFormat) { + EXPECT_THROW(HttpDateTime::fromAsctime("20020131T235959"), + HttpTimeConversionError); +} + +} diff --git a/src/lib/http/tests/http_header_unittests.cc b/src/lib/http/tests/http_header_unittests.cc new file mode 100644 index 0000000..df9d5bb --- /dev/null +++ b/src/lib/http/tests/http_header_unittests.cc @@ -0,0 +1,54 @@ +// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <exceptions/exceptions.h> +#include <http/http_header.h> +#include <gtest/gtest.h> + +using namespace isc; +using namespace isc::http; + +namespace { + +// Test that HTTP header can be created. +TEST(HttpHeader, create) { + HttpHeader hdr("Content-Type", "application/json"); + EXPECT_EQ("Content-Type", hdr.getName()); + EXPECT_EQ("application/json", hdr.getValue()); +} + +// Test that the numeric value can be retrieved from a header and that +// an exception is thrown if the header value is not a valid number. +TEST(HttpHeader, getUint64Value) { + HttpHeader hdr64("Content-Length", "64"); + EXPECT_EQ(64, hdr64.getUint64Value()); + + HttpHeader hdr_foo("Content-Length", "foo"); + EXPECT_THROW(hdr_foo.getUint64Value(), isc::BadValue); +} + +// Test that header name can be retrieved in lower case. +TEST(HttpHeader, getLowerCaseName) { + HttpHeader hdr("ConnectioN", "Keep-Alive"); + EXPECT_EQ("connection", hdr.getLowerCaseName()); +} + +// Test that header value can be retrieved in lower case. +TEST(HttpHeader, getLowerCaseValue) { + HttpHeader hdr("Connection", "Keep-Alive"); + EXPECT_EQ("keep-alive", hdr.getLowerCaseValue()); +} + +// Test that header value comparison is case insensitive. +TEST(HttpHeader, equalsCaseInsensitive) { + HttpHeader hdr("Connection", "KeEp-ALIve"); + EXPECT_TRUE(hdr.isValueEqual("keep-alive")); + EXPECT_TRUE(hdr.isValueEqual("KEEP-ALIVE")); + EXPECT_TRUE(hdr.isValueEqual("kEeP-AlIvE")); +} + +} // end of anonymous namespace diff --git a/src/lib/http/tests/post_request_json_unittests.cc b/src/lib/http/tests/post_request_json_unittests.cc new file mode 100644 index 0000000..bb717cd --- /dev/null +++ b/src/lib/http/tests/post_request_json_unittests.cc @@ -0,0 +1,197 @@ +// Copyright (C) 2016-2019 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <cc/data.h> +#include <http/http_types.h> +#include <http/post_request_json.h> +#include <http/tests/request_test.h> +#include <gtest/gtest.h> +#include <map> +#include <sstream> + +using namespace isc::data; +using namespace isc::http; +using namespace isc::http::test; + +namespace { + +/// @brief Test fixture class for @ref PostHttpRequestJson. +class PostHttpRequestJsonTest : + public HttpRequestTestBase<PostHttpRequestJson> { +public: + + /// @brief Constructor. + PostHttpRequestJsonTest() + : HttpRequestTestBase<PostHttpRequestJson>(), + json_body_("{ \"service\": \"dhcp4\", \"param1\": \"foo\" }") { + } + + /// @brief Sets new JSON body for the HTTP request context. + /// + /// If the body parameter is empty, it will use the value of + /// @ref json_body_ member. Otherwise, it will assign the body + /// provided as parameter. + /// + /// @param body new body value. + void setBody(const std::string& body = "") { + request_->context()->body_ = body.empty() ? json_body_ : body; + } + + /// @brief Default value of the JSON body. + std::string json_body_; +}; + +// This test verifies that PostHttpRequestJson class only accepts +// POST messages. +TEST_F(PostHttpRequestJsonTest, requiredPost) { + // Use a GET method that is not supported. + setContextBasics("GET", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + ASSERT_THROW(request_->create(), HttpRequestError); + + // Now use POST. It should be accepted. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + EXPECT_NO_THROW(request_->create()); +} + +// This test verifies that PostHttpRequest requires "Content-Length" +// header equal to "application/json". +TEST_F(PostHttpRequestJsonTest, requireContentTypeJson) { + // Specify "Content-Type" other than "application/json". + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "text/html"); + + ASSERT_THROW(request_->create(), HttpRequestError); + + // This time specify correct "Content-Type". It should pass. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + EXPECT_NO_THROW(request_->create()); +} + +// This test verifies that PostHttpRequest requires "Content-Length" +// header. +TEST_F(PostHttpRequestJsonTest, requireContentLength) { + // "Content-Length" is not specified initially. It should fail. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Type", "text/html"); + + ASSERT_THROW(request_->create(), HttpRequestError); + + // Specify "Content-Length". It should pass. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); +} + +// This test verifies that JSON body can be retrieved from the +// HTTP request. +TEST_F(PostHttpRequestJsonTest, getBodyAsJson) { + // Create HTTP POST request with JSON body. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + setBody(); + + ASSERT_NO_THROW(request_->finalize()); + + // Try to retrieve pointer to the root element of the JSON body. + ConstElementPtr json = request_->getBodyAsJson(); + ASSERT_TRUE(json); + + // Iterate over JSON values and store them in a simple map. + std::map<std::string, std::string> config_values; + for (auto config_element = json->mapValue().begin(); + config_element != json->mapValue().end(); + ++config_element) { + ASSERT_FALSE(config_element->first.empty()); + ASSERT_TRUE(config_element->second); + config_values[config_element->first] = config_element->second->stringValue(); + } + + // Verify the values. + EXPECT_EQ("dhcp4", config_values["service"]); + EXPECT_EQ("foo", config_values["param1"]); +} + +// This test verifies that an attempt to parse/retrieve malformed +// JSON structure will cause an exception. +TEST_F(PostHttpRequestJsonTest, getBodyAsJsonMalformed) { + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + // No colon before 123. + setBody("{ \"command\" 123 }" ); + + EXPECT_THROW(request_->finalize(), HttpRequestJsonError); +} + +// This test verifies that NULL pointer is returned when trying to +// retrieve root element of the empty JSON structure. +TEST_F(PostHttpRequestJsonTest, getEmptyJsonBody) { + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + ASSERT_NO_THROW(request_->finalize()); + + ConstElementPtr json = request_->getBodyAsJson(); + EXPECT_FALSE(json); +} + +// This test verifies that the specific JSON element can be retrieved. +TEST_F(PostHttpRequestJsonTest, getJsonElement) { + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + setBody(); + + ASSERT_NO_THROW(request_->finalize()); + + ConstElementPtr element; + ASSERT_NO_THROW(element = request_->getJsonElement("service")); + ASSERT_TRUE(element); + EXPECT_EQ("dhcp4", element->stringValue()); + + // An attempt to retrieve non-existing element should return NULL. + EXPECT_FALSE(request_->getJsonElement("bar")); +} + +// This test verifies that it is possible to create client side request +// containing JSON body. +TEST_F(PostHttpRequestJsonTest, clientRequest) { + request_->setDirection(HttpMessage::OUTBOUND); + + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Type", "application/json"); + + ElementPtr json = Element::fromJSON(json_body_); + request_->setBodyAsJson(json); + + // Commit and validate the data. + ASSERT_NO_THROW(request_->finalize()); + + std::ostringstream expected_request_text; + expected_request_text << "POST /isc/org HTTP/1.0\r\n" + "Content-Length: " << json->str().size() << "\r\n" + "Content-Type: application/json\r\n" + "\r\n" + << json->str(); + + EXPECT_EQ(expected_request_text.str(), request_->toString()); +} + +} diff --git a/src/lib/http/tests/post_request_unittests.cc b/src/lib/http/tests/post_request_unittests.cc new file mode 100644 index 0000000..18f2fda --- /dev/null +++ b/src/lib/http/tests/post_request_unittests.cc @@ -0,0 +1,83 @@ +// Copyright (C) 2016-2019 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <http/post_request.h> +#include <http/tests/request_test.h> +#include <gtest/gtest.h> + +using namespace isc::http; +using namespace isc::http::test; + +namespace { + +/// @brief Test fixture class for @ref PostHttpRequest. +class PostHttpRequestTest : public HttpRequestTestBase<PostHttpRequest> { +public: + + /// @brief Constructor. + PostHttpRequestTest() + : HttpRequestTestBase<PostHttpRequest>(), + json_body_("{ \"service\": \"dhcp4\", \"param1\": \"foo\" }") { + } + + /// @brief Default value of the JSON body. + std::string json_body_; +}; + +// This test verifies that PostHttpRequest class only accepts POST +// messages. +TEST_F(PostHttpRequestTest, requirePost) { + // Use a GET method that is not supported. + setContextBasics("GET", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + ASSERT_THROW(request_->create(), HttpRequestError); + + // Now use POST. It should be accepted. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); + + EXPECT_NO_THROW(request_->create()); +} + +// This test verifies that PostHttpRequest requires "Content-Length" +// header. +TEST_F(PostHttpRequestTest, requireContentType) { + // No "Content-Type". It should fail. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + + ASSERT_THROW(request_->create(), HttpRequestError); + + // There is "Content-Type". It should pass. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "text/html"); + + EXPECT_NO_THROW(request_->create()); + +} + +// This test verifies that PostHttpRequest requires "Content-Type" +// header. +TEST_F(PostHttpRequestTest, requireContentLength) { + // No "Content-Length". It should fail. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Type", "text/html"); + + ASSERT_THROW(request_->create(), HttpRequestError); + + // There is "Content-Length". It should pass. + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body_.length()); + addHeaderToContext("Content-Type", "application/json"); +} + +} diff --git a/src/lib/http/tests/request_parser_unittests.cc b/src/lib/http/tests/request_parser_unittests.cc new file mode 100644 index 0000000..0756711 --- /dev/null +++ b/src/lib/http/tests/request_parser_unittests.cc @@ -0,0 +1,387 @@ +// Copyright (C) 2016-2019 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <cc/data.h> +#include <http/http_types.h> +#include <http/request_parser.h> +#include <http/post_request_json.h> +#include <gtest/gtest.h> +#include <sstream> + +using namespace isc::data; +using namespace isc::http; + +namespace { + +/// @brief Test fixture class for @ref HttpRequestParser. +class HttpRequestParserTest : public ::testing::Test { +public: + + /// @brief Creates HTTP request string. + /// + /// @param preamble A string including HTTP request's first line + /// and all headers except "Content-Length". + /// @param payload A string containing HTTP request payload. + std::string createRequestString(const std::string& preamble, + const std::string& payload) { + std::ostringstream s; + s << preamble; + s << "Content-Length: " << payload.length() << "\r\n\r\n" + << payload; + return (s.str()); + } + + /// @brief Parses the HTTP request and checks that parsing was + /// successful. + /// + /// @param http_req HTTP request string. + void doParse(const std::string& http_req) { + HttpRequestParser parser(request_); + ASSERT_NO_THROW(parser.initModel()); + + parser.postBuffer(&http_req[0], http_req.size()); + ASSERT_NO_THROW(parser.poll()); + + ASSERT_FALSE(parser.needData()); + ASSERT_TRUE(parser.httpParseOk()); + EXPECT_TRUE(parser.getErrorMessage().empty()); + } + + /// @brief Tests that parsing fails when malformed HTTP request + /// is received. + /// + /// @param http_req HTTP request string. + void testInvalidHttpRequest(const std::string& http_req) { + HttpRequestParser parser(request_); + ASSERT_NO_THROW(parser.initModel()); + + parser.postBuffer(&http_req[0], http_req.size()); + ASSERT_NO_THROW(parser.poll()); + + EXPECT_FALSE(parser.needData()); + EXPECT_FALSE(parser.httpParseOk()); + EXPECT_FALSE(parser.getErrorMessage().empty()); + } + + /// @brief Instance of the HttpRequest used by the unit tests. + HttpRequest request_; +}; + +// Test test verifies that an HTTP request including JSON body is parsed +// successfully. +TEST_F(HttpRequestParserTest, postHttpRequestWithJson) { + std::string http_req = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n"; + std::string json = "{ \"service\": \"dhcp4\", \"command\": \"shutdown\" }"; + + http_req = createRequestString(http_req, json); + + // Create HTTP request which accepts POST method and JSON as a body. + PostHttpRequestJson request; + + // Create a parser and make it use the request we created. + HttpRequestParser parser(request); + ASSERT_NO_THROW(parser.initModel()); + + // Simulate receiving HTTP request in chunks. + for (size_t i = 0; i < http_req.size(); i += http_req.size() / 10) { + bool done = false; + // Get the size of the data chunk. + size_t chunk = http_req.size() / 10; + // When we're near the end of the data stream, the chunk length may + // vary. + if (i + chunk > http_req.size()) { + chunk = http_req.size() - i; + done = true; + } + // Feed the parser with a data chunk and parse it. + parser.postBuffer(&http_req[i], chunk); + parser.poll(); + if (!done) { + ASSERT_TRUE(parser.needData()); + } + } + + // Parser should have parsed the request and should expect no more data. + ASSERT_FALSE(parser.needData()); + // Parsing should be successful. + ASSERT_TRUE(parser.httpParseOk()); + // There should be no error message. + EXPECT_TRUE(parser.getErrorMessage().empty()); + + // Verify parsed headers etc. + EXPECT_EQ(HttpRequest::Method::HTTP_POST, request.getMethod()); + EXPECT_EQ("/foo/bar", request.getUri()); + EXPECT_EQ("application/json", request.getHeaderValue("Content-Type")); + EXPECT_EQ(json.length(), request.getHeaderValueAsUint64("Content-Length")); + EXPECT_EQ(1, request.getHttpVersion().major_); + EXPECT_EQ(0, request.getHttpVersion().minor_); + + // Try to retrieve values carried in JSON payload. + ConstElementPtr json_element; + ASSERT_NO_THROW(json_element = request.getJsonElement("service")); + EXPECT_EQ("dhcp4", json_element->stringValue()); + + ASSERT_NO_THROW(json_element = request.getJsonElement("command")); + EXPECT_EQ("shutdown", json_element->stringValue()); +} + +// This test verifies that extraneous data in the request will not cause +// an error if "Content-Length" value refers to the length of the valid +// part of the request. +TEST_F(HttpRequestParserTest, extraneousDataInRequest) { + std::string http_req = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n"; + std::string json = "{ \"service\": \"dhcp4\", \"command\": \"shutdown\" }"; + + // Create valid request; + http_req = createRequestString(http_req, json); + + // Add some garbage at the end. + http_req += "some stuff which, if parsed, will cause errors"; + + // Create HTTP request which accepts POST method and JSON as a body. + PostHttpRequestJson request; + + // Create a parser and make it use the request we created. + HttpRequestParser parser(request); + ASSERT_NO_THROW(parser.initModel()); + + // Feed the parser with the request containing some garbage at the end. + parser.postBuffer(&http_req[0], http_req.size()); + ASSERT_NO_THROW(parser.poll()); + + // The parser should only parse the valid part of the request as indicated + // by the Content-Length. + ASSERT_FALSE(parser.needData()); + ASSERT_TRUE(parser.httpParseOk()); + // There should be no error message. + EXPECT_TRUE(parser.getErrorMessage().empty()); + + // Do another poll() to see if the parser will parse the garbage. We + // expect that it doesn't. + ASSERT_NO_THROW(parser.poll()); + EXPECT_FALSE(parser.needData()); + EXPECT_TRUE(parser.httpParseOk()); + EXPECT_TRUE(parser.getErrorMessage().empty()); +} + + +// This test verifies that LWS is parsed correctly. The LWS marks line breaks +// in the HTTP header values. +TEST_F(HttpRequestParserTest, getLWS) { + // "User-Agent" header contains line breaks with whitespaces in the new + // lines to mark continuation of the header value. + std::string http_req = "GET /foo/bar HTTP/1.1\r\n" + "Content-Type: text/html\r\n" + "User-Agent: Kea/1.2 Command \r\n" + " Control \r\n" + "\tClient\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + // Verify parsed values. + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ("Kea/1.2 Command Control Client", + request_.getHeaderValue("User-Agent")); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(1, request_.getHttpVersion().minor_); +} + +// This test verifies that the HTTP request with no headers is +// parsed correctly. +TEST_F(HttpRequestParserTest, noHeaders) { + std::string http_req = "GET /foo/bar HTTP/1.1\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + // Verify the values. + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(1, request_.getHttpVersion().minor_); +} + +// This test verifies that the HTTP method can be specified in lower +// case. +TEST_F(HttpRequestParserTest, getLowerCase) { + std::string http_req = "get /foo/bar HTTP/1.1\r\n" + "Content-Type: text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(1, request_.getHttpVersion().minor_); +} + +// This test verifies that headers are case insensitive. +TEST_F(HttpRequestParserTest, headersCaseInsensitive) { + std::string http_req = "get /foo/bar HTTP/1.1\r\n" + "Content-type: text/html\r\n" + "connection: keep-Alive\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeader("Content-Type")->getValue()); + EXPECT_EQ("keep-alive", request_.getHeader("Connection")->getLowerCaseValue()); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(1, request_.getHttpVersion().minor_); +} + +// This test verifies that other value of the HTTP version can be +// specified in the request. +TEST_F(HttpRequestParserTest, http20) { + std::string http_req = "get /foo/bar HTTP/2.0\r\n" + "Content-Type: text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ(2, request_.getHttpVersion().major_); + EXPECT_EQ(0, request_.getHttpVersion().minor_); +} + +// This test verifies that the header with no whitespace between the +// colon and header value is accepted. +TEST_F(HttpRequestParserTest, noHeaderWhitespace) { + std::string http_req = "get /foo/bar HTTP/1.0\r\n" + "Content-Type:text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(0, request_.getHttpVersion().minor_); +} + +// This test verifies that the header value preceded with multiple +// whitespaces is accepted. +TEST_F(HttpRequestParserTest, multipleLeadingHeaderWhitespaces) { + std::string http_req = "get /foo/bar HTTP/1.0\r\n" + "Content-Type: text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_.getMethod()); + EXPECT_EQ("/foo/bar", request_.getUri()); + EXPECT_EQ("text/html", request_.getHeaderValue("Content-Type")); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(0, request_.getHttpVersion().minor_); +} + +// This test verifies that error is reported when unsupported HTTP +// method is used. +TEST_F(HttpRequestParserTest, unsupportedMethod) { + std::string http_req = "POSTX /foo/bar HTTP/2.0\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when URI contains +// an invalid character. +TEST_F(HttpRequestParserTest, invalidUri) { + std::string http_req = "POST /foo/\r HTTP/2.0\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that the request containing a typo in the +// HTTP version string causes parsing error. +TEST_F(HttpRequestParserTest, invalidHTTPString) { + std::string http_req = "POST /foo/ HTLP/2.0\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when the HTTP version +// string doesn't contain a slash character. +TEST_F(HttpRequestParserTest, invalidHttpVersionNoSlash) { + std::string http_req = "POST /foo/ HTTP 1.1\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when HTTP version string +// doesn't contain the minor version number. +TEST_F(HttpRequestParserTest, invalidHttpNoMinorVersion) { + std::string http_req = "POST /foo/ HTTP/1\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when HTTP header name +// contains an invalid character. +TEST_F(HttpRequestParserTest, invalidHeaderName) { + std::string http_req = "POST /foo/ HTTP/1.1\r\n" + "Content-;: text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that error is reported when HTTP header value +// is not preceded with the colon character. +TEST_F(HttpRequestParserTest, noColonInHttpHeader) { + std::string http_req = "POST /foo/ HTTP/1.1\r\n" + "Content-Type text/html\r\n\r\n"; + testInvalidHttpRequest(http_req); +} + +// This test verifies that the input buffer of the HTTP request can be +// retrieved as text formatted for logging. +TEST_F(HttpRequestParserTest, getBufferAsString) { + std::string http_req = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n"; + + // Create HTTP request. + PostHttpRequestJson request; + + // Create a parser and make it use the request we created. + HttpRequestParser parser(request); + ASSERT_NO_THROW(parser.initModel()); + + // Insert data into the request. + ASSERT_NO_THROW(parser.postBuffer(&http_req[0], http_req.size())); + + // limit = 0 means no limit + EXPECT_EQ(http_req, parser.getBufferAsString(0)); + + // large enough limit should not cause the truncation. + EXPECT_EQ(http_req, parser.getBufferAsString(1024)); + + // Only 3 characters requested. The request should be truncated. + EXPECT_EQ("POS.........\n(truncating HTTP message larger than 3 characters)\n", + parser.getBufferAsString(3)); +} + +TEST_F(HttpRequestParserTest, parseEmptyRequest) { + std::string http_req = "POST / HTTP/1.1\r\n" + "Content-Type: application/json\r\n"; + std::string json = ""; + + http_req = createRequestString(http_req, json); + + ASSERT_NO_FATAL_FAILURE(doParse(http_req)); + + EXPECT_EQ(HttpRequest::Method::HTTP_POST, request_.getMethod()); + EXPECT_EQ("/", request_.getUri()); + EXPECT_EQ("", request_.getBody()); + EXPECT_EQ(1, request_.getHttpVersion().major_); + EXPECT_EQ(1, request_.getHttpVersion().minor_); +} + +} diff --git a/src/lib/http/tests/request_test.h b/src/lib/http/tests/request_test.h new file mode 100644 index 0000000..f73b31f --- /dev/null +++ b/src/lib/http/tests/request_test.h @@ -0,0 +1,82 @@ +// Copyright (C) 2016-2019 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef HTTP_REQUEST_TEST_H +#define HTTP_REQUEST_TEST_H + +#include <http/http_types.h> +#include <http/request.h> +#include <boost/lexical_cast.hpp> +#include <boost/shared_ptr.hpp> +#include <gtest/gtest.h> +#include <string> +#include <utility> + +namespace isc { +namespace http { +namespace test { + +/// @brief Base test fixture class for testing @ref HttpRequest class and its +/// derivations. +/// +/// @tparam HttpRequestType Class under test. +template<typename HttpRequestType> +class HttpRequestTestBase : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Creates HTTP request to be used in unit tests. + HttpRequestTestBase() + : request_(new HttpRequestType()) { + } + + /// @brief Destructor. + /// + /// Does nothing. + virtual ~HttpRequestTestBase() { + } + + /// @brief Initializes HTTP request context with basic information. + /// + /// It sets: + /// - HTTP method, + /// - URI, + /// - HTTP version number. + /// + /// @param method HTTP method as string. + /// @param uri URI. + /// @param version A pair of values of which the first is the major HTTP + /// version and the second is the minor HTTP version. + void setContextBasics(const std::string& method, const std::string& uri, + const HttpVersion& version) { + request_->context()->method_ = method; + request_->context()->uri_ = uri; + request_->context()->http_version_major_ = version.major_; + request_->context()->http_version_minor_ = version.minor_; + } + + /// @brief Adds HTTP header to the context. + /// + /// @param header_name HTTP header name. + /// @param header_value HTTP header value. This value will be converted to + /// a string using @c boost::lexical_cast. + /// @tparam ValueType Header value type. + template<typename ValueType> + void addHeaderToContext(const std::string& header_name, + const ValueType& header_value) { + request_->context()->headers_.push_back(HttpHeaderContext(header_name, header_value)); + } + + /// @brief Instance of the @ref HttpRequest or its derivation. + boost::shared_ptr<HttpRequestType> request_; +}; + +} // namespace test +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/tests/request_unittests.cc b/src/lib/http/tests/request_unittests.cc new file mode 100644 index 0000000..2c18090 --- /dev/null +++ b/src/lib/http/tests/request_unittests.cc @@ -0,0 +1,422 @@ +// Copyright (C) 2016-2023 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <http/request.h> +#include <http/date_time.h> +#include <http/http_header.h> +#include <http/http_types.h> +#include <http/tests/request_test.h> +#include <boost/lexical_cast.hpp> +#include <gtest/gtest.h> +#include <utility> + +using namespace isc::http; +using namespace isc::http::test; + +namespace { + +/// @brief Test fixture class for @c HttpRequest class. +class HttpRequestTest : public HttpRequestTestBase<HttpRequest> { +public: + + /// @brief Tests connection persistence for the given HTTP version + /// and header value. + /// + /// This method creates a dummy HTTP request and sets the specified + /// version and header. Next, it returns the value if @c isPersistent + /// method for this request. The unit test verifies this value for + /// correctness. + /// + /// @param http_version HTTP version. + /// @param http_header HTTP header to be included in the request. If + /// the header has an empty value, it is not included. + /// + /// @return true if request indicates that connection is to be + /// persistent. + bool isPersistent(const HttpVersion& http_version, + const HttpHeader& http_header = HttpHeader("Connection")) { + try { + // We need to add some JSON body. + std::string json_body = "{ \"param1\": \"foo\" }"; + + // Set method, path, version and content length. + setContextBasics("POST", "/isc/org", http_version); + addHeaderToContext("Content-Length", json_body.length()); + + // If additional header has been specified (typically "Connection"), + // include it. + if (!http_header.getValue().empty()) { + addHeaderToContext(http_header.getName(), http_header.getValue()); + } + // Attach JSON body. + request_->context()->body_ = json_body; + request_->create(); + + } catch (...) { + ADD_FAILURE() << "failed to create HTTP request while testing" + " connection persistence"; + } + + return (request_->isPersistent()); + } + +}; + +// This test verifies that a minimal request can be created. +TEST_F(HttpRequestTest, minimal) { + setContextBasics("GET", "/isc/org", HttpVersion(1, 1)); + ASSERT_NO_THROW(request_->create()); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_->getMethod()); + EXPECT_EQ("/isc/org", request_->getUri()); + EXPECT_EQ(1, request_->getHttpVersion().major_); + EXPECT_EQ(1, request_->getHttpVersion().minor_); + EXPECT_TRUE(request_->getRemote().empty()); + request_->setRemote("127.0.0.1"); + EXPECT_EQ("127.0.0.1", request_->getRemote()); + + EXPECT_THROW(request_->getHeaderValue("Content-Length"), + HttpMessageNonExistingHeader); +} + +// This test verifies that empty Host header is included in the +// request if it is not explicitly specified. +TEST_F(HttpRequestTest, hostHeaderDefault) { + ASSERT_NO_THROW(request_.reset(new HttpRequest(HttpRequest::Method::HTTP_GET, + "/isc/org", + HttpVersion(1, 0)))); + + ASSERT_NO_THROW(request_->finalize()); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_->getMethod()); + EXPECT_EQ("/isc/org", request_->getUri()); + EXPECT_EQ(1, request_->getHttpVersion().major_); + EXPECT_EQ(0, request_->getHttpVersion().minor_); + + std::string host_hdr; + ASSERT_NO_THROW(host_hdr = request_->getHeaderValue("Host")); + EXPECT_TRUE(host_hdr.empty()); +} + +// This test verifies that it is possible to explicitly specify a +// Host header value while creating a request. +TEST_F(HttpRequestTest, hostHeaderCustom) { + ASSERT_NO_THROW(request_.reset(new HttpRequest(HttpRequest::Method::HTTP_GET, + "/isc/org", + HttpVersion(1, 1), + HostHttpHeader("www.example.org")))); + + ASSERT_NO_THROW(request_->finalize()); + + EXPECT_EQ(HttpRequest::Method::HTTP_GET, request_->getMethod()); + EXPECT_EQ("/isc/org", request_->getUri()); + EXPECT_EQ(1, request_->getHttpVersion().major_); + EXPECT_EQ(1, request_->getHttpVersion().minor_); + + std::string host_hdr; + ASSERT_NO_THROW(host_hdr = request_->getHeaderValue("Host")); + EXPECT_EQ("www.example.org", host_hdr); +} + +// This test verifies that headers can be included in a request. +TEST_F(HttpRequestTest, includeHeaders) { + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", "1024"); + addHeaderToContext("Content-Type", "application/json"); + ASSERT_NO_THROW(request_->create()); + + EXPECT_EQ(HttpRequest::Method::HTTP_POST, request_->getMethod()); + EXPECT_EQ("/isc/org", request_->getUri()); + EXPECT_EQ(1, request_->getHttpVersion().major_); + EXPECT_EQ(0, request_->getHttpVersion().minor_); + + std::string content_type; + ASSERT_NO_THROW(content_type = request_->getHeaderValue("Content-Type")); + EXPECT_EQ("application/json", content_type); + + uint64_t content_length = 0; + ASSERT_NO_THROW( + content_length = request_->getHeaderValueAsUint64("Content-Length") + ); + EXPECT_EQ(1024, content_length); +} + +// This test verifies that it is possible to specify required +// methods for the request and that an error is thrown if the +// selected method doesn't match. +TEST_F(HttpRequestTest, requiredMethods) { + request_->requireHttpMethod(HttpRequest::Method::HTTP_GET); + request_->requireHttpMethod(HttpRequest::Method::HTTP_POST); + + setContextBasics("GET", "/isc/org", HttpVersion(1, 1)); + + ASSERT_NO_THROW(request_->create()); + + request_->context()->method_ = "POST"; + ASSERT_NO_THROW(request_->create()); + + request_->context()->method_ = "PUT"; + EXPECT_THROW(request_->create(), HttpRequestError); +} + +// This test verifies that it is possible to specify required +// HTTP version for the request and that an error is thrown if +// the selected HTTP version doesn't match. +TEST_F(HttpRequestTest, requiredHttpVersion) { + request_->requireHttpVersion(HttpVersion(1, 0)); + request_->requireHttpVersion(HttpVersion(1, 1)); + + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + EXPECT_NO_THROW(request_->create()); + + setContextBasics("POST", "/isc/org", HttpVersion(1, 1)); + EXPECT_NO_THROW(request_->create()); + + setContextBasics("POST", "/isc/org", HttpVersion(2, 0)); + EXPECT_THROW(request_->create(), HttpRequestError); +} + +// This test verifies that it is possible to specify required +// HTTP headers for the request and that an error is thrown if +// the required header is not included. +TEST_F(HttpRequestTest, requiredHeader) { + request_->requireHeader("Content-Length"); + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + + ASSERT_THROW(request_->create(), HttpRequestError); + + addHeaderToContext("Content-Type", "application/json"); + ASSERT_THROW(request_->create(), HttpRequestError); + + addHeaderToContext("Content-Length", "2048"); + EXPECT_NO_THROW(request_->create()); +} + +// This test verifies that it is possible to specify required +// HTTP header value for the request and that an error is thrown +// if the value doesn't match. +TEST_F(HttpRequestTest, requiredHeaderValue) { + request_->requireHeaderValue("Content-Type", "application/json"); + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Type", "text/html"); + + ASSERT_THROW(request_->create(), HttpRequestError); + + addHeaderToContext("Content-Type", "application/json"); + + EXPECT_NO_THROW(request_->create()); +} + +// This test verifies that an error is thrown upon an attempt to +// fetch request properties before the request is finalized. +TEST_F(HttpRequestTest, notCreated) { + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Type", "text/html"); + addHeaderToContext("Content-Length", "1024"); + + EXPECT_THROW(static_cast<void>(request_->getMethod()), HttpMessageError); + EXPECT_THROW(static_cast<void>(request_->getHttpVersion()), + HttpMessageError); + EXPECT_THROW(static_cast<void>(request_->getUri()), HttpMessageError); + EXPECT_THROW(static_cast<void>(request_->getHeaderValue("Content-Type")), + HttpMessageError); + EXPECT_THROW(static_cast<void>(request_->getHeaderValueAsUint64("Content-Length")), + HttpMessageError); + EXPECT_THROW(static_cast<void>(request_->getBody()), HttpMessageError); + + ASSERT_NO_THROW(request_->finalize()); + + EXPECT_NO_THROW(static_cast<void>(request_->getMethod())); + EXPECT_NO_THROW(static_cast<void>(request_->getHttpVersion())); + EXPECT_NO_THROW(static_cast<void>(request_->getUri())); + EXPECT_NO_THROW(static_cast<void>(request_->getHeaderValue("Content-Type"))); + EXPECT_NO_THROW( + static_cast<void>(request_->getHeaderValueAsUint64("Content-Length")) + ); + EXPECT_NO_THROW(static_cast<void>(request_->getBody())); +} + +// This test verifies that it is possible to fetch the request +// body. +TEST_F(HttpRequestTest, getBody) { + std::string json_body = "{ \"param1\": \"foo\" }"; + + setContextBasics("POST", "/isc/org", HttpVersion(1, 0)); + addHeaderToContext("Content-Length", json_body.length()); + + request_->context()->body_ = json_body; + + ASSERT_NO_THROW(request_->finalize()); + + EXPECT_EQ(json_body, request_->getBody()); +} + +// This test verifies the behavior of the requiresBody function. +TEST_F(HttpRequestTest, requiresBody) { + ASSERT_FALSE(request_->requiresBody()); + request_->requireHeader("Content-Length"); + EXPECT_TRUE(request_->requiresBody()); +} + +// This test verifies that HTTP/1.0 connections are not persistent +// by default. +TEST_F(HttpRequestTest, isPersistentHttp10) { + // In HTTP 1.0 the connection is by default non-persistent. + EXPECT_FALSE(isPersistent(HttpVersion(1, 0))); +} + +// This test verifies that HTTP/1.1 connections are persistent +// by default. +TEST_F(HttpRequestTest, isPersistentHttp11) { + // In HTTP 1.1 the connection is by default persistent. + EXPECT_TRUE(isPersistent(HttpVersion(1, 1))); +} + +// This test verifies that HTTP/1.0 connection becomes persistent +// when keep-alive value of the Connection header is included. +TEST_F(HttpRequestTest, isPersistentHttp10KeepAlive) { + // In HTTP 1.0 the client indicates that the connection is desired to be + // persistent by including "Connection: keep-alive" header. + EXPECT_TRUE( + isPersistent(HttpVersion(1, 0), HttpHeader("Connection", "Keep-alive")) + ); +} + +// This test verifies that HTTP/1.1 connection is closed when the +// close value of the Connection header is included. +TEST_F(HttpRequestTest, isPersistentHttp11Close) { + // In HTTP 1.1 the client would include "Connection: close" header if it + // desires to close the connection. + EXPECT_FALSE( + isPersistent(HttpVersion(1, 1), HttpHeader("Connection", "close")) + ); +} + +// This test verifies the contents of the HTTP outbound request. +TEST_F(HttpRequestTest, clientRequest) { + ASSERT_NO_THROW( + request_.reset(new HttpRequest(HttpRequest::Method::HTTP_POST, + "/isc/org", + HttpVersion(1, 0), + HostHttpHeader("www.example.org"))); + ); + + // Capture current date and time. + HttpDateTime date_time; + + // Add headers. + request_->context()->headers_.push_back(HttpHeaderContext("Date", date_time.rfc1123Format())); + request_->context()->headers_.push_back(HttpHeaderContext("Content-Type", "text/html")); + request_->context()->headers_.push_back(HttpHeaderContext("Accept", "text/html")); + // Add a body. + request_->context()->body_ = "<html></html>"; + // Commit and validate the data. + ASSERT_NO_THROW(request_->finalize()); + + // Check that the HTTP request in the textual format is correct. Note that + // it should include "Content-Length", even though we haven't explicitly set + // this header. It is dynamically computed from the body size. + EXPECT_EQ("POST /isc/org HTTP/1.0\r\n" + "Host: www.example.org\r\n" + "Accept: text/html\r\n" + "Content-Length: 13\r\n" + "Content-Type: text/html\r\n" + "Date: " + date_time.rfc1123Format() + "\r\n" + "\r\n" + "<html></html>", + request_->toString()); +} + +// This test verifies the contents of the HTTP outbound request +// which lacks body. +TEST_F(HttpRequestTest, clientRequestNoBody) { + setContextBasics("GET", "/isc/org", HttpVersion(1, 1)); + // Add headers. + request_->context()->headers_.push_back(HttpHeaderContext("Content-Type", "text/html")); + // Commit and validate the data. + ASSERT_NO_THROW(request_->finalize()); + + // Check that the HTTP request in the textual format is correct. Note that + // there should be no Content-Length included, because the body is empty. + EXPECT_EQ("GET /isc/org HTTP/1.1\r\n" + "Content-Type: text/html\r\n" + "\r\n", + request_->toString()); +} + +// This test verifies the first line of the HTTP request. +TEST_F(HttpRequestTest, toBriefString) { + // Create the request. + setContextBasics("POST", "/isc/org", HttpVersion(1, 1)); + // Add headers. + request_->context()->headers_.push_back(HttpHeaderContext("Content-Type", "application/json")); + // Must be finalized before can be used. + ASSERT_NO_THROW(request_->finalize()); + // Check that the brief string is correct. + EXPECT_EQ("POST /isc/org HTTP/1.1", request_->toBriefString()); +} + +// This test verifies that no basic HTTP authentication is supported. +TEST_F(HttpRequestTest, noBasicAuth) { + ASSERT_NO_THROW(request_.reset(new HttpRequest(HttpRequest::Method::HTTP_GET, + "/isc/org", + HttpVersion(1, 1), + HostHttpHeader("www.example.org")))); + + ASSERT_NO_THROW(request_->finalize()); + ASSERT_THROW(request_->getHeader("Authorization"), + HttpMessageNonExistingHeader); +} + +// This test verifies that basic HTTP authentication works as expected. +TEST_F(HttpRequestTest, basicAuth) { + BasicHttpAuthPtr basic_auth; + EXPECT_NO_THROW(basic_auth.reset(new BasicHttpAuth("foo", "bar"))); + ASSERT_TRUE(basic_auth); + + ASSERT_NO_THROW(request_.reset(new HttpRequest(HttpRequest::Method::HTTP_GET, + "/isc/org", + HttpVersion(1, 1), + HostHttpHeader("www.example.org"), + basic_auth))); + + ASSERT_NO_THROW(request_->finalize()); + + std::string value; + EXPECT_NO_THROW(value = request_->getHeaderValue("Authorization")); + EXPECT_EQ(value, "Basic " + basic_auth->getCredential()); +} + +/// This test verifies that access parameters are handled as expected. +TEST_F(HttpRequestTest, parameters) { + setContextBasics("GET", "/isc/org", HttpVersion(1, 1)); + ASSERT_NO_THROW(request_->create()); + + EXPECT_TRUE(request_->getRemote().empty()); + EXPECT_FALSE(request_->getTls()); + EXPECT_TRUE(request_->getSubject().empty()); + EXPECT_TRUE(request_->getIssuer().empty()); + EXPECT_TRUE(request_->getBasicAuth().empty()); + EXPECT_TRUE(request_->getCustom().empty()); + + request_->setRemote("my-remote"); + request_->setTls(true); + request_->setSubject("my-subject"); + request_->setIssuer("my-issuer"); + request_->setBasicAuth("foo"); + request_->setCustom("bar"); + + EXPECT_EQ("my-remote", request_->getRemote()); + EXPECT_TRUE(request_->getTls()); + EXPECT_EQ("my-subject", request_->getSubject()); + EXPECT_EQ("my-issuer", request_->getIssuer()); + EXPECT_EQ("foo", request_->getBasicAuth()); + EXPECT_EQ("bar", request_->getCustom()); +} + +} diff --git a/src/lib/http/tests/response_creator_unittests.cc b/src/lib/http/tests/response_creator_unittests.cc new file mode 100644 index 0000000..c5370cf --- /dev/null +++ b/src/lib/http/tests/response_creator_unittests.cc @@ -0,0 +1,340 @@ +// Copyright (C) 2016-2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <http/basic_auth.h> +#include <http/basic_auth_config.h> +#include <http/http_types.h> +#include <http/request.h> +#include <http/response.h> +#include <http/response_creator.h> +#include <http/response_json.h> +#include <http/tests/response_test.h> +#include <testutils/log_utils.h> +#include <boost/shared_ptr.hpp> +#include <gtest/gtest.h> + +using namespace isc::dhcp::test; +using namespace isc::http; +using namespace isc::http::test; +using namespace std; + +namespace { + +/// @brief Test HTTP response. +typedef TestHttpResponseBase<HttpResponseJson> Response; + +/// @brief Pointer to test HTTP response. +typedef boost::shared_ptr<Response> ResponsePtr; + +/// @brief Implementation of the @ref HttpResponseCreator. +class TestHttpResponseCreator : public HttpResponseCreator { +public: + + /// @brief Create a new request. + /// + /// @return Pointer to the new instance of the @ref HttpRequest. + virtual HttpRequestPtr + createNewHttpRequest() const { + return (HttpRequestPtr(new HttpRequest())); + } + +private: + + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP response. + virtual HttpResponsePtr + createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const { + // The request hasn't been finalized so the request object + // doesn't contain any information about the HTTP version number + // used. But, the context should have this data (assuming the + // HTTP version is parsed ok). + HttpVersion http_version(request->context()->http_version_major_, + request->context()->http_version_minor_); + // This will generate the response holding JSON content. + ResponsePtr response(new Response(http_version, status_code)); + response->finalize(); + return (response); + } + + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP OK response with no content. + virtual HttpResponsePtr + createDynamicHttpResponse(HttpRequestPtr request) { + // The simplest thing is to create a response with no content. + // We don't need content to test our class. + ResponsePtr response(new Response(request->getHttpVersion(), + HttpStatusCode::OK)); + response->finalize(); + return (response); + } +}; + +/// @brief Pointer to test HTTP response creator. +typedef boost::shared_ptr<TestHttpResponseCreator> TestHttpResponseCreatorPtr; + +// This test verifies that Bad Request status is generated when the request +// hasn't been finalized. +TEST(HttpResponseCreatorTest, badRequest) { + HttpResponsePtr response; + // Create a request but do not finalize it. + HttpRequestPtr request(new HttpRequest()); + request->context()->http_version_major_ = 1; + request->context()->http_version_minor_ = 0; + request->context()->method_ = "GET"; + request->context()->uri_ = "/foo"; + + // Use test specific implementation of Response Creator. It should + // generate HTTP error 400. + TestHttpResponseCreator creator; + ASSERT_NO_THROW(response = creator.createHttpResponse(request)); + ASSERT_TRUE(response); + + EXPECT_EQ("HTTP/1.0 400 Bad Request\r\n" + "Content-Length: 40\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n\r\n" + "{ \"result\": 400, \"text\": \"Bad Request\" }", + response->toString()); +} + +// This test verifies that response is generated successfully from the +// finalized/parsed request. +TEST(HttpResponseCreatorTest, goodRequest) { + // There is no credentials so it checks also what happens when + // authentication is not required. + + HttpResponsePtr response; + // Create request and finalize it. + HttpRequestPtr request(new HttpRequest()); + request->context()->http_version_major_ = 1; + request->context()->http_version_minor_ = 0; + request->context()->method_ = "GET"; + request->context()->uri_ = "/foo"; + ASSERT_NO_THROW(request->finalize()); + + // Use test specific implementation of the Response Creator to generate + // a response. + TestHttpResponseCreator creator; + ASSERT_NO_THROW(response = creator.createHttpResponse(request)); + ASSERT_TRUE(response); + + EXPECT_EQ("HTTP/1.0 200 OK\r\n" + "Content-Length: 0\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n\r\n", + response->toString()); +} + +/// @brief Test fixture for HTTP response creator authentication. +class HttpResponseCreatorAuthTest : public LogContentTest { }; + +// This test verifies that missing required authentication header gives +// unauthorized error. +TEST_F(HttpResponseCreatorAuthTest, noAuth) { + // Create basic HTTP authentication configuration. + BasicHttpAuthConfigPtr auth_config(new BasicHttpAuthConfig()); + EXPECT_NO_THROW(auth_config->add("test", "", "123\xa3", "")); + const BasicHttpAuthMap& credentials = auth_config->getCredentialMap(); + auto cred = credentials.find("dGVzdDoxMjPCow=="); + EXPECT_NE(cred, credentials.end()); + EXPECT_EQ(cred->second, "test"); + auth_config->setRealm("ISC.ORG"); + + // Create request and finalize it. + HttpRequestPtr request(new HttpRequest()); + request->context()->http_version_major_ = 1; + request->context()->http_version_minor_ = 0; + request->context()->method_ = "GET"; + request->context()->uri_ = "/foo"; + ASSERT_NO_THROW(request->finalize()); + HttpRequest::recordBasicAuth_ = true; + + HttpResponsePtr response; + TestHttpResponseCreatorPtr creator(new TestHttpResponseCreator());; + ASSERT_NO_THROW(response = auth_config->checkAuth(*creator, request)); + ASSERT_TRUE(response); + + EXPECT_EQ("HTTP/1.0 401 Unauthorized\r\n" + "Content-Length: 41\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "WWW-Authenticate: Basic realm=\"ISC.ORG\"\r\n\r\n" + "{ \"result\": 401, \"text\": \"Unauthorized\" }", + response->toString()); + + EXPECT_TRUE(request->getBasicAuth().empty()); + addString("HTTP_CLIENT_REQUEST_NO_AUTH_HEADER received HTTP request " + "without required authentication header"); + EXPECT_TRUE(checkFile()); +} + +// This test verifies that too short authentication header is rejected. +TEST_F(HttpResponseCreatorAuthTest, authTooShort) { + // Create basic HTTP authentication configuration. + BasicHttpAuthConfigPtr auth_config(new BasicHttpAuthConfig()); + EXPECT_NO_THROW(auth_config->add("test", "", "123\xa3", "")); + const BasicHttpAuthMap& credentials = auth_config->getCredentialMap(); + auto cred = credentials.find("dGVzdDoxMjPCow=="); + EXPECT_NE(cred, credentials.end()); + EXPECT_EQ(cred->second, "test"); + auth_config->setRealm("ISC.ORG"); + + // Create request and finalize it. + HttpRequestPtr request(new HttpRequest()); + request->context()->http_version_major_ = 1; + request->context()->http_version_minor_ = 0; + request->context()->method_ = "GET"; + request->context()->uri_ = "/foo"; + HttpHeaderContext auth("Authorization", "Basic ="); + request->context()->headers_.push_back(auth); + ASSERT_NO_THROW(request->finalize()); + HttpRequest::recordBasicAuth_ = true; + + HttpResponsePtr response; + TestHttpResponseCreatorPtr creator(new TestHttpResponseCreator());; + ASSERT_NO_THROW(response = auth_config->checkAuth(*creator, request)); + ASSERT_TRUE(response); + + EXPECT_EQ("HTTP/1.0 401 Unauthorized\r\n" + "Content-Length: 41\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "WWW-Authenticate: Basic realm=\"ISC.ORG\"\r\n\r\n" + "{ \"result\": 401, \"text\": \"Unauthorized\" }", + response->toString()); + + EXPECT_TRUE(request->getBasicAuth().empty()); + addString("HTTP_CLIENT_REQUEST_BAD_AUTH_HEADER received HTTP request " + "with malformed authentication header: " + "header content is too short"); + EXPECT_TRUE(checkFile()); +} + +// This test verifies that another authentication schema is rejected. +TEST_F(HttpResponseCreatorAuthTest, badScheme) { + // Create basic HTTP authentication configuration. + BasicHttpAuthConfigPtr auth_config(new BasicHttpAuthConfig()); + EXPECT_NO_THROW(auth_config->add("test", "", "123\xa3", "")); + const BasicHttpAuthMap& credentials = auth_config->getCredentialMap(); + auto cred = credentials.find("dGVzdDoxMjPCow=="); + EXPECT_NE(cred, credentials.end()); + EXPECT_EQ(cred->second, "test"); + auth_config->setRealm("ISC.ORG"); + + // Create request and finalize it. + HttpRequestPtr request(new HttpRequest()); + request->context()->http_version_major_ = 1; + request->context()->http_version_minor_ = 0; + request->context()->method_ = "GET"; + request->context()->uri_ = "/foo"; + HttpHeaderContext auth("Authorization", "Basis dGVzdDoxMjPCow=="); + request->context()->headers_.push_back(auth); + ASSERT_NO_THROW(request->finalize()); + HttpRequest::recordBasicAuth_ = true; + + HttpResponsePtr response; + TestHttpResponseCreatorPtr creator(new TestHttpResponseCreator());; + ASSERT_NO_THROW(response = auth_config->checkAuth(*creator, request)); + ASSERT_TRUE(response); + + EXPECT_EQ("HTTP/1.0 401 Unauthorized\r\n" + "Content-Length: 41\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "WWW-Authenticate: Basic realm=\"ISC.ORG\"\r\n\r\n" + "{ \"result\": 401, \"text\": \"Unauthorized\" }", + response->toString()); + + EXPECT_TRUE(request->getBasicAuth().empty()); + addString("HTTP_CLIENT_REQUEST_BAD_AUTH_HEADER received HTTP request " + "with malformed authentication header: " + "not basic authentication"); + EXPECT_TRUE(checkFile()); +} + +// This test verifies that not matching credential is rejected. +TEST_F(HttpResponseCreatorAuthTest, notMatching) { + // Create basic HTTP authentication configuration. + BasicHttpAuthConfigPtr auth_config(new BasicHttpAuthConfig()); + EXPECT_NO_THROW(auth_config->add("test", "", "123\xa3", "")); + const BasicHttpAuthMap& credentials = auth_config->getCredentialMap(); + auto cred = credentials.find("dGVzdDoxMjPCow=="); + EXPECT_NE(cred, credentials.end()); + EXPECT_EQ(cred->second, "test"); + auth_config->setRealm("ISC.ORG"); + + // Create request and finalize it. + HttpRequestPtr request(new HttpRequest()); + request->context()->http_version_major_ = 1; + request->context()->http_version_minor_ = 0; + request->context()->method_ = "GET"; + request->context()->uri_ = "/foo"; + // Slightly different credential... + HttpHeaderContext auth("Authorization", "Basic dGvZdDoxMjPcOw=="); + request->context()->headers_.push_back(auth); + ASSERT_NO_THROW(request->finalize()); + HttpRequest::recordBasicAuth_ = true; + + HttpResponsePtr response; + TestHttpResponseCreatorPtr creator(new TestHttpResponseCreator());; + ASSERT_NO_THROW(response = auth_config->checkAuth(*creator, request)); + ASSERT_TRUE(response); + + EXPECT_EQ("HTTP/1.0 401 Unauthorized\r\n" + "Content-Length: 41\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "WWW-Authenticate: Basic realm=\"ISC.ORG\"\r\n\r\n" + "{ \"result\": 401, \"text\": \"Unauthorized\" }", + response->toString()); + + EXPECT_TRUE(request->getBasicAuth().empty()); + addString("HTTP_CLIENT_REQUEST_NOT_AUTHORIZED received HTTP request " + "with not matching authentication header"); + EXPECT_TRUE(checkFile()); +} + +// This test verifies that matching credential is accepted. +TEST_F(HttpResponseCreatorAuthTest, matching) { + // Create basic HTTP authentication configuration. + BasicHttpAuthConfigPtr auth_config(new BasicHttpAuthConfig()); + EXPECT_NO_THROW(auth_config->add("test", "", "123\xa3", "")); + const BasicHttpAuthMap& credentials = auth_config->getCredentialMap(); + auto cred = credentials.find("dGVzdDoxMjPCow=="); + EXPECT_NE(cred, credentials.end()); + EXPECT_EQ(cred->second, "test"); + auth_config->setRealm("ISC.ORG"); + + // Create request and finalize it. + HttpRequestPtr request(new HttpRequest()); + request->context()->http_version_major_ = 1; + request->context()->http_version_minor_ = 0; + request->context()->method_ = "GET"; + request->context()->uri_ = "/foo"; + HttpHeaderContext auth("Authorization", "Basic dGVzdDoxMjPCow=="); + request->context()->headers_.push_back(auth); + ASSERT_NO_THROW(request->finalize()); + HttpRequest::recordBasicAuth_ = true; + + HttpResponsePtr response; + TestHttpResponseCreatorPtr creator(new TestHttpResponseCreator());; + ASSERT_NO_THROW(response = auth_config->checkAuth(*creator, request)); + EXPECT_FALSE(response); + + EXPECT_EQ("test", request->getBasicAuth()); + addString("HTTP_CLIENT_REQUEST_AUTHORIZED received HTTP request " + "authorized for 'test'"); + EXPECT_TRUE(checkFile()); + HttpRequest::recordBasicAuth_ = false; +} + +} diff --git a/src/lib/http/tests/response_json_unittests.cc b/src/lib/http/tests/response_json_unittests.cc new file mode 100644 index 0000000..e3829b5 --- /dev/null +++ b/src/lib/http/tests/response_json_unittests.cc @@ -0,0 +1,151 @@ +// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <cc/data.h> +#include <http/http_types.h> +#include <http/response_json.h> +#include <http/tests/response_test.h> +#include <gtest/gtest.h> +#include <sstream> + +using namespace isc::data; +using namespace isc::http; +using namespace isc::http::test; + +namespace { + +/// @brief Response type used in tests. +typedef TestHttpResponseBase<HttpResponseJson> TestHttpResponseJson; + +/// @brief Test fixture class for @ref HttpResponseJson. +class HttpResponseJsonTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Initializes the following class members: + /// - json_string_ - which is a pretty formatted JSON content, + /// - json_ - A structure of Element objects representing the JSON, + /// - json_string_from_json_ - which is a JSON text converted back from + /// the json_ data structure. It is the same content as json_string_ + /// but has different whitespaces. + HttpResponseJsonTest() + : json_(), json_string_(), json_string_from_json_() { + json_string_ = + "[" + " {" + " \"pid\": 8080," + " \"status\": 10," + " \"comment\": \"Nice comment from 8080\"" + " }," + " {" + " \"pid\": 8081," + " \"status\": 12," + " \"comment\": \"A comment from 8081\"" + " }" + "]"; + + json_ = Element::fromJSON(json_string_); + json_string_from_json_ = json_->str(); + } + + /// @brief Test that the response format is correct. + /// + /// @param status_code HTTP status code for which the response should + /// be tested. + /// @param status_message HTTP status message. + void testGenericResponse(const HttpStatusCode& status_code, + const std::string& status_message) { + TestHttpResponseJson response(HttpVersion(1, 0), status_code); + ASSERT_NO_THROW(response.finalize()); + std::ostringstream status_message_json; + // Build the expected content. + status_message_json << "{ \"result\": " + << static_cast<uint16_t>(status_code) + << ", \"text\": " + << "\"" << status_message << "\" }"; + std::ostringstream response_string; + response_string << + "HTTP/1.0 " << static_cast<uint16_t>(status_code) << " " + << status_message << "\r\n"; + + // The content must only be generated for error codes. + if (HttpResponse::isClientError(status_code) || + HttpResponse::isServerError(status_code)) { + response_string << "Content-Length: " << status_message_json.str().size() + << "\r\n"; + } else { + response_string << "Content-Length: 0\r\n"; + } + + // Content-Type and Date are automatically included. + response_string << "Content-Type: application/json\r\n" + "Date: " << response.getDateHeaderValue() << "\r\n\r\n"; + + if (HttpResponse::isClientError(status_code) || + HttpResponse::isServerError(status_code)) { + response_string << status_message_json.str(); + } + + // Check that the output is as expected. + EXPECT_EQ(response_string.str(), response.toString()); + } + + /// @brief JSON content represented as structure of Element objects. + ConstElementPtr json_; + + /// @brief Pretty formatted JSON content. + std::string json_string_; + + /// @brief JSON content parsed back from json_ structure. + std::string json_string_from_json_; + +}; + +// Test that the response with custom JSON content is generated properly. +TEST_F(HttpResponseJsonTest, responseWithContent) { + TestHttpResponseJson response(HttpVersion(1, 1), HttpStatusCode::OK); + ASSERT_NO_THROW(response.setBodyAsJson(json_)); + ASSERT_NO_THROW(response.finalize()); + + std::ostringstream response_string; + response_string << + "HTTP/1.1 200 OK\r\n" + "Content-Length: " << json_string_from_json_.length() << "\r\n" + "Content-Type: application/json\r\n" + "Date: " << response.getDateHeaderValue() << "\r\n\r\n" + << json_string_from_json_; + EXPECT_EQ(response_string.str(), response.toString()); +} + +// Test that generic responses are created properly. +TEST_F(HttpResponseJsonTest, genericResponse) { + testGenericResponse(HttpStatusCode::OK, "OK"); + testGenericResponse(HttpStatusCode::CREATED, "Created"); + testGenericResponse(HttpStatusCode::ACCEPTED, "Accepted"); + testGenericResponse(HttpStatusCode::NO_CONTENT, "No Content"); + testGenericResponse(HttpStatusCode::MULTIPLE_CHOICES, + "Multiple Choices"); + testGenericResponse(HttpStatusCode::MOVED_PERMANENTLY, + "Moved Permanently"); + testGenericResponse(HttpStatusCode::MOVED_TEMPORARILY, + "Moved Temporarily"); + testGenericResponse(HttpStatusCode::NOT_MODIFIED, "Not Modified"); + testGenericResponse(HttpStatusCode::BAD_REQUEST, "Bad Request"); + testGenericResponse(HttpStatusCode::UNAUTHORIZED, "Unauthorized"); + testGenericResponse(HttpStatusCode::FORBIDDEN, "Forbidden"); + testGenericResponse(HttpStatusCode::NOT_FOUND, "Not Found"); + testGenericResponse(HttpStatusCode::REQUEST_TIMEOUT, "Request Timeout"); + testGenericResponse(HttpStatusCode::INTERNAL_SERVER_ERROR, + "Internal Server Error"); + testGenericResponse(HttpStatusCode::NOT_IMPLEMENTED, "Not Implemented"); + testGenericResponse(HttpStatusCode::BAD_GATEWAY, "Bad Gateway"); + testGenericResponse(HttpStatusCode::SERVICE_UNAVAILABLE, + "Service Unavailable"); +} + +} diff --git a/src/lib/http/tests/response_parser_unittests.cc b/src/lib/http/tests/response_parser_unittests.cc new file mode 100644 index 0000000..58d479d --- /dev/null +++ b/src/lib/http/tests/response_parser_unittests.cc @@ -0,0 +1,351 @@ +// Copyright (C) 2017-2019 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <cc/data.h> +#include <http/response_json.h> +#include <http/response_parser.h> +#include <gtest/gtest.h> +#include <string> + +using namespace isc; +using namespace isc::data; +using namespace isc::http; + +namespace { + +/// @brief Test fixture class for @ref HttpResponseParser. +class HttpResponseParserTest : public ::testing::Test { +public: + + /// @brief Creates HTTP response string. + /// + /// @param preamble A string including HTTP response's first line + /// and all headers except "Content-Length". + /// @param payload A string containing HTTP response payload. + std::string createResponseString(const std::string& preamble, + const std::string& payload) { + std::ostringstream s; + s << preamble; + s << "Content-Length: " << payload.length() << "\r\n\r\n" + << payload; + return (s.str()); + } + + /// @brief Parses the HTTP response and checks that parsing was + /// successful. + /// + /// @param http_resp HTTP response string. + void doParse(const std::string& http_resp) { + HttpResponseParser parser(response_); + ASSERT_NO_THROW(parser.initModel()); + + parser.postBuffer(&http_resp[0], http_resp.size()); + ASSERT_NO_THROW(parser.poll()); + + ASSERT_FALSE(parser.needData()); + ASSERT_TRUE(parser.httpParseOk()); + EXPECT_TRUE(parser.getErrorMessage().empty()); + } + + /// @brief Tests that parsing fails when malformed HTTP response + /// is received. + /// + /// @param http_resp HTTP response string. + void testInvalidHttpResponse(const std::string& http_resp) { + HttpResponseParser parser(response_); + ASSERT_NO_THROW(parser.initModel()); + + parser.postBuffer(&http_resp[0], http_resp.size()); + ASSERT_NO_THROW(parser.poll()); + + EXPECT_FALSE(parser.needData()); + EXPECT_FALSE(parser.httpParseOk()); + EXPECT_FALSE(parser.getErrorMessage().empty()); + } + + /// @brief Tests that the response specified with (header, body) can + /// be parsed properly. + /// + /// @param header specifies the header of the response to be parsed + /// @param json specifies the body of the response (JSON in text format) to be parsed + /// @param expect_success whether the parsing is expected to be successful + /// + /// @return a parser that parsed the response for further inspection + HttpResponseJson testResponseWithJson(const std::string& header, + const std::string& json, + bool expect_success = true) { + std::string http_resp = createResponseString(header, json); + + // Create HTTP response which accepts JSON as a body. + HttpResponseJson response; + + // Create a parser and make it use the response we created. + HttpResponseParser parser(response); + EXPECT_NO_THROW(parser.initModel()); + + // Simulate receiving HTTP response in chunks. + const unsigned chunk_size = 10; + while (!http_resp.empty()) { + size_t chunk = http_resp.size() % chunk_size; + if (chunk == 0) { + chunk = chunk_size; + } + + parser.postBuffer(&http_resp[0], chunk); + http_resp.erase(0, chunk); + parser.poll(); + if (chunk < chunk_size) { + EXPECT_TRUE(parser.needData()); + if (!parser.needData()) { + ADD_FAILURE() << "Parser completed prematurely"; + return (response); + } + } + } + + if (expect_success) { + // Parser should have parsed the response and should expect no more data. + EXPECT_FALSE(parser.needData()); + // Parsing should be successful. + EXPECT_TRUE(parser.httpParseOk()) << parser.getErrorMessage(); + // There should be no error message. + EXPECT_TRUE(parser.getErrorMessage().empty()); + } + + return (response); + } + + /// @brief Instance of the HttpResponse used by the unit tests. + HttpResponse response_; +}; + +// Test test verifies that an HTTP response including JSON body is parsed +// successfully. +TEST_F(HttpResponseParserTest, responseWithJson) { + std::string http_resp = "HTTP/1.1 408 Request Timeout\r\n" + "Content-Type: application/json\r\n"; + std::string json = "{ \"result\": 0, \"text\": \"All ok\" }"; + + HttpResponseJson response = testResponseWithJson(http_resp, json); + + // Verify HTTP version, status code and phrase. + EXPECT_EQ(1, response.getHttpVersion().major_); + EXPECT_EQ(1, response.getHttpVersion().minor_); + EXPECT_EQ(HttpStatusCode::REQUEST_TIMEOUT, response.getStatusCode()); + EXPECT_EQ("Request Timeout", response.getStatusPhrase()); + + // Try to retrieve values carried in JSON payload. + ConstElementPtr json_element; + ASSERT_NO_THROW(json_element = response.getJsonElement("result")); + EXPECT_EQ(0, json_element->intValue()); + + ASSERT_NO_THROW(json_element = response.getJsonElement("text")); + EXPECT_EQ("All ok", json_element->stringValue()); +} + +// This test verifies that extraneous data in the response will not cause +// an error if "Content-Length" value refers to the length of the valid +// part of the response. +TEST_F(HttpResponseParserTest, extraneousDataInResponse) { + std::string http_resp = "HTTP/1.0 200 OK\r\n" + "Content-Type: application/json\r\n"; + std::string json = "{ \"service\": \"dhcp4\", \"command\": \"shutdown\" }"; + + // Create valid response. + http_resp = createResponseString(http_resp, json); + + // Add some garbage at the end. + http_resp += "some stuff which, if parsed, will cause errors"; + + // Create HTTP response which accepts JSON as a body. + HttpResponseJson response; + + // Create a parser and make it use the response we created. + HttpResponseParser parser(response); + ASSERT_NO_THROW(parser.initModel()); + + // Feed the parser with the response containing some garbage at the end. + parser.postBuffer(&http_resp[0], http_resp.size()); + ASSERT_NO_THROW(parser.poll()); + + // The parser should only parse the valid part of the response as indicated + // by the Content-Length. + ASSERT_FALSE(parser.needData()); + ASSERT_TRUE(parser.httpParseOk()); + // There should be no error message. + EXPECT_TRUE(parser.getErrorMessage().empty()); + + // Do another poll() to see if the parser will parse the garbage. We + // expect that it doesn't. + ASSERT_NO_THROW(parser.poll()); + EXPECT_FALSE(parser.needData()); + EXPECT_TRUE(parser.httpParseOk()); + EXPECT_TRUE(parser.getErrorMessage().empty()); +} + +// This test verifies that LWS is parsed correctly. The LWS (linear white +// space) marks line breaks in the HTTP header values. +TEST_F(HttpResponseParserTest, getLWS) { + // "User-Agent" header contains line breaks with whitespaces in the new + // lines to mark continuation of the header value. + std::string http_resp = "HTTP/1.1 200 OK\r\n" + "Content-Type: text/html\r\n" + "User-Agent: Kea/1.2 Command \r\n" + " Control \r\n" + "\tClient\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_resp)); + + // Verify parsed values. + EXPECT_EQ(1, response_.getHttpVersion().major_); + EXPECT_EQ(1, response_.getHttpVersion().minor_); + EXPECT_EQ(HttpStatusCode::OK, response_.getStatusCode()); + EXPECT_EQ("OK", response_.getStatusPhrase()); + EXPECT_EQ("text/html", response_.getHeaderValue("Content-Type")); + EXPECT_EQ("Kea/1.2 Command Control Client", + response_.getHeaderValue("User-Agent")); +} + +// This test verifies that the HTTP response with no headers is +// parsed correctly. +TEST_F(HttpResponseParserTest, noHeaders) { + std::string http_resp = "HTTP/1.1 204 No Content\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_resp)); + + // Verify the values. + EXPECT_EQ(1, response_.getHttpVersion().major_); + EXPECT_EQ(1, response_.getHttpVersion().minor_); + EXPECT_EQ(HttpStatusCode::NO_CONTENT, response_.getStatusCode()); +} + +// This test verifies that headers are case insensitive. +TEST_F(HttpResponseParserTest, headersCaseInsensitive) { + std::string http_resp = "HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "connection: clOSe\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_resp)); + + EXPECT_EQ("text/html", response_.getHeader("Content-Type")->getValue()); + EXPECT_EQ("close", response_.getHeader("Connection")->getLowerCaseValue()); + EXPECT_EQ(1, response_.getHttpVersion().major_); + EXPECT_EQ(1, response_.getHttpVersion().minor_); + EXPECT_EQ(HttpStatusCode::OK, response_.getStatusCode()); + EXPECT_EQ("OK", response_.getStatusPhrase()); +} + +// This test verifies that the header with no whitespace between the +// colon and header value is accepted. +TEST_F(HttpResponseParserTest, noHeaderWhitespace) { + std::string http_resp = "HTTP/1.0 200 OK\r\n" + "Content-Type:text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_resp)); + + EXPECT_EQ("text/html", response_.getHeaderValue("Content-Type")); + EXPECT_EQ(1, response_.getHttpVersion().major_); + EXPECT_EQ(0, response_.getHttpVersion().minor_); + EXPECT_EQ(HttpStatusCode::OK, response_.getStatusCode()); + EXPECT_EQ("OK", response_.getStatusPhrase()); +} + +// This test verifies that the header value preceded with multiple +// whitespaces is accepted. +TEST_F(HttpResponseParserTest, multipleLeadingHeaderWhitespaces) { + std::string http_resp = "HTTP/1.0 200 OK\r\n" + "Content-Type: text/html\r\n\r\n"; + + ASSERT_NO_FATAL_FAILURE(doParse(http_resp)); + + EXPECT_EQ("text/html", response_.getHeaderValue("Content-Type")); + EXPECT_EQ(1, response_.getHttpVersion().major_); + EXPECT_EQ(0, response_.getHttpVersion().minor_); + EXPECT_EQ(HttpStatusCode::OK, response_.getStatusCode()); + EXPECT_EQ("OK", response_.getStatusPhrase()); +} + +// This test verifies that the response containing a typo in the +// HTTP version string causes parsing error. +TEST_F(HttpResponseParserTest, invalidHTTPString) { + std::string http_resp = "HTLP/2.0 100 OK\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpResponse(http_resp); +} + +// This test verifies that error is reported when the HTTP version +// string doesn't contain a slash character. +TEST_F(HttpResponseParserTest, invalidHttpVersionNoSlash) { + std::string http_resp = "HTTP 1.1 100 OK\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpResponse(http_resp); +} + +// This test verifies that error is reported when HTTP version string +// doesn't contain the minor version number. +TEST_F(HttpResponseParserTest, invalidHttpNoMinorVersion) { + std::string http_resp = "HTTP/1 200 OK\r\n" + "Content-Type: text/html\r\n\r\n"; + testInvalidHttpResponse(http_resp); +} + +// This test verifies that error is reported when HTTP header name +// contains an invalid character. +TEST_F(HttpResponseParserTest, invalidHeaderName) { + std::string http_resp = "HTTP/1.1 200 OK\r\n" + "Content-;: text/html\r\n\r\n"; + testInvalidHttpResponse(http_resp); +} + +// This test verifies that error is reported when HTTP header value +// is not preceded with the colon character. +TEST_F(HttpResponseParserTest, noColonInHttpHeader) { + std::string http_resp = "HTTP/1.1 200 OK\r\n" + "Content-Type text/html\r\n\r\n"; + testInvalidHttpResponse(http_resp); +} + +// This test verifies that the HTTP response is formatted for logging. +TEST_F(HttpResponseParserTest, logFormatHttpMessage) { + std::string message = "POST / HTTP/1.1\r\n" + "Host: 127.0.0.1:8080\r\n" + "User-Agent: curl/7.59.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 51\r\n\r\n" + "{ \"command\": \"config-get\", \"service\": [ \"dhcp4\" ] }"; + + // limit = 0 means no limit + EXPECT_EQ(message, HttpResponseParser::logFormatHttpMessage(message, 0)); + + // large enough limit should not cause the truncation. + EXPECT_EQ(message, HttpResponseParser::logFormatHttpMessage(message, 1024)); + + // Only 3 characters requested. The request should be truncated. + EXPECT_EQ("POS.........\n(truncating HTTP message larger than 3 characters)\n", + HttpResponseParser::logFormatHttpMessage(message, 3)); +} + +TEST_F(HttpResponseParserTest, parseEmptyResponse) { + std::string http_resp = "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json\r\n"; + std::string json = ""; + + http_resp = createResponseString(http_resp, json); + + ASSERT_NO_FATAL_FAILURE(doParse(http_resp)); + + HttpResponseJson response = testResponseWithJson(http_resp, json); + + EXPECT_EQ("", response_.getBody()); + EXPECT_EQ(1, response_.getHttpVersion().major_); + EXPECT_EQ(1, response_.getHttpVersion().minor_); + EXPECT_EQ(HttpStatusCode::OK, response_.getStatusCode()); + EXPECT_EQ("OK", response_.getStatusPhrase()); +} + +} diff --git a/src/lib/http/tests/response_test.h b/src/lib/http/tests/response_test.h new file mode 100644 index 0000000..d342a64 --- /dev/null +++ b/src/lib/http/tests/response_test.h @@ -0,0 +1,62 @@ +// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef HTTP_RESPONSE_TEST_H +#define HTTP_RESPONSE_TEST_H + +#include <http/http_types.h> +#include <http/response.h> +#include <boost/lexical_cast.hpp> +#include <cstdint> + +namespace isc { +namespace http { +namespace test { + +/// @brief Base class for test HTTP response. +template<typename HttpResponseType> +class TestHttpResponseBase : public HttpResponseType { +public: + + /// @brief Constructor. + /// + /// @param version HTTP version for the response. + /// @param status_code HTTP status code. + TestHttpResponseBase(const HttpVersion& version, + const HttpStatusCode& status_code) + : HttpResponseType(version, status_code) { + } + + /// @brief Returns fixed header value. + /// + /// Including fixed header value in the response makes the + /// response deterministic, which is critical for the unit + /// tests. + virtual std::string getDateHeaderValue() const { + return ("Tue, 19 Dec 2016 18:53:35 GMT"); + } + + /// @brief Returns date header value. + std::string generateDateHeaderValue() const { + return (HttpResponseType::getDateHeaderValue()); + } + + /// @brief Sets custom content length. + /// + /// @param content_length Content length value. + void setContentLength(const uint64_t content_length) { + HttpHeaderPtr length_header(new HttpHeader("Content-Length", + boost::lexical_cast<std::string> + (content_length))); + HttpResponseType::headers_["content-length"] = length_header; + } +}; + +} // namespace test +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/tests/response_unittests.cc b/src/lib/http/tests/response_unittests.cc new file mode 100644 index 0000000..f54e65d --- /dev/null +++ b/src/lib/http/tests/response_unittests.cc @@ -0,0 +1,169 @@ +// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <http/date_time.h> +#include <http/http_types.h> +#include <http/response.h> +#include <http/tests/response_test.h> +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +using namespace boost::posix_time; +using namespace isc::http; +using namespace isc::http::test; + +namespace { + +/// @brief Response type used in tests. +typedef TestHttpResponseBase<HttpResponse> TestHttpResponse; + +/// @brief Test fixture class for @ref HttpResponse. +class HttpResponseTest : public ::testing::Test { +public: + + /// @brief Checks if the format of the response is correct. + /// + /// @param status_code HTTP status code in the response. + /// @param status_message HTTP status message in the response. + void testResponse(const HttpStatusCode& status_code, + const std::string& status_message) { + // Create the response. Because we're using derived class + // it returns the fixed value of the Date header, which is + // very useful in unit tests. + TestHttpResponse response(HttpVersion(1, 0), status_code); + response.context()->headers_.push_back(HttpHeaderContext("Content-Type", "text/html")); + ASSERT_NO_THROW(response.finalize()); + std::ostringstream response_string; + response_string << "HTTP/1.0 " << static_cast<uint16_t>(status_code) + << " " << status_message; + EXPECT_EQ(response_string.str(), response.toBriefString()); + + response_string + << "\r\nContent-Length: 0\r\n" + << "Content-Type: text/html\r\n" + << "Date: " << response.getDateHeaderValue() << "\r\n\r\n"; + EXPECT_EQ(response_string.str(), response.toString()); + } +}; + +// Test the case of HTTP OK message. +TEST_F(HttpResponseTest, responseOK) { + // Include HTML body. + const std::string sample_body = + "<html>" + "<head><title>Kea page title</title></head>" + "<body><h1>Some header</h1></body>" + "</html>"; + + // Create the message and add some headers. + TestHttpResponse response(HttpVersion(1, 0), HttpStatusCode::OK); + response.context()->headers_.push_back(HttpHeaderContext("Content-Type", "text/html")); + response.context()->headers_.push_back(HttpHeaderContext("Host", "kea.example.org")); + response.context()->body_ = sample_body; + ASSERT_NO_THROW(response.finalize()); + + // Create a string holding expected response. Note that the Date + // is a fixed value returned by the customized TestHttpResponse + // class. + std::ostringstream response_string; + response_string << + "HTTP/1.0 200 OK\r\n" + "Content-Length: " << sample_body.length() << "\r\n" + "Content-Type: text/html\r\n" + "Date: " << response.getDateHeaderValue() << "\r\n" + "Host: kea.example.org\r\n\r\n" << sample_body; + + EXPECT_EQ(response_string.str(), response.toString()); +} + +// Test generic responses for various status codes. +TEST_F(HttpResponseTest, genericResponse) { + testResponse(HttpStatusCode::OK, "OK"); + testResponse(HttpStatusCode::CREATED, "Created"); + testResponse(HttpStatusCode::ACCEPTED, "Accepted"); + testResponse(HttpStatusCode::NO_CONTENT, "No Content"); + testResponse(HttpStatusCode::MULTIPLE_CHOICES, "Multiple Choices"); + testResponse(HttpStatusCode::MOVED_PERMANENTLY, "Moved Permanently"); + testResponse(HttpStatusCode::MOVED_TEMPORARILY, "Moved Temporarily"); + testResponse(HttpStatusCode::NOT_MODIFIED, "Not Modified"); + testResponse(HttpStatusCode::BAD_REQUEST, "Bad Request"); + testResponse(HttpStatusCode::UNAUTHORIZED, "Unauthorized"); + testResponse(HttpStatusCode::FORBIDDEN, "Forbidden"); + testResponse(HttpStatusCode::NOT_FOUND, "Not Found"); + testResponse(HttpStatusCode::REQUEST_TIMEOUT, "Request Timeout"); + testResponse(HttpStatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error"); + testResponse(HttpStatusCode::NOT_IMPLEMENTED, "Not Implemented"); + testResponse(HttpStatusCode::BAD_GATEWAY, "Bad Gateway"); + testResponse(HttpStatusCode::SERVICE_UNAVAILABLE, "Service Unavailable"); +} + +// Test if the class correctly identifies client errors. +TEST_F(HttpResponseTest, isClientError) { + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::OK)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::CREATED)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::ACCEPTED)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::NO_CONTENT)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::MULTIPLE_CHOICES)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::MOVED_PERMANENTLY)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::MOVED_TEMPORARILY)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::NOT_MODIFIED)); + EXPECT_TRUE(HttpResponse::isClientError(HttpStatusCode::BAD_REQUEST)); + EXPECT_TRUE(HttpResponse::isClientError(HttpStatusCode::UNAUTHORIZED)); + EXPECT_TRUE(HttpResponse::isClientError(HttpStatusCode::FORBIDDEN)); + EXPECT_TRUE(HttpResponse::isClientError(HttpStatusCode::NOT_FOUND)); + EXPECT_TRUE(HttpResponse::isClientError(HttpStatusCode::REQUEST_TIMEOUT)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::INTERNAL_SERVER_ERROR)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::NOT_IMPLEMENTED)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::BAD_GATEWAY)); + EXPECT_FALSE(HttpResponse::isClientError(HttpStatusCode::SERVICE_UNAVAILABLE)); +} + +// Test if the class correctly identifies server errors. +TEST_F(HttpResponseTest, isServerError) { + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::OK)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::CREATED)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::ACCEPTED)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::NO_CONTENT)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::MULTIPLE_CHOICES)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::MOVED_PERMANENTLY)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::MOVED_TEMPORARILY)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::NOT_MODIFIED)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::BAD_REQUEST)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::UNAUTHORIZED)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::FORBIDDEN)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::NOT_FOUND)); + EXPECT_FALSE(HttpResponse::isServerError(HttpStatusCode::REQUEST_TIMEOUT)); + EXPECT_TRUE(HttpResponse::isServerError(HttpStatusCode::INTERNAL_SERVER_ERROR)); + EXPECT_TRUE(HttpResponse::isServerError(HttpStatusCode::NOT_IMPLEMENTED)); + EXPECT_TRUE(HttpResponse::isServerError(HttpStatusCode::BAD_GATEWAY)); + EXPECT_TRUE(HttpResponse::isServerError(HttpStatusCode::SERVICE_UNAVAILABLE)); +} + +// Test that the generated time value, being included in the Date +// header, is correct. +TEST_F(HttpResponseTest, getDateHeaderValue) { + // Create a response and retrieve the value to be included in the + // Date header. This value should hold a current time in the + // RFC1123 format. + TestHttpResponse response(HttpVersion(1, 0), HttpStatusCode::OK); + std::string generated_date = response.generateDateHeaderValue(); + + // Use our date/time utilities to parse this value into the ptime. + HttpDateTime parsed_time = HttpDateTime::fromRfc1123(generated_date); + + // Now that we have it converted back, we can check how far this + // value is from the current time. To be on the safe side, we check + // that it is not later than 10 seconds apart, rather than checking + // it for equality. In fact, checking it for equality would almost + // certainly cause an error. Especially on a virtual machine. + time_duration parsed_to_current = + microsec_clock::universal_time() - parsed_time.getPtime(); + EXPECT_LT(parsed_to_current.seconds(), 10); +} + +} diff --git a/src/lib/http/tests/run_unittests.cc b/src/lib/http/tests/run_unittests.cc new file mode 100644 index 0000000..17255dc --- /dev/null +++ b/src/lib/http/tests/run_unittests.cc @@ -0,0 +1,21 @@ +// Copyright (C) 2016-2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <log/logger_support.h> +#include <http/http_log.h> +#include <gtest/gtest.h> + +int +main(int argc, char* argv[]) { + ::testing::InitGoogleTest(&argc, argv); + isc::log::initLogger(); + + int result = RUN_ALL_TESTS(); + + return (result); +} diff --git a/src/lib/http/tests/server_client_unittests.cc b/src/lib/http/tests/server_client_unittests.cc new file mode 100644 index 0000000..2ab76f3 --- /dev/null +++ b/src/lib/http/tests/server_client_unittests.cc @@ -0,0 +1,2041 @@ +// Copyright (C) 2017-2023 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <asiolink/asio_wrapper.h> +#include <asiolink/interval_timer.h> +#include <asiolink/tls_acceptor.h> +#include <cc/data.h> +#include <test_http_client.h> +#include <http/client.h> +#include <http/http_types.h> +#include <http/listener.h> +#include <http/listener_impl.h> +#include <http/post_request_json.h> +#include <http/response_creator.h> +#include <http/response_creator_factory.h> +#include <http/response_json.h> +#include <http/tests/response_test.h> +#include <http/url.h> +#include <util/multi_threading_mgr.h> + +#include <boost/asio/buffer.hpp> +#include <boost/asio/ip/tcp.hpp> +#include <boost/pointer_cast.hpp> +#include <gtest/gtest.h> + +#include <functional> +#include <list> +#include <sstream> +#include <string> + +using namespace boost::asio::ip; +using namespace isc::asiolink; +using namespace isc::data; +using namespace isc::http; +using namespace isc::http::test; +using namespace isc::util; +namespace ph = std::placeholders; + +namespace { + +/// @brief IP address to which HTTP service is bound. +const std::string SERVER_ADDRESS = "127.0.0.1"; + +/// @brief IPv6 address to whch HTTP service is bound. +const std::string IPV6_SERVER_ADDRESS = "::1"; + +/// @brief Port number to which HTTP service is bound. +const unsigned short SERVER_PORT = 18123; + +/// @brief Request Timeout used in most of the tests (ms). +const long REQUEST_TIMEOUT = 10000; + +/// @brief Persistent connection idle timeout used in most of the tests (ms). +const long IDLE_TIMEOUT = 10000; + +/// @brief Persistent connection idle timeout used in tests where idle connections +/// are tested (ms). +const long SHORT_IDLE_TIMEOUT = 200; + +/// @brief Test timeout (ms). +const long TEST_TIMEOUT = 10000; + +/// @brief Test HTTP response. +typedef TestHttpResponseBase<HttpResponseJson> Response; + +/// @brief Pointer to test HTTP response. +typedef boost::shared_ptr<Response> ResponsePtr; + +/// @brief Generic test HTTP response. +typedef TestHttpResponseBase<HttpResponse> GenericResponse; + +/// @brief Pointer to generic test HTTP response. +typedef boost::shared_ptr<GenericResponse> GenericResponsePtr; + +/// @brief Implementation of the @ref HttpResponseCreator. +class TestHttpResponseCreator : public HttpResponseCreator { +public: + + /// @brief Create a new request. + /// + /// @return Pointer to the new instance of the @ref HttpRequest. + virtual HttpRequestPtr + createNewHttpRequest() const { + return (HttpRequestPtr(new PostHttpRequestJson())); + } + +private: + + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP response. + virtual HttpResponsePtr + createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const { + // The request hasn't been finalized so the request object + // doesn't contain any information about the HTTP version number + // used. But, the context should have this data (assuming the + // HTTP version is parsed ok). + HttpVersion http_version(request->context()->http_version_major_, + request->context()->http_version_minor_); + // This will generate the response holding JSON content. + ResponsePtr response(new Response(http_version, status_code)); + response->finalize(); + return (response); + } + + /// @brief Creates HTTP response. + /// + /// This method generates 3 types of responses: + /// - response with a requested content type, + /// - partial response with incomplete JSON body, + /// - response with JSON body copied from the request. + /// + /// The first one is useful to test situations when received response can't + /// be parsed because of the content type mismatch. The second one is useful + /// to test request timeouts. The third type is used by most of the unit tests + /// to test successful transactions. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP OK response with no content. + virtual HttpResponsePtr + createDynamicHttpResponse(HttpRequestPtr request) { + // Request must always be JSON. + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast<PostHttpRequestJson>(request); + ConstElementPtr body; + if (request_json) { + body = request_json->getBodyAsJson(); + if (body) { + // Check if the client requested one of the two first response + // types. + GenericResponsePtr response; + ConstElementPtr content_type = body->get("requested-content-type"); + ConstElementPtr partial_response = body->get("partial-response"); + if (content_type || partial_response) { + // The first two response types can only be generated using the + // generic response as we have to explicitly modify some of the + // values. + response.reset(new GenericResponse(request->getHttpVersion(), + HttpStatusCode::OK)); + HttpResponseContextPtr ctx = response->context(); + + if (content_type) { + // Provide requested content type. + ctx->headers_.push_back(HttpHeaderContext("Content-Type", + content_type->stringValue())); + // It doesn't matter what body is there. + ctx->body_ = "abcd"; + response->finalize(); + + } else { + // Generate JSON response. + ctx->headers_.push_back(HttpHeaderContext("Content-Type", + "application/json")); + // The body lacks '}' so the client will be waiting for it and + // eventually should time out. + ctx->body_ = "{"; + response->finalize(); + // The auto generated Content-Length header would be based on the + // body size (so set to 1 byte). We have to override it to + // account for the missing '}' character. + response->setContentLength(2); + } + return (response); + } + } + } + + // Third type of response is requested. + ResponsePtr response(new Response(request->getHttpVersion(), + HttpStatusCode::OK)); + // If body was included in the request. Let's copy it. + if (body) { + response->setBodyAsJson(body); + } + + response->finalize(); + return (response); + } +}; + +/// @brief Implementation of the test @ref HttpResponseCreatorFactory. +/// +/// This factory class creates @ref TestHttpResponseCreator instances. +class TestHttpResponseCreatorFactory : public HttpResponseCreatorFactory { +public: + + /// @brief Creates @ref TestHttpResponseCreator instance. + virtual HttpResponseCreatorPtr create() const { + HttpResponseCreatorPtr response_creator(new TestHttpResponseCreator()); + return (response_creator); + } +}; + +/// @brief Implementation of the HTTP listener used in tests. +/// +/// This implementation replaces the @c HttpConnection type with a custom +/// implementation. +/// +/// @tparam HttpConnectionType Type of the connection object to be used by +/// the listener implementation. +template<typename HttpConnectionType> +class HttpListenerImplCustom : public HttpListenerImpl { +public: + + HttpListenerImplCustom(IOService& io_service, + const IOAddress& server_address, + const unsigned short server_port, + const TlsContextPtr& tls_context, + const HttpResponseCreatorFactoryPtr& creator_factory, + const long request_timeout, + const long idle_timeout) + : HttpListenerImpl(io_service, server_address, server_port, + tls_context, creator_factory, request_timeout, + idle_timeout) { + } + +protected: + + /// @brief Creates an instance of the @c HttpConnection. + /// + /// This method is virtual so as it can be overridden when customized + /// connections are to be used, e.g. in case of unit testing. + /// + /// @param response_creator Pointer to the response creator object used to + /// create HTTP response from the HTTP request received. + /// @param callback Callback invoked when new connection is accepted. + /// + /// @return Pointer to the created connection. + virtual HttpConnectionPtr createConnection(const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback) { + HttpConnectionPtr + conn(new HttpConnectionType(io_service_, acceptor_, + tls_context_, connections_, + response_creator, callback, + request_timeout_, idle_timeout_)); + return (conn); + } +}; + +/// @brief Derivation of the @c HttpListener used in tests. +/// +/// This class replaces the default implementation instance with the +/// @c HttpListenerImplCustom using the customized connection type. +/// +/// @tparam HttpConnectionType Type of the connection object to be used by +/// the listener implementation. +template<typename HttpConnectionType> +class HttpListenerCustom : public HttpListener { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the listener. + /// @param server_address Address on which the HTTP service should run. + /// @param server_port Port number on which the HTTP service should run. + /// @param tls_context TLS context. + /// @param creator_factory Pointer to the caller-defined + /// @ref HttpResponseCreatorFactory derivation which should be used to + /// create @ref HttpResponseCreator instances. + /// @param request_timeout Timeout after which the HTTP Request Timeout + /// is generated. + /// @param idle_timeout Timeout after which an idle persistent HTTP + /// connection is closed by the server. + /// + /// @throw HttpListenerError when any of the specified parameters is + /// invalid. + HttpListenerCustom(IOService& io_service, + const IOAddress& server_address, + const unsigned short server_port, + const TlsContextPtr& tls_context, + const HttpResponseCreatorFactoryPtr& creator_factory, + const HttpListener::RequestTimeout& request_timeout, + const HttpListener::IdleTimeout& idle_timeout) + : HttpListener(io_service, server_address, server_port, + tls_context, creator_factory, + request_timeout, idle_timeout) { + // Replace the default implementation with the customized version + // using the custom derivation of the HttpConnection. + impl_.reset(new HttpListenerImplCustom<HttpConnectionType> + (io_service, server_address, server_port, + tls_context, creator_factory, request_timeout.value_, + idle_timeout.value_)); + } +}; + +/// @brief Implementation of the @c HttpConnection which injects greater +/// length value than the buffer size into the write socket callback. +class HttpConnectionLongWriteBuffer : public HttpConnection { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the connection. + /// @param acceptor Pointer to the TCP acceptor object used to listen for + /// new HTTP connections. + /// @param tls_context TLS context. + /// @param connection_pool Connection pool in which this connection is + /// stored. + /// @param response_creator Pointer to the response creator object used to + /// create HTTP response from the HTTP request received. + /// @param callback Callback invoked when new connection is accepted. + /// @param request_timeout Configured timeout for a HTTP request. + /// @param idle_timeout Timeout after which persistent HTTP connection is + /// closed by the server. + HttpConnectionLongWriteBuffer(IOService& io_service, + const HttpAcceptorPtr& acceptor, + const TlsContextPtr& tls_context, + HttpConnectionPool& connection_pool, + const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback, + const long request_timeout, + const long idle_timeout) + : HttpConnection(io_service, acceptor, tls_context, connection_pool, + response_creator, callback, request_timeout, + idle_timeout) { + } + + /// @brief Callback invoked when data is sent over the socket. + /// + /// @param transaction Pointer to the transaction for which the callback + /// is invoked. + /// @param ec Error code. + /// @param length Length of the data sent. + virtual void socketWriteCallback(HttpConnection::TransactionPtr transaction, + boost::system::error_code ec, + size_t length) { + // Pass greater length of the data written. The callback should deal + // with this and adjust the data length. + HttpConnection::socketWriteCallback(transaction, ec, length + 1); + } +}; + +/// @brief Implementation of the @c HttpConnection which replaces +/// transaction instance prior to calling write socket callback. +class HttpConnectionTransactionChange : public HttpConnection { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the connection. + /// @param acceptor Pointer to the TCP acceptor object used to listen for + /// new HTTP connections. + /// @param context TLS tls_context. + /// @param connection_pool Connection pool in which this connection is + /// stored. + /// @param response_creator Pointer to the response creator object used to + /// create HTTP response from the HTTP request received. + /// @param callback Callback invoked when new connection is accepted. + /// @param request_timeout Configured timeout for a HTTP request. + /// @param idle_timeout Timeout after which persistent HTTP connection is + /// closed by the server. + HttpConnectionTransactionChange(IOService& io_service, + const HttpAcceptorPtr& acceptor, + const TlsContextPtr& tls_context, + HttpConnectionPool& connection_pool, + const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback, + const long request_timeout, + const long idle_timeout) + : HttpConnection(io_service, acceptor, tls_context, connection_pool, + response_creator, callback, request_timeout, + idle_timeout) { + } + + /// @brief Callback invoked when data is sent over the socket. + /// + /// @param transaction Pointer to the transaction for which the callback + /// is invoked. + /// @param ec Error code. + /// @param length Length of the data sent. + virtual void socketWriteCallback(HttpConnection::TransactionPtr transaction, + boost::system::error_code ec, + size_t length) { + // Replace the transaction. The socket callback should deal with this + // gracefully. It should detect that the output buffer is empty. Then + // try to see if the connection is persistent. This check should fail, + // because the request hasn't been created/finalized. The exception + // thrown upon checking the persistence should be caught and the + // connection closed. + transaction = HttpConnection::Transaction::create(response_creator_); + HttpConnection::socketWriteCallback(transaction, ec, length); + } +}; + +/// @brief Pointer to the TestHttpClient. +typedef boost::shared_ptr<TestHttpClient> TestHttpClientPtr; + +/// @brief Test fixture class for @ref HttpListener. +class HttpListenerTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Starts test timer which detects timeouts. + HttpListenerTest() + : io_service_(), factory_(new TestHttpResponseCreatorFactory()), + test_timer_(io_service_), run_io_service_timer_(io_service_), clients_() { + test_timer_.setup(std::bind(&HttpListenerTest::timeoutHandler, this, true), + TEST_TIMEOUT, IntervalTimer::ONE_SHOT); + } + + /// @brief Destructor. + /// + /// Removes active HTTP clients. + virtual ~HttpListenerTest() { + for (auto client = clients_.begin(); client != clients_.end(); + ++client) { + (*client)->close(); + } + } + + /// @brief Connect to the endpoint. + /// + /// This method creates TestHttpClient instance and retains it in the clients_ + /// list. + /// + /// @param request String containing the HTTP request to be sent. + void startRequest(const std::string& request) { + TestHttpClientPtr client(new TestHttpClient(io_service_)); + clients_.push_back(client); + clients_.back()->startRequest(request); + } + + /// @brief Callback function invoke upon test timeout. + /// + /// It stops the IO service and reports test timeout. + /// + /// @param fail_on_timeout Specifies if test failure should be reported. + void timeoutHandler(const bool fail_on_timeout) { + if (fail_on_timeout) { + ADD_FAILURE() << "Timeout occurred while running the test!"; + } + io_service_.stop(); + } + + /// @brief Runs IO service with optional timeout. + /// + /// @param timeout Optional value specifying for how long the io service + /// should be ran. + void runIOService(long timeout = 0) { + io_service_.get_io_service().reset(); + + if (timeout > 0) { + run_io_service_timer_.setup(std::bind(&HttpListenerTest::timeoutHandler, + this, false), + timeout, IntervalTimer::ONE_SHOT); + } + io_service_.run(); + io_service_.get_io_service().reset(); + io_service_.poll(); + } + + /// @brief Returns HTTP OK response expected by unit tests. + /// + /// @param http_version HTTP version. + /// + /// @return HTTP OK response expected by unit tests. + std::string httpOk(const HttpVersion& http_version) { + std::ostringstream s; + s << "HTTP/" << http_version.major_ << "." << http_version.minor_ << " 200 OK\r\n" + "Content-Length: 33\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"remote-address\": \"127.0.0.1\" }"; + return (s.str()); + } + + /// @brief Tests that HTTP request timeout status is returned when the + /// server does not receive the entire request. + /// + /// @param request Partial request for which the parser will be waiting for + /// the next chunks of data. + /// @param expected_version HTTP version expected in the response. + void testRequestTimeout(const std::string& request, + const HttpVersion& expected_version) { + // Open the listener with the Request Timeout of 1 sec and post the + // partial request. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, TlsContextPtr(), + factory_, HttpListener::RequestTimeout(1000), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + + // Build the reference response. + std::ostringstream expected_response; + expected_response + << "HTTP/" << expected_version.major_ << "." << expected_version.minor_ + << " 408 Request Timeout\r\n" + "Content-Length: 44\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"result\": 408, \"text\": \"Request Timeout\" }"; + + // The server should wait for the missing part of the request for 1 second. + // The missing part never arrives so the server should respond with the + // HTTP Request Timeout status. + EXPECT_EQ(expected_response.str(), client->getResponse()); + } + + /// @brief Tests various cases when unexpected data is passed to the + /// socket write handler. + /// + /// This test uses the custom listener and the test specific derivations of + /// the @c HttpConnection class to enforce injection of the unexpected + /// data to the socket write callback. The two example applications of + /// this test are: + /// - injecting greater length value than the output buffer size, + /// - replacing the transaction with another transaction. + /// + /// It is expected that the socket write callback deals gracefully with + /// those situations. + /// + /// @tparam HttpConnectionType Test specific derivation of the + /// @c HttpConnection class. + template<typename HttpConnectionType> + void testWriteBufferIssues() { + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + // Use custom listener and the specialized connection object. + HttpListenerCustom<HttpConnectionType> + listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request. + ASSERT_NO_THROW(startRequest(request)); + + // Injecting unexpected data should not result in an exception. + ASSERT_NO_THROW(runIOService()); + + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + } + + /// @brief IO service used in the tests. + IOService io_service_; + + /// @brief Pointer to the response creator factory. + HttpResponseCreatorFactoryPtr factory_; + + /// @brief Asynchronous timer service to detect timeouts. + IntervalTimer test_timer_; + + /// @brief Asynchronous timer for running IO service for a specified amount + /// of time. + IntervalTimer run_io_service_timer_; + + /// @brief List of client connections. + std::list<TestHttpClientPtr> clients_; +}; + +// This test verifies that HTTP connection can be established and used to +// transmit HTTP request and receive a response. +TEST_F(HttpListenerTest, listen) { + const std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + ASSERT_EQ(SERVER_ADDRESS, listener.getLocalAddress().toText()); + ASSERT_EQ(SERVER_PORT, listener.getLocalPort()); + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + listener.stop(); + io_service_.poll(); +} + + +// This test verifies that persistent HTTP connection can be established when +// "Connection: Keep-Alive" header value is specified. +TEST_F(HttpListenerTest, keepAlive) { + + // The first request contains the keep-alive header which instructs the server + // to maintain the TCP connection after sending a response. + std::string request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: Keep-Alive\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request with the keep-alive header. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + // We have sent keep-alive header so we expect that the connection with + // the server remains active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Test that we can send another request via the same connection. This time + // it lacks the keep-alive header, so the server should close the connection + // after sending the response. + request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + // Send request reusing the existing connection. + ASSERT_NO_THROW(client->sendRequest(request)); + ASSERT_NO_THROW(runIOService()); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + // Connection should have been closed by the server. + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that persistent HTTP connection is established by default +// when HTTP/1.1 is in use. +TEST_F(HttpListenerTest, persistentConnection) { + + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the first request. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + // HTTP/1.1 connection is persistent by default. + ASSERT_TRUE(client->isConnectionAlive()); + + // Test that we can send another request via the same connection. This time + // it includes the "Connection: close" header which instructs the server to + // close the connection after responding. + request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n" + "{ }"; + + // Send request reusing the existing connection. + ASSERT_NO_THROW(client->sendRequest(request)); + ASSERT_NO_THROW(runIOService()); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + // Connection should have been closed by the server. + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that "keep-alive" connection is closed by the server after +// an idle time. +TEST_F(HttpListenerTest, keepAliveTimeout) { + + // The first request contains the keep-alive header which instructs the server + // to maintain the TCP connection after sending a response. + std::string request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: Keep-Alive\r\n\r\n" + "{ }"; + + // Specify the idle timeout of 500ms. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(500)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request with the keep-alive header. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + // We have sent keep-alive header so we expect that the connection with + // the server remains active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Run IO service for 1000ms. The idle time is set to 500ms, so the connection + // should be closed by the server while we wait here. + runIOService(1000); + + // Make sure the connection has been closed. + EXPECT_TRUE(client->isConnectionClosed()); + + // Check if we can re-establish the connection and send another request. + clients_.clear(); + request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that persistent connection is closed by the server after +// an idle time. +TEST_F(HttpListenerTest, persistentConnectionTimeout) { + + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + // Specify the idle timeout of 500ms. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(500)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + // The connection should remain active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Run IO service for 1000ms. The idle time is set to 500ms, so the connection + // should be closed by the server while we wait here. + runIOService(1000); + + // Make sure the connection has been closed. + EXPECT_TRUE(client->isConnectionClosed()); + + // Check if we can re-establish the connection and send another request. + clients_.clear(); + request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n" + "{ }"; + + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that HTTP/1.1 connection remains open even if there is an +// error in the message body. +TEST_F(HttpListenerTest, persistentConnectionBadBody) { + + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 12\r\n\r\n" + "{ \"a\": abc }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n" + "Content-Length: 40\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"result\": 400, \"text\": \"Bad Request\" }", + client->getResponse()); + + // The connection should remain active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Make sure that we can send another request. This time we specify the + // "close" connection-token to force the connection to close. + request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n" + "{ }"; + + // Send request reusing the existing connection. + ASSERT_NO_THROW(client->sendRequest(request)); + ASSERT_NO_THROW(runIOService()); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that the HTTP listener can't be started twice. +TEST_F(HttpListenerTest, startTwice) { + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + EXPECT_THROW(listener.start(), HttpListenerError); +} + +// This test verifies that Bad Request status is returned when the request +// is malformed. +TEST_F(HttpListenerTest, badRequest) { + // Content-Type is wrong. This should result in Bad Request status. + const std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: foo\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n" + "Content-Length: 40\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"result\": 400, \"text\": \"Bad Request\" }", + client->getResponse()); +} + +// This test verifies that NULL pointer can't be specified for the +// HttpResponseCreatorFactory. +TEST_F(HttpListenerTest, invalidFactory) { + EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, TlsContextPtr(), + HttpResponseCreatorFactoryPtr(), + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)), + HttpListenerError); +} + +// This test verifies that the timeout of 0 can't be specified for the +// Request Timeout. +TEST_F(HttpListenerTest, invalidRequestTimeout) { + EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, TlsContextPtr(), factory_, + HttpListener::RequestTimeout(0), + HttpListener::IdleTimeout(IDLE_TIMEOUT)), + HttpListenerError); +} + +// This test verifies that the timeout of 0 can't be specified for the +// idle persistent connection timeout. +TEST_F(HttpListenerTest, invalidIdleTimeout) { + EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(0)), + HttpListenerError); +} + +// This test verifies that listener can't be bound to the port to which +// other server is bound. +TEST_F(HttpListenerTest, addressInUse) { + tcp::acceptor acceptor(io_service_.get_io_service()); + // Use other port than SERVER_PORT to make sure that this TCP connection + // doesn't affect subsequent tests. + tcp::endpoint endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT + 1); + acceptor.open(endpoint.protocol()); + acceptor.bind(endpoint); + + // Listener should report an error when we try to start it because another + // acceptor is bound to that port and address. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT + 1, TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + EXPECT_THROW(listener.start(), HttpListenerError); +} + +// This test verifies that HTTP Request Timeout status is returned as +// expected when the read part of the request contains the HTTP +// version number. The timeout response should contain the same +// HTTP version number as the partial request. +TEST_F(HttpListenerTest, requestTimeoutHttpVersionFound) { + // The part of the request specified here is correct but it is not + // a complete request. + const std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length:"; + + testRequestTimeout(request, HttpVersion::HTTP_11()); +} + +// This test verifies that HTTP Request Timeout status is returned as +// expected when the read part of the request does not contain +// the HTTP version number. The timeout response should by default +// contain HTTP/1.0 version number. +TEST_F(HttpListenerTest, requestTimeoutHttpVersionNotFound) { + // The part of the request specified here is correct but it is not + // a complete request. + const std::string request = "POST /foo/bar HTTP"; + + testRequestTimeout(request, HttpVersion::HTTP_10()); +} + +// This test verifies that injecting length value greater than the +// output buffer length to the socket write callback does not cause +// an exception. +TEST_F(HttpListenerTest, tooLongWriteBuffer) { + testWriteBufferIssues<HttpConnectionLongWriteBuffer>(); +} + +// This test verifies that changing the transaction before calling +// the socket write callback does not cause an exception. +TEST_F(HttpListenerTest, transactionChangeDuringWrite) { + testWriteBufferIssues<HttpConnectionTransactionChange>(); +} + +/// @brief Test fixture class for testing HTTP client. +class HttpClientTest : public HttpListenerTest { +public: + + /// @brief Constructor. + HttpClientTest() + : HttpListenerTest(), + listener_(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)), + listener2_(io_service_, IOAddress(IPV6_SERVER_ADDRESS), SERVER_PORT + 1, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)), + listener3_(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT + 2, + TlsContextPtr(), factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(SHORT_IDLE_TIMEOUT)) { + MultiThreadingMgr::instance().setMode(false); + } + + /// @brief Destructor. + ~HttpClientTest() { + listener_.stop(); + listener2_.stop(); + listener3_.stop(); + io_service_.poll(); + MultiThreadingMgr::instance().setMode(false); + } + + /// @brief Creates HTTP request with JSON body. + /// + /// It includes a JSON parameter with a specified value. + /// + /// @param parameter_name JSON parameter to be included. + /// @param value JSON parameter value. + /// @param version HTTP version to be used. Default is HTTP/1.1. + template<typename ValueType> + PostHttpRequestJsonPtr createRequest(const std::string& parameter_name, + const ValueType& value, + const HttpVersion& version = HttpVersion(1, 1)) { + // Create POST request with JSON body. + PostHttpRequestJsonPtr request(new PostHttpRequestJson(HttpRequest::Method::HTTP_POST, + "/", version)); + // Body is a map with a specified parameter included. + ElementPtr body = Element::createMap(); + body->set(parameter_name, Element::create(value)); + request->setBodyAsJson(body); + try { + request->finalize(); + + } catch (const std::exception& ex) { + ADD_FAILURE() << "failed to create request: " << ex.what(); + } + + return (request); + } + + /// @brief Test that two consecutive requests can be sent over the same + /// connection (if persistent, if not persistent two connections will + /// be used). + /// + /// @param version HTTP version to be used. + void testConsecutiveRequests(const HttpVersion& version) { + // Start the server. + ASSERT_NO_THROW(listener_.start()); + + // Create a client and specify the URL on which the server can be reached. + HttpClient client(io_service_, false); + Url url("http://127.0.0.1:18123"); + + // Initiate request to the server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1, version); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request1, response1, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + EXPECT_FALSE(ec); + })); + + // Initiate another request to the destination. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2, version); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request2, response2, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + EXPECT_FALSE(ec); + })); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // Make sure that the received responses are different. We check that by + // comparing value of the sequence parameters. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + } + + /// @brief Test that the client can communicate with two different + /// destinations simultaneously. + void testMultipleDestinations() { + // Start two servers running on different ports. + ASSERT_NO_THROW(listener_.start()); + ASSERT_NO_THROW(listener2_.start()); + + // Create the client. It will be communicating with the two servers. + HttpClient client(io_service_, false); + + // Specify the URLs on which the servers are available. + Url url1("http://127.0.0.1:18123"); + Url url2("http://[::1]:18124"); + + // Create a request to the first server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(url1, TlsContextPtr(), + request1, response1, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + EXPECT_FALSE(ec); + })); + + // Create a request to the second server. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url2, TlsContextPtr(), + request2, response2, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + EXPECT_FALSE(ec); + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + + // Make sure we have received two different responses. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + } + + /// @brief Test that the client can communicate with the same destination + /// address and port but with different TLS contexts so + void testMultipleTlsContexts() { + // Start only one server. + ASSERT_NO_THROW(listener_.start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL on which the server is available. + Url url("http://127.0.0.1:18123"); + + // Create a request to the first server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request1, response1, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Create a request with the second TLS context. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request2, response2, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + + // Make sure we have received two different responses. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + } + + /// @brief Test that idle connection can be resumed for second request. + void testIdleConnection() { + // Start the server that has short idle timeout. It closes the idle + // connection after 200ms. + ASSERT_NO_THROW(listener3_.start()); + + // Create the client that will communicate with this server. + HttpClient client(io_service_, false); + + // Specify the URL of this server. + Url url("http://127.0.0.1:18125"); + + // Create the first request. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1); + HttpResponseJsonPtr response1(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request1, response1, + [this](const boost::system::error_code& ec, const HttpResponsePtr&, + const std::string&) { + io_service_.stop(); + EXPECT_FALSE(ec); + })); + + // Run the IO service until the response is received. + ASSERT_NO_THROW(runIOService()); + + // Make sure the response has been received. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + // Delay the generation of the second request by 2x server idle timeout. + // This should be enough to cause the server to close the connection. + ASSERT_NO_THROW(runIOService(SHORT_IDLE_TIMEOUT * 2)); + + // Create another request. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request2, response2, + [this](const boost::system::error_code& ec, const HttpResponsePtr&, + const std::string&) { + io_service_.stop(); + EXPECT_FALSE(ec); + })); + + // Actually trigger the second request. + ASSERT_NO_THROW(runIOService()); + + // Make sire that the server has responded. + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + + // Make sure that two different responses have been received. + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + } + + /// @brief This test verifies that the client returns IO error code when the + /// server is unreachable. + void testUnreachable () { + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. This server is down. + Url url("http://127.0.0.1:18123"); + + // Create the request. + PostHttpRequestJsonPtr request = createRequest("sequence", 1); + HttpResponseJsonPtr response(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request, response, + [this](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + io_service_.stop(); + // The server should have returned an IO error. + EXPECT_TRUE(ec); + })); + + // Actually trigger the request. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Test that an error is returned by the client if the server + /// response is malformed. + void testMalformedResponse () { + // Start the server. + ASSERT_NO_THROW(listener_.start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. + Url url("http://127.0.0.1:18123"); + + // The response is going to be malformed in such a way that it holds + // an invalid content type. We affect the content type by creating + // a request that holds a JSON parameter requesting a specific + // content type. + PostHttpRequestJsonPtr request = createRequest("requested-content-type", + "text/html"); + HttpResponseJsonPtr response(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request, response, + [this](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string& parsing_error) { + io_service_.stop(); + // There should be no IO error (answer from the server is received). + EXPECT_FALSE(ec); + // The response object is NULL because it couldn't be finalized. + EXPECT_FALSE(response); + // The message parsing error should be returned. + EXPECT_FALSE(parsing_error.empty()); + })); + + // Actually trigger the request. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Test that client times out when it doesn't receive the entire + /// response from the server within a desired time. + void testClientRequestTimeout() { + // Start the server. + ASSERT_NO_THROW(listener_.start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. + Url url("http://127.0.0.1:18123"); + + unsigned cb_num = 0; + + // Create the request which asks the server to generate a partial + // (although well formed) response. The client will be waiting for the + // rest of the response to be provided and will eventually time out. + PostHttpRequestJsonPtr request1 = createRequest("partial-response", true); + HttpResponseJsonPtr response1(new HttpResponseJson()); + // This value will be set to true if the connection close callback is + // invoked upon time out. + auto connection_closed = false; + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request1, response1, + [this, &cb_num](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string&) { + if (++cb_num > 1) { + io_service_.stop(); + } + // In this particular case we know exactly the type of the + // IO error returned, because the client explicitly sets this + // error code. + EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); + // There should be no response returned. + EXPECT_FALSE(response); + }, + HttpClient::RequestTimeout(100), + HttpClient::ConnectHandler(), + HttpClient::HandshakeHandler(), + [&connection_closed](const int) { + // This callback is called when the connection gets closed + // by the client. + connection_closed = true; + }) + ); + + // Create another request after the timeout. It should be handled ok. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 1); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request2, response2, + [this, &cb_num](const boost::system::error_code& /*ec*/, + const HttpResponsePtr&, + const std::string&) { + if (++cb_num > 1) { + io_service_.stop(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + // Make sure that the client has closed the connection upon timeout. + EXPECT_TRUE(connection_closed); + } + + /// @brief Test that client times out when connection takes too long. + void testClientConnectTimeout() { + // Start the server. + ASSERT_NO_THROW(listener_.start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. + Url url("http://127.0.0.1:18123"); + + unsigned cb_num = 0; + + PostHttpRequestJsonPtr request = createRequest("sequence", 1); + HttpResponseJsonPtr response(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request, response, + [this, &cb_num](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string&) { + if (++cb_num > 1) { + io_service_.stop(); + } + // In this particular case we know exactly the type of the + // IO error returned, because the client explicitly sets this + // error code. + EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); + // There should be no response returned. + EXPECT_FALSE(response); + + }, HttpClient::RequestTimeout(100), + + // This callback is invoked upon an attempt to connect to the + // server. The false value indicates to the HttpClient to not + // try to send a request to the server. This simulates the + // case of connect() taking very long and should eventually + // cause the transaction to time out. + [](const boost::system::error_code& /*ec*/, int) { + return (false); + })); + + // Create another request after the timeout. It should be handled ok. + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request, response, + [this, &cb_num](const boost::system::error_code& /*ec*/, + const HttpResponsePtr&, + const std::string&) { + if (++cb_num > 1) { + io_service_.stop(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Tests the behavior of the HTTP client when the premature + /// timeout occurs. + /// + /// The premature timeout may occur when the system clock is moved + /// during the transaction. This test simulates this behavior by + /// starting new transaction and waiting for the timeout to occur + /// before the IO service is ran. The timeout handler is invoked + /// first and it resets the transaction state. This test verifies + /// that the started transaction tears down gracefully after the + /// transaction state is reset. + /// + /// There are two variants of this test. The first variant schedules + /// one transaction before running the IO service. The second variant + /// schedules two transactions prior to running the IO service. The + /// second transaction is queued, so it is expected that it doesn't + /// time out and it runs successfully. + /// + /// @param queue_two_requests Boolean value indicating if a single + /// transaction should be queued (false), or two (true). + void testClientRequestLateStart(const bool queue_two_requests) { + // Start the server. + ASSERT_NO_THROW(listener_.start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. + Url url("http://127.0.0.1:18123"); + + // Specify the TLS context of the server. + TlsContextPtr tls_context; + + // Generate first request. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1); + HttpResponseJsonPtr response1(new HttpResponseJson()); + + // Use very short timeout to make sure that it occurs before we actually + // run the transaction. + ASSERT_NO_THROW(client.asyncSendRequest(url, tls_context, + request1, response1, + [](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string&) { + + // In this particular case we know exactly the type of the + // IO error returned, because the client explicitly sets this + // error code. + EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); + // There should be no response returned. + EXPECT_FALSE(response); + }, HttpClient::RequestTimeout(1))); + + if (queue_two_requests) { + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, tls_context, + request2, response2, + [](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string&) { + + // This second request should be successful. + EXPECT_TRUE(ec.value() == 0); + EXPECT_TRUE(response); + })); + } + + // This waits for 3ms to make sure that the timeout occurs before we + // run the transaction. This leads to an unusual situation that the + // transaction state is reset as a result of the timeout but the + // transaction is alive. We want to make sure that the client can + // gracefully deal with this situation. + usleep(3000); + + // Run the transaction and hope it will gracefully tear down. + ASSERT_NO_THROW(runIOService(100)); + + // Now try to send another request to make sure that the client + // is healthy. + PostHttpRequestJsonPtr request3 = createRequest("sequence", 3); + HttpResponseJsonPtr response3(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request3, response3, + [this](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + io_service_.stop(); + + // Everything should be ok. + EXPECT_TRUE(ec.value() == 0); + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Tests that underlying TCP socket can be registered and + /// unregistered via connection and close callbacks. + /// + /// It conducts to consecutive requests over the same client. + /// + /// @param version HTTP version to be used. + void testConnectCloseCallbacks(const HttpVersion& version) { + // Start the server. + ASSERT_NO_THROW(listener_.start()); + + // Create a client and specify the URL on which the server can be reached. + HttpClient client(io_service_, false); + Url url("http://127.0.0.1:18123"); + + // Initiate request to the server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1, version); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ExternalMonitor monitor; + + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request1, response1, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + + EXPECT_FALSE(ec); + }, + HttpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Initiate another request to the destination. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2, version); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request2, response2, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + EXPECT_FALSE(ec); + }, + HttpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // We should have had 2 connect invocations, no closes + // and a valid registered fd + EXPECT_EQ(2, monitor.connect_cnt_); + EXPECT_EQ(0, monitor.close_cnt_); + EXPECT_GT(monitor.registered_fd_, -1); + + // Make sure that the received responses are different. We check that by + // comparing value of the sequence parameters. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + + // Stopping the client the close the connection. + client.stop(); + + // We should have had 2 connect invocations, 1 closes + // and an invalid registered fd + EXPECT_EQ(2, monitor.connect_cnt_); + EXPECT_EQ(1, monitor.close_cnt_); + EXPECT_EQ(-1, monitor.registered_fd_); + } + + /// @brief Tests detection and handling out-of-band socket events + /// + /// It initiates a transaction and verifies that a mid-transaction call + /// to HttpClient::closeIfOutOfBand() has no affect on the connection. + /// After successful completion of the transaction, a second call to + /// HttpClient::closeIfOutOfBand() is made. This should result in the + /// connection being closed. + /// This step is repeated to verify that after an OOB closure, transactions + /// to the same destination can be processed. + /// + /// Lastly, we verify that HttpClient::stop() closes the connection correctly. + /// + /// @param version HTTP version to be used. + void testCloseIfOutOfBand(const HttpVersion& version) { + // Start the server. + ASSERT_NO_THROW(listener_.start()); + + // Create a client and specify the URL on which the server can be reached. + HttpClient client(io_service_, false); + Url url("http://127.0.0.1:18123"); + + // Initiate request to the server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1, version); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ExternalMonitor monitor; + + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request1, response1, + [this, &client, &resp_num, &monitor](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num == 1) { + io_service_.stop(); + } + + EXPECT_EQ(1, monitor.connect_cnt_); // We should have 1 connect. + EXPECT_EQ(0, monitor.close_cnt_); // We should have 0 closes + ASSERT_GT(monitor.registered_fd_, -1); // We should have a valid fd. + int orig_fd = monitor.registered_fd_; + + // Test our socket for OOBness. + client.closeIfOutOfBand(monitor.registered_fd_); + + // Since we're in a transaction, we should have no closes and + // the same valid fd. + EXPECT_EQ(0, monitor.close_cnt_); + ASSERT_EQ(monitor.registered_fd_, orig_fd); + + EXPECT_FALSE(ec); + }, + HttpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // Make sure that we received a response. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + EXPECT_EQ(1, sequence1->intValue()); + + // We should have had 1 connect invocations, no closes + // and a valid registered fd + EXPECT_EQ(1, monitor.connect_cnt_); + EXPECT_EQ(0, monitor.close_cnt_); + EXPECT_GT(monitor.registered_fd_, -1); + + // Test our socket for OOBness. + client.closeIfOutOfBand(monitor.registered_fd_); + + // Since we're in a transaction, we should have no closes and + // the same valid fd. + EXPECT_EQ(1, monitor.close_cnt_); + EXPECT_EQ(-1, monitor.registered_fd_); + + // Now let's do another request to the destination to verify that + // we'll reopen the connection without issue. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2, version); + HttpResponseJsonPtr response2(new HttpResponseJson()); + resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(url, TlsContextPtr(), + request2, response2, + [this, &client, &resp_num, &monitor](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num == 1) { + io_service_.stop(); + } + + EXPECT_EQ(2, monitor.connect_cnt_); // We should have 1 connect. + EXPECT_EQ(1, monitor.close_cnt_); // We should have 0 closes + ASSERT_GT(monitor.registered_fd_, -1); // We should have a valid fd. + int orig_fd = monitor.registered_fd_; + + // Test our socket for OOBness. + client.closeIfOutOfBand(monitor.registered_fd_); + + // Since we're in a transaction, we should have no closes and + // the same valid fd. + EXPECT_EQ(1, monitor.close_cnt_); + ASSERT_EQ(monitor.registered_fd_, orig_fd); + + EXPECT_FALSE(ec); + }, + HttpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // Make sure that we received the second response. + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + EXPECT_EQ(2, sequence2->intValue()); + + // Stopping the client the close the connection. + client.stop(); + + // We should have had 2 connect invocations, 2 closes + // and an invalid registered fd + EXPECT_EQ(2, monitor.connect_cnt_); + EXPECT_EQ(2, monitor.close_cnt_); + EXPECT_EQ(-1, monitor.registered_fd_); + } + + /// @brief Simulates external registery of Connection TCP sockets + /// + /// Provides methods compatible with Connection callbacks for connect + /// and close operations. + class ExternalMonitor { + public: + /// @brief Constructor + ExternalMonitor() + : registered_fd_(-1), connect_cnt_(0), close_cnt_(0) { + } + + /// @brief Connect callback handler + /// @param ec Error status of the ASIO connect + /// @param tcp_native_fd socket descriptor to register + bool connectHandler(const boost::system::error_code& ec, int tcp_native_fd) { + ++connect_cnt_; + if ((!ec || (ec.value() == boost::asio::error::in_progress)) + && (tcp_native_fd >= 0)) { + registered_fd_ = tcp_native_fd; + return (true); + } else if ((ec.value() == boost::asio::error::already_connected) + && (registered_fd_ != tcp_native_fd)) { + return (false); + } + + // ec indicates an error, return true, so that error can be handled + // by Connection logic. + return (true); + } + + /// @brief Handshake callback handler + /// @param ec Error status of the ASIO connect + bool handshakeHandler(const boost::system::error_code&, int) { + ADD_FAILURE() << "handshake callback handler is called"; + // ec indicates an error, return true, so that error can be handled + // by Connection logic. + return (true); + } + + /// @brief Close callback handler + /// + /// @param tcp_native_fd socket descriptor to register + void closeHandler(int tcp_native_fd) { + ++close_cnt_; + EXPECT_EQ(tcp_native_fd, registered_fd_) << "closeHandler fd mismatch"; + if (tcp_native_fd >= 0) { + registered_fd_ = -1; + } + } + + /// @brief Keeps track of socket currently "registered" for external monitoring. + int registered_fd_; + + /// @brief Tracks how many times the connect callback is invoked. + int connect_cnt_; + + /// @brief Tracks how many times the close callback is invoked. + int close_cnt_; + }; + + /// @brief Instance of the listener used in the tests. + HttpListener listener_; + + /// @brief Instance of the second listener used in the tests. + HttpListener listener2_; + + /// @brief Instance of the third listener used in the tests (with short idle + /// timeout). + HttpListener listener3_; + +}; + +// Test that two consecutive requests can be sent over the same (persistent) +// connection. +TEST_F(HttpClientTest, consecutiveRequests) { + ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 1))); +} + +// Test that two consecutive requests can be sent over the same (persistent) +// connection. +TEST_F(HttpClientTest, consecutiveRequestsMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 1))); +} + +// Test that two consecutive requests can be sent over non-persistent connection. +// This is achieved by sending HTTP/1.0 requests, which are non-persistent by +// default. The client should close the connection right after receiving a response +// from the server. +TEST_F(HttpClientTest, closeBetweenRequests) { + ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 0))); +} + +// Test that two consecutive requests can be sent over non-persistent connection. +// This is achieved by sending HTTP/1.0 requests, which are non-persistent by +// default. The client should close the connection right after receiving a response +// from the server. +TEST_F(HttpClientTest, closeBetweenRequestsMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 0))); +} + +// Test that the client can communicate with two different destinations +// simultaneously. +TEST_F(HttpClientTest, multipleDestinations) { + ASSERT_NO_FATAL_FAILURE(testMultipleDestinations()); +} + +// Test that the client can communicate with two different destinations +// simultaneously. +TEST_F(HttpClientTest, multipleDestinationsMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testMultipleDestinations()); +} + +// Test that the client can use two different TLS contexts to the same +// destination address and port simultaneously. +TEST_F(HttpClientTest, multipleTlsContexts) { + ASSERT_NO_FATAL_FAILURE(testMultipleTlsContexts()); +} + +// Test that the client can use two different TLS contexts to the same +// destination address and port simultaneously. +TEST_F(HttpClientTest, multipleTlsContextsMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testMultipleTlsContexts()); +} + +// Test that idle connection can be resumed for second request. +TEST_F(HttpClientTest, idleConnection) { + ASSERT_NO_FATAL_FAILURE(testIdleConnection()); +} + +// Test that idle connection can be resumed for second request. +TEST_F(HttpClientTest, idleConnectionMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testIdleConnection()); +} + +// This test verifies that the client returns IO error code when the +// server is unreachable. +TEST_F(HttpClientTest, unreachable) { + ASSERT_NO_FATAL_FAILURE(testUnreachable()); +} + +// This test verifies that the client returns IO error code when the +// server is unreachable. +TEST_F(HttpClientTest, unreachableMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testUnreachable()); +} + +// Test that an error is returned by the client if the server response is +// malformed. +TEST_F(HttpClientTest, malformedResponse) { + ASSERT_NO_FATAL_FAILURE(testMalformedResponse()); +} + +// Test that an error is returned by the client if the server response is +// malformed. +TEST_F(HttpClientTest, malformedResponseMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testMalformedResponse()); +} + +// Test that client times out when it doesn't receive the entire response +// from the server within a desired time. +TEST_F(HttpClientTest, clientRequestTimeout) { + ASSERT_NO_FATAL_FAILURE(testClientRequestTimeout()); +} + +// Test that client times out when it doesn't receive the entire response +// from the server within a desired time. +TEST_F(HttpClientTest, clientRequestTimeoutMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testClientRequestTimeout()); +} + +// This test verifies the behavior of the HTTP client when the premature +// (and unexpected) timeout occurs. The premature timeout may be caused +// by the system clock move. +TEST_F(HttpClientTest, clientRequestLateStartNoQueue) { + testClientRequestLateStart(false); +} + +// This test verifies the behavior of the HTTP client when the premature +// (and unexpected) timeout occurs. The premature timeout may be caused +// by the system clock move. +TEST_F(HttpClientTest, clientRequestLateStartNoQueueMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + testClientRequestLateStart(false); +} + +// This test verifies the behavior of the HTTP client when the premature +// timeout occurs and there are requests queued after the request which +// times out. +TEST_F(HttpClientTest, clientRequestLateStartQueue) { + testClientRequestLateStart(true); +} + +// This test verifies the behavior of the HTTP client when the premature +// timeout occurs and there are requests queued after the request which +// times out. +TEST_F(HttpClientTest, clientRequestLateStartQueueMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + testClientRequestLateStart(true); +} + +// Test that client times out when connection takes too long. +TEST_F(HttpClientTest, clientConnectTimeout) { + ASSERT_NO_FATAL_FAILURE(testClientConnectTimeout()); +} + +// Test that client times out when connection takes too long. +TEST_F(HttpClientTest, clientConnectTimeoutMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testClientConnectTimeout()); +} + +/// Tests that connect and close callbacks work correctly. +TEST_F(HttpClientTest, connectCloseCallbacks) { + ASSERT_NO_FATAL_FAILURE(testConnectCloseCallbacks(HttpVersion(1, 1))); +} + +/// Tests that connect and close callbacks work correctly. +TEST_F(HttpClientTest, connectCloseCallbacksMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testConnectCloseCallbacks(HttpVersion(1, 1))); +} + +/// Tests that HttpClient::closeIfOutOfBand works correctly. +TEST_F(HttpClientTest, closeIfOutOfBand) { + ASSERT_NO_FATAL_FAILURE(testCloseIfOutOfBand(HttpVersion(1, 1))); +} + +/// Tests that HttpClient::closeIfOutOfBand works correctly. +TEST_F(HttpClientTest, closeIfOutOfBandMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testCloseIfOutOfBand(HttpVersion(1, 1))); +} + +} diff --git a/src/lib/http/tests/test_http_client.h b/src/lib/http/tests/test_http_client.h new file mode 100644 index 0000000..f95b111 --- /dev/null +++ b/src/lib/http/tests/test_http_client.h @@ -0,0 +1,273 @@ +// Copyright (C) 2017-2021 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef TEST_HTTP_CLIENT_H +#define TEST_HTTP_CLIENT_H + +#include <cc/data.h> +#include <http/client.h> +#include <http/http_types.h> + +#include <boost/asio/read.hpp> +#include <boost/asio/buffer.hpp> +#include <boost/asio/ip/tcp.hpp> +#include <gtest/gtest.h> + +using namespace boost::asio::ip; +using namespace isc::asiolink; + +/// @brief Entity which can connect to the HTTP server endpoint. +class TestHttpClient : public boost::noncopyable { +public: + + /// @brief Constructor. + /// + /// This constructor creates new socket instance. It doesn't connect. Call + /// connect() to connect to the server. + /// + /// @param io_service IO service to be stopped on error or completion. + /// @param server_address string containing the IP address of the server. + /// @param port port number of the server. + explicit TestHttpClient(IOService& io_service, + const std::string& server_address = "127.0.0.1", + uint16_t port = 18123) + : io_service_(io_service.get_io_service()), socket_(io_service_), + buf_(), response_(), server_address_(server_address), + server_port_(port), receive_done_(false) { + } + + /// @brief Destructor. + /// + /// Closes the underlying socket if it is open. + ~TestHttpClient() { + close(); + } + + /// @brief Send HTTP request specified in textual format. + /// + /// @param request HTTP request in the textual format. + void startRequest(const std::string& request) { + tcp::endpoint endpoint(address::from_string(server_address_), server_port_); + socket_.async_connect(endpoint, + [this, request](const boost::system::error_code& ec) { + receive_done_ = false; + if (ec) { + // One would expect that async_connect wouldn't return + // EINPROGRESS error code, but simply wait for the connection + // to get established before the handler is invoked. It turns out, + // however, that on some OSes the connect handler may receive this + // error code which doesn't necessarily indicate a problem. + // Making an attempt to write and read from this socket will + // typically succeed. So, we ignore this error. + if (ec.value() != boost::asio::error::in_progress) { + ADD_FAILURE() << "error occurred while connecting: " + << ec.message(); + io_service_.stop(); + return; + } + } + sendRequest(request); + }); + } + + /// @brief Send HTTP request. + /// + /// @param request HTTP request in the textual format. + void sendRequest(const std::string& request) { + sendPartialRequest(request); + } + + /// @brief Send part of the HTTP request. + /// + /// @param request part of the HTTP request to be sent. + void sendPartialRequest(std::string request) { + socket_.async_send(boost::asio::buffer(request.data(), request.size()), + [this, request](const boost::system::error_code& ec, + std::size_t bytes_transferred) mutable { + if (ec) { + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + } else if ((ec.value() == boost::asio::error::try_again) || + (ec.value() == boost::asio::error::would_block)) { + // If we should try again make sure there is no garbage in the + // bytes_transferred. + bytes_transferred = 0; + + } else { + ADD_FAILURE() << "error occurred while connecting: " + << ec.message(); + io_service_.stop(); + return; + } + } + + // Remove the part of the request which has been sent. + if (bytes_transferred > 0 && (request.size() <= bytes_transferred)) { + request.erase(0, bytes_transferred); + } + + // Continue sending request data if there are still some data to be + // sent. + if (!request.empty()) { + sendPartialRequest(request); + + } else { + // Request has been sent. Start receiving response. + response_.clear(); + receivePartialResponse(); + } + }); + } + + /// @brief Receive response from the server. + void receivePartialResponse() { + socket_.async_read_some(boost::asio::buffer(buf_.data(), buf_.size()), + [this](const boost::system::error_code& ec, + std::size_t bytes_transferred) { + if (ec) { + // IO service stopped so simply return. + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + } else if ((ec.value() == boost::asio::error::try_again) || + (ec.value() == boost::asio::error::would_block)) { + // If we should try again, make sure that there is no garbage + // in the bytes_transferred. + bytes_transferred = 0; + + } else { + // Error occurred, bail... + ADD_FAILURE() << "error occurred while receiving HTTP" + " response from the server: " << ec.message(); + io_service_.stop(); + } + } + + if (bytes_transferred > 0) { + response_.insert(response_.end(), buf_.data(), + buf_.data() + bytes_transferred); + } + + // Two consecutive new lines end the part of the response we're + // expecting. + if (response_.find("\r\n\r\n", 0) != std::string::npos) { + receive_done_ = true; + io_service_.stop(); + } else { + receivePartialResponse(); + } + }); + } + + /// @brief Checks if the TCP connection is still open. + /// + /// Tests the TCP connection by trying to read from the socket. + /// Unfortunately expected failure depends on a race between the read + /// and the other side close so to check if the connection is closed + /// please use @c isConnectionClosed instead. + /// + /// @return true if the TCP connection is open. + bool isConnectionAlive() { + // Remember the current non blocking setting. + const bool non_blocking_orig = socket_.non_blocking(); + // Set the socket to non blocking mode. We're going to test if the socket + // returns would_block status on the attempt to read from it. + socket_.non_blocking(true); + + // We need to provide a buffer for a call to read. + char data[2]; + boost::system::error_code ec; + boost::asio::read(socket_, boost::asio::buffer(data, sizeof(data)), ec); + + // Revert the original non_blocking flag on the socket. + socket_.non_blocking(non_blocking_orig); + + // If the connection is alive we'd typically get would_block status code. + // If there are any data that haven't been read we may also get success + // status. We're guessing that try_again may also be returned by some + // implementations in some situations. Any other error code indicates a + // problem with the connection so we assume that the connection has been + // closed. + return (!ec || (ec.value() == boost::asio::error::try_again) || + (ec.value() == boost::asio::error::would_block)); + } + + /// @brief Checks if the TCP connection is already closed. + /// + /// Tests the TCP connection by trying to read from the socket. + /// The read can block so this must be used to check if a connection + /// is alive so to check if the connection is alive please always + /// use @c isConnectionAlive. + /// + /// @return true if the TCP connection is closed. + bool isConnectionClosed() { + // Remember the current non blocking setting. + const bool non_blocking_orig = socket_.non_blocking(); + // Set the socket to blocking mode. We're going to test if the socket + // returns eof status on the attempt to read from it. + socket_.non_blocking(false); + + // We need to provide a buffer for a call to read. + char data[2]; + boost::system::error_code ec; + boost::asio::read(socket_, boost::asio::buffer(data, sizeof(data)), ec); + + // Revert the original non_blocking flag on the socket. + socket_.non_blocking(non_blocking_orig); + + // If the connection is closed we'd typically get eof status code. + return (ec.value() == boost::asio::error::eof); + } + + /// @brief Close connection. + void close() { + socket_.close(); + } + + /// @brief Returns the HTTP response string. + /// + /// @return string containing the response. + std::string getResponse() const { + return (response_); + } + + /// @brief Returns true if the receive completed without error. + /// + /// @return True if the receive completed successfully, false + /// otherwise. + bool receiveDone() { + return (receive_done_); + } + +private: + + /// @brief Holds reference to the IO service. + boost::asio::io_service& io_service_; + + /// @brief A socket used for the connection. + boost::asio::ip::tcp::socket socket_; + + /// @brief Buffer into which response is written. + std::array<char, 8192> buf_; + + /// @brief Response in the textual format. + std::string response_; + + /// @brief IP address of the server. + std::string server_address_; + + /// @brief IP port of the server. + uint16_t server_port_; + + /// @brief Set to true when the receive has completed successfully. + bool receive_done_; +}; + +/// @brief Pointer to the TestHttpClient. +typedef boost::shared_ptr<TestHttpClient> TestHttpClientPtr; + +#endif diff --git a/src/lib/http/tests/testdata/empty b/src/lib/http/tests/testdata/empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/lib/http/tests/testdata/empty diff --git a/src/lib/http/tests/testdata/hiddenp b/src/lib/http/tests/testdata/hiddenp new file mode 100644 index 0000000..e8b2395 --- /dev/null +++ b/src/lib/http/tests/testdata/hiddenp @@ -0,0 +1 @@ +KeaTest
\ No newline at end of file diff --git a/src/lib/http/tests/testdata/hiddens b/src/lib/http/tests/testdata/hiddens new file mode 100644 index 0000000..f52fb83 --- /dev/null +++ b/src/lib/http/tests/testdata/hiddens @@ -0,0 +1 @@ +kea:test
\ No newline at end of file diff --git a/src/lib/http/tests/testdata/hiddenu b/src/lib/http/tests/testdata/hiddenu new file mode 100644 index 0000000..801489a --- /dev/null +++ b/src/lib/http/tests/testdata/hiddenu @@ -0,0 +1 @@ +keatest
\ No newline at end of file diff --git a/src/lib/http/tests/tls_client_unittests.cc b/src/lib/http/tests/tls_client_unittests.cc new file mode 100644 index 0000000..fc999f5 --- /dev/null +++ b/src/lib/http/tests/tls_client_unittests.cc @@ -0,0 +1,1398 @@ +// Copyright (C) 2017-2023 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <asiolink/asio_wrapper.h> +#include <asiolink/interval_timer.h> +#include <asiolink/tls_acceptor.h> +#include <asiolink/testutils/test_tls.h> +#include <cc/data.h> +#include <http/client.h> +#include <http/http_types.h> +#include <http/listener.h> +#include <http/listener_impl.h> +#include <http/post_request_json.h> +#include <http/response_creator.h> +#include <http/response_creator_factory.h> +#include <http/response_json.h> +#include <http/tests/response_test.h> +#include <http/url.h> +#include <util/multi_threading_mgr.h> + +#include <boost/asio/buffer.hpp> +#include <boost/asio/ip/tcp.hpp> +#include <boost/pointer_cast.hpp> +#include <gtest/gtest.h> + +#include <functional> +#include <list> +#include <sstream> +#include <string> + +#ifdef WITH_BOTAN +#define DISABLE_SOME_TESTS +#endif +#ifdef WITH_OPENSSL +#if !defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER < 0x10100000L) +#define DISABLE_SOME_TESTS +#endif +#endif + +using namespace boost::asio; +using namespace boost::asio::ip; +using namespace isc::asiolink; +using namespace isc::asiolink::test; +using namespace isc::data; +using namespace isc::http; +using namespace isc::http::test; +using namespace isc::util; +namespace ph = std::placeholders; + +/// @todo: put the common part of client and server tests in its own file(s). + +namespace { + +/// @brief IP address to which HTTP service is bound. +const std::string SERVER_ADDRESS = "127.0.0.1"; + +/// @brief IPv6 address to whch HTTP service is bound. +const std::string IPV6_SERVER_ADDRESS = "::1"; + +/// @brief Port number to which HTTP service is bound. +const unsigned short SERVER_PORT = 18123; + +/// @brief Request Timeout used in most of the tests (ms). +const long REQUEST_TIMEOUT = 10000; + +/// @brief Persistent connection idle timeout used in most of the tests (ms). +const long IDLE_TIMEOUT = 10000; + +/// @brief Persistent connection idle timeout used in tests where idle connections +/// are tested (ms). +const long SHORT_IDLE_TIMEOUT = 200; + +/// @brief Test timeout (ms). +const long TEST_TIMEOUT = 10000; + +/// @brief Test HTTP response. +typedef TestHttpResponseBase<HttpResponseJson> Response; + +/// @brief Pointer to test HTTP response. +typedef boost::shared_ptr<Response> ResponsePtr; + +/// @brief Generic test HTTP response. +typedef TestHttpResponseBase<HttpResponse> GenericResponse; + +/// @brief Pointer to generic test HTTP response. +typedef boost::shared_ptr<GenericResponse> GenericResponsePtr; + +/// @brief Implementation of the @ref HttpResponseCreator. +class TestHttpResponseCreator : public HttpResponseCreator { +public: + + /// @brief Create a new request. + /// + /// @return Pointer to the new instance of the @ref HttpRequest. + virtual HttpRequestPtr + createNewHttpRequest() const { + return (HttpRequestPtr(new PostHttpRequestJson())); + } + +private: + + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP response. + virtual HttpResponsePtr + createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const { + // The request hasn't been finalized so the request object + // doesn't contain any information about the HTTP version number + // used. But, the context should have this data (assuming the + // HTTP version is parsed ok). + HttpVersion http_version(request->context()->http_version_major_, + request->context()->http_version_minor_); + // This will generate the response holding JSON content. + ResponsePtr response(new Response(http_version, status_code)); + response->finalize(); + return (response); + } + + /// @brief Creates HTTP response. + /// + /// This method generates 3 types of responses: + /// - response with a requested content type, + /// - partial response with incomplete JSON body, + /// - response with JSON body copied from the request. + /// + /// The first one is useful to test situations when received response can't + /// be parsed because of the content type mismatch. The second one is useful + /// to test request timeouts. The third type is used by most of the unit tests + /// to test successful transactions. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP OK response with no content. + virtual HttpResponsePtr + createDynamicHttpResponse(HttpRequestPtr request) { + // Check access parameters. + if (HttpRequest::recordSubject_) { + EXPECT_TRUE(request->getTls()); + EXPECT_EQ("kea-client", request->getSubject()); + } + if (HttpRequest::recordIssuer_) { + EXPECT_TRUE(request->getTls()); + EXPECT_EQ("kea-ca", request->getIssuer()); + } + // Request must always be JSON. + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast<PostHttpRequestJson>(request); + ConstElementPtr body; + if (request_json) { + body = request_json->getBodyAsJson(); + if (body) { + // Check if the client requested one of the two first response + // types. + GenericResponsePtr response; + ConstElementPtr content_type = body->get("requested-content-type"); + ConstElementPtr partial_response = body->get("partial-response"); + if (content_type || partial_response) { + // The first two response types can only be generated using the + // generic response as we have to explicitly modify some of the + // values. + response.reset(new GenericResponse(request->getHttpVersion(), + HttpStatusCode::OK)); + HttpResponseContextPtr ctx = response->context(); + + if (content_type) { + // Provide requested content type. + ctx->headers_.push_back(HttpHeaderContext("Content-Type", + content_type->stringValue())); + // It doesn't matter what body is there. + ctx->body_ = "abcd"; + response->finalize(); + + } else { + // Generate JSON response. + ctx->headers_.push_back(HttpHeaderContext("Content-Type", + "application/json")); + // The body lacks '}' so the client will be waiting for it and + // eventually should time out. + ctx->body_ = "{"; + response->finalize(); + // The auto generated Content-Length header would be based on the + // body size (so set to 1 byte). We have to override it to + // account for the missing '}' character. + response->setContentLength(2); + } + return (response); + } + } + } + + // Third type of response is requested. + ResponsePtr response(new Response(request->getHttpVersion(), + HttpStatusCode::OK)); + // If body was included in the request. Let's copy it. + if (body) { + response->setBodyAsJson(body); + } + + response->finalize(); + return (response); + } +}; + +/// @brief Implementation of the test @ref HttpResponseCreatorFactory. +/// +/// This factory class creates @ref TestHttpResponseCreator instances. +class TestHttpResponseCreatorFactory : public HttpResponseCreatorFactory { +public: + + /// @brief Creates @ref TestHttpResponseCreator instance. + virtual HttpResponseCreatorPtr create() const { + HttpResponseCreatorPtr response_creator(new TestHttpResponseCreator()); + return (response_creator); + } +}; + +/// @brief Test fixture class for @ref HttpListener. +class HttpListenerTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Starts test timer which detects timeouts. + HttpListenerTest() + : io_service_(), factory_(new TestHttpResponseCreatorFactory()), + test_timer_(io_service_), run_io_service_timer_(io_service_) { + test_timer_.setup(std::bind(&HttpListenerTest::timeoutHandler, this, true), + TEST_TIMEOUT, IntervalTimer::ONE_SHOT); + } + + /// @brief Callback function invoke upon test timeout. + /// + /// It stops the IO service and reports test timeout. + /// + /// @param fail_on_timeout Specifies if test failure should be reported. + void timeoutHandler(const bool fail_on_timeout) { + if (fail_on_timeout) { + ADD_FAILURE() << "Timeout occurred while running the test!"; + } + io_service_.stop(); + } + + /// @brief Runs IO service with optional timeout. + /// + /// @param timeout Optional value specifying for how long the io service + /// should be ran (ms). + void runIOService(long timeout = 0) { + io_service_.get_io_service().reset(); + + if (timeout > 0) { + run_io_service_timer_.setup(std::bind(&HttpListenerTest::timeoutHandler, + this, false), + timeout, IntervalTimer::ONE_SHOT); + } + io_service_.run(); + io_service_.get_io_service().reset(); + io_service_.poll(); + } + + /// @brief IO service used in the tests. + IOService io_service_; + + /// @brief Pointer to the response creator factory. + HttpResponseCreatorFactoryPtr factory_; + + /// @brief Asynchronous timer service to detect timeouts. + IntervalTimer test_timer_; + + /// @brief Asynchronous timer for running IO service for a specified amount + /// of time. + IntervalTimer run_io_service_timer_; +}; + +/// @brief Test fixture class for testing HTTP client. +class HttpsClientTest : public HttpListenerTest { +public: + + /// @brief Constructor. + HttpsClientTest() + : HttpListenerTest(), listener_(), listener2_(), listener3_(), + server_context_(), client_context_() { + configServer(server_context_); + configClient(client_context_); + listener_.reset(new HttpListener(io_service_, + IOAddress(SERVER_ADDRESS), + SERVER_PORT, + server_context_, + factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT))); + listener2_.reset(new HttpListener(io_service_, + IOAddress(IPV6_SERVER_ADDRESS), + SERVER_PORT + 1, + server_context_, + factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT))); + listener3_.reset(new HttpListener(io_service_, + IOAddress(SERVER_ADDRESS), + SERVER_PORT + 2, + server_context_, + factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(SHORT_IDLE_TIMEOUT))); + MultiThreadingMgr::instance().setMode(false); + } + + /// @brief Destructor. + ~HttpsClientTest() { + listener_->stop(); + listener2_->stop(); + listener3_->stop(); + io_service_.poll(); + MultiThreadingMgr::instance().setMode(false); + HttpRequest::recordSubject_ = false; + HttpRequest::recordIssuer_ = false; + } + + /// @brief Creates HTTP request with JSON body. + /// + /// It includes a JSON parameter with a specified value. + /// + /// @param parameter_name JSON parameter to be included. + /// @param value JSON parameter value. + /// @param version HTTP version to be used. Default is HTTP/1.1. + template<typename ValueType> + PostHttpRequestJsonPtr createRequest(const std::string& parameter_name, + const ValueType& value, + const HttpVersion& version = HttpVersion(1, 1)) { + // Create POST request with JSON body. + PostHttpRequestJsonPtr request(new PostHttpRequestJson(HttpRequest::Method::HTTP_POST, + "/", version)); + // Body is a map with a specified parameter included. + ElementPtr body = Element::createMap(); + body->set(parameter_name, Element::create(value)); + request->setBodyAsJson(body); + try { + request->finalize(); + + } catch (const std::exception& ex) { + ADD_FAILURE() << "failed to create request: " << ex.what(); + } + + return (request); + } + + /// @brief Test that two consecutive requests can be sent over the same + /// connection (if persistent, if not persistent two connections will + /// be used). + /// + /// @param version HTTP version to be used. + void testConsecutiveRequests(const HttpVersion& version) { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create a client and specify the URL on which the server can be reached. + HttpClient client(io_service_, false); + Url url("http://127.0.0.1:18123"); + + // Initiate request to the server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1, version); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request1, response1, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Initiate another request to the destination. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2, version); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request2, response2, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // Make sure that the received responses are different. We check that by + // comparing value of the sequence parameters. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + } + + /// @brief Test that the client can communicate with two different + /// destinations simultaneously. + void testMultipleDestinations() { + // Start two servers running on different ports. + ASSERT_NO_THROW(listener_->start()); + ASSERT_NO_THROW(listener2_->start()); + + // Create the client. It will be communicating with the two servers. + HttpClient client(io_service_, false); + + // Specify the URLs on which the servers are available. + Url url1("http://127.0.0.1:18123"); + Url url2("http://[::1]:18124"); + + // Create a request to the first server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(url1, client_context_, + request1, response1, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Create a request to the second server. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url2, client_context_, + request2, response2, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + + // Make sure we have received two different responses. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + } + + /// @brief Test that the client can communicate with the same destination + /// address and port but with different TLS contexts so + void testMultipleTlsContexts() { + // Start only one server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Create a second client context. + TlsContextPtr client_context2; + configClient(client_context2); + + // Specify the URL on which the server is available. + Url url("http://127.0.0.1:18123"); + + // Create a request to the first server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request1, response1, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Create a request with the second TLS context. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context2, + request2, response2, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Record subject and issuer: they will be checked during response creation. + HttpRequest::recordSubject_ = true; + HttpRequest::recordIssuer_ = true; + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + + // Make sure we have received two different responses. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + } + + /// @brief Test that idle connection can be resumed for second request. + void testIdleConnection() { + // Start the server that has short idle timeout. It closes the idle + // connection after 200ms. + ASSERT_NO_THROW(listener3_->start()); + + // Create the client that will communicate with this server. + HttpClient client(io_service_, false); + + // Specify the URL of this server. + Url url("http://127.0.0.1:18125"); + + // Create the first request. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1); + HttpResponseJsonPtr response1(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request1, response1, + [this](const boost::system::error_code& ec, const HttpResponsePtr&, + const std::string&) { + io_service_.stop(); + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Run the IO service until the response is received. + ASSERT_NO_THROW(runIOService()); + + // Make sure the response has been received. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + // Delay the generation of the second request by 2x server idle timeout. + // This should be enough to cause the server to close the connection. + ASSERT_NO_THROW(runIOService(SHORT_IDLE_TIMEOUT * 2)); + + // Create another request. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request2, response2, + [this](const boost::system::error_code& ec, const HttpResponsePtr&, + const std::string&) { + io_service_.stop(); + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the second request. + ASSERT_NO_THROW(runIOService()); + + // Make sire that the server has responded. + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + + // Make sure that two different responses have been received. + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + } + + /// @brief This test verifies that the client returns IO error code when the + /// server is unreachable. + void testUnreachable () { + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. This server is down. + Url url("http://127.0.0.1:18123"); + + // Create the request. + PostHttpRequestJsonPtr request = createRequest("sequence", 1); + HttpResponseJsonPtr response(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request, response, + [this](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + io_service_.stop(); + // The server should have returned an IO error. + if (!ec) { + ADD_FAILURE() << "asyncSendRequest didn't fail"; + } + })); + + // Actually trigger the request. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Test that an error is returned by the client if the server + /// response is malformed. + void testMalformedResponse () { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. + Url url("http://127.0.0.1:18123"); + + // The response is going to be malformed in such a way that it holds + // an invalid content type. We affect the content type by creating + // a request that holds a JSON parameter requesting a specific + // content type. + PostHttpRequestJsonPtr request = createRequest("requested-content-type", + "text/html"); + HttpResponseJsonPtr response(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request, response, + [this](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string& parsing_error) { + io_service_.stop(); + // There should be no IO error (answer from the server is received). + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + // The response object is NULL because it couldn't be finalized. + EXPECT_FALSE(response); + // The message parsing error should be returned. + EXPECT_FALSE(parsing_error.empty()); + })); + + // Actually trigger the request. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Test that client times out when it doesn't receive the entire + /// response from the server within a desired time. + void testClientRequestTimeout() { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. + Url url("http://127.0.0.1:18123"); + + unsigned cb_num = 0; + + // Create the request which asks the server to generate a partial + // (although well formed) response. The client will be waiting for the + // rest of the response to be provided and will eventually time out. + PostHttpRequestJsonPtr request1 = createRequest("partial-response", true); + HttpResponseJsonPtr response1(new HttpResponseJson()); + // This value will be set to true if the connection close callback is + // invoked upon time out. + auto connection_closed = false; + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request1, response1, + [this, &cb_num](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string&) { + if (++cb_num > 1) { + io_service_.stop(); + } + // In this particular case we know exactly the type of the + // IO error returned, because the client explicitly sets this + // error code. + EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); + // There should be no response returned. + EXPECT_FALSE(response); + }, + HttpClient::RequestTimeout(100), + HttpClient::ConnectHandler(), + HttpClient::HandshakeHandler(), + [&connection_closed](const int) { + // This callback is called when the connection gets closed + // by the client. + connection_closed = true; + }) + ); + + // Create another request after the timeout. It should be handled ok. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 1); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request2, response2, + [this, &cb_num](const boost::system::error_code& /*ec*/, + const HttpResponsePtr&, + const std::string&) { + if (++cb_num > 1) { + io_service_.stop(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + // Make sure that the client has closed the connection upon timeout. + EXPECT_TRUE(connection_closed); + } + + /// @brief Test that client times out when connection takes too long. + void testClientConnectTimeout() { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. + Url url("http://127.0.0.1:18123"); + + unsigned cb_num = 0; + + PostHttpRequestJsonPtr request = createRequest("sequence", 1); + HttpResponseJsonPtr response(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request, response, + [this, &cb_num](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string&) { + if (++cb_num > 1) { + io_service_.stop(); + } + // In this particular case we know exactly the type of the + // IO error returned, because the client explicitly sets this + // error code. + EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); + // There should be no response returned. + EXPECT_FALSE(response); + }, + HttpClient::RequestTimeout(100), + + // This callback is invoked upon an attempt to connect to the + // server. The false value indicates to the HttpClient to not + // try to send a request to the server. This simulates the + // case of connect() taking very long and should eventually + // cause the transaction to time out. + [](const boost::system::error_code& /*ec*/, int) { + return (false); + })); + + // Create another request after the timeout. It should be handled ok. + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request, response, + [this, &cb_num](const boost::system::error_code& /*ec*/, + const HttpResponsePtr&, + const std::string&) { + if (++cb_num > 1) { + io_service_.stop(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Tests the behavior of the HTTP client when the premature + /// timeout occurs. + /// + /// The premature timeout may occur when the system clock is moved + /// during the transaction. This test simulates this behavior by + /// starting new transaction and waiting for the timeout to occur + /// before the IO service is ran. The timeout handler is invoked + /// first and it resets the transaction state. This test verifies + /// that the started transaction tears down gracefully after the + /// transaction state is reset. + /// + /// There are two variants of this test. The first variant schedules + /// one transaction before running the IO service. The second variant + /// schedules two transactions prior to running the IO service. The + /// second transaction is queued, so it is expected that it doesn't + /// time out and it runs successfully. + /// + /// @param queue_two_requests Boolean value indicating if a single + /// transaction should be queued (false), or two (true). + void testClientRequestLateStart(const bool queue_two_requests) { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create the client. + HttpClient client(io_service_, false); + + // Specify the URL of the server. + Url url("http://127.0.0.1:18123"); + + // Generate first request. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1); + HttpResponseJsonPtr response1(new HttpResponseJson()); + + // Use very short timeout to make sure that it occurs before we actually + // run the transaction. + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request1, response1, + [](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string&) { + + // In this particular case we know exactly the type of the + // IO error returned, because the client explicitly sets this + // error code. + EXPECT_TRUE(ec.value() == boost::asio::error::timed_out); + // There should be no response returned. + EXPECT_FALSE(response); + }, + HttpClient::RequestTimeout(1))); + + if (queue_two_requests) { + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request2, response2, + [](const boost::system::error_code& ec, + const HttpResponsePtr& response, + const std::string&) { + + // This second request should be successful. + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + EXPECT_TRUE(response); + })); + } + + // This waits for 3ms to make sure that the timeout occurs before we + // run the transaction. This leads to an unusual situation that the + // transaction state is reset as a result of the timeout but the + // transaction is alive. We want to make sure that the client can + // gracefully deal with this situation. + usleep(3000); + + // Run the transaction and hope it will gracefully tear down. + ASSERT_NO_THROW(runIOService(100)); + + // Now try to send another request to make sure that the client + // is healthy. + PostHttpRequestJsonPtr request3 = createRequest("sequence", 3); + HttpResponseJsonPtr response3(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request3, response3, + [this](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + io_service_.stop(); + + // Everything should be ok. + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + })); + + // Actually trigger the requests. + ASSERT_NO_THROW(runIOService()); + } + + /// @brief Tests that underlying TCP socket can be registered and + /// unregistered via connection and close callbacks. + /// + /// It conducts to consecutive requests over the same client. + /// + /// @param version HTTP version to be used. + void testConnectCloseCallbacks(const HttpVersion& version) { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create a client and specify the URL on which the server can be reached. + HttpClient client(io_service_, false); + Url url("http://127.0.0.1:18123"); + + // Initiate request to the server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1, version); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ExternalMonitor monitor; + + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request1, response1, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + }, + HttpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Initiate another request to the destination. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2, version); + HttpResponseJsonPtr response2(new HttpResponseJson()); + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request2, response2, + [this, &resp_num](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num > 1) { + io_service_.stop(); + } + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + }, + HttpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // We should have had 2 connect invocations, no closes + // and a valid registered fd + EXPECT_EQ(2, monitor.connect_cnt_); + EXPECT_EQ(0, monitor.close_cnt_); + EXPECT_GT(monitor.registered_fd_, -1); + + // Make sure that the received responses are different. We check that by + // comparing value of the sequence parameters. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + EXPECT_NE(sequence1->intValue(), sequence2->intValue()); + + // Stopping the client the close the connection. + client.stop(); + + // We should have had 2 connect invocations, 1 closes + // and an invalid registered fd + EXPECT_EQ(2, monitor.connect_cnt_); + EXPECT_EQ(1, monitor.close_cnt_); + EXPECT_EQ(-1, monitor.registered_fd_); + } + + /// @brief Tests detection and handling out-of-band socket events + /// + /// It initiates a transaction and verifies that a mid-transaction call + /// to HttpClient::closeIfOutOfBand() has no affect on the connection. + /// After successful completion of the transaction, a second call to + /// HttpClient::closeIfOutOfBand() is made. This should result in the + /// connection being closed. + /// This step is repeated to verify that after an OOB closure, transactions + /// to the same destination can be processed. + /// + /// Lastly, we verify that HttpClient::stop() closes the connection correctly. + /// + /// @param version HTTP version to be used. + void testCloseIfOutOfBand(const HttpVersion& version) { + // Start the server. + ASSERT_NO_THROW(listener_->start()); + + // Create a client and specify the URL on which the server can be reached. + HttpClient client(io_service_, false); + Url url("http://127.0.0.1:18123"); + + // Initiate request to the server. + PostHttpRequestJsonPtr request1 = createRequest("sequence", 1, version); + HttpResponseJsonPtr response1(new HttpResponseJson()); + unsigned resp_num = 0; + ExternalMonitor monitor; + + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request1, response1, + [this, &client, &resp_num, &monitor](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num == 1) { + io_service_.stop(); + } + + // We should have 1 connect. + EXPECT_EQ(1, monitor.connect_cnt_); + // We should have 1 handshake. + EXPECT_EQ(1, monitor.handshake_cnt_); + // We should have 0 closes + EXPECT_EQ(0, monitor.close_cnt_); + // We should have a valid fd. + ASSERT_GT(monitor.registered_fd_, -1); + int orig_fd = monitor.registered_fd_; + + // Test our socket for OOBness. + client.closeIfOutOfBand(monitor.registered_fd_); + + // Since we're in a transaction, we should have no closes and + // the same valid fd. + EXPECT_EQ(0, monitor.close_cnt_); + ASSERT_EQ(monitor.registered_fd_, orig_fd); + + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + }, + HttpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // Make sure that we received a response. + ASSERT_TRUE(response1); + ConstElementPtr sequence1 = response1->getJsonElement("sequence"); + ASSERT_TRUE(sequence1); + EXPECT_EQ(1, sequence1->intValue()); + + // We should have had 1 connect invocations, no closes + // and a valid registered fd + EXPECT_EQ(1, monitor.connect_cnt_); + EXPECT_EQ(0, monitor.close_cnt_); + EXPECT_GT(monitor.registered_fd_, -1); + + // Test our socket for OOBness. + client.closeIfOutOfBand(monitor.registered_fd_); + + // Since we're in a transaction, we should have no closes and + // the same valid fd. + EXPECT_EQ(1, monitor.close_cnt_); + EXPECT_EQ(-1, monitor.registered_fd_); + + // Now let's do another request to the destination to verify that + // we'll reopen the connection without issue. + PostHttpRequestJsonPtr request2 = createRequest("sequence", 2, version); + HttpResponseJsonPtr response2(new HttpResponseJson()); + resp_num = 0; + ASSERT_NO_THROW(client.asyncSendRequest(url, client_context_, + request2, response2, + [this, &client, &resp_num, &monitor](const boost::system::error_code& ec, + const HttpResponsePtr&, + const std::string&) { + if (++resp_num == 1) { + io_service_.stop(); + } + + // We should have 1 connect. + EXPECT_EQ(2, monitor.connect_cnt_); + // We should have 2 handshake. + EXPECT_EQ(2, monitor.handshake_cnt_); + // We should have 0 closes + EXPECT_EQ(1, monitor.close_cnt_); + // We should have a valid fd. + ASSERT_GT(monitor.registered_fd_, -1); + int orig_fd = monitor.registered_fd_; + + // Test our socket for OOBness. + client.closeIfOutOfBand(monitor.registered_fd_); + + // Since we're in a transaction, we should have no closes and + // the same valid fd. + EXPECT_EQ(1, monitor.close_cnt_); + ASSERT_EQ(monitor.registered_fd_, orig_fd); + + if (ec) { + ADD_FAILURE() << "asyncSendRequest failed: " << ec.message(); + } + }, + HttpClient::RequestTimeout(10000), + std::bind(&ExternalMonitor::connectHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::handshakeHandler, &monitor, ph::_1, ph::_2), + std::bind(&ExternalMonitor::closeHandler, &monitor, ph::_1) + )); + + // Actually trigger the requests. The requests should be handlded by the + // server one after another. While the first request is being processed + // the server should queue another one. + ASSERT_NO_THROW(runIOService()); + + // Make sure that we received the second response. + ASSERT_TRUE(response2); + ConstElementPtr sequence2 = response2->getJsonElement("sequence"); + ASSERT_TRUE(sequence2); + EXPECT_EQ(2, sequence2->intValue()); + + // Stopping the client the close the connection. + client.stop(); + + // We should have had 2 connect invocations, 2 closes + // and an invalid registered fd + EXPECT_EQ(2, monitor.connect_cnt_); + EXPECT_EQ(2, monitor.close_cnt_); + EXPECT_EQ(-1, monitor.registered_fd_); + } + + /// @brief Simulates external registry of Connection TCP sockets + /// + /// Provides methods compatible with Connection callbacks for connect + /// and close operations. + class ExternalMonitor { + public: + /// @brief Constructor + ExternalMonitor() + : registered_fd_(-1), connect_cnt_(0), handshake_cnt_(0), + close_cnt_(0) { + } + + /// @brief Connect callback handler + /// @param ec Error status of the ASIO connect + /// @param tcp_native_fd socket descriptor to register + bool connectHandler(const boost::system::error_code& ec, int tcp_native_fd) { + ++connect_cnt_; + if ((!ec || (ec.value() == boost::asio::error::in_progress)) + && (tcp_native_fd >= 0)) { + registered_fd_ = tcp_native_fd; + return (true); + } else if ((ec.value() == boost::asio::error::already_connected) + && (registered_fd_ != tcp_native_fd)) { + return (false); + } + + // ec indicates an error, return true, so that error can be handled + // by Connection logic. + return (true); + } + + /// @brief Handshake callback handler + /// @param ec Error status of the ASIO connect + bool handshakeHandler(const boost::system::error_code&, int) { + ++handshake_cnt_; + // ec indicates an error, return true, so that error can be handled + // by Connection logic. + return (true); + } + + /// @brief Close callback handler + /// + /// @param tcp_native_fd socket descriptor to register + void closeHandler(int tcp_native_fd) { + ++close_cnt_; + EXPECT_EQ(tcp_native_fd, registered_fd_) << "closeHandler fd mismatch"; + if (tcp_native_fd >= 0) { + registered_fd_ = -1; + } + } + + /// @brief Keeps track of socket currently "registered" for external monitoring. + int registered_fd_; + + /// @brief Tracks how many times the connect callback is invoked. + int connect_cnt_; + + /// @brief Tracks how many times the handshake callback is invoked. + int handshake_cnt_; + + /// @brief Tracks how many times the close callback is invoked. + int close_cnt_; + }; + + /// @brief Instance of the listener used in the tests. + std::unique_ptr<HttpListener> listener_; + + /// @brief Instance of the second listener used in the tests. + std::unique_ptr<HttpListener> listener2_; + + /// @brief Instance of the third listener used in the tests (with short idle + /// timeout). + std::unique_ptr<HttpListener> listener3_; + + /// @brief Server TLS context. + TlsContextPtr server_context_; + + /// @brief Client TLS context. + TlsContextPtr client_context_; +}; + +#ifndef DISABLE_SOME_TESTS +// Test that two consecutive requests can be sent over the same (persistent) +// connection. +TEST_F(HttpsClientTest, consecutiveRequests) { + + ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 1))); +} + +// Test that two consecutive requests can be sent over the same (persistent) +// connection. +TEST_F(HttpsClientTest, consecutiveRequestsMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + + ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 1))); +} +#endif + +// Test that two consecutive requests can be sent over non-persistent connection. +// This is achieved by sending HTTP/1.0 requests, which are non-persistent by +// default. The client should close the connection right after receiving a response +// from the server. +TEST_F(HttpsClientTest, closeBetweenRequests) { + ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 0))); +} + +// Test that two consecutive requests can be sent over non-persistent connection. +// This is achieved by sending HTTP/1.0 requests, which are non-persistent by +// default. The client should close the connection right after receiving a response +// from the server. +TEST_F(HttpsClientTest, closeBetweenRequestsMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testConsecutiveRequests(HttpVersion(1, 0))); +} + +// Test that the client can communicate with two different destinations +// simultaneously. +TEST_F(HttpsClientTest, multipleDestinations) { + ASSERT_NO_FATAL_FAILURE(testMultipleDestinations()); +} + +// Test that the client can communicate with two different destinations +// simultaneously. +TEST_F(HttpsClientTest, multipleDestinationsMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testMultipleDestinations()); +} + +// Test that the client can use two different TLS contexts to the same +// destination address and port simultaneously. +TEST_F(HttpsClientTest, multipleTlsContexts) { + ASSERT_NO_FATAL_FAILURE(testMultipleTlsContexts()); +} + +// Test that the client can use two different TLS contexts to the same +// destination address and port simultaneously. +TEST_F(HttpsClientTest, multipleTlsContextsMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testMultipleTlsContexts()); +} + +// Test that idle connection can be resumed for second request. +TEST_F(HttpsClientTest, idleConnection) { + ASSERT_NO_FATAL_FAILURE(testIdleConnection()); +} + +// Test that idle connection can be resumed for second request. +TEST_F(HttpsClientTest, idleConnectionMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testIdleConnection()); +} + +// This test verifies that the client returns IO error code when the +// server is unreachable. +TEST_F(HttpsClientTest, unreachable) { + ASSERT_NO_FATAL_FAILURE(testUnreachable()); +} + +// This test verifies that the client returns IO error code when the +// server is unreachable. +TEST_F(HttpsClientTest, unreachableMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testUnreachable()); +} + +// Test that an error is returned by the client if the server response is +// malformed. +TEST_F(HttpsClientTest, malformedResponse) { + ASSERT_NO_FATAL_FAILURE(testMalformedResponse()); +} + +// Test that an error is returned by the client if the server response is +// malformed. +TEST_F(HttpsClientTest, malformedResponseMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testMalformedResponse()); +} + +// Test that client times out when it doesn't receive the entire response +// from the server within a desired time. +TEST_F(HttpsClientTest, clientRequestTimeout) { + ASSERT_NO_FATAL_FAILURE(testClientRequestTimeout()); +} + +// Test that client times out when it doesn't receive the entire response +// from the server within a desired time. +TEST_F(HttpsClientTest, clientRequestTimeoutMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testClientRequestTimeout()); +} + +// This test verifies the behavior of the HTTP client when the premature +// (and unexpected) timeout occurs. The premature timeout may be caused +// by the system clock move. +TEST_F(HttpsClientTest, DISABLED_clientRequestLateStartNoQueue) { + testClientRequestLateStart(false); +} + +// This test verifies the behavior of the HTTP client when the premature +// (and unexpected) timeout occurs. The premature timeout may be caused +// by the system clock move. +TEST_F(HttpsClientTest, DISABLED_clientRequestLateStartNoQueueMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + testClientRequestLateStart(false); +} + +#ifndef DISABLE_SOME_TESTS +// This test verifies the behavior of the HTTP client when the premature +// timeout occurs and there are requests queued after the request which +// times out. +TEST_F(HttpsClientTest, clientRequestLateStartQueue) { + + testClientRequestLateStart(true); +} + +// This test verifies the behavior of the HTTP client when the premature +// timeout occurs and there are requests queued after the request which +// times out. +TEST_F(HttpsClientTest, clientRequestLateStartQueueMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + testClientRequestLateStart(true); +} +#endif + +// Test that client times out when connection takes too long. +TEST_F(HttpsClientTest, clientConnectTimeout) { + ASSERT_NO_FATAL_FAILURE(testClientConnectTimeout()); +} + +// Test that client times out when connection takes too long. +TEST_F(HttpsClientTest, clientConnectTimeoutMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testClientConnectTimeout()); +} + +#ifndef DISABLE_SOME_TESTS +/// Tests that connect and close callbacks work correctly. +TEST_F(HttpsClientTest, connectCloseCallbacks) { + ASSERT_NO_FATAL_FAILURE(testConnectCloseCallbacks(HttpVersion(1, 1))); +} + +/// Tests that connect and close callbacks work correctly. +TEST_F(HttpsClientTest, connectCloseCallbacksMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testConnectCloseCallbacks(HttpVersion(1, 1))); +} +#endif + +/// Tests that HttpClient::closeIfOutOfBand works correctly. +TEST_F(HttpsClientTest, closeIfOutOfBand) { + ASSERT_NO_FATAL_FAILURE(testCloseIfOutOfBand(HttpVersion(1, 1))); +} + +/// Tests that HttpClient::closeIfOutOfBand works correctly. +TEST_F(HttpsClientTest, closeIfOutOfBandMultiThreading) { + MultiThreadingMgr::instance().setMode(true); + ASSERT_NO_FATAL_FAILURE(testCloseIfOutOfBand(HttpVersion(1, 1))); +} + +} diff --git a/src/lib/http/tests/tls_server_unittests.cc b/src/lib/http/tests/tls_server_unittests.cc new file mode 100644 index 0000000..a2a6f9d --- /dev/null +++ b/src/lib/http/tests/tls_server_unittests.cc @@ -0,0 +1,1253 @@ +// Copyright (C) 2017-2022 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <asiolink/asio_wrapper.h> +#include <asiolink/interval_timer.h> +#include <asiolink/tls_acceptor.h> +#include <asiolink/testutils/test_tls.h> +#include <cc/data.h> +#include <http/client.h> +#include <http/http_types.h> +#include <http/listener.h> +#include <http/listener_impl.h> +#include <http/post_request_json.h> +#include <http/response_creator.h> +#include <http/response_creator_factory.h> +#include <http/response_json.h> +#include <http/tests/response_test.h> +#include <http/url.h> +#include <util/multi_threading_mgr.h> + +#include <boost/asio/buffer.hpp> +#include <boost/asio/ip/tcp.hpp> +#include <boost/pointer_cast.hpp> +#include <gtest/gtest.h> + +#include <functional> +#include <list> +#include <sstream> +#include <string> + +using namespace boost::asio; +using namespace boost::asio::ip; +using namespace isc::asiolink; +using namespace isc::asiolink::test; +using namespace isc::data; +using namespace isc::http; +using namespace isc::http::test; +using namespace isc::util; +namespace ph = std::placeholders; + +/// @todo: put the common part of client and server tests in its own file(s). + +namespace { + +/// @brief IP address to which HTTP service is bound. +const std::string SERVER_ADDRESS = "127.0.0.1"; + +/// @brief IPv6 address to whch HTTP service is bound. +const std::string IPV6_SERVER_ADDRESS = "::1"; + +/// @brief Port number to which HTTP service is bound. +const unsigned short SERVER_PORT = 18123; + +/// @brief Request Timeout used in most of the tests (ms). +const long REQUEST_TIMEOUT = 10000; + +/// @brief Persistent connection idle timeout used in most of the tests (ms). +const long IDLE_TIMEOUT = 10000; + +/// @brief Persistent connection idle timeout used in tests where idle connections +/// are tested (ms). +const long SHORT_IDLE_TIMEOUT = 200; + +/// @brief Test timeout (ms). +const long TEST_TIMEOUT = 10000; + +/// @brief Test HTTP response. +typedef TestHttpResponseBase<HttpResponseJson> Response; + +/// @brief Pointer to test HTTP response. +typedef boost::shared_ptr<Response> ResponsePtr; + +/// @brief Generic test HTTP response. +typedef TestHttpResponseBase<HttpResponse> GenericResponse; + +/// @brief Pointer to generic test HTTP response. +typedef boost::shared_ptr<GenericResponse> GenericResponsePtr; + +/// @brief Implementation of the @ref HttpResponseCreator. +class TestHttpResponseCreator : public HttpResponseCreator { +public: + + /// @brief Create a new request. + /// + /// @return Pointer to the new instance of the @ref HttpRequest. + virtual HttpRequestPtr + createNewHttpRequest() const { + return (HttpRequestPtr(new PostHttpRequestJson())); + } + +private: + + /// @brief Creates HTTP response. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP response. + virtual HttpResponsePtr + createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const { + // The request hasn't been finalized so the request object + // doesn't contain any information about the HTTP version number + // used. But, the context should have this data (assuming the + // HTTP version is parsed ok). + HttpVersion http_version(request->context()->http_version_major_, + request->context()->http_version_minor_); + // This will generate the response holding JSON content. + ResponsePtr response(new Response(http_version, status_code)); + response->finalize(); + return (response); + } + + /// @brief Creates HTTP response. + /// + /// This method generates 3 types of responses: + /// - response with a requested content type, + /// - partial response with incomplete JSON body, + /// - response with JSON body copied from the request. + /// + /// The first one is useful to test situations when received response can't + /// be parsed because of the content type mismatch. The second one is useful + /// to test request timeouts. The third type is used by most of the unit tests + /// to test successful transactions. + /// + /// @param request Pointer to the HTTP request. + /// @return Pointer to the generated HTTP OK response with no content. + virtual HttpResponsePtr + createDynamicHttpResponse(HttpRequestPtr request) { + // Request must always be JSON. + PostHttpRequestJsonPtr request_json = + boost::dynamic_pointer_cast<PostHttpRequestJson>(request); + ConstElementPtr body; + if (request_json) { + body = request_json->getBodyAsJson(); + if (body) { + // Check if the client requested one of the two first response + // types. + GenericResponsePtr response; + ConstElementPtr content_type = body->get("requested-content-type"); + ConstElementPtr partial_response = body->get("partial-response"); + if (content_type || partial_response) { + // The first two response types can only be generated using the + // generic response as we have to explicitly modify some of the + // values. + response.reset(new GenericResponse(request->getHttpVersion(), + HttpStatusCode::OK)); + HttpResponseContextPtr ctx = response->context(); + + if (content_type) { + // Provide requested content type. + ctx->headers_.push_back(HttpHeaderContext("Content-Type", + content_type->stringValue())); + // It doesn't matter what body is there. + ctx->body_ = "abcd"; + response->finalize(); + + } else { + // Generate JSON response. + ctx->headers_.push_back(HttpHeaderContext("Content-Type", + "application/json")); + // The body lacks '}' so the client will be waiting for it and + // eventually should time out. + ctx->body_ = "{"; + response->finalize(); + // The auto generated Content-Length header would be based on the + // body size (so set to 1 byte). We have to override it to + // account for the missing '}' character. + response->setContentLength(2); + } + return (response); + } + } + } + + // Third type of response is requested. + ResponsePtr response(new Response(request->getHttpVersion(), + HttpStatusCode::OK)); + // If body was included in the request. Let's copy it. + if (body) { + response->setBodyAsJson(body); + } + + response->finalize(); + return (response); + } +}; + +/// @brief Implementation of the test @ref HttpResponseCreatorFactory. +/// +/// This factory class creates @ref TestHttpResponseCreator instances. +class TestHttpResponseCreatorFactory : public HttpResponseCreatorFactory { +public: + + /// @brief Creates @ref TestHttpResponseCreator instance. + virtual HttpResponseCreatorPtr create() const { + HttpResponseCreatorPtr response_creator(new TestHttpResponseCreator()); + return (response_creator); + } +}; + +/// @brief Implementation of the HTTP listener used in tests. +/// +/// This implementation replaces the @c HttpConnection type with a custom +/// implementation. +/// +/// @tparam HttpConnectionType Type of the connection object to be used by +/// the listener implementation. +template<typename HttpConnectionType> +class HttpListenerImplCustom : public HttpListenerImpl { +public: + + HttpListenerImplCustom(IOService& io_service, + const IOAddress& server_address, + const unsigned short server_port, + const TlsContextPtr& tls_context, + const HttpResponseCreatorFactoryPtr& creator_factory, + const long request_timeout, + const long idle_timeout) + : HttpListenerImpl(io_service, server_address, server_port, + tls_context, creator_factory, request_timeout, + idle_timeout) { + } + +protected: + + /// @brief Creates an instance of the @c HttpConnection. + /// + /// This method is virtual so as it can be overridden when customized + /// connections are to be used, e.g. in case of unit testing. + /// + /// @param response_creator Pointer to the response creator object used to + /// create HTTP response from the HTTP request received. + /// @param callback Callback invoked when new connection is accepted. + /// + /// @return Pointer to the created connection. + virtual HttpConnectionPtr createConnection(const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback) { + TlsContextPtr tls_context; + configClient(tls_context); + HttpConnectionPtr + conn(new HttpConnectionType(io_service_, acceptor_, + tls_context_, connections_, + response_creator, callback, + request_timeout_, idle_timeout_)); + return (conn); + } +}; + +/// @brief Derivation of the @c HttpListener used in tests. +/// +/// This class replaces the default implementation instance with the +/// @c HttpListenerImplCustom using the customized connection type. +/// +/// @tparam HttpConnectionType Type of the connection object to be used by +/// the listener implementation. +template<typename HttpConnectionType> +class HttpListenerCustom : public HttpListener { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the listener. + /// @param server_address Address on which the HTTP service should run. + /// @param server_port Port number on which the HTTP service should run. + /// @param tls_context TLS context. + /// @param creator_factory Pointer to the caller-defined + /// @ref HttpResponseCreatorFactory derivation which should be used to + /// create @ref HttpResponseCreator instances. + /// @param request_timeout Timeout after which the HTTP Request Timeout + /// is generated. + /// @param idle_timeout Timeout after which an idle persistent HTTP + /// connection is closed by the server. + /// + /// @throw HttpListenerError when any of the specified parameters is + /// invalid. + HttpListenerCustom(IOService& io_service, + const IOAddress& server_address, + const unsigned short server_port, + const TlsContextPtr& tls_context, + const HttpResponseCreatorFactoryPtr& creator_factory, + const HttpListener::RequestTimeout& request_timeout, + const HttpListener::IdleTimeout& idle_timeout) + : HttpListener(io_service, server_address, server_port, + tls_context, creator_factory, + request_timeout, idle_timeout) { + // Replace the default implementation with the customized version + // using the custom derivation of the HttpConnection. + impl_.reset(new HttpListenerImplCustom<HttpConnectionType> + (io_service, server_address, server_port, + tls_context, creator_factory, request_timeout.value_, + idle_timeout.value_)); + } +}; + +/// @brief Implementation of the @c HttpConnection which injects greater +/// length value than the buffer size into the write socket callback. +class HttpConnectionLongWriteBuffer : public HttpConnection { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the connection. + /// @param acceptor Pointer to the TCP acceptor object used to listen for + /// new HTTP connections. + /// @param tls_context TLS context. + /// @param connection_pool Connection pool in which this connection is + /// stored. + /// @param response_creator Pointer to the response creator object used to + /// create HTTP response from the HTTP request received. + /// @param callback Callback invoked when new connection is accepted. + /// @param request_timeout Configured timeout for a HTTP request. + /// @param idle_timeout Timeout after which persistent HTTP connection is + /// closed by the server. + HttpConnectionLongWriteBuffer(IOService& io_service, + const HttpAcceptorPtr& acceptor, + const TlsContextPtr& tls_context, + HttpConnectionPool& connection_pool, + const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback, + const long request_timeout, + const long idle_timeout) + : HttpConnection(io_service, acceptor, tls_context, connection_pool, + response_creator, callback, request_timeout, + idle_timeout) { + } + + /// @brief Callback invoked when data is sent over the socket. + /// + /// @param transaction Pointer to the transaction for which the callback + /// is invoked. + /// @param ec Error code. + /// @param length Length of the data sent. + virtual void socketWriteCallback(HttpConnection::TransactionPtr transaction, + boost::system::error_code ec, + size_t length) { + // Pass greater length of the data written. The callback should deal + // with this and adjust the data length. + HttpConnection::socketWriteCallback(transaction, ec, length + 1); + } +}; + +/// @brief Implementation of the @c HttpConnection which replaces +/// transaction instance prior to calling write socket callback. +class HttpConnectionTransactionChange : public HttpConnection { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the connection. + /// @param acceptor Pointer to the TCP acceptor object used to listen for + /// new HTTP connections. + /// @param tls_context TLS context. + /// @param connection_pool Connection pool in which this connection is + /// stored. + /// @param response_creator Pointer to the response creator object used to + /// create HTTP response from the HTTP request received. + /// @param callback Callback invoked when new connection is accepted. + /// @param request_timeout Configured timeout for a HTTP request. + /// @param idle_timeout Timeout after which persistent HTTP connection is + /// closed by the server. + HttpConnectionTransactionChange(IOService& io_service, + const HttpAcceptorPtr& acceptor, + const TlsContextPtr& tls_context, + HttpConnectionPool& connection_pool, + const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback, + const long request_timeout, + const long idle_timeout) + : HttpConnection(io_service, acceptor, tls_context, connection_pool, + response_creator, callback, request_timeout, + idle_timeout) { + } + + /// @brief Callback invoked when data is sent over the socket. + /// + /// @param transaction Pointer to the transaction for which the callback + /// is invoked. + /// @param ec Error code. + /// @param length Length of the data sent. + virtual void socketWriteCallback(HttpConnection::TransactionPtr transaction, + boost::system::error_code ec, + size_t length) { + // Replace the transaction. The socket callback should deal with this + // gracefully. It should detect that the output buffer is empty. Then + // try to see if the connection is persistent. This check should fail, + // because the request hasn't been created/finalized. The exception + // thrown upon checking the persistence should be caught and the + // connection closed. + transaction = HttpConnection::Transaction::create(response_creator_); + HttpConnection::socketWriteCallback(transaction, ec, length); + } +}; + + +/// @brief Entity which can connect to the HTTP server endpoint. +class TestHttpClient : public boost::noncopyable { +public: + + /// @brief Constructor. + /// + /// This constructor creates new socket instance. It doesn't connect. Call + /// connect() to connect to the server. + /// + /// @param io_service IO service to be stopped on error. + /// @param tls_context TLS context. + TestHttpClient(IOService& io_service, TlsContextPtr tls_context) + : io_service_(io_service.get_io_service()), + stream_(io_service_, tls_context->getContext()), + buf_(), response_() { + } + + /// @brief Destructor. + /// + /// Closes the underlying socket if it is open. + ~TestHttpClient() { + close(); + } + + /// @brief Send HTTP request specified in textual format. + /// + /// @param request HTTP request in the textual format. + void startRequest(const std::string& request) { + tcp::endpoint endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT); + stream_.lowest_layer().async_connect(endpoint, + [this, request](const boost::system::error_code& ec) { + if (ec) { + // One would expect that async_connect wouldn't return + // EINPROGRESS error code, but simply wait for the connection + // to get established before the handler is invoked. It turns out, + // however, that on some OSes the connect handler may receive this + // error code which doesn't necessarily indicate a problem. + // Making an attempt to write and read from this socket will + // typically succeed. So, we ignore this error. + if (ec.value() != boost::asio::error::in_progress) { + ADD_FAILURE() << "error occurred while connecting: " + << ec.message(); + io_service_.stop(); + return; + } + } + stream_.async_handshake(roleToImpl(TlsRole::CLIENT), + [this, request](const boost::system::error_code& ec) { + if (ec) { + ADD_FAILURE() << "error occurred during handshake: " + << ec.message(); + io_service_.stop(); + return; + } + sendRequest(request); + }); + }); + } + + /// @brief Send HTTP request. + /// + /// @param request HTTP request in the textual format. + void sendRequest(const std::string& request) { + sendPartialRequest(request); + } + + /// @brief Send part of the HTTP request. + /// + /// @param request part of the HTTP request to be sent. + void sendPartialRequest(std::string request) { + boost::asio::async_write(stream_, + boost::asio::buffer(request.data(), request.size()), + [this, request](const boost::system::error_code& ec, + std::size_t bytes_transferred) mutable { + if (ec) { + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + } else if ((ec.value() == boost::asio::error::try_again) || + (ec.value() == boost::asio::error::would_block)) { + // If we should try again make sure there is no garbage in the + // bytes_transferred. + bytes_transferred = 0; + + } else { + ADD_FAILURE() << "error occurred while connecting: " + << ec.message(); + io_service_.stop(); + return; + } + } + + // Remove the part of the request which has been sent. + if (bytes_transferred > 0 && (request.size() <= bytes_transferred)) { + request.erase(0, bytes_transferred); + } + + // Continue sending request data if there are still some data to be + // sent. + if (!request.empty()) { + sendPartialRequest(request); + + } else { + // Request has been sent. Start receiving response. + response_.clear(); + receivePartialResponse(); + } + }); + } + + /// @brief Receive response from the server. + void receivePartialResponse() { + stream_.async_read_some(boost::asio::buffer(buf_.data(), buf_.size()), + [this](const boost::system::error_code& ec, + std::size_t bytes_transferred) { + if (ec) { + // IO service stopped so simply return. + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + } else if ((ec.value() == boost::asio::error::try_again) || + (ec.value() == boost::asio::error::would_block)) { + // If we should try again, make sure that there is no garbage + // in the bytes_transferred. + bytes_transferred = 0; + + } else { + // Error occurred, bail... + ADD_FAILURE() << "error occurred while receiving HTTP" + " response from the server: " << ec.message(); + io_service_.stop(); + } + } + + if (bytes_transferred > 0) { + response_.insert(response_.end(), buf_.data(), + buf_.data() + bytes_transferred); + } + + // Two consecutive new lines end the part of the response we're + // expecting. + if (response_.find("\r\n\r\n", 0) != std::string::npos) { + io_service_.stop(); + + } else { + receivePartialResponse(); + } + + }); + } + + /// @brief Checks if the TCP connection is still open. + /// + /// Tests the TCP connection by trying to read from the socket. + /// Unfortunately expected failure depends on a race between the read + /// and the other side close so to check if the connection is closed + /// please use @c isConnectionClosed instead. + /// + /// @return true if the TCP connection is open. + bool isConnectionAlive() { + // Remember the current non blocking setting. + const bool non_blocking_orig = stream_.lowest_layer().non_blocking(); + // Set the socket to non blocking mode. We're going to test if the socket + // returns would_block status on the attempt to read from it. + stream_.lowest_layer().non_blocking(true); + + // We need to provide a buffer for a call to read. + char data[2]; + boost::system::error_code ec; + boost::asio::read(stream_, boost::asio::buffer(data, sizeof(data)), ec); + + // Revert the original non_blocking flag on the socket. + stream_.lowest_layer().non_blocking(non_blocking_orig); + + // If the connection is alive we'd typically get would_block status code. + // If there are any data that haven't been read we may also get success + // status. We're guessing that try_again may also be returned by some + // implementations in some situations. Any other error code indicates a + // problem with the connection so we assume that the connection has been + // closed. + return (!ec || (ec.value() == boost::asio::error::try_again) || + (ec.value() == boost::asio::error::would_block)); + } + + /// @brief Checks if the TCP connection is already closed. + /// + /// Tests the TCP connection by trying to read from the socket. + /// The read can block so this must be used to check if a connection + /// is alive so to check if the connection is alive please always + /// use @c isConnectionAlive. + /// + /// @return true if the TCP connection is closed. + bool isConnectionClosed() { + // Remember the current non blocking setting. + const bool non_blocking_orig = stream_.lowest_layer().non_blocking(); + // Set the socket to blocking mode. We're going to test if the socket + // returns eof status on the attempt to read from it. + stream_.lowest_layer().non_blocking(false); + + // We need to provide a buffer for a call to read. + char data[2]; + boost::system::error_code ec; + boost::asio::read(stream_, boost::asio::buffer(data, sizeof(data)), ec); + + // Revert the original non_blocking flag on the socket. + stream_.lowest_layer().non_blocking(non_blocking_orig); + + // If the connection is closed we'd typically get eof or + // stream_truncated status code. + return ((ec.value() == boost::asio::error::eof) || + (ec.value() == STREAM_TRUNCATED)); + } + + /// @brief Close connection. + void close() { + stream_.lowest_layer().close(); + } + + std::string getResponse() const { + return (response_); + } + +private: + + /// @brief Holds reference to the IO service. + boost::asio::io_service& io_service_; + + /// @brief A socket used for the connection. + TlsStreamImpl stream_; + + /// @brief Buffer into which response is written. + std::array<char, 8192> buf_; + + /// @brief Response in the textual format. + std::string response_; +}; + +/// @brief Pointer to the TestHttpClient. +typedef boost::shared_ptr<TestHttpClient> TestHttpClientPtr; + +/// @brief Test fixture class for @ref HttpListener. +class HttpsListenerTest : public ::testing::Test { +public: + + /// @brief Constructor. + /// + /// Starts test timer which detects timeouts. + HttpsListenerTest() + : io_service_(), factory_(new TestHttpResponseCreatorFactory()), + test_timer_(io_service_), run_io_service_timer_(io_service_), + clients_(), server_context_(), client_context_() { + configServer(server_context_); + configClient(client_context_); + test_timer_.setup(std::bind(&HttpsListenerTest::timeoutHandler, this, true), + TEST_TIMEOUT, IntervalTimer::ONE_SHOT); + } + + /// @brief Destructor. + /// + /// Removes active HTTP clients. + virtual ~HttpsListenerTest() { + for (auto client = clients_.begin(); client != clients_.end(); + ++client) { + (*client)->close(); + } + } + + /// @brief Connect to the endpoint. + /// + /// This method creates TestHttpClient instance and retains it in the clients_ + /// list. + /// + /// @param request String containing the HTTP request to be sent. + void startRequest(const std::string& request) { + TestHttpClientPtr client(new TestHttpClient(io_service_, + client_context_)); + clients_.push_back(client); + clients_.back()->startRequest(request); + } + + /// @brief Callback function invoke upon test timeout. + /// + /// It stops the IO service and reports test timeout. + /// + /// @param fail_on_timeout Specifies if test failure should be reported. + void timeoutHandler(const bool fail_on_timeout) { + if (fail_on_timeout) { + ADD_FAILURE() << "Timeout occurred while running the test!"; + } + io_service_.stop(); + } + + /// @brief Runs IO service with optional timeout. + /// + /// @param timeout Optional value specifying for how long the io service + /// should be ran. + void runIOService(long timeout = 0) { + io_service_.get_io_service().reset(); + + if (timeout > 0) { + run_io_service_timer_.setup(std::bind(&HttpsListenerTest::timeoutHandler, + this, false), + timeout, IntervalTimer::ONE_SHOT); + } + io_service_.run(); + io_service_.get_io_service().reset(); + io_service_.poll(); + } + + /// @brief Returns HTTP OK response expected by unit tests. + /// + /// @param http_version HTTP version. + /// + /// @return HTTP OK response expected by unit tests. + std::string httpOk(const HttpVersion& http_version) { + std::ostringstream s; + s << "HTTP/" << http_version.major_ << "." << http_version.minor_ << " 200 OK\r\n" + "Content-Length: 33\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"remote-address\": \"127.0.0.1\" }"; + return (s.str()); + } + + /// @brief Tests that HTTP request timeout status is returned when the + /// server does not receive the entire request. + /// + /// @param request Partial request for which the parser will be waiting for + /// the next chunks of data. + /// @param expected_version HTTP version expected in the response. + void testRequestTimeout(const std::string& request, + const HttpVersion& expected_version) { + // Open the listener with the Request Timeout of 1 sec and post the + // partial request. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, server_context_, + factory_, HttpListener::RequestTimeout(1000), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + + // Build the reference response. + std::ostringstream expected_response; + expected_response + << "HTTP/" << expected_version.major_ << "." << expected_version.minor_ + << " 408 Request Timeout\r\n" + "Content-Length: 44\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"result\": 408, \"text\": \"Request Timeout\" }"; + + // The server should wait for the missing part of the request for 1 second. + // The missing part never arrives so the server should respond with the + // HTTP Request Timeout status. + EXPECT_EQ(expected_response.str(), client->getResponse()); + } + + /// @brief Tests various cases when unexpected data is passed to the + /// socket write handler. + /// + /// This test uses the custom listener and the test specific derivations of + /// the @c HttpConnection class to enforce injection of the unexpected + /// data to the socket write callback. The two example applications of + /// this test are: + /// - injecting greater length value than the output buffer size, + /// - replacing the transaction with another transaction. + /// + /// It is expected that the socket write callback deals gracefully with + /// those situations. + /// + /// @tparam HttpConnectionType Test specific derivation of the + /// @c HttpConnection class. + template<typename HttpConnectionType> + void testWriteBufferIssues() { + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + // Use custom listener and the specialized connection object. + HttpListenerCustom<HttpConnectionType> + listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request. + ASSERT_NO_THROW(startRequest(request)); + + // Injecting unexpected data should not result in an exception. + ASSERT_NO_THROW(runIOService()); + + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + } + + /// @brief IO service used in the tests. + IOService io_service_; + + /// @brief Pointer to the response creator factory. + HttpResponseCreatorFactoryPtr factory_; + + /// @brief Asynchronous timer service to detect timeouts. + IntervalTimer test_timer_; + + /// @brief Asynchronous timer for running IO service for a specified amount + /// of time. + IntervalTimer run_io_service_timer_; + + /// @brief List of client connections. + std::list<TestHttpClientPtr> clients_; + + /// @brief Server TLS context. + TlsContextPtr server_context_; + + /// @brief Client TLS context. + TlsContextPtr client_context_; +}; + +// This test verifies that HTTP connection can be established and used to +// transmit HTTP request and receive a response. +TEST_F(HttpsListenerTest, listen) { + const std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + ASSERT_EQ(SERVER_ADDRESS, listener.getLocalAddress().toText()); + ASSERT_EQ(SERVER_PORT, listener.getLocalPort()); + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + listener.stop(); + io_service_.poll(); +} + + +// This test verifies that persistent HTTP connection can be established when +// "Connection: Keep-Alive" header value is specified. +TEST_F(HttpsListenerTest, keepAlive) { + + // The first request contains the keep-alive header which instructs the server + // to maintain the TCP connection after sending a response. + std::string request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: Keep-Alive\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request with the keep-alive header. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + // We have sent keep-alive header so we expect that the connection with + // the server remains active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Test that we can send another request via the same connection. This time + // it lacks the keep-alive header, so the server should close the connection + // after sending the response. + request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + // Send request reusing the existing connection. + ASSERT_NO_THROW(client->sendRequest(request)); + ASSERT_NO_THROW(runIOService()); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + // Connection should have been closed by the server. + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that persistent HTTP connection is established by default +// when HTTP/1.1 is in use. +TEST_F(HttpsListenerTest, persistentConnection) { + + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the first request. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + // HTTP/1.1 connection is persistent by default. + ASSERT_TRUE(client->isConnectionAlive()); + + // Test that we can send another request via the same connection. This time + // it includes the "Connection: close" header which instructs the server to + // close the connection after responding. + request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n" + "{ }"; + + // Send request reusing the existing connection. + ASSERT_NO_THROW(client->sendRequest(request)); + ASSERT_NO_THROW(runIOService()); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + // Connection should have been closed by the server. + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that "keep-alive" connection is closed by the server after +// an idle time. +TEST_F(HttpsListenerTest, keepAliveTimeout) { + + // The first request contains the keep-alive header which instructs the server + // to maintain the TCP connection after sending a response. + std::string request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: Keep-Alive\r\n\r\n" + "{ }"; + + // Specify the idle timeout of 500ms. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(500)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request with the keep-alive header. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + // We have sent keep-alive header so we expect that the connection with + // the server remains active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Run IO service for 1000ms. The idle time is set to 500ms, so the connection + // should be closed by the server while we wait here. + runIOService(1000); + + // Make sure the connection has been closed. + EXPECT_TRUE(client->isConnectionClosed()); + + // Check if we can re-establish the connection and send another request. + clients_.clear(); + request = "POST /foo/bar HTTP/1.0\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_10()), client->getResponse()); + + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that persistent connection is closed by the server after +// an idle time. +TEST_F(HttpsListenerTest, persistentConnectionTimeout) { + + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + // Specify the idle timeout of 500ms. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(500)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + // The connection should remain active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Run IO service for 1000ms. The idle time is set to 500ms, so the connection + // should be closed by the server while we wait here. + runIOService(1000); + + // Make sure the connection has been closed. + EXPECT_TRUE(client->isConnectionClosed()); + + // Check if we can re-establish the connection and send another request. + clients_.clear(); + request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n" + "{ }"; + + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that HTTP/1.1 connection remains open even if there is an +// error in the message body. +TEST_F(HttpsListenerTest, persistentConnectionBadBody) { + + // The HTTP/1.1 requests are by default persistent. + std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 12\r\n\r\n" + "{ \"a\": abc }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + + ASSERT_NO_THROW(listener.start()); + + // Send the request. + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n" + "Content-Length: 40\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"result\": 400, \"text\": \"Bad Request\" }", + client->getResponse()); + + // The connection should remain active. + ASSERT_TRUE(client->isConnectionAlive()); + + // Make sure that we can send another request. This time we specify the + // "close" connection-token to force the connection to close. + request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length: 3\r\n" + "Connection: close\r\n\r\n" + "{ }"; + + // Send request reusing the existing connection. + ASSERT_NO_THROW(client->sendRequest(request)); + ASSERT_NO_THROW(runIOService()); + EXPECT_EQ(httpOk(HttpVersion::HTTP_11()), client->getResponse()); + + EXPECT_TRUE(client->isConnectionClosed()); + + listener.stop(); + io_service_.poll(); +} + +// This test verifies that the HTTP listener can't be started twice. +TEST_F(HttpsListenerTest, startTwice) { + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + EXPECT_THROW(listener.start(), HttpListenerError); +} + +// This test verifies that Bad Request status is returned when the request +// is malformed. +TEST_F(HttpsListenerTest, badRequest) { + // Content-Type is wrong. This should result in Bad Request status. + const std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: foo\r\n" + "Content-Length: 3\r\n\r\n" + "{ }"; + + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), SERVER_PORT, + server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + ASSERT_NO_THROW(listener.start()); + ASSERT_NO_THROW(startRequest(request)); + ASSERT_NO_THROW(runIOService()); + ASSERT_EQ(1, clients_.size()); + TestHttpClientPtr client = *clients_.begin(); + ASSERT_TRUE(client); + EXPECT_EQ("HTTP/1.1 400 Bad Request\r\n" + "Content-Length: 40\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 19 Dec 2016 18:53:35 GMT\r\n" + "\r\n" + "{ \"result\": 400, \"text\": \"Bad Request\" }", + client->getResponse()); +} + +// This test verifies that NULL pointer can't be specified for the +// HttpResponseCreatorFactory. +TEST_F(HttpsListenerTest, invalidFactory) { + EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, server_context_, + HttpResponseCreatorFactoryPtr(), + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)), + HttpListenerError); +} + +// This test verifies that the timeout of 0 can't be specified for the +// Request Timeout. +TEST_F(HttpsListenerTest, invalidRequestTimeout) { + EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, server_context_, factory_, + HttpListener::RequestTimeout(0), + HttpListener::IdleTimeout(IDLE_TIMEOUT)), + HttpListenerError); +} + +// This test verifies that the timeout of 0 can't be specified for the +// idle persistent connection timeout. +TEST_F(HttpsListenerTest, invalidIdleTimeout) { + EXPECT_THROW(HttpListener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT, server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(0)), + HttpListenerError); +} + +// This test verifies that listener can't be bound to the port to which +// other server is bound. +TEST_F(HttpsListenerTest, addressInUse) { + tcp::acceptor acceptor(io_service_.get_io_service()); + // Use other port than SERVER_PORT to make sure that this TCP connection + // doesn't affect subsequent tests. + tcp::endpoint endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT + 1); + acceptor.open(endpoint.protocol()); + acceptor.bind(endpoint); + + // Listener should report an error when we try to start it because another + // acceptor is bound to that port and address. + HttpListener listener(io_service_, IOAddress(SERVER_ADDRESS), + SERVER_PORT + 1, server_context_, factory_, + HttpListener::RequestTimeout(REQUEST_TIMEOUT), + HttpListener::IdleTimeout(IDLE_TIMEOUT)); + EXPECT_THROW(listener.start(), HttpListenerError); +} + +// This test verifies that HTTP Request Timeout status is returned as +// expected when the read part of the request contains the HTTP +// version number. The timeout response should contain the same +// HTTP version number as the partial request. +TEST_F(HttpsListenerTest, requestTimeoutHttpVersionFound) { + // The part of the request specified here is correct but it is not + // a complete request. + const std::string request = "POST /foo/bar HTTP/1.1\r\n" + "Content-Type: application/json\r\n" + "Content-Length:"; + + testRequestTimeout(request, HttpVersion::HTTP_11()); +} + +// This test verifies that HTTP Request Timeout status is returned as +// expected when the read part of the request does not contain +// the HTTP version number. The timeout response should by default +// contain HTTP/1.0 version number. +TEST_F(HttpsListenerTest, requestTimeoutHttpVersionNotFound) { + // The part of the request specified here is correct but it is not + // a complete request. + const std::string request = "POST /foo/bar HTTP"; + + testRequestTimeout(request, HttpVersion::HTTP_10()); +} + +// This test verifies that injecting length value greater than the +// output buffer length to the socket write callback does not cause +// an exception. +TEST_F(HttpsListenerTest, tooLongWriteBuffer) { + testWriteBufferIssues<HttpConnectionLongWriteBuffer>(); +} + +// This test verifies that changing the transaction before calling +// the socket write callback does not cause an exception. +TEST_F(HttpsListenerTest, transactionChangeDuringWrite) { + testWriteBufferIssues<HttpConnectionTransactionChange>(); +} + +} diff --git a/src/lib/http/tests/url_unittests.cc b/src/lib/http/tests/url_unittests.cc new file mode 100644 index 0000000..f024e61 --- /dev/null +++ b/src/lib/http/tests/url_unittests.cc @@ -0,0 +1,115 @@ +// Copyright (C) 2017-2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> +#include <http/url.h> +#include <gtest/gtest.h> +#include <string> + +using namespace isc::http; + +namespace { + +/// @brief Test fixture class for @c Url class. +class UrlTest : public ::testing::Test { +public: + + /// @brief Test valid URL. + /// + /// @param text_url URL is the text form. + /// @param expected_scheme Expected scheme. + /// @param expected_hostname Expected hostname. + /// @param expected_port Expected port. + /// @param expected_path Expected path. + void testValidUrl(const std::string& text_url, + const Url::Scheme& expected_scheme, + const std::string& expected_hostname, + const unsigned expected_port, + const std::string& expected_path) { + Url url(text_url); + ASSERT_TRUE(url.isValid()) << url.getErrorMessage(); + EXPECT_EQ(expected_scheme, url.getScheme()); + EXPECT_EQ(expected_hostname, url.getStrippedHostname()); + EXPECT_EQ(expected_port, url.getPort()); + EXPECT_EQ(expected_path, url.getPath()); + } + + /// @brief Test invalid URL. + /// + /// @param text_url URL is the text form. + void testInvalidUrl(const std::string& text_url) { + Url url(text_url); + EXPECT_FALSE(url.isValid()); + } +}; + +// URL contains scheme and hostname. +TEST_F(UrlTest, schemeHostname) { + testValidUrl("http://example.org", Url::HTTP, "example.org", 0, ""); +} + +// URL contains scheme, hostname and slash. +TEST_F(UrlTest, schemeHostnameSlash) { + testValidUrl("http://example.org/", Url::HTTP, "example.org", 0, "/"); +} + +// URL contains scheme, IPv6 address and slash. +TEST_F(UrlTest, schemeIPv6AddressSlash) { + testValidUrl("http://[2001:db8:1::100]/", Url::HTTP, "2001:db8:1::100", 0, "/"); +} + +// URL contains scheme, IPv4 address and slash. +TEST_F(UrlTest, schemeIPv4AddressSlash) { + testValidUrl("http://192.0.2.2/", Url::HTTP, "192.0.2.2", 0, "/"); +} + +// URL contains scheme, hostname and path. +TEST_F(UrlTest, schemeHostnamePath) { + testValidUrl("http://example.org/some/path", Url::HTTP, "example.org", 0, + "/some/path"); +} + +// URL contains scheme, hostname and port. +TEST_F(UrlTest, schemeHostnamePort) { + testValidUrl("http://example.org:8080/", Url::HTTP, "example.org", 8080, "/"); +} + +// URL contains scheme, hostname, port and slash. +TEST_F(UrlTest, schemeHostnamePortSlash) { + testValidUrl("http://example.org:8080/", Url::HTTP, "example.org", 8080, "/"); +} + +// URL contains scheme, IPv6 address and port. +TEST_F(UrlTest, schemeIPv6AddressPort) { + testValidUrl("http://[2001:db8:1::1]:8080/", Url::HTTP, "2001:db8:1::1", 8080, "/"); +} + +// URL contains scheme, hostname, port and path. +TEST_F(UrlTest, schemeHostnamePortPath) { + testValidUrl("http://example.org:8080/path/", Url::HTTP, "example.org", 8080, + "/path/"); +} + +// URL contains https scheme, hostname, port and path. +TEST_F(UrlTest, secureSchemeHostnamePortPath) { + testValidUrl("https://example.org:8080/path/", Url::HTTPS, "example.org", 8080, + "/path/"); +} + +// Tests various invalid URLS. +TEST_F(UrlTest, invalidUrls) { + testInvalidUrl("example.org"); + testInvalidUrl("file://example.org"); + testInvalidUrl("http//example.org"); + testInvalidUrl("http:/example.org"); + testInvalidUrl("http://"); + testInvalidUrl("http://[]"); + testInvalidUrl("http://[2001:db8:1::1"); + testInvalidUrl("http://example.org:"); + testInvalidUrl("http://example.org:abc"); +} + +} |