diff options
Diffstat (limited to 'src/lib/http')
88 files changed, 23064 insertions, 0 deletions
diff --git a/src/lib/http/Makefile.am b/src/lib/http/Makefile.am new file mode 100644 index 0000000..d6b428e --- /dev/null +++ b/src/lib/http/Makefile.am @@ -0,0 +1,131 @@ +SUBDIRS = . tests + +AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) +AM_CXXFLAGS = $(KEA_CXXFLAGS) + +EXTRA_DIST = http.dox + +# Ensure that the message file is included in the distribution +EXTRA_DIST += auth_messages.mes http_messages.mes + +CLEANFILES = *.gcno *.gcda + +lib_LTLIBRARIES = libkea-http.la +libkea_http_la_SOURCES = client.cc client.h +libkea_http_la_SOURCES += connection.cc connection.h +libkea_http_la_SOURCES += connection_pool.cc connection_pool.h +libkea_http_la_SOURCES += date_time.cc date_time.h +libkea_http_la_SOURCES += http_log.cc http_log.h +libkea_http_la_SOURCES += header_context.h +libkea_http_la_SOURCES += http_acceptor.h +libkea_http_la_SOURCES += http_header.cc http_header.h +libkea_http_la_SOURCES += http_message.cc http_message.h +libkea_http_la_SOURCES += http_message_parser_base.cc http_message_parser_base.h +libkea_http_la_SOURCES += http_messages.cc http_messages.h +libkea_http_la_SOURCES += http_types.h +libkea_http_la_SOURCES += listener.cc listener.h +libkea_http_la_SOURCES += listener_impl.cc listener_impl.h +libkea_http_la_SOURCES += post_request.cc post_request.h +libkea_http_la_SOURCES += post_request_json.cc post_request_json.h +libkea_http_la_SOURCES += request.cc request.h +libkea_http_la_SOURCES += request_context.h +libkea_http_la_SOURCES += request_parser.cc request_parser.h +libkea_http_la_SOURCES += response.cc response.h +libkea_http_la_SOURCES += response_parser.cc response_parser.h +libkea_http_la_SOURCES += response_context.h +libkea_http_la_SOURCES += response_creator.cc response_creator.h +libkea_http_la_SOURCES += response_creator_factory.h +libkea_http_la_SOURCES += response_json.cc response_json.h +libkea_http_la_SOURCES += url.cc url.h +libkea_http_la_SOURCES += auth_config.h +libkea_http_la_SOURCES += auth_log.cc auth_log.h +libkea_http_la_SOURCES += auth_messages.cc auth_messages.h +libkea_http_la_SOURCES += basic_auth_config.cc basic_auth_config.h +libkea_http_la_SOURCES += basic_auth.cc basic_auth.h + +libkea_http_la_CXXFLAGS = $(AM_CXXFLAGS) +libkea_http_la_CPPFLAGS = $(AM_CPPFLAGS) +libkea_http_la_LDFLAGS = $(AM_LDFLAGS) +libkea_http_la_LDFLAGS += -no-undefined -version-info 56:0:0 + +libkea_http_la_LIBADD = $(top_builddir)/src/lib/hooks/libkea-hooks.la +libkea_http_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la +libkea_http_la_LIBADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la +libkea_http_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la +libkea_http_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la +libkea_http_la_LIBADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +libkea_http_la_LIBADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS) $(CRYPTO_LIBS) + +# If we want to get rid of all generated messages files, we need to use +# make maintainer-clean. The proper way to introduce custom commands for +# that operation is to define maintainer-clean-local target. However, +# make maintainer-clean also removes Makefile, so running configure script +# is required. To make it easy to rebuild messages without going through +# reconfigure, a new target messages-clean has been added. +maintainer-clean-local: + rm -f auth_messages.cc auth_messages.h + rm -f http_messages.h http_messages.cc + +# To regenerate messages files, one can do: +# +# make messages-clean +# make messages +# +# This is needed only when a .mes file is modified. +messages-clean: maintainer-clean-local + +if GENERATE_MESSAGES + +# Define rule to build logging source files from message file +messages: auth_messages.cc auth_messages.h http_messages.h http_messages.cc + @echo Message files regenerated + +auth_messages.cc auth_messages.h: auth_messages.mes + $(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/lib/http/auth_messages.mes + +http_messages.h http_messages.cc: http_messages.mes + $(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/lib/http/http_messages.mes + +else + +messages http_messages.h http_messages.cc: + @echo Messages generation disabled. Configure with --enable-generate-messages to enable it. + +endif + +# Specify the headers for copying into the installation directory tree. +libkea_http_includedir = $(pkgincludedir)/http +libkea_http_include_HEADERS = \ + auth_config.h \ + auth_log.h \ + auth_messages.h \ + basic_auth.h \ + basic_auth_config.h \ + client.h \ + connection.h \ + connection_pool.h \ + date_time.h \ + header_context.h \ + http_acceptor.h \ + http_header.h \ + http_log.h \ + http_message.h \ + http_message_parser_base.h \ + http_messages.h \ + http_types.h \ + listener.h \ + listener_impl.h \ + post_request.h \ + post_request_json.h \ + request.h \ + request_context.h \ + request_parser.h \ + response.h \ + response_context.h \ + response_creator.h \ + response_creator_factory.h \ + response_json.h \ + response_parser.h \ + url.h + diff --git a/src/lib/http/Makefile.in b/src/lib/http/Makefile.in new file mode 100644 index 0000000..b4a6f1a --- /dev/null +++ b/src/lib/http/Makefile.in @@ -0,0 +1,1310 @@ +# 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@ +subdir = src/lib/http +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 $(libkea_http_include_HEADERS) \ + $(am__DIST_COMMON) +mkinstalldirs = $(install_sh) -d +CONFIG_HEADER = $(top_builddir)/config.h +CONFIG_CLEAN_FILES = +CONFIG_CLEAN_VPATH_FILES = +am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; +am__vpath_adj = case $$p in \ + $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \ + *) f=$$p;; \ + esac; +am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`; +am__install_max = 40 +am__nobase_strip_setup = \ + srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'` +am__nobase_strip = \ + for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||" +am__nobase_list = $(am__nobase_strip_setup); \ + for p in $$list; do echo "$$p $$p"; done | \ + sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \ + $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \ + if (++n[$$2] == $(am__install_max)) \ + { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \ + END { for (dir in files) print dir, files[dir] }' +am__base_list = \ + sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \ + sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g' +am__uninstall_files_from_dir = { \ + test -z "$$files" \ + || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \ + || { echo " ( cd '$$dir' && rm -f" $$files ")"; \ + $(am__cd) "$$dir" && rm -f $$files; }; \ + } +am__installdirs = "$(DESTDIR)$(libdir)" \ + "$(DESTDIR)$(libkea_http_includedir)" +LTLIBRARIES = $(lib_LTLIBRARIES) +am__DEPENDENCIES_1 = +libkea_http_la_DEPENDENCIES = \ + $(top_builddir)/src/lib/hooks/libkea-hooks.la \ + $(top_builddir)/src/lib/cc/libkea-cc.la \ + $(top_builddir)/src/lib/asiolink/libkea-asiolink.la \ + $(top_builddir)/src/lib/log/libkea-log.la \ + $(top_builddir)/src/lib/util/libkea-util.la \ + $(top_builddir)/src/lib/exceptions/libkea-exceptions.la \ + $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_1) \ + $(am__DEPENDENCIES_1) +am_libkea_http_la_OBJECTS = libkea_http_la-client.lo \ + libkea_http_la-connection.lo libkea_http_la-connection_pool.lo \ + libkea_http_la-date_time.lo libkea_http_la-http_log.lo \ + libkea_http_la-http_header.lo libkea_http_la-http_message.lo \ + libkea_http_la-http_message_parser_base.lo \ + libkea_http_la-http_messages.lo libkea_http_la-listener.lo \ + libkea_http_la-listener_impl.lo libkea_http_la-post_request.lo \ + libkea_http_la-post_request_json.lo libkea_http_la-request.lo \ + libkea_http_la-request_parser.lo libkea_http_la-response.lo \ + libkea_http_la-response_parser.lo \ + libkea_http_la-response_creator.lo \ + libkea_http_la-response_json.lo libkea_http_la-url.lo \ + libkea_http_la-auth_log.lo libkea_http_la-auth_messages.lo \ + libkea_http_la-basic_auth_config.lo \ + libkea_http_la-basic_auth.lo +libkea_http_la_OBJECTS = $(am_libkea_http_la_OBJECTS) +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 = +libkea_http_la_LINK = $(LIBTOOL) $(AM_V_lt) --tag=CXX \ + $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=link $(CXXLD) \ + $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) \ + $(libkea_http_la_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)/libkea_http_la-auth_log.Plo \ + ./$(DEPDIR)/libkea_http_la-auth_messages.Plo \ + ./$(DEPDIR)/libkea_http_la-basic_auth.Plo \ + ./$(DEPDIR)/libkea_http_la-basic_auth_config.Plo \ + ./$(DEPDIR)/libkea_http_la-client.Plo \ + ./$(DEPDIR)/libkea_http_la-connection.Plo \ + ./$(DEPDIR)/libkea_http_la-connection_pool.Plo \ + ./$(DEPDIR)/libkea_http_la-date_time.Plo \ + ./$(DEPDIR)/libkea_http_la-http_header.Plo \ + ./$(DEPDIR)/libkea_http_la-http_log.Plo \ + ./$(DEPDIR)/libkea_http_la-http_message.Plo \ + ./$(DEPDIR)/libkea_http_la-http_message_parser_base.Plo \ + ./$(DEPDIR)/libkea_http_la-http_messages.Plo \ + ./$(DEPDIR)/libkea_http_la-listener.Plo \ + ./$(DEPDIR)/libkea_http_la-listener_impl.Plo \ + ./$(DEPDIR)/libkea_http_la-post_request.Plo \ + ./$(DEPDIR)/libkea_http_la-post_request_json.Plo \ + ./$(DEPDIR)/libkea_http_la-request.Plo \ + ./$(DEPDIR)/libkea_http_la-request_parser.Plo \ + ./$(DEPDIR)/libkea_http_la-response.Plo \ + ./$(DEPDIR)/libkea_http_la-response_creator.Plo \ + ./$(DEPDIR)/libkea_http_la-response_json.Plo \ + ./$(DEPDIR)/libkea_http_la-response_parser.Plo \ + ./$(DEPDIR)/libkea_http_la-url.Plo +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 = $(libkea_http_la_SOURCES) +DIST_SOURCES = $(libkea_http_la_SOURCES) +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 +HEADERS = $(libkea_http_include_HEADERS) +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 +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 = . tests +AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib \ + $(BOOST_INCLUDES) $(CRYPTO_CFLAGS) $(CRYPTO_INCLUDES) +AM_CXXFLAGS = $(KEA_CXXFLAGS) + +# Ensure that the message file is included in the distribution +EXTRA_DIST = http.dox auth_messages.mes http_messages.mes +CLEANFILES = *.gcno *.gcda +lib_LTLIBRARIES = libkea-http.la +libkea_http_la_SOURCES = client.cc client.h connection.cc connection.h \ + connection_pool.cc connection_pool.h date_time.cc date_time.h \ + http_log.cc http_log.h header_context.h http_acceptor.h \ + http_header.cc http_header.h http_message.cc http_message.h \ + http_message_parser_base.cc http_message_parser_base.h \ + http_messages.cc http_messages.h http_types.h listener.cc \ + listener.h listener_impl.cc listener_impl.h post_request.cc \ + post_request.h post_request_json.cc post_request_json.h \ + request.cc request.h request_context.h request_parser.cc \ + request_parser.h response.cc response.h response_parser.cc \ + response_parser.h response_context.h response_creator.cc \ + response_creator.h response_creator_factory.h response_json.cc \ + response_json.h url.cc url.h auth_config.h auth_log.cc \ + auth_log.h auth_messages.cc auth_messages.h \ + basic_auth_config.cc basic_auth_config.h basic_auth.cc \ + basic_auth.h +libkea_http_la_CXXFLAGS = $(AM_CXXFLAGS) +libkea_http_la_CPPFLAGS = $(AM_CPPFLAGS) +libkea_http_la_LDFLAGS = $(AM_LDFLAGS) -no-undefined -version-info \ + 56:0:0 +libkea_http_la_LIBADD = $(top_builddir)/src/lib/hooks/libkea-hooks.la \ + $(top_builddir)/src/lib/cc/libkea-cc.la \ + $(top_builddir)/src/lib/asiolink/libkea-asiolink.la \ + $(top_builddir)/src/lib/log/libkea-log.la \ + $(top_builddir)/src/lib/util/libkea-util.la \ + $(top_builddir)/src/lib/exceptions/libkea-exceptions.la \ + $(LOG4CPLUS_LIBS) $(BOOST_LIBS) $(CRYPTO_LIBS) + +# Specify the headers for copying into the installation directory tree. +libkea_http_includedir = $(pkgincludedir)/http +libkea_http_include_HEADERS = \ + auth_config.h \ + auth_log.h \ + auth_messages.h \ + basic_auth.h \ + basic_auth_config.h \ + client.h \ + connection.h \ + connection_pool.h \ + date_time.h \ + header_context.h \ + http_acceptor.h \ + http_header.h \ + http_log.h \ + http_message.h \ + http_message_parser_base.h \ + http_messages.h \ + http_types.h \ + listener.h \ + listener_impl.h \ + post_request.h \ + post_request_json.h \ + request.h \ + request_context.h \ + request_parser.h \ + response.h \ + response_context.h \ + response_creator.h \ + response_creator_factory.h \ + response_json.h \ + response_parser.h \ + url.h + +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/Makefile'; \ + $(am__cd) $(top_srcdir) && \ + $(AUTOMAKE) --foreign src/lib/http/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): + +install-libLTLIBRARIES: $(lib_LTLIBRARIES) + @$(NORMAL_INSTALL) + @list='$(lib_LTLIBRARIES)'; test -n "$(libdir)" || list=; \ + list2=; for p in $$list; do \ + if test -f $$p; then \ + list2="$$list2 $$p"; \ + else :; fi; \ + done; \ + test -z "$$list2" || { \ + echo " $(MKDIR_P) '$(DESTDIR)$(libdir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(libdir)" || exit 1; \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 '$(DESTDIR)$(libdir)'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=install $(INSTALL) $(INSTALL_STRIP_FLAG) $$list2 "$(DESTDIR)$(libdir)"; \ + } + +uninstall-libLTLIBRARIES: + @$(NORMAL_UNINSTALL) + @list='$(lib_LTLIBRARIES)'; test -n "$(libdir)" || list=; \ + for p in $$list; do \ + $(am__strip_dir) \ + echo " $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f '$(DESTDIR)$(libdir)/$$f'"; \ + $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=uninstall rm -f "$(DESTDIR)$(libdir)/$$f"; \ + done + +clean-libLTLIBRARIES: + -test -z "$(lib_LTLIBRARIES)" || rm -f $(lib_LTLIBRARIES) + @list='$(lib_LTLIBRARIES)'; \ + locs=`for p in $$list; do echo $$p; done | \ + sed 's|^[^/]*$$|.|; s|/[^/]*$$||; s|$$|/so_locations|' | \ + sort -u`; \ + test -z "$$locs" || { \ + echo rm -f $${locs}; \ + rm -f $${locs}; \ + } + +libkea-http.la: $(libkea_http_la_OBJECTS) $(libkea_http_la_DEPENDENCIES) $(EXTRA_libkea_http_la_DEPENDENCIES) + $(AM_V_CXXLD)$(libkea_http_la_LINK) -rpath $(libdir) $(libkea_http_la_OBJECTS) $(libkea_http_la_LIBADD) $(LIBS) + +mostlyclean-compile: + -rm -f *.$(OBJEXT) + +distclean-compile: + -rm -f *.tab.c + +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-auth_log.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-auth_messages.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-basic_auth.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-basic_auth_config.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-client.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-connection.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-connection_pool.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-date_time.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-http_header.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-http_log.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-http_message.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-http_message_parser_base.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-http_messages.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-listener.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-listener_impl.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-post_request.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-post_request_json.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-request.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-request_parser.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-response.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-response_creator.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-response_json.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-response_parser.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libkea_http_la-url.Plo@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 $@ $< + +libkea_http_la-client.lo: client.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-client.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-client.Tpo -c -o libkea_http_la-client.lo `test -f 'client.cc' || echo '$(srcdir)/'`client.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-client.Tpo $(DEPDIR)/libkea_http_la-client.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='client.cc' object='libkea_http_la-client.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-client.lo `test -f 'client.cc' || echo '$(srcdir)/'`client.cc + +libkea_http_la-connection.lo: connection.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-connection.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-connection.Tpo -c -o libkea_http_la-connection.lo `test -f 'connection.cc' || echo '$(srcdir)/'`connection.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-connection.Tpo $(DEPDIR)/libkea_http_la-connection.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='connection.cc' object='libkea_http_la-connection.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-connection.lo `test -f 'connection.cc' || echo '$(srcdir)/'`connection.cc + +libkea_http_la-connection_pool.lo: connection_pool.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-connection_pool.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-connection_pool.Tpo -c -o libkea_http_la-connection_pool.lo `test -f 'connection_pool.cc' || echo '$(srcdir)/'`connection_pool.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-connection_pool.Tpo $(DEPDIR)/libkea_http_la-connection_pool.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='connection_pool.cc' object='libkea_http_la-connection_pool.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-connection_pool.lo `test -f 'connection_pool.cc' || echo '$(srcdir)/'`connection_pool.cc + +libkea_http_la-date_time.lo: date_time.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-date_time.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-date_time.Tpo -c -o libkea_http_la-date_time.lo `test -f 'date_time.cc' || echo '$(srcdir)/'`date_time.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-date_time.Tpo $(DEPDIR)/libkea_http_la-date_time.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='date_time.cc' object='libkea_http_la-date_time.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-date_time.lo `test -f 'date_time.cc' || echo '$(srcdir)/'`date_time.cc + +libkea_http_la-http_log.lo: http_log.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-http_log.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-http_log.Tpo -c -o libkea_http_la-http_log.lo `test -f 'http_log.cc' || echo '$(srcdir)/'`http_log.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-http_log.Tpo $(DEPDIR)/libkea_http_la-http_log.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='http_log.cc' object='libkea_http_la-http_log.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-http_log.lo `test -f 'http_log.cc' || echo '$(srcdir)/'`http_log.cc + +libkea_http_la-http_header.lo: http_header.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-http_header.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-http_header.Tpo -c -o libkea_http_la-http_header.lo `test -f 'http_header.cc' || echo '$(srcdir)/'`http_header.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-http_header.Tpo $(DEPDIR)/libkea_http_la-http_header.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='http_header.cc' object='libkea_http_la-http_header.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-http_header.lo `test -f 'http_header.cc' || echo '$(srcdir)/'`http_header.cc + +libkea_http_la-http_message.lo: http_message.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-http_message.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-http_message.Tpo -c -o libkea_http_la-http_message.lo `test -f 'http_message.cc' || echo '$(srcdir)/'`http_message.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-http_message.Tpo $(DEPDIR)/libkea_http_la-http_message.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='http_message.cc' object='libkea_http_la-http_message.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-http_message.lo `test -f 'http_message.cc' || echo '$(srcdir)/'`http_message.cc + +libkea_http_la-http_message_parser_base.lo: http_message_parser_base.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-http_message_parser_base.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-http_message_parser_base.Tpo -c -o libkea_http_la-http_message_parser_base.lo `test -f 'http_message_parser_base.cc' || echo '$(srcdir)/'`http_message_parser_base.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-http_message_parser_base.Tpo $(DEPDIR)/libkea_http_la-http_message_parser_base.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='http_message_parser_base.cc' object='libkea_http_la-http_message_parser_base.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-http_message_parser_base.lo `test -f 'http_message_parser_base.cc' || echo '$(srcdir)/'`http_message_parser_base.cc + +libkea_http_la-http_messages.lo: http_messages.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-http_messages.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-http_messages.Tpo -c -o libkea_http_la-http_messages.lo `test -f 'http_messages.cc' || echo '$(srcdir)/'`http_messages.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-http_messages.Tpo $(DEPDIR)/libkea_http_la-http_messages.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='http_messages.cc' object='libkea_http_la-http_messages.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-http_messages.lo `test -f 'http_messages.cc' || echo '$(srcdir)/'`http_messages.cc + +libkea_http_la-listener.lo: listener.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-listener.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-listener.Tpo -c -o libkea_http_la-listener.lo `test -f 'listener.cc' || echo '$(srcdir)/'`listener.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-listener.Tpo $(DEPDIR)/libkea_http_la-listener.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='listener.cc' object='libkea_http_la-listener.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-listener.lo `test -f 'listener.cc' || echo '$(srcdir)/'`listener.cc + +libkea_http_la-listener_impl.lo: listener_impl.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-listener_impl.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-listener_impl.Tpo -c -o libkea_http_la-listener_impl.lo `test -f 'listener_impl.cc' || echo '$(srcdir)/'`listener_impl.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-listener_impl.Tpo $(DEPDIR)/libkea_http_la-listener_impl.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='listener_impl.cc' object='libkea_http_la-listener_impl.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-listener_impl.lo `test -f 'listener_impl.cc' || echo '$(srcdir)/'`listener_impl.cc + +libkea_http_la-post_request.lo: post_request.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-post_request.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-post_request.Tpo -c -o libkea_http_la-post_request.lo `test -f 'post_request.cc' || echo '$(srcdir)/'`post_request.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-post_request.Tpo $(DEPDIR)/libkea_http_la-post_request.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='post_request.cc' object='libkea_http_la-post_request.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-post_request.lo `test -f 'post_request.cc' || echo '$(srcdir)/'`post_request.cc + +libkea_http_la-post_request_json.lo: post_request_json.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-post_request_json.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-post_request_json.Tpo -c -o libkea_http_la-post_request_json.lo `test -f 'post_request_json.cc' || echo '$(srcdir)/'`post_request_json.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-post_request_json.Tpo $(DEPDIR)/libkea_http_la-post_request_json.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='post_request_json.cc' object='libkea_http_la-post_request_json.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-post_request_json.lo `test -f 'post_request_json.cc' || echo '$(srcdir)/'`post_request_json.cc + +libkea_http_la-request.lo: request.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-request.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-request.Tpo -c -o libkea_http_la-request.lo `test -f 'request.cc' || echo '$(srcdir)/'`request.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-request.Tpo $(DEPDIR)/libkea_http_la-request.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='request.cc' object='libkea_http_la-request.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-request.lo `test -f 'request.cc' || echo '$(srcdir)/'`request.cc + +libkea_http_la-request_parser.lo: request_parser.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-request_parser.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-request_parser.Tpo -c -o libkea_http_la-request_parser.lo `test -f 'request_parser.cc' || echo '$(srcdir)/'`request_parser.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-request_parser.Tpo $(DEPDIR)/libkea_http_la-request_parser.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='request_parser.cc' object='libkea_http_la-request_parser.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-request_parser.lo `test -f 'request_parser.cc' || echo '$(srcdir)/'`request_parser.cc + +libkea_http_la-response.lo: response.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-response.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-response.Tpo -c -o libkea_http_la-response.lo `test -f 'response.cc' || echo '$(srcdir)/'`response.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-response.Tpo $(DEPDIR)/libkea_http_la-response.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response.cc' object='libkea_http_la-response.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-response.lo `test -f 'response.cc' || echo '$(srcdir)/'`response.cc + +libkea_http_la-response_parser.lo: response_parser.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-response_parser.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-response_parser.Tpo -c -o libkea_http_la-response_parser.lo `test -f 'response_parser.cc' || echo '$(srcdir)/'`response_parser.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-response_parser.Tpo $(DEPDIR)/libkea_http_la-response_parser.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_parser.cc' object='libkea_http_la-response_parser.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-response_parser.lo `test -f 'response_parser.cc' || echo '$(srcdir)/'`response_parser.cc + +libkea_http_la-response_creator.lo: response_creator.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-response_creator.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-response_creator.Tpo -c -o libkea_http_la-response_creator.lo `test -f 'response_creator.cc' || echo '$(srcdir)/'`response_creator.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-response_creator.Tpo $(DEPDIR)/libkea_http_la-response_creator.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_creator.cc' object='libkea_http_la-response_creator.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-response_creator.lo `test -f 'response_creator.cc' || echo '$(srcdir)/'`response_creator.cc + +libkea_http_la-response_json.lo: response_json.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-response_json.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-response_json.Tpo -c -o libkea_http_la-response_json.lo `test -f 'response_json.cc' || echo '$(srcdir)/'`response_json.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-response_json.Tpo $(DEPDIR)/libkea_http_la-response_json.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='response_json.cc' object='libkea_http_la-response_json.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-response_json.lo `test -f 'response_json.cc' || echo '$(srcdir)/'`response_json.cc + +libkea_http_la-url.lo: url.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-url.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-url.Tpo -c -o libkea_http_la-url.lo `test -f 'url.cc' || echo '$(srcdir)/'`url.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-url.Tpo $(DEPDIR)/libkea_http_la-url.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='url.cc' object='libkea_http_la-url.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-url.lo `test -f 'url.cc' || echo '$(srcdir)/'`url.cc + +libkea_http_la-auth_log.lo: auth_log.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-auth_log.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-auth_log.Tpo -c -o libkea_http_la-auth_log.lo `test -f 'auth_log.cc' || echo '$(srcdir)/'`auth_log.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-auth_log.Tpo $(DEPDIR)/libkea_http_la-auth_log.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='auth_log.cc' object='libkea_http_la-auth_log.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-auth_log.lo `test -f 'auth_log.cc' || echo '$(srcdir)/'`auth_log.cc + +libkea_http_la-auth_messages.lo: auth_messages.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-auth_messages.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-auth_messages.Tpo -c -o libkea_http_la-auth_messages.lo `test -f 'auth_messages.cc' || echo '$(srcdir)/'`auth_messages.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-auth_messages.Tpo $(DEPDIR)/libkea_http_la-auth_messages.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='auth_messages.cc' object='libkea_http_la-auth_messages.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-auth_messages.lo `test -f 'auth_messages.cc' || echo '$(srcdir)/'`auth_messages.cc + +libkea_http_la-basic_auth_config.lo: basic_auth_config.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-basic_auth_config.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-basic_auth_config.Tpo -c -o libkea_http_la-basic_auth_config.lo `test -f 'basic_auth_config.cc' || echo '$(srcdir)/'`basic_auth_config.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-basic_auth_config.Tpo $(DEPDIR)/libkea_http_la-basic_auth_config.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='basic_auth_config.cc' object='libkea_http_la-basic_auth_config.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-basic_auth_config.lo `test -f 'basic_auth_config.cc' || echo '$(srcdir)/'`basic_auth_config.cc + +libkea_http_la-basic_auth.lo: basic_auth.cc +@am__fastdepCXX_TRUE@ $(AM_V_CXX)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -MT libkea_http_la-basic_auth.lo -MD -MP -MF $(DEPDIR)/libkea_http_la-basic_auth.Tpo -c -o libkea_http_la-basic_auth.lo `test -f 'basic_auth.cc' || echo '$(srcdir)/'`basic_auth.cc +@am__fastdepCXX_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libkea_http_la-basic_auth.Tpo $(DEPDIR)/libkea_http_la-basic_auth.Plo +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ $(AM_V_CXX)source='basic_auth.cc' object='libkea_http_la-basic_auth.lo' libtool=yes @AMDEPBACKSLASH@ +@AMDEP_TRUE@@am__fastdepCXX_FALSE@ DEPDIR=$(DEPDIR) $(CXXDEPMODE) $(depcomp) @AMDEPBACKSLASH@ +@am__fastdepCXX_FALSE@ $(AM_V_CXX@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CXX $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CXX) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libkea_http_la_CPPFLAGS) $(CPPFLAGS) $(libkea_http_la_CXXFLAGS) $(CXXFLAGS) -c -o libkea_http_la-basic_auth.lo `test -f 'basic_auth.cc' || echo '$(srcdir)/'`basic_auth.cc + +mostlyclean-libtool: + -rm -f *.lo + +clean-libtool: + -rm -rf .libs _libs +install-libkea_http_includeHEADERS: $(libkea_http_include_HEADERS) + @$(NORMAL_INSTALL) + @list='$(libkea_http_include_HEADERS)'; test -n "$(libkea_http_includedir)" || list=; \ + if test -n "$$list"; then \ + echo " $(MKDIR_P) '$(DESTDIR)$(libkea_http_includedir)'"; \ + $(MKDIR_P) "$(DESTDIR)$(libkea_http_includedir)" || exit 1; \ + fi; \ + for p in $$list; do \ + if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \ + echo "$$d$$p"; \ + done | $(am__base_list) | \ + while read files; do \ + echo " $(INSTALL_HEADER) $$files '$(DESTDIR)$(libkea_http_includedir)'"; \ + $(INSTALL_HEADER) $$files "$(DESTDIR)$(libkea_http_includedir)" || exit $$?; \ + done + +uninstall-libkea_http_includeHEADERS: + @$(NORMAL_UNINSTALL) + @list='$(libkea_http_include_HEADERS)'; test -n "$(libkea_http_includedir)" || list=; \ + files=`for p in $$list; do echo $$p; done | sed -e 's|^.*/||'`; \ + dir='$(DESTDIR)$(libkea_http_includedir)'; $(am__uninstall_files_from_dir) + +# 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 + +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 +check: check-recursive +all-am: Makefile $(LTLIBRARIES) $(HEADERS) +installdirs: installdirs-recursive +installdirs-am: + for dir in "$(DESTDIR)$(libdir)" "$(DESTDIR)$(libkea_http_includedir)"; do \ + test -z "$$dir" || $(MKDIR_P) "$$dir"; \ + done +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-libLTLIBRARIES clean-libtool \ + mostlyclean-am + +distclean: distclean-recursive + -rm -f ./$(DEPDIR)/libkea_http_la-auth_log.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-auth_messages.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-basic_auth.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-basic_auth_config.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-client.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-connection.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-connection_pool.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-date_time.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_header.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_log.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_message.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_message_parser_base.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_messages.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-listener.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-listener_impl.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-post_request.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-post_request_json.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-request.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-request_parser.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-response.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-response_creator.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-response_json.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-response_parser.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-url.Plo + -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-libkea_http_includeHEADERS + +install-dvi: install-dvi-recursive + +install-dvi-am: + +install-exec-am: install-libLTLIBRARIES + +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)/libkea_http_la-auth_log.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-auth_messages.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-basic_auth.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-basic_auth_config.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-client.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-connection.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-connection_pool.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-date_time.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_header.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_log.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_message.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_message_parser_base.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-http_messages.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-listener.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-listener_impl.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-post_request.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-post_request_json.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-request.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-request_parser.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-response.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-response_creator.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-response_json.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-response_parser.Plo + -rm -f ./$(DEPDIR)/libkea_http_la-url.Plo + -rm -f Makefile +maintainer-clean-am: distclean-am maintainer-clean-generic \ + maintainer-clean-local + +mostlyclean: mostlyclean-recursive + +mostlyclean-am: mostlyclean-compile mostlyclean-generic \ + mostlyclean-libtool + +pdf: pdf-recursive + +pdf-am: + +ps: ps-recursive + +ps-am: + +uninstall-am: uninstall-libLTLIBRARIES \ + uninstall-libkea_http_includeHEADERS + +.MAKE: $(am__recursive_targets) install-am install-strip + +.PHONY: $(am__recursive_targets) CTAGS GTAGS TAGS all all-am \ + am--depfiles check check-am clean clean-generic \ + clean-libLTLIBRARIES clean-libtool 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-libLTLIBRARIES \ + install-libkea_http_includeHEADERS 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 \ + maintainer-clean-local mostlyclean mostlyclean-compile \ + mostlyclean-generic mostlyclean-libtool pdf pdf-am ps ps-am \ + tags tags-am uninstall uninstall-am uninstall-libLTLIBRARIES \ + uninstall-libkea_http_includeHEADERS + +.PRECIOUS: Makefile + + +# If we want to get rid of all generated messages files, we need to use +# make maintainer-clean. The proper way to introduce custom commands for +# that operation is to define maintainer-clean-local target. However, +# make maintainer-clean also removes Makefile, so running configure script +# is required. To make it easy to rebuild messages without going through +# reconfigure, a new target messages-clean has been added. +maintainer-clean-local: + rm -f auth_messages.cc auth_messages.h + rm -f http_messages.h http_messages.cc + +# To regenerate messages files, one can do: +# +# make messages-clean +# make messages +# +# This is needed only when a .mes file is modified. +messages-clean: maintainer-clean-local + +# Define rule to build logging source files from message file +@GENERATE_MESSAGES_TRUE@messages: auth_messages.cc auth_messages.h http_messages.h http_messages.cc +@GENERATE_MESSAGES_TRUE@ @echo Message files regenerated + +@GENERATE_MESSAGES_TRUE@auth_messages.cc auth_messages.h: auth_messages.mes +@GENERATE_MESSAGES_TRUE@ $(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/lib/http/auth_messages.mes + +@GENERATE_MESSAGES_TRUE@http_messages.h http_messages.cc: http_messages.mes +@GENERATE_MESSAGES_TRUE@ $(top_builddir)/src/lib/log/compiler/kea-msg-compiler $(top_srcdir)/src/lib/http/http_messages.mes + +@GENERATE_MESSAGES_FALSE@messages http_messages.h http_messages.cc: +@GENERATE_MESSAGES_FALSE@ @echo Messages generation disabled. Configure with --enable-generate-messages to enable it. + +# 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/auth_config.h b/src/lib/http/auth_config.h new file mode 100644 index 0000000..d8f74eb --- /dev/null +++ b/src/lib/http/auth_config.h @@ -0,0 +1,102 @@ +// 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/. + +#ifndef HTTP_AUTH_CONFIG_H +#define HTTP_AUTH_CONFIG_H + +#include <cc/cfg_to_element.h> +#include <cc/data.h> +#include <cc/simple_parser.h> +#include <cc/user_context.h> +#include <http/request.h> +#include <http/response_creator.h> +#include <http/response_json.h> + +namespace isc { +namespace http { + +/// @brief Base type of HTTP authentication configuration. +class HttpAuthConfig : public isc::data::UserContext, + public isc::data::CfgToElement { +public: + + /// @brief Destructor. + virtual ~HttpAuthConfig() { } + + /// @brief Set the realm. + /// + /// @param realm New realm. + void setRealm(const std::string& realm) { + realm_ = realm; + } + + /// @brief Returns the realm. + /// + /// @return The HTTP authentication realm. + const std::string& getRealm() const { + return (realm_); + } + + /// @brief Set the common part for file paths (usually a directory). + /// + /// @param directory New directory. + void setDirectory(const std::string& directory) { + directory_ = directory; + } + + /// @brief Returns the common part for file paths (usually a directory). + /// + /// @return The common part for file paths (usually a directory). + const std::string& getDirectory() const { + return (directory_); + } + + /// @brief Empty predicate. + /// + /// @return true if the configuration is empty so authentication + /// is not required. + virtual bool empty() const = 0; + + /// @brief Clear configuration. + virtual void clear() = 0; + + /// @brief Parses HTTP authentication configuration. + /// + /// @param config Element holding the basic HTTP authentication + /// configuration to be parsed. + /// @throw DhcpConfigError when the configuration is invalid. + virtual void parse(const isc::data::ConstElementPtr& config) = 0; + + /// @brief Unparses HTTP authentication configuration. + /// + /// @return A pointer to unparsed HTTP authentication configuration. + virtual isc::data::ElementPtr toElement() const = 0; + + /// @brief Validate HTTP request. + /// + /// @param creator The HTTP response creator. + /// @param request The HTTP request to validate. + /// @return Error HTTP response if validation failed, null otherwise. + virtual isc::http::HttpResponseJsonPtr + checkAuth(const isc::http::HttpResponseCreator& creator, + const isc::http::HttpRequestPtr& request) const = 0; + +private: + + /// @brief The realm. + std::string realm_; + + /// @brief Common part for file paths (usually a directory). + std::string directory_; +}; + +/// @brief Type of shared pointers to HTTP authentication configuration. +typedef boost::shared_ptr<HttpAuthConfig> HttpAuthConfigPtr; + +} // end of namespace isc::http +} // end of namespace isc + +#endif // endif HTTP_AUTH_CONFIG_H diff --git a/src/lib/http/auth_log.cc b/src/lib/http/auth_log.cc new file mode 100644 index 0000000..d2c805c --- /dev/null +++ b/src/lib/http/auth_log.cc @@ -0,0 +1,20 @@ +// Copyright (C) 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/. + +/// Defines the logger used by the HTTP authentication. + +#include <config.h> + +#include <http/auth_log.h> + +namespace isc { +namespace http { + +/// @brief Defines the logger used by the HTTP authentication. +isc::log::Logger auth_logger("auth"); + +} // namespace http +} // namespace isc diff --git a/src/lib/http/auth_log.h b/src/lib/http/auth_log.h new file mode 100644 index 0000000..8ebf5c3 --- /dev/null +++ b/src/lib/http/auth_log.h @@ -0,0 +1,23 @@ +// Copyright (C) 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 AUTH_LOG_H +#define AUTH_LOG_H + +#include <log/logger_support.h> +#include <log/macros.h> +#include <http/auth_messages.h> + +namespace isc { +namespace http { + +/// Define the HTTP authentication logger. +extern isc::log::Logger auth_logger; + +} // namespace http +} // namespace isc + +#endif // AUTH_LOG_H diff --git a/src/lib/http/auth_messages.cc b/src/lib/http/auth_messages.cc new file mode 100644 index 0000000..ebf9da5 --- /dev/null +++ b/src/lib/http/auth_messages.cc @@ -0,0 +1,31 @@ +// File created from ../../../src/lib/http/auth_messages.mes + +#include <cstddef> +#include <log/message_types.h> +#include <log/message_initializer.h> + +namespace isc { +namespace http { + +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_AUTHORIZED = "HTTP_CLIENT_REQUEST_AUTHORIZED"; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_BAD_AUTH_HEADER = "HTTP_CLIENT_REQUEST_BAD_AUTH_HEADER"; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_NOT_AUTHORIZED = "HTTP_CLIENT_REQUEST_NOT_AUTHORIZED"; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_NO_AUTH_HEADER = "HTTP_CLIENT_REQUEST_NO_AUTH_HEADER"; + +} // namespace http +} // namespace isc + +namespace { + +const char* values[] = { + "HTTP_CLIENT_REQUEST_AUTHORIZED", "received HTTP request authorized for '%1'", + "HTTP_CLIENT_REQUEST_BAD_AUTH_HEADER", "received HTTP request with malformed authentication header: %1", + "HTTP_CLIENT_REQUEST_NOT_AUTHORIZED", "received HTTP request with not matching authentication header", + "HTTP_CLIENT_REQUEST_NO_AUTH_HEADER", "received HTTP request without required authentication header", + NULL +}; + +const isc::log::MessageInitializer initializer(values); + +} // Anonymous namespace + diff --git a/src/lib/http/auth_messages.h b/src/lib/http/auth_messages.h new file mode 100644 index 0000000..ff02ef5 --- /dev/null +++ b/src/lib/http/auth_messages.h @@ -0,0 +1,19 @@ +// File created from ../../../src/lib/http/auth_messages.mes + +#ifndef AUTH_MESSAGES_H +#define AUTH_MESSAGES_H + +#include <log/message_types.h> + +namespace isc { +namespace http { + +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_AUTHORIZED; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_BAD_AUTH_HEADER; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_NOT_AUTHORIZED; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_NO_AUTH_HEADER; + +} // namespace http +} // namespace isc + +#endif // AUTH_MESSAGES_H diff --git a/src/lib/http/auth_messages.mes b/src/lib/http/auth_messages.mes new file mode 100644 index 0000000..685bdb3 --- /dev/null +++ b/src/lib/http/auth_messages.mes @@ -0,0 +1,24 @@ +# Copyright (C) 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/. + +$NAMESPACE isc::http + +% HTTP_CLIENT_REQUEST_AUTHORIZED received HTTP request authorized for '%1' +This information message is issued when the server receives with a matching +authentication header. The argument provides the user id. + +% HTTP_CLIENT_REQUEST_BAD_AUTH_HEADER received HTTP request with malformed authentication header: %1 +This information message is issued when the server receives a request with +a malformed authentication header. The argument explains the problem. + +% HTTP_CLIENT_REQUEST_NOT_AUTHORIZED received HTTP request with not matching authentication header +This information message is issued when the server receives a request with +authentication header carrying not recognized credential: the user +provided incorrect user id and/or password. + +% HTTP_CLIENT_REQUEST_NO_AUTH_HEADER received HTTP request without required authentication header +This information message is issued when the server receives a request without +a required authentication header. diff --git a/src/lib/http/basic_auth.cc b/src/lib/http/basic_auth.cc new file mode 100644 index 0000000..f3a1d87 --- /dev/null +++ b/src/lib/http/basic_auth.cc @@ -0,0 +1,45 @@ +// 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 <util/encode/base64.h> +#include <util/encode/utf8.h> + +using namespace isc::util::encode; +using namespace std; + +namespace isc { +namespace http { + +BasicHttpAuth::BasicHttpAuth(const std::string& user, + const std::string& password) + : user_(user), password_(password) { + if (user.find(':') != string::npos) { + isc_throw(BadValue, "user '" << user << "' must not contain a ':'"); + } + buildSecret(); + buildCredential(); +} + +BasicHttpAuth::BasicHttpAuth(const std::string& secret) : secret_(secret) { + if (secret.find(':') == string::npos) { + isc_throw(BadValue, "secret '" << secret << "' must contain a ':"); + } + buildCredential(); +} + +void BasicHttpAuth::buildSecret() { + secret_ = user_ + ":" + password_; +} + +void BasicHttpAuth::buildCredential() { + credential_ = encodeBase64(encodeUtf8(secret_)); +} + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/basic_auth.h b/src/lib/http/basic_auth.h new file mode 100644 index 0000000..45301af --- /dev/null +++ b/src/lib/http/basic_auth.h @@ -0,0 +1,87 @@ +// 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/. + +#ifndef BASIC_HTTP_AUTH_H +#define BASIC_HTTP_AUTH_H + +#include <http/header_context.h> +#include <exceptions/exceptions.h> +#include <boost/shared_ptr.hpp> +#include <unordered_set> + +namespace isc { +namespace http { + +/// @brief Represents a basic HTTP authentication. +/// +/// It computes the credential from user and password. +class BasicHttpAuth { +public: + + /// @brief Constructor. + /// + /// @param user User id + /// @param password Password + /// @throw BadValue if user contains the ':' character. + BasicHttpAuth(const std::string& user, const std::string& password); + + /// @brief Constructor. + /// + /// @param secret user:password string + /// @throw BadValue if secret does not contain the ':' character. + BasicHttpAuth(const std::string& secret); + + /// @brief Returns the secret. + const std::string& getSecret() const { + return (secret_); + } + + /// @brief Returns the credential (base64 of the UTF-8 secret). + const std::string& getCredential() const { + return (credential_); + } + +private: + + /// @brief Build the secret from user and password. + void buildSecret(); + + /// @brief Build the credential from the secret. + void buildCredential(); + + /// @brief User id e.g. johndoe. + std::string user_; + + /// @brief Password e.g. secret1. + std::string password_; + + /// @brief Secret e.g. johndoe:secret1. + std::string secret_; + + /// @brief Credential: base64 encoding of UTF-8 secret, + /// e.g. am9obmRvZTpzZWNyZXQx. + std::string credential_; +}; + +/// @brief Type of pointers to basic HTTP authentication objects. +typedef boost::shared_ptr<BasicHttpAuth> BasicHttpAuthPtr; + +/// @brief Represents basic HTTP authentication header. +struct BasicAuthHttpHeaderContext : public HttpHeaderContext { + + /// @brief Constructor. + /// + /// @param basic_auth Basic HTTP authentication object. + explicit BasicAuthHttpHeaderContext(const BasicHttpAuth& basic_auth) + : HttpHeaderContext("Authorization", + "Basic " + basic_auth.getCredential()) { + } +}; + +} // end of namespace isc::http +} // end of namespace isc + +#endif // endif BASIC_HTTP_AUTH_H diff --git a/src/lib/http/basic_auth_config.cc b/src/lib/http/basic_auth_config.cc new file mode 100644 index 0000000..0c98a4a --- /dev/null +++ b/src/lib/http/basic_auth_config.cc @@ -0,0 +1,399 @@ +// 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/auth_log.h> +#include <http/basic_auth_config.h> +#include <util/file_utilities.h> +#include <util/strutil.h> + +using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; +using namespace isc::util; +using namespace std; + +namespace isc { +namespace http { + +BasicHttpAuthClient::BasicHttpAuthClient(const std::string& user, + const std::string& password, + const isc::data::ConstElementPtr& user_context) + : user_(user), user_file_(""), password_(password), + password_file_(""), password_file_only_(false) { + if (user_context) { + setContext(user_context); + } +} + +BasicHttpAuthClient::BasicHttpAuthClient(const std::string& user, + const std::string& user_file, + const std::string& password, + const std::string& password_file, + bool password_file_only, + const isc::data::ConstElementPtr& user_context) + : user_(user), user_file_(user_file), password_(password), + password_file_(password_file), password_file_only_(password_file_only) { + if (user_context) { + setContext(user_context); + } +} + +ElementPtr +BasicHttpAuthClient::toElement() const { + ElementPtr result = Element::createMap(); + + // Set user-context + contextToElement(result); + + // Set password file or password. + if (!password_file_.empty()) { + result->set("password-file", Element::create(password_file_)); + } else { + result->set("password", Element::create(password_)); + } + + // Set user-file or user. + if (!password_file_only_) { + if (!user_file_.empty()) { + result->set("user-file", Element::create(user_file_)); + } else { + result->set("user", Element::create(user_)); + } + } + + return (result); +} + +void +BasicHttpAuthConfig::add(const std::string& user, + const std::string& user_file, + const std::string& password, + const std::string& password_file, + bool password_file_only, + const ConstElementPtr& user_context) { + BasicHttpAuth basic_auth(user, password); + list_.push_back(BasicHttpAuthClient(user, user_file, password, + password_file, password_file_only, + user_context)); + map_[basic_auth.getCredential()] = user; +} + +void +BasicHttpAuthConfig::clear() { + list_.clear(); + map_.clear(); +} + +bool +BasicHttpAuthConfig::empty() const { + return (map_.empty()); +} + +string +BasicHttpAuthConfig::getFileContent(const std::string& file_name) const { + // Build path. + string path = getDirectory(); + // Add a trailing '/' if the last character is not already a '/'. + if (path.empty() || (path[path.size() - 1] != '/')) { + path += "/"; + } + // Don't add a second '/'. + if (file_name.empty() || (file_name[0] != '/')) { + path += file_name; + } else { + path += file_name.substr(1); + } + + try { + return (file::getContent(path)); + } catch (const isc::BadValue& ex) { + isc_throw(DhcpConfigError, ex.what()); + } +} + +ElementPtr +BasicHttpAuthConfig::toElement() const { + ElementPtr result = Element::createMap(); + + // Set user-context + contextToElement(result); + + // Set type + result->set("type", Element::create(string("basic"))); + + // Set realm + result->set("realm", Element::create(getRealm())); + + // Set directory. + result->set("directory", Element::create(getDirectory())); + + // Set clients + ElementPtr clients = Element::createList(); + for (auto client : list_) { + clients->add(client.toElement()); + } + result->set("clients", clients); + + return (result); +} + +void +BasicHttpAuthConfig::parse(const ConstElementPtr& config) { + if (!config) { + return; + } + if (config->getType() != Element::map) { + isc_throw(DhcpConfigError, "authentication must be a map (" + << config->getPosition() << ")"); + } + + // Get and verify the type. + ConstElementPtr type = config->get("type"); + if (!type) { + isc_throw(DhcpConfigError, "type is required in authentication (" + << config->getPosition() << ")"); + } + if (type->getType() != Element::string) { + isc_throw(DhcpConfigError, "type must be a string (" + << type->getPosition() << ")"); + } + if (type->stringValue() != "basic") { + isc_throw(DhcpConfigError, "only basic HTTP authentication is " + << "supported: type is '" << type->stringValue() + << "' not 'basic' (" << type->getPosition() << ")"); + } + + // Get the realm. + ConstElementPtr realm = config->get("realm"); + if (realm) { + if (realm->getType() != Element::string) { + isc_throw(DhcpConfigError, "realm must be a string (" + << realm->getPosition() << ")"); + } + setRealm(realm->stringValue()); + } + + // Get the directory. + ConstElementPtr directory = config->get("directory"); + if (directory) { + if (directory->getType() != Element::string) { + isc_throw(DhcpConfigError, "directory must be a string (" + << directory->getPosition() << ")"); + } + setDirectory(directory->stringValue()); + } + + // Get user context. + ConstElementPtr user_context_cfg = config->get("user-context"); + if (user_context_cfg) { + if (user_context_cfg->getType() != Element::map) { + isc_throw(DhcpConfigError, "user-context must be a map (" + << user_context_cfg->getPosition() << ")"); + } + setContext(user_context_cfg); + } + + // Get clients. + ConstElementPtr clients = config->get("clients"); + if (!clients) { + return; + } + if (clients->getType() != Element::list) { + isc_throw(DhcpConfigError, "clients must be a list (" + << clients->getPosition() << ")"); + } + + // Iterate on clients. + for (auto client : clients->listValue()) { + if (client->getType() != Element::map) { + isc_throw(DhcpConfigError, "clients items must be maps (" + << client->getPosition() << ")"); + } + + // password. + string password; + ConstElementPtr password_cfg = client->get("password"); + if (password_cfg) { + if (password_cfg->getType() != Element::string) { + isc_throw(DhcpConfigError, "password must be a string (" + << password_cfg->getPosition() << ")"); + } + password = password_cfg->stringValue(); + } + + // password file. + string password_file; + ConstElementPtr password_file_cfg = client->get("password-file"); + if (password_file_cfg) { + if (password_cfg) { + isc_throw(DhcpConfigError, "password (" + << password_cfg->getPosition() + << ") and password-file (" + << password_file_cfg->getPosition() + << ") are mutually exclusive"); + } + if (password_file_cfg->getType() != Element::string) { + isc_throw(DhcpConfigError, "password-file must be a string (" + << password_file_cfg->getPosition() << ")"); + } + password_file = password_file_cfg->stringValue(); + } + + ConstElementPtr user_cfg = client->get("user"); + ConstElementPtr user_file_cfg = client->get("user-file"); + bool password_file_only = false; + if (!user_cfg && !user_file_cfg) { + if (password_file_cfg) { + password_file_only = true; + } else { + isc_throw(DhcpConfigError, "user is required in clients " + << "items (" << client->getPosition() << ")"); + } + } + + // user. + string user; + if (user_cfg) { + if (user_file_cfg) { + isc_throw(DhcpConfigError, "user (" << user_cfg->getPosition() + << ") and user-file (" + << user_file_cfg->getPosition() + << ") are mutually exclusive"); + } + if (user_cfg->getType() != Element::string) { + isc_throw(DhcpConfigError, "user must be a string (" + << user_cfg->getPosition() << ")"); + } + user = user_cfg->stringValue(); + if (user.empty()) { + isc_throw(DhcpConfigError, "user must not be empty (" + << user_cfg->getPosition() << ")"); + } + if (user.find(':') != string::npos) { + isc_throw(DhcpConfigError, "user must not contain a ':': '" + << user << "' (" << user_cfg->getPosition() << ")"); + } + } + + // user file. + string user_file; + if (user_file_cfg) { + if (user_file_cfg->getType() != Element::string) { + isc_throw(DhcpConfigError, "user-file must be a string (" + << user_file_cfg->getPosition() << ")"); + } + user_file = user_file_cfg->stringValue(); + user = getFileContent(user_file); + if (user.empty()) { + isc_throw(DhcpConfigError, "user must not be empty " + << "from user-file '" << user_file << "' (" + << user_file_cfg->getPosition() << ")"); + } + if (user.find(':') != string::npos) { + isc_throw(DhcpConfigError, "user must not contain a ':' " + << "from user-file '" << user_file << "' (" + << user_file_cfg->getPosition() << ")"); + } + } + + // Solve password file. + if (password_file_cfg) { + if (password_file_only) { + string content = getFileContent(password_file); + auto pos = content.find(':'); + if (pos == string::npos) { + isc_throw(DhcpConfigError, "can't find the user id part " + << "in password-file '" << password_file << "' (" + << password_file_cfg->getPosition() << ")"); + } + user = content.substr(0, pos); + password = content.substr(pos + 1); + } else { + password = getFileContent(password_file); + } + } + + // user context. + ConstElementPtr user_context = client->get("user-context"); + if (user_context) { + if (user_context->getType() != Element::map) { + isc_throw(DhcpConfigError, "user-context must be a map (" + << user_context->getPosition() << ")"); + } + } + + // add it. + try { + add(user, user_file, password, password_file, password_file_only, + user_context); + } catch (const std::exception& ex) { + isc_throw(DhcpConfigError, ex.what() << " (" + << client->getPosition() << ")"); + } + } +} + +HttpResponseJsonPtr +BasicHttpAuthConfig::checkAuth(const HttpResponseCreator& creator, + const HttpRequestPtr& request) const { + const BasicHttpAuthMap& credentials = getCredentialMap(); + bool authentic = false; + if (credentials.empty()) { + authentic = true; + } else try { + string value = request->getHeaderValue("Authorization"); + // Trim space characters. + value = str::trim(value); + if (value.size() < 8) { + isc_throw(BadValue, "header content is too short"); + } + // Get the authentication scheme which must be "basic". + string scheme = value.substr(0, 5); + str::lowercase(scheme); + if (scheme != "basic") { + isc_throw(BadValue, "not basic authentication"); + } + // Skip the authentication scheme name and space characters. + value = value.substr(5); + value = str::trim(value); + // Verify the credential is in the list. + const auto it = credentials.find(value); + if (it != credentials.end()) { + LOG_INFO(auth_logger, HTTP_CLIENT_REQUEST_AUTHORIZED) + .arg(it->second); + if (HttpRequest::recordBasicAuth_) { + request->setBasicAuth(it->second); + } + authentic = true; + } else { + LOG_INFO(auth_logger, HTTP_CLIENT_REQUEST_NOT_AUTHORIZED); + authentic = false; + } + } catch (const HttpMessageNonExistingHeader&) { + LOG_INFO(auth_logger, HTTP_CLIENT_REQUEST_NO_AUTH_HEADER); + } catch (const BadValue& ex) { + LOG_INFO(auth_logger, HTTP_CLIENT_REQUEST_BAD_AUTH_HEADER) + .arg(ex.what()); + } + if (authentic) { + return (HttpResponseJsonPtr()); + } + const string& realm = getRealm(); + const string& scheme = "Basic"; + HttpResponsePtr response = + creator.createStockHttpResponse(request, HttpStatusCode::UNAUTHORIZED); + response->reset(); + response->context()->headers_.push_back( + HttpHeaderContext("WWW-Authenticate", + scheme + " realm=\"" + realm + "\"")); + response->finalize(); + return (boost::dynamic_pointer_cast<HttpResponseJson>(response)); +} + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/basic_auth_config.h b/src/lib/http/basic_auth_config.h new file mode 100644 index 0000000..413dd22 --- /dev/null +++ b/src/lib/http/basic_auth_config.h @@ -0,0 +1,204 @@ +// 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/. + +#ifndef HTTP_BASIC_AUTH_CONFIG_H +#define HTTP_BASIC_AUTH_CONFIG_H + +#include <http/auth_config.h> +#include <http/basic_auth.h> +#include <list> +#include <unordered_map> + +namespace isc { +namespace http { + +/// @brief Type of basic HTTP authentication credential and user id map, +/// e.g. map["am9obmRvZTpzZWNyZXQx"] = "johndoe". +/// +/// The map is used to verify a received credential: if it is not in it +/// the authentication fails, if it is in it the user id is logged. +typedef std::unordered_map<std::string, std::string> BasicHttpAuthMap; + +/// @brief Basic HTTP authentication client configuration. +class BasicHttpAuthClient : public isc::data::UserContext, + public isc::data::CfgToElement { +public: + + /// @brief Constructor (legacy). + /// + /// @param user User id. + /// @param password Password. + /// @param user_context Optional user context. + BasicHttpAuthClient(const std::string& user, + const std::string& password, + const isc::data::ConstElementPtr& user_context); + + /// @brief Constructor. + /// + /// @param user User id. + /// @param user_file File with the user id. + /// @param password Password. + /// @param password_file File with the password. + /// @param password_file_only Flag true if the password file includes + /// the user id too. + /// @param user_context Optional user context. + BasicHttpAuthClient(const std::string& user, + const std::string& user_file, + const std::string& password, + const std::string& password_file, + bool password_file_only, + const isc::data::ConstElementPtr& user_context); + + /// @brief Returns the user id. + /// + /// @return The user id. + const std::string& getUser() const { + return (user_); + } + + /// @brief Returns the user id file. + /// + /// @return The user id file. + const std::string& getUserFile() const { + return (user_file_); + } + + /// @brief Returns the password. + /// + /// @return The password. + const std::string& getPassword() const { + return (password_); + } + + /// @brief Returns the password file. + /// + /// @return The password file. + const std::string& getPasswordFile() const { + return (password_file_); + } + + /// @brief Returns the password file only flag. + /// + /// @return The password file only flag. + bool getPasswordFileOnly() const { + return (password_file_only_); + } + + /// @brief Unparses basic HTTP authentication client configuration. + /// + /// @return A pointer to unparsed client configuration. + virtual isc::data::ElementPtr toElement() const; + +private: + + /// @brief The user id e.g. johndoe. + std::string user_; + + /// @brief The user id file. + std::string user_file_; + + /// @brief The password e.g. secret1. + std::string password_; + + /// @brief The password file. + std::string password_file_; + + /// @brief The password file only flag. + bool password_file_only_; +}; + +/// @brief Type of basic HTTP authentication client configuration list. +typedef std::list<BasicHttpAuthClient> BasicHttpAuthClientList; + +/// @brief Basic HTTP authentication configuration. +class BasicHttpAuthConfig : public HttpAuthConfig { +public: + /// @brief Destructor. + virtual ~BasicHttpAuthConfig() { } + + /// @brief Add a client configuration. + /// + /// @param user User id. + /// @param user_file File with the user id. + /// @param password Password. + /// @param password_file File with the password. + /// @param password_file_only Flag true if the password file includes + /// the user id too. + /// @param user_context Optional user context. + /// @throw BadValue if the user id contains the ':' character. + void add(const std::string& user, + const std::string& user_file, + const std::string& password, + const std::string& password_file, + bool password_file_only = false, + const isc::data::ConstElementPtr& user_context = isc::data::ConstElementPtr()); + + /// @brief Empty predicate. + /// + /// @return true if the configuration is empty so authentication + /// is not required. + virtual bool empty() const; + + /// @brief Clear configuration. + virtual void clear(); + + /// @brief Get the content of {directory}/{file-name} regular file. + /// + /// @param file_name The file name. + /// @return The content of the {directory}/{file-name} regular file. + std::string getFileContent(const std::string& file_name) const; + + /// @brief Returns the list of client configuration. + /// + /// @return List of basic HTTP authentication client configuration. + const BasicHttpAuthClientList& getClientList() const { + return (list_); + } + + /// @brief Returns the credential and user id map. + /// + /// @return The basic HTTP authentication credential and user id map. + const BasicHttpAuthMap& getCredentialMap() const { + return (map_); + } + + /// @brief Parses basic HTTP authentication configuration. + /// + /// @param config Element holding the basic HTTP authentication + /// configuration to be parsed. + /// @throw DhcpConfigError when the configuration is invalid. + void parse(const isc::data::ConstElementPtr& config); + + /// @brief Unparses basic HTTP authentication configuration. + /// + /// @return A pointer to unparsed basic HTTP authentication configuration. + virtual isc::data::ElementPtr toElement() const; + + /// @brief Validate HTTP request. + /// + /// @param creator The HTTP response creator. + /// @param request The HTTP request to validate. + /// @return Error HTTP response if validation failed, null otherwise. + virtual isc::http::HttpResponseJsonPtr + checkAuth(const isc::http::HttpResponseCreator& creator, + const isc::http::HttpRequestPtr& request) const; + +private: + + /// @brief The list of basic HTTP authentication client configuration. + BasicHttpAuthClientList list_; + + /// @brief The basic HTTP authentication credential and user id map. + BasicHttpAuthMap map_; +}; + +/// @brief Type of shared pointers to basic HTTP authentication configuration. +typedef boost::shared_ptr<BasicHttpAuthConfig> BasicHttpAuthConfigPtr; + +} // end of namespace isc::http +} // end of namespace isc + +#endif // endif HTTP_BASIC_AUTH_CONFIG_H diff --git a/src/lib/http/client.cc b/src/lib/http/client.cc new file mode 100644 index 0000000..1f139a3 --- /dev/null +++ b/src/lib/http/client.cc @@ -0,0 +1,2062 @@ +// Copyright (C) 2018-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/io_service_thread_pool.h> +#include <asiolink/tls_socket.h> +#include <http/client.h> +#include <http/http_log.h> +#include <http/http_messages.h> +#include <http/response_json.h> +#include <http/response_parser.h> +#include <util/boost_time_utils.h> +#include <util/multi_threading_mgr.h> +#include <util/unlock_guard.h> + +#include <boost/enable_shared_from_this.hpp> +#include <boost/weak_ptr.hpp> + +#include <atomic> +#include <array> +#include <functional> +#include <iostream> +#include <map> +#include <mutex> +#include <queue> +#include <thread> + + +using namespace isc; +using namespace isc::asiolink; +using namespace isc::http; +using namespace isc::util; +using namespace boost::posix_time; + +namespace ph = std::placeholders; + +namespace { + +/// @brief Maximum size of the HTTP message that can be logged. +/// +/// The part of the HTTP message beyond this value is truncated. +constexpr size_t MAX_LOGGED_MESSAGE_SIZE = 1024; + +/// @brief TCP / TLS socket callback function type. +typedef std::function<void(boost::system::error_code ec, size_t length)> +SocketCallbackFunction; + +/// @brief Socket callback class required by the TCPSocket and TLSSocket APIs. +/// +/// Its function call operator ignores callbacks invoked with "operation aborted" +/// error codes. Such status codes are generated when the posted IO operations +/// are canceled. +class SocketCallback { +public: + + /// @brief Constructor. + /// + /// Stores pointer to a callback function provided by a caller. + /// + /// @param socket_callback Pointer to a callback function. + SocketCallback(SocketCallbackFunction socket_callback) + : callback_(socket_callback) { + } + + /// @brief Function call operator. + /// + /// Invokes the callback for all error codes except "operation aborted". + /// + /// @param ec Error code. + /// @param length Length of the data transmitted over the socket. + void operator()(boost::system::error_code ec, size_t length = 0) { + if (ec.value() == boost::asio::error::operation_aborted) { + return; + } + callback_(ec, length); + } + +private: + + /// @brief Holds pointer to a supplied callback. + SocketCallbackFunction callback_; + +}; + +class ConnectionPool; + +/// @brief Shared pointer to a connection pool. +typedef boost::shared_ptr<ConnectionPool> ConnectionPoolPtr; + +/// @brief Client side HTTP connection to the server. +/// +/// Each connection is established with a unique destination identified by the +/// specified URL and TLS context. Multiple requests to the same destination +/// can be sent over the same connection, if the connection is persistent. +/// If the server closes the TCP connection (e.g. after sending a response), +/// the connection is closed. +/// +/// If new request is created while the previous request is still in progress, +/// the new request is stored in the FIFO queue. The queued requests to the +/// particular URL are sent to the server when the current transaction ends. +/// +/// The communication over the transport socket is asynchronous. The caller is +/// notified about the completion of the transaction via a callback that the +/// caller supplies when initiating the transaction. +class Connection : public boost::enable_shared_from_this<Connection> { +public: + + /// @brief Constructor. + /// + /// @param io_service IO service to be used for the connection. + /// @param tls_context TLS context to be used for the connection. + /// @param conn_pool Back pointer to the connection pool to which this + /// connection belongs. + /// @param url URL associated with this connection. + explicit Connection(IOService& io_service, + const TlsContextPtr& tls_context, + const ConnectionPoolPtr& conn_pool, + const Url& url); + + /// @brief Destructor. + ~Connection(); + + /// @brief Starts new asynchronous transaction (HTTP request and response). + /// + /// This method expects that all pointers provided as argument are non-null. + /// + /// @param request Pointer to the request to be sent to the server. + /// @param response Pointer to the object into which the response is stored. + /// The caller should create a response object of the type which matches the + /// content type expected by the caller, e.g. HttpResponseJson when JSON + /// content type is expected to be received. + /// @param request_timeout Request timeout in milliseconds. + /// @param callback Pointer to the callback function to be invoked when the + /// transaction completes. + /// @param connect_callback Pointer to the callback function to be invoked + /// when the client connects to the server. + /// @param handshake_callback Optional callback invoked when the client + /// performs the TLS handshake with the server. + /// @param close_callback Pointer to the callback function to be invoked + /// when the client closes the socket to the server. + void doTransaction(const HttpRequestPtr& request, + const HttpResponsePtr& response, + const long request_timeout, + const HttpClient::RequestHandler& callback, + const HttpClient::ConnectHandler& connect_callback, + const HttpClient::HandshakeHandler& handshake_callback, + const HttpClient::CloseHandler& close_callback); + + /// @brief Closes the socket and cancels the request timer. + void close(); + + /// @brief Checks if a transaction has been initiated over this connection. + /// + /// @return true if transaction has been initiated, false otherwise. + bool isTransactionOngoing() const { + return (started_); + } + + /// @brief Checks if the socket has been closed. + /// + /// @return true if the socket has been closed. + bool isClosed() const { + return (closed_); + } + + /// @brief Checks if the peer has closed the idle socket at its side. + /// + /// If the socket is open but is not usable the peer has closed + /// the socket at its side so we close it. + void isClosedByPeer(); + + /// @brief Checks if a socket descriptor belongs to this connection. + /// + /// @param socket_fd socket descriptor to check + /// + /// @return True if the socket fd belongs to this connection. + bool isMySocket(int socket_fd) const; + + /// @brief Checks and logs if premature transaction timeout is suspected. + /// + /// There are cases when the premature timeout occurs, e.g. as a result of + /// moving system clock, during the transaction. In such case, the + /// @c terminate function is called which resets the transaction state but + /// the transaction handlers may be already waiting for the execution. + /// Each such handler should call this function to check if the transaction + /// it is participating in is still alive. If it is not, it should simply + /// return. This method also logs such situation. + /// + /// @param transid identifier of the transaction for which the handler + /// is being invoked. It is compared against the current transaction + /// id for this connection. + /// + /// @return true if the premature timeout is suspected, false otherwise. + bool checkPrematureTimeout(const uint64_t transid); + +private: + + /// @brief Starts new asynchronous transaction (HTTP request and response). + /// + /// Should be called in a thread safe context. + /// + /// This method expects that all pointers provided as argument are non-null. + /// + /// @param request Pointer to the request to be sent to the server. + /// @param response Pointer to the object into which the response is stored. + /// The caller should create a response object of the type which matches the + /// content type expected by the caller, e.g. HttpResponseJson when JSON + /// content type is expected to be received. + /// @param request_timeout Request timeout in milliseconds. + /// @param callback Pointer to the callback function to be invoked when the + /// transaction completes. + /// @param connect_callback Pointer to the callback function to be invoked + /// when the client connects to the server. + /// @param handshake_callback Optional callback invoked when the client + /// performs the TLS handshake with the server. + /// @param close_callback Pointer to the callback function to be invoked + /// when the client closes the socket to the server. + void doTransactionInternal(const HttpRequestPtr& request, + const HttpResponsePtr& response, + const long request_timeout, + const HttpClient::RequestHandler& callback, + const HttpClient::ConnectHandler& connect_callback, + const HttpClient::HandshakeHandler& handshake_callback, + const HttpClient::CloseHandler& close_callback); + + /// @brief Closes the socket and cancels the request timer. + /// + /// Should be called in a thread safe context. + void closeInternal(); + + /// @brief Checks if the peer has closed the socket at its side. + /// + /// Should be called in a thread safe context. + /// + /// If the socket is open but is not usable the peer has closed + /// the socket at its side so we close it. + void isClosedByPeerInternal(); + + /// @brief Checks and logs if premature transaction timeout is suspected. + /// + /// Should be called in a thread safe context. + /// + /// There are cases when the premature timeout occurs, e.g. as a result of + /// moving system clock, during the transaction. In such case, the + /// @c terminate function is called which resets the transaction state but + /// the transaction handlers may be already waiting for the execution. + /// Each such handler should call this function to check if the transaction + /// it is participating in is still alive. If it is not, it should simply + /// return. This method also logs such situation. + /// + /// @param transid identifier of the transaction for which the handler + /// is being invoked. It is compared against the current transaction + /// id for this connection. + /// + /// @return true if the premature timeout is suspected, false otherwise. + bool checkPrematureTimeoutInternal(const uint64_t transid); + + /// @brief Resets the state of the object. + /// + /// Should be called in a thread safe context. + /// + /// In particular, it removes instances of objects provided for the previous + /// transaction by a caller. It doesn't close the socket, though. + void resetState(); + + /// @brief Performs tasks required after receiving a response or after an + /// error. + /// + /// This method triggers user's callback, resets the state of the connection + /// and initiates next transaction if there is any transaction queued for the + /// URL associated with this connection. + /// + /// @param ec Error code received as a result of the IO operation. + /// @param parsing_error Message parsing error. + void terminate(const boost::system::error_code& ec, + const std::string& parsing_error = ""); + + /// @brief Performs tasks required after receiving a response or after an + /// error. + /// + /// Should be called in a thread safe context. + /// + /// This method triggers user's callback, resets the state of the connection + /// and initiates next transaction if there is any transaction queued for the + /// URL associated with this connection. + /// + /// @param ec Error code received as a result of the IO operation. + /// @param parsing_error Message parsing error. + void terminateInternal(const boost::system::error_code& ec, + const std::string& parsing_error = ""); + + /// @brief Run parser and check if more data is needed. + /// + /// @param ec Error code received as a result of the IO operation. + /// @param length Number of bytes received. + /// + /// @return true if more data is needed, false otherwise. + bool runParser(const boost::system::error_code& ec, size_t length); + + /// @brief Run parser and check if more data is needed. + /// + /// Should be called in a thread safe context. + /// + /// @param ec Error code received as a result of the IO operation. + /// @param length Number of bytes received. + /// + /// @return true if more data is needed, false otherwise. + bool runParserInternal(const boost::system::error_code& ec, size_t length); + + /// @brief This method schedules timer or reschedules existing timer. + /// + /// @param request_timeout New timer interval in milliseconds. + void scheduleTimer(const long request_timeout); + + /// @brief Asynchronously performs the TLS handshake. + /// + /// The TLS handshake is performed once on TLS sockets. + /// + /// @param transid Current transaction id. + void doHandshake(const uint64_t transid); + + /// @brief Asynchronously sends data over the socket. + /// + /// The data sent over the socket are stored in the @c buf_. + /// + /// @param transid Current transaction id. + void doSend(const uint64_t transid); + + /// @brief Asynchronously receives data over the socket. + /// + /// The data received over the socket are store into the @c input_buf_. + /// + /// @param transid Current transaction id. + void doReceive(const uint64_t transid); + + /// @brief Local callback invoked when the connection is established. + /// + /// If the connection is successfully established, this callback will start + /// to asynchronously send the request over the socket or perform the + /// TLS handshake with the server. + /// + /// @param Pointer to the callback to be invoked when client connects to + /// the server. + /// @param transid Current transaction id. + /// @param ec Error code being a result of the connection attempt. + void connectCallback(HttpClient::ConnectHandler connect_callback, + const uint64_t transid, + const boost::system::error_code& ec); + + /// @brief Local callback invoked when the handshake is performed. + /// + /// If the handshake is successfully performed, this callback will start + /// to asynchronously send the request over the socket. + /// + /// @param Pointer to the callback to be invoked when client performs + /// the TLS handshake with the server. + /// @param transid Current transaction id. + /// @param ec Error code being a result of the connection attempt. + void handshakeCallback(HttpClient::HandshakeHandler handshake_callback, + const uint64_t transid, + const boost::system::error_code& ec); + + /// @brief Local callback invoked when an attempt to send a portion of data + /// over the socket has ended. + /// + /// The portion of data that has been sent is removed from the buffer. If all + /// data from the buffer were sent, the callback will start to asynchronously + /// receive a response from the server. + /// + /// @param transid Current transaction id. + /// @param ec Error code being a result of sending the data. + /// @param length Number of bytes sent. + void sendCallback(const uint64_t transid, const boost::system::error_code& ec, + size_t length); + + /// @brief Local callback invoked when an attempt to receive a portion of data + /// over the socket has ended. + /// + /// @param transid Current transaction id. + /// @param ec Error code being a result of receiving the data. + /// @param length Number of bytes received. + void receiveCallback(const uint64_t transid, const boost::system::error_code& ec, + size_t length); + + /// @brief Local callback invoked when request timeout occurs. + void timerCallback(); + + /// @brief Local callback invoked when the connection is closed. + /// + /// Invokes the close callback (if one), passing in the socket's + /// descriptor, when the connection's socket about to be closed. + /// The callback invocation is wrapped in a try-catch to ensure + /// exception safety. + /// + /// @param clear dictates whether or not the callback is discarded + /// after invocation. Defaults to false. + void closeCallback(const bool clear = false); + + /// @brief Pointer to the connection pool owning this connection. + /// + /// This is a weak pointer to avoid circular dependency between the + /// Connection and ConnectionPool. + boost::weak_ptr<ConnectionPool> conn_pool_; + + /// @brief URL for this connection. + Url url_; + + /// @brief TLS context for this connection. + TlsContextPtr tls_context_; + + /// @brief TCP socket to be used for this connection. + std::unique_ptr<TCPSocket<SocketCallback> > tcp_socket_; + + /// @brief TLS socket to be used for this connection. + std::unique_ptr<TLSSocket<SocketCallback> > tls_socket_; + + /// @brief Interval timer used for detecting request timeouts. + IntervalTimer timer_; + + /// @brief Holds currently sent request. + HttpRequestPtr current_request_; + + /// @brief Holds pointer to an object where response is to be stored. + HttpResponsePtr current_response_; + + /// @brief Pointer to the HTTP response parser. + HttpResponseParserPtr parser_; + + /// @brief User supplied callback. + HttpClient::RequestHandler current_callback_; + + /// @brief Output buffer. + std::string buf_; + + /// @brief Input buffer. + std::array<char, 32768> input_buf_; + + /// @brief Identifier of the current transaction. + uint64_t current_transid_; + + /// @brief User supplied handshake callback. + HttpClient::HandshakeHandler handshake_callback_; + + /// @brief User supplied close callback. + HttpClient::CloseHandler close_callback_; + + /// @brief Flag to indicate that a transaction is running. + std::atomic<bool> started_; + + /// @brief Flag to indicate that the TLS handshake has to be performed. + std::atomic<bool> need_handshake_; + + /// @brief Flag to indicate that the socket was closed. + std::atomic<bool> closed_; + + /// @brief Mutex to protect the internal state. + std::mutex mutex_; +}; + +/// @brief Shared pointer to the connection. +typedef boost::shared_ptr<Connection> ConnectionPtr; + +/// @brief Connection pool for managing multiple connections. +/// +/// Connection pool creates and destroys URL destinations. It manages +/// connections to and requests for URLs. Each time a request is +/// submitted for a URL, it assigns it to an available idle connection, +/// or if no idle connections are available, pushes the request on the queue +/// for that URL. +class ConnectionPool : public boost::enable_shared_from_this<ConnectionPool> { +public: + + /// @brief Constructor. + /// + /// @param io_service Reference to the IO service to be used by the + /// connections. + /// @param max_url_connections maximum number of concurrent + /// connections allowed per URL. + explicit ConnectionPool(IOService& io_service, size_t max_url_connections) + : io_service_(io_service), destinations_(), pool_mutex_(), + max_url_connections_(max_url_connections) { + } + + /// @brief Destructor. + /// + /// Closes all connections. + ~ConnectionPool() { + closeAll(); + } + + /// @brief Process next queued request for the given URL and TLS context. + /// + /// @param url URL for which next queued request should be processed. + /// @param tls_context TLS context for which next queued request + /// should be processed. + void processNextRequest(const Url& url, const TlsContextPtr& tls_context) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(pool_mutex_); + return (processNextRequestInternal(url, tls_context)); + } else { + return (processNextRequestInternal(url, tls_context)); + } + } + + /// @brief Schedule processing of next queued request. + /// + /// @param url URL for which next queued request should be processed. + /// @param tls_context TLS context for which next queued request + /// should be processed. + void postProcessNextRequest(const Url& url, + const TlsContextPtr& tls_context) { + io_service_.post(std::bind(&ConnectionPool::processNextRequest, + shared_from_this(), url, tls_context)); + } + + /// @brief Queue next request for sending to the server. + /// + /// A new transaction is started immediately, if there is no other request + /// in progress for the given URL. Otherwise, the request is queued. + /// + /// @param url Destination where the request should be sent. + /// @param tls_context TLS context to be used for the connection. + /// @param request Pointer to the request to be sent to the server. + /// @param response Pointer to the object into which the response should be + /// stored. + /// @param request_timeout Requested timeout for the transaction in + /// milliseconds. + /// @param request_callback Pointer to the user callback to be invoked when the + /// transaction ends. + /// @param connect_callback Pointer to the user callback to be invoked when the + /// client connects to the server. + /// @param handshake_callback Optional callback invoked when the client + /// performs the TLS handshake with the server. + /// @param close_callback Pointer to the user callback to be invoked when the + /// client closes the connection to the server. + void queueRequest(const Url& url, + const TlsContextPtr& tls_context, + const HttpRequestPtr& request, + const HttpResponsePtr& response, + const long request_timeout, + const HttpClient::RequestHandler& request_callback, + const HttpClient::ConnectHandler& connect_callback, + const HttpClient::HandshakeHandler& handshake_callback, + const HttpClient::CloseHandler& close_callback) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(pool_mutex_); + return (queueRequestInternal(url, tls_context, request, response, + request_timeout, request_callback, + connect_callback, handshake_callback, + close_callback)); + } else { + return (queueRequestInternal(url, tls_context, request, response, + request_timeout, request_callback, + connect_callback, handshake_callback, + close_callback)); + } + } + + /// @brief Closes all URLs and removes associated information from + /// the connection pool. + void closeAll() { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(pool_mutex_); + closeAllInternal(); + } else { + closeAllInternal(); + } + } + + /// @brief Closes a connection if it has an out-of-band socket event + /// + /// If the pool contains a connection using the given socket and that + /// connection is currently in a transaction the method returns as this + /// indicates a normal ready event. If the connection is not in an + /// ongoing transaction, then the connection is closed. + /// + /// This is method is intended to be used to detect and clean up then + /// sockets that are marked ready outside of transactions. The most common + /// case is the other end of the socket being closed. + /// + /// @param socket_fd socket descriptor to check + void closeIfOutOfBand(int socket_fd) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(pool_mutex_); + closeIfOutOfBandInternal(socket_fd); + } else { + closeIfOutOfBandInternal(socket_fd); + } + } + +private: + + /// @brief Process next queued request for the given URL and TLS context. + /// + /// This method should be called in a thread safe context. + /// + /// @param url URL for which next queued request should be retrieved. + /// @param tls_context TLS context for which next queued request + /// should be processed. + void processNextRequestInternal(const Url& url, + const TlsContextPtr& tls_context) { + // Check if there is a queue for this URL. If there is no queue, there + // is no request queued either. + DestinationPtr destination = findDestination(url, tls_context); + if (destination) { + // Remove closed connections. + destination->garbageCollectConnections(); + if (!destination->queueEmpty()) { + // We have at least one queued request. Do we have an + // idle connection? + ConnectionPtr connection = destination->getIdleConnection(); + if (!connection) { + // No idle connections. + if (destination->connectionsFull()) { + return; + } + // Room to make another connection with this destination, + // so make one. + connection.reset(new Connection(io_service_, tls_context, + shared_from_this(), url)); + destination->addConnection(connection); + } + + // Dequeue the oldest request and start a transaction for it using + // the idle connection. + RequestDescriptor desc = destination->popNextRequest(); + connection->doTransaction(desc.request_, desc.response_, + desc.request_timeout_, desc.callback_, + desc.connect_callback_, + desc.handshake_callback_, + desc.close_callback_); + } + } + } + + /// @brief Queue next request for sending to the server. + /// + /// A new transaction is started immediately, if there is no other request + /// in progress for the given URL. Otherwise, the request is queued. + /// + /// This method should be called in a thread safe context. + /// + /// @param url Destination where the request should be sent. + /// @param tls_context TLS context to be used for the connection. + /// @param request Pointer to the request to be sent to the server. + /// @param response Pointer to the object into which the response should be + /// stored. + /// @param request_timeout Requested timeout for the transaction in + /// milliseconds. + /// @param request_callback Pointer to the user callback to be invoked when the + /// transaction ends. + /// @param connect_callback Pointer to the user callback to be invoked when the + /// client connects to the server. + /// @param handshake_callback Optional callback invoked when the client + /// performs the TLS handshake with the server. + /// @param close_callback Pointer to the user callback to be invoked when the + /// client closes the connection to the server. + void queueRequestInternal(const Url& url, + const TlsContextPtr& tls_context, + const HttpRequestPtr& request, + const HttpResponsePtr& response, + const long request_timeout, + const HttpClient::RequestHandler& request_callback, + const HttpClient::ConnectHandler& connect_callback, + const HttpClient::HandshakeHandler& handshake_callback, + const HttpClient::CloseHandler& close_callback) { + ConnectionPtr connection; + // Find the destination for the requested URL. + DestinationPtr destination = findDestination(url, tls_context); + if (destination) { + // Remove closed connections. + destination->garbageCollectConnections(); + // Found it, look for an idle connection. + connection = destination->getIdleConnection(); + } else { + // Doesn't exist yet so it's a new destination. + destination = addDestination(url, tls_context); + } + + if (!connection) { + if (destination->connectionsFull()) { + // All connections busy, queue it. + destination->pushRequest(RequestDescriptor(request, response, + request_timeout, + request_callback, + connect_callback, + handshake_callback, + close_callback)); + return; + } + + // Room to make another connection with this destination, so make one. + connection.reset(new Connection(io_service_, tls_context, + shared_from_this(), url)); + destination->addConnection(connection); + } + + // Use the connection to start the transaction. + connection->doTransaction(request, response, request_timeout, request_callback, + connect_callback, handshake_callback, close_callback); + } + + /// @brief Closes all connections for all URLs and removes associated + /// information from the connection pool. + /// + /// This method should be called in a thread safe context. + void closeAllInternal() { + for (auto const& destination : destinations_) { + destination.second->closeAllConnections(); + } + + destinations_.clear(); + } + + /// @brief Closes a connection if it has an out-of-band socket event + /// + /// If the pool contains a connection using the given socket and that + /// connection is currently in a transaction the method returns as this + /// indicates a normal ready event. If the connection is not in an + /// ongoing transaction, then the connection is closed. + /// + /// This is method is intended to be used to detect and clean up then + /// sockets that are marked ready outside of transactions. The most common + /// case is the other end of the socket being closed. + /// + /// This method should be called in a thread safe context. + /// + /// @param socket_fd socket descriptor to check + void closeIfOutOfBandInternal(int socket_fd) { + for (auto const& destination : destinations_) { + // First we look for a connection with the socket. + ConnectionPtr connection = destination.second->findBySocketFd(socket_fd); + if (connection) { + if (!connection->isTransactionOngoing()) { + // Socket has no transaction, so any ready event is + // out-of-band (other end probably closed), so + // let's close it. Note we do not remove any queued + // requests, as this might somehow be occurring in + // between them. + destination.second->closeConnection(connection); + } + + return; + } + } + } + + /// @brief Request descriptor holds parameters associated with the + /// particular request. + struct RequestDescriptor { + /// @brief Constructor. + /// + /// @param request Pointer to the request to be sent. + /// @param response Pointer to the object into which the response will + /// be stored. + /// @param request_timeout Requested timeout for the transaction. + /// @param callback Pointer to the user callback. + /// @param connect_callback pointer to the user callback to be invoked + /// when the client connects to the server. + /// @param handshake_callback Optional callback invoked when the client + /// performs the TLS handshake with the server. + /// @param close_callback pointer to the user callback to be invoked + /// when the client closes the connection to the server. + RequestDescriptor(const HttpRequestPtr& request, + const HttpResponsePtr& response, + const long& request_timeout, + const HttpClient::RequestHandler& callback, + const HttpClient::ConnectHandler& connect_callback, + const HttpClient::HandshakeHandler& handshake_callback, + const HttpClient::CloseHandler& close_callback) + : request_(request), response_(response), + request_timeout_(request_timeout), callback_(callback), + connect_callback_(connect_callback), + handshake_callback_(handshake_callback), + close_callback_(close_callback) { + } + + /// @brief Holds pointer to the request. + HttpRequestPtr request_; + + /// @brief Holds pointer to the response. + HttpResponsePtr response_; + + /// @brief Holds requested timeout value. + long request_timeout_; + + /// @brief Holds pointer to the user callback. + HttpClient::RequestHandler callback_; + + /// @brief Holds pointer to the user callback for connect. + HttpClient::ConnectHandler connect_callback_; + + /// @brief Holds pointer to the user callback for handshake. + HttpClient::HandshakeHandler handshake_callback_; + + /// @brief Holds pointer to the user callback for close. + HttpClient::CloseHandler close_callback_; + }; + + /// @brief Type of URL and TLS context pairs. + typedef std::pair<Url, TlsContextPtr> DestinationDescriptor; + + /// @brief Encapsulates connections and requests for a given URL + class Destination { + public: + /// @brief Number of queued requests allowed without warnings being emitted. + const size_t QUEUE_SIZE_THRESHOLD = 2048; + /// @brief Interval between queue size warnings. + const int QUEUE_WARN_SECS = 5; + + /// @brief Constructor + /// + /// @param url server URL of this destination + /// @param tls_context server TLS context of this destination + /// @param max_connections maximum number of concurrent connections + /// allowed for in the list URL + Destination(Url const& url, TlsContextPtr tls_context, size_t max_connections) + : url_(url), tls_context_(tls_context), + max_connections_(max_connections), connections_(), queue_(), + last_queue_warn_time_(min_date_time), last_queue_size_(0) { + } + + /// @brief Destructor + ~Destination() { + closeAllConnections(); + } + + /// @brief Adds a new connection + /// + /// @param connection the connection to add + /// + /// @throw BadValue if the maximum number of connections already + /// exist. + /// @note This should be called in a thread safe context. + void addConnection(ConnectionPtr connection) { + if (connectionsFull()) { + isc_throw(BadValue, "URL: " << url_.toText() + << ", already at maximum connections: " + << max_connections_); + } + + connections_.push_back(connection); + } + + /// @brief Closes a connection and removes it from the list. + /// + /// @param connection the connection to remove + /// @note This should be called in a thread safe context. + void closeConnection(ConnectionPtr connection) { + for (auto it = connections_.begin(); it != connections_.end(); ++it) { + if (*it == connection) { + (*it)->close(); + connections_.erase(it); + break; + } + } + } + + /// @brief Closes all connections and clears the list. + /// @note This should be called in a thread safe context. + void closeAllConnections() { + // Flush the queue. + while (!queue_.empty()) { + queue_.pop(); + } + + for (auto const& connection : connections_) { + connection->close(); + } + + connections_.clear(); + } + + /// @brief Removes closed connections. + /// + /// This method should be called before @ref getIdleConnection. + /// + /// In a first step it closes not usable idle connections + /// (idle means no current transaction and not closed, + /// usable means the peer side did not close it at that time). + /// In a second step it removes (collects) closed connections. + /// + /// @note a connection is closed when the transaction is finished + /// and the connection is persistent, or when the connection was + /// idle and the first step of the garbage collector detects that + /// it was closed by peer, so is not usable. + /// + /// @note there are two races here: + /// - the peer side closes the connection after the first step + /// - a not persistent connection finishes its transaction and + /// closes + /// The second race is avoided by setting the closed flag before + /// the started flag and by unconditionally posting a process next + /// request action. + /// + /// @note This should be called in a thread safe context. + void garbageCollectConnections() { + for (auto it = connections_.begin(); it != connections_.end();) { + (*it)->isClosedByPeer(); + if (!(*it)->isClosed()) { + ++it; + } else { + it = connections_.erase(it); + } + } + } + + /// @brief Finds the first idle connection. + /// + /// Iterates over the existing connections and returns the + /// first connection which is not currently in a transaction and + /// is not closed. + /// + /// @note @ref garbageCollectConnections should be called before. + /// This removes connections which were closed at that time. + /// + /// @return The first idle connection or an empty pointer if + /// all connections are busy or closed. + ConnectionPtr getIdleConnection() { + for (auto const& connection : connections_) { + if (!connection->isTransactionOngoing() && + !connection->isClosed()) { + return (connection); + } + } + + return (ConnectionPtr()); + } + + /// @brief Find a connection by its socket descriptor. + /// + /// @param socket_fd socket descriptor to find + /// + /// @return The connection or an empty pointer if no matching + /// connection exists. + ConnectionPtr findBySocketFd(int socket_fd) { + for (auto const& connection : connections_) { + if (connection->isMySocket(socket_fd)) { + return (connection); + } + } + + return (ConnectionPtr()); + } + + /// @brief Indicates if there are no connections in the list. + /// + /// @return true if the list is empty. + bool connectionsEmpty() { + return (connections_.empty()); + } + + /// @brief Indicates if list contains the maximum number. + /// + /// @return true if the list is full. + bool connectionsFull() { + return (connections_.size() >= max_connections_); + } + + /// @brief Fetches the number of connections in the list. + /// + /// @return the number of connections in the list. + size_t connectionCount() { + return (connections_.size()); + } + + /// @brief Fetches the maximum number of connections. + /// + /// @return the maxim number of connections. + size_t getMaxConnections() const { + return (max_connections_); + } + + /// @brief Indicates if request queue is empty. + /// + /// @return true if there are no requests queued. + bool queueEmpty() const { + return (queue_.empty()); + } + + /// @brief Adds a request to the end of the request queue. + /// + /// If the size of the queue exceeds a threshold and appears + /// to be growing it will emit a warning log. + /// + /// @param desc RequestDescriptor to queue. + void pushRequest(RequestDescriptor const& desc) { + queue_.push(desc); + size_t size = queue_.size(); + // If the queue size is larger than the threshold and growing, issue a + // periodic warning. + if ((size > QUEUE_SIZE_THRESHOLD) && (size > last_queue_size_)) { + ptime now = microsec_clock::universal_time(); + if ((now - last_queue_warn_time_) > seconds(QUEUE_WARN_SECS)) { + LOG_WARN(http_logger, HTTP_CLIENT_QUEUE_SIZE_GROWING) + .arg(url_.toText()) + .arg(size); + // Remember the last time we warned. + last_queue_warn_time_ = now; + } + } + + // Remember the previous size. + last_queue_size_ = size; + } + + /// @brief Removes a request from the front of the request queue. + /// + /// @return desc RequestDescriptor of the removed request. + RequestDescriptor popNextRequest() { + if (queue_.empty()) { + isc_throw(InvalidOperation, "cannot pop, queue is empty"); + } + + RequestDescriptor desc = queue_.front(); + queue_.pop(); + return (desc); + } + + private: + /// @brief URL supported by this destination. + Url url_; + + /// @brief TLS context to use with this destination. + TlsContextPtr tls_context_; + + /// @brief Maximum number of concurrent connections for this destination. + size_t max_connections_; + + /// @brief List of concurrent connections. + std::list<ConnectionPtr> connections_; + + /// @brief Holds the queue of request for this destination. + std::queue<RequestDescriptor> queue_; + + /// @brief Time the last queue size warning was issued. + ptime last_queue_warn_time_; + + /// @brief Size of the queue after last push. + size_t last_queue_size_; + }; + + /// @brief Pointer to a Destination. + typedef boost::shared_ptr<Destination> DestinationPtr; + + /// @brief Creates a new destination for the given URL and TLS context. + /// + /// @param url URL of the new destination. + /// @param tls_context TLS context for the new destination. + /// + /// @return Pointer to the newly created destination. + /// @note Must be called from within a thread-safe context. + DestinationPtr addDestination(const Url& url, + const TlsContextPtr& tls_context) { + const DestinationDescriptor& desc = std::make_pair(url, tls_context); + DestinationPtr destination(new Destination(url, tls_context, + max_url_connections_)); + destinations_[desc] = destination; + return (destination); + } + + /// @brief Fetches a destination by URL and TLS context. + /// + /// @param url URL of the destination desired. + /// @param tls_context TLS context for the destination desired. + /// + /// @return pointer the desired destination, empty pointer + /// if the destination does not exist. + /// @note Must be called from within a thread-safe context. + DestinationPtr findDestination(const Url& url, + const TlsContextPtr& tls_context) const { + const DestinationDescriptor& desc = std::make_pair(url, tls_context); + auto it = destinations_.find(desc); + if (it != destinations_.end()) { + return (it->second); + } + + return (DestinationPtr()); + } + + /// @brief Removes a destination by URL and TLS context. + /// + /// Closes all of the destination's connections and + /// discards all of its queued requests while removing + /// the destination from the list of known destinations. + /// + /// @note not used yet. + /// + /// @param url URL of the destination to be removed. + /// @param tls_context TLS context for the destination to be removed. + /// @note Must be called from within a thread-safe context. + void removeDestination(const Url& url, + const TlsContextPtr& tls_context) { + const DestinationDescriptor& desc = std::make_pair(url, tls_context); + auto it = destinations_.find(desc); + if (it != destinations_.end()) { + it->second->closeAllConnections(); + destinations_.erase(it); + } + } + + /// @brief A reference to the IOService that drives socket IO. + IOService& io_service_; + + /// @brief Map of Destinations by URL and TLS context. + std::map<DestinationDescriptor, DestinationPtr> destinations_; + + /// @brief Mutex to protect the internal state. + std::mutex pool_mutex_; + + /// @brief Maximum number of connections per URL and TLS context. + size_t max_url_connections_; +}; + +Connection::Connection(IOService& io_service, + const TlsContextPtr& tls_context, + const ConnectionPoolPtr& conn_pool, + const Url& url) + : conn_pool_(conn_pool), url_(url), tls_context_(tls_context), + tcp_socket_(), tls_socket_(), timer_(io_service), + current_request_(), current_response_(), parser_(), + current_callback_(), buf_(), input_buf_(), current_transid_(0), + close_callback_(), started_(false), need_handshake_(false), + closed_(false) { + if (!tls_context) { + tcp_socket_.reset(new asiolink::TCPSocket<SocketCallback>(io_service)); + } else { + tls_socket_.reset(new asiolink::TLSSocket<SocketCallback>(io_service, + tls_context)); + need_handshake_ = true; + } +} + +Connection::~Connection() { + close(); +} + +void +Connection::resetState() { + started_ = false; + current_request_.reset(); + current_response_.reset(); + parser_.reset(); + current_callback_ = HttpClient::RequestHandler(); +} + +void +Connection::closeCallback(const bool clear) { + if (close_callback_) { + try { + if (tcp_socket_) { + close_callback_(tcp_socket_->getNative()); + } else if (tls_socket_) { + close_callback_(tls_socket_->getNative()); + } else { + isc_throw(Unexpected, + "internal error: can't find a socket to close"); + } + } catch (...) { + LOG_ERROR(http_logger, HTTP_CONNECTION_CLOSE_CALLBACK_FAILED); + } + } + + if (clear) { + close_callback_ = HttpClient::CloseHandler(); + } +} + +void +Connection::isClosedByPeer() { + // This method applies only to idle connections. + if (started_ || closed_) { + return; + } + // This code was guarded by a lock so keep this. + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + isClosedByPeerInternal(); + } else { + isClosedByPeerInternal(); + } +} + +void +Connection::isClosedByPeerInternal() { + // If the socket is open we check if it is possible to transmit + // the data over this socket by reading from it with message + // peeking. If the socket is not usable, we close it and then + // re-open it. There is a narrow window of time between checking + // the socket usability and actually transmitting the data over + // this socket, when the peer may close the connection. In this + // case we'll need to re-transmit but we don't handle it here. + if (tcp_socket_) { + if (tcp_socket_->getASIOSocket().is_open() && + !tcp_socket_->isUsable()) { + closeCallback(); + closed_ = true; + tcp_socket_->close(); + } + } else if (tls_socket_) { + if (tls_socket_->getASIOSocket().is_open() && + !tls_socket_->isUsable()) { + closeCallback(); + closed_ = true; + tls_socket_->close(); + } + } else { + isc_throw(Unexpected, "internal error: can't find the sending socket"); + } +} + +void +Connection::doTransaction(const HttpRequestPtr& request, + const HttpResponsePtr& response, + const long request_timeout, + const HttpClient::RequestHandler& callback, + const HttpClient::ConnectHandler& connect_callback, + const HttpClient::HandshakeHandler& handshake_callback, + const HttpClient::CloseHandler& close_callback) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + doTransactionInternal(request, response, request_timeout, + callback, connect_callback, handshake_callback, + close_callback); + } else { + doTransactionInternal(request, response, request_timeout, + callback, connect_callback, handshake_callback, + close_callback); + } +} + +void +Connection::doTransactionInternal(const HttpRequestPtr& request, + const HttpResponsePtr& response, + const long request_timeout, + const HttpClient::RequestHandler& callback, + const HttpClient::ConnectHandler& connect_callback, + const HttpClient::HandshakeHandler& handshake_callback, + const HttpClient::CloseHandler& close_callback) { + try { + started_ = true; + current_request_ = request; + current_response_ = response; + parser_.reset(new HttpResponseParser(*current_response_)); + parser_->initModel(); + current_callback_ = callback; + handshake_callback_ = handshake_callback; + close_callback_ = close_callback; + + // Starting new transaction. Generate new transaction id. + ++current_transid_; + + buf_ = request->toString(); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_CLIENT_REQUEST_SEND) + .arg(request->toBriefString()) + .arg(url_.toText()); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL_DATA, + HTTP_CLIENT_REQUEST_SEND_DETAILS) + .arg(url_.toText()) + .arg(HttpMessageParserBase::logFormatHttpMessage(request->toString(), + MAX_LOGGED_MESSAGE_SIZE)); + + // Setup request timer. + scheduleTimer(request_timeout); + + /// @todo We're getting a hostname but in fact it is expected to be an IP address. + /// We should extend the TCPEndpoint to also accept names. Currently, it will fall + /// over for names. + TCPEndpoint endpoint(url_.getStrippedHostname(), + static_cast<unsigned short>(url_.getPort())); + SocketCallback socket_cb(std::bind(&Connection::connectCallback, shared_from_this(), + connect_callback, current_transid_, + ph::_1)); + + // Establish new connection or use existing connection. + if (tcp_socket_) { + tcp_socket_->open(&endpoint, socket_cb); + return; + } + if (tls_socket_) { + tls_socket_->open(&endpoint, socket_cb); + return; + } + + // Should never reach this point. + isc_throw(Unexpected, "internal error: can't find a socket to open"); + + } catch (const std::exception& ex) { + // Re-throw with the expected exception type. + isc_throw(HttpClientError, ex.what()); + } +} + +void +Connection::close() { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + return (closeInternal()); + } else { + return (closeInternal()); + } +} + +void +Connection::closeInternal() { + // Pass in true to discard the callback. + closeCallback(true); + + closed_ = true; + timer_.cancel(); + if (tcp_socket_) { + tcp_socket_->close(); + } + if (tls_socket_) { + tls_socket_->close(); + } + + resetState(); +} + +bool +Connection::isMySocket(int socket_fd) const { + if (tcp_socket_) { + return (tcp_socket_->getNative() == socket_fd); + } else if (tls_socket_) { + return (tls_socket_->getNative() == socket_fd); + } + // Should never reach this point. + std::cerr << "internal error: can't find my socket\n"; + return (false); +} + +bool +Connection::checkPrematureTimeout(const uint64_t transid) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + return (checkPrematureTimeoutInternal(transid)); + } else { + return (checkPrematureTimeoutInternal(transid)); + } +} + +bool +Connection::checkPrematureTimeoutInternal(const uint64_t transid) { + // If there is no transaction but the handlers are invoked it means + // that the last transaction in the queue timed out prematurely. + // Also, if there is a transaction in progress but the ID of that + // transaction doesn't match the one associated with the handler it, + // also means that the transaction timed out prematurely. + if (!isTransactionOngoing() || (transid != current_transid_)) { + LOG_WARN(http_logger, HTTP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED) + .arg(isTransactionOngoing()) + .arg(transid) + .arg(current_transid_); + return (true); + } + + return (false); +} + +void +Connection::terminate(const boost::system::error_code& ec, + const std::string& parsing_error) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + terminateInternal(ec, parsing_error); + } else { + terminateInternal(ec, parsing_error); + } +} + +void +Connection::terminateInternal(const boost::system::error_code& ec, + const std::string& parsing_error) { + HttpResponsePtr response; + if (isTransactionOngoing()) { + + timer_.cancel(); + if (tcp_socket_) { + tcp_socket_->cancel(); + } + if (tls_socket_) { + tls_socket_->cancel(); + } + + if (!ec && current_response_->isFinalized()) { + response = current_response_; + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_SERVER_RESPONSE_RECEIVED) + .arg(url_.toText()); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + HTTP_SERVER_RESPONSE_RECEIVED_DETAILS) + .arg(url_.toText()) + .arg(parser_ ? + parser_->getBufferAsString(MAX_LOGGED_MESSAGE_SIZE) : + "[HttpResponseParser is null]"); + + } else { + std::string err = parsing_error.empty() ? ec.message() : + parsing_error; + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_BAD_SERVER_RESPONSE_RECEIVED) + .arg(url_.toText()) + .arg(err); + + // Only log the details if we have received anything and tried + // to parse it. + if (!parsing_error.empty()) { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + HTTP_BAD_SERVER_RESPONSE_RECEIVED_DETAILS) + .arg(url_.toText()) + .arg(parser_ ? + parser_->getBufferAsString(MAX_LOGGED_MESSAGE_SIZE) : + "[HttpResponseParser is null]"); + } + } + + try { + // The callback should take care of its own exceptions but one + // never knows. + if (MultiThreadingMgr::instance().getMode()) { + UnlockGuard<std::mutex> lock(mutex_); + current_callback_(ec, response, parsing_error); + } else { + current_callback_(ec, response, parsing_error); + } + } catch (...) { + } + + // If we're not requesting connection persistence or the + // connection has timed out, we should close the socket. + if (!closed_ && + (!current_request_->isPersistent() || + (ec == boost::asio::error::timed_out))) { + closeInternal(); + } + + resetState(); + } + + // Check if there are any requests queued for this destination and start + // another transaction if there is at least one. + ConnectionPoolPtr conn_pool = conn_pool_.lock(); + if (conn_pool) { + conn_pool->postProcessNextRequest(url_, tls_context_); + } +} + +void +Connection::scheduleTimer(const long request_timeout) { + if (request_timeout > 0) { + timer_.setup(std::bind(&Connection::timerCallback, this), request_timeout, + IntervalTimer::ONE_SHOT); + } +} + +void +Connection::doHandshake(const uint64_t transid) { + // Skip the handshake if it is not needed. + if (!need_handshake_) { + doSend(transid); + return; + } + + SocketCallback socket_cb(std::bind(&Connection::handshakeCallback, + shared_from_this(), + handshake_callback_, + transid, + ph::_1)); + try { + tls_socket_->handshake(socket_cb); + + } catch (...) { + terminate(boost::asio::error::not_connected); + } +} + +void +Connection::doSend(const uint64_t transid) { + SocketCallback socket_cb(std::bind(&Connection::sendCallback, + shared_from_this(), + transid, + ph::_1, + ph::_2)); + try { + if (tcp_socket_) { + tcp_socket_->asyncSend(&buf_[0], buf_.size(), socket_cb); + return; + } + + if (tls_socket_) { + tls_socket_->asyncSend(&buf_[0], buf_.size(), socket_cb); + return; + } + + // Should never reach this point. + std::cerr << "internal error: can't find a socket to send to\n"; + isc_throw(Unexpected, + "internal error: can't find a socket to send to"); + } catch (...) { + terminate(boost::asio::error::not_connected); + } +} + +void +Connection::doReceive(const uint64_t transid) { + TCPEndpoint endpoint; + SocketCallback socket_cb(std::bind(&Connection::receiveCallback, + shared_from_this(), + transid, + ph::_1, + ph::_2)); + try { + if (tcp_socket_) { + tcp_socket_->asyncReceive(static_cast<void*>(input_buf_.data()), + input_buf_.size(), 0, + &endpoint, socket_cb); + return; + } + if (tls_socket_) { + tls_socket_->asyncReceive(static_cast<void*>(input_buf_.data()), + input_buf_.size(), 0, + &endpoint, socket_cb); + return; + } + // Should never reach this point. + std::cerr << "internal error: can't find a socket to receive from\n"; + isc_throw(Unexpected, + "internal error: can't find a socket to receive from"); + + } catch (...) { + terminate(boost::asio::error::not_connected); + } +} + +void +Connection::connectCallback(HttpClient::ConnectHandler connect_callback, + const uint64_t transid, + const boost::system::error_code& ec) { + if (checkPrematureTimeout(transid)) { + return; + } + + // Run user defined connect callback if specified. + if (connect_callback) { + // If the user defined callback indicates that the connection + // should not be continued. + if (tcp_socket_) { + if (!connect_callback(ec, tcp_socket_->getNative())) { + return; + } + } else if (tls_socket_) { + if (!connect_callback(ec, tls_socket_->getNative())) { + return; + } + } else { + // Should never reach this point. + std::cerr << "internal error: can't find a socket to connect\n"; + } + } + + if (ec && (ec.value() == boost::asio::error::operation_aborted)) { + return; + + // In some cases the "in progress" status code may be returned. It doesn't + // indicate an error. Sending the request over the socket is expected to + // be successful. Getting such status appears to be highly dependent on + // the operating system. + } else if (ec && + (ec.value() != boost::asio::error::in_progress) && + (ec.value() != boost::asio::error::already_connected)) { + terminate(ec); + + } else { + // Start the TLS handshake asynchronously. + doHandshake(transid); + } +} + +void +Connection::handshakeCallback(HttpClient::ConnectHandler handshake_callback, + const uint64_t transid, + const boost::system::error_code& ec) { + need_handshake_ = false; + if (checkPrematureTimeout(transid)) { + return; + } + + // Run user defined handshake callback if specified. + if (handshake_callback) { + // If the user defined callback indicates that the connection + // should not be continued. + if (tls_socket_) { + if (!handshake_callback(ec, tls_socket_->getNative())) { + return; + } + } else { + // Should never reach this point. + std::cerr << "internal error: can't find TLS socket\n"; + } + } + + if (ec && (ec.value() == boost::asio::error::operation_aborted)) { + return; + } else if (ec) { + terminate(ec); + + } else { + // Start sending the request asynchronously. + doSend(transid); + } +} + +void +Connection::sendCallback(const uint64_t transid, + const boost::system::error_code& ec, + size_t length) { + if (checkPrematureTimeout(transid)) { + return; + } + + if (ec) { + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + // EAGAIN and EWOULDBLOCK don't really indicate an error. The length + // should be 0 in this case but let's be sure. + } else if ((ec.value() == boost::asio::error::would_block) || + (ec.value() == boost::asio::error::try_again)) { + length = 0; + + } else { + // Any other error should cause the transaction to terminate. + terminate(ec); + return; + } + } + + // Sending is in progress, so push back the timeout. + scheduleTimer(timer_.getInterval()); + + // If any data have been sent, remove it from the buffer and only leave the + // portion that still has to be sent. + if (length > 0) { + buf_.erase(0, length); + } + + // If there is no more data to be sent, start receiving a response. Otherwise, + // continue sending. + if (buf_.empty()) { + doReceive(transid); + + } else { + doSend(transid); + } +} + +void +Connection::receiveCallback(const uint64_t transid, + const boost::system::error_code& ec, + size_t length) { + if (checkPrematureTimeout(transid)) { + return; + } + + if (ec) { + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + // EAGAIN and EWOULDBLOCK don't indicate an error in this case. All + // other errors should terminate the transaction. + } + if ((ec.value() != boost::asio::error::try_again) && + (ec.value() != boost::asio::error::would_block)) { + terminate(ec); + return; + + } else { + // For EAGAIN and EWOULDBLOCK the length should be 0 anyway, but let's + // make sure. + length = 0; + } + } + + // Receiving is in progress, so push back the timeout. + scheduleTimer(timer_.getInterval()); + + if (runParser(ec, length)) { + doReceive(transid); + } +} + +bool +Connection::runParser(const boost::system::error_code& ec, size_t length) { + if (MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + return (runParserInternal(ec, length)); + } else { + return (runParserInternal(ec, length)); + } +} + +bool +Connection::runParserInternal(const boost::system::error_code& ec, + size_t length) { + // If we have received any data, let's feed the parser with it. + if (length != 0) { + parser_->postBuffer(static_cast<void*>(input_buf_.data()), length); + parser_->poll(); + } + + // If the parser still needs data, let's schedule another receive. + if (parser_->needData()) { + return (true); + + } else if (parser_->httpParseOk()) { + // No more data needed and parsing has been successful so far. Let's + // try to finalize the response parsing. + try { + current_response_->finalize(); + terminateInternal(ec); + + } catch (const std::exception& ex) { + // If there is an error here, we need to return the error message. + terminateInternal(ec, ex.what()); + } + + } else { + // Parsing was unsuccessful. Let's pass the error message held in the + // parser. + terminateInternal(ec, parser_->getErrorMessage()); + } + + return (false); +} + +void +Connection::timerCallback() { + // Request timeout occurred. + terminate(boost::asio::error::timed_out); +} + +} + +namespace isc { +namespace http { + +/// @brief HttpClient implementation. +class HttpClientImpl { +public: + /// @brief Constructor. + /// + /// If single-threading: + /// - Creates the connection pool passing in the caller's IOService + /// and a maximum number of connections per URL value of 1. + /// If multi-threading: + /// - Creates a private IOService + /// - Creates a thread pool with the thread_pool_size threads + /// - Creates the connection pool passing the private IOService + /// and the thread_pool_size as the maximum number of connections + /// per URL. + /// + /// @param io_service IOService that will drive connection IO in single + /// threaded mode. (Currently ignored in multi-threaded mode) + /// @param thread_pool_size maximum number of concurrent threads + /// Internally this also sets the maximum number of concurrent connections + /// per URL. + /// @param defer_thread_start When true, starting of the pool threads is + /// deferred until a subsequent call to @ref start(). In this case the + /// pool's operational state after construction is STOPPED. Otherwise, + /// the thread pool threads will be created and started, with the + /// operational state being RUNNING. Applicable only when thread-pool size + /// is greater than zero. + HttpClientImpl(IOService& io_service, size_t thread_pool_size = 0, + bool defer_thread_start = false) + : thread_pool_size_(thread_pool_size), thread_pool_() { + if (thread_pool_size_ > 0) { + // Create our own private IOService. + thread_io_service_.reset(new IOService()); + + // Create the connection pool. Note that we use the thread_pool_size + // as the maximum connections per URL value. + conn_pool_.reset(new ConnectionPool(*thread_io_service_, thread_pool_size_)); + + // Create the thread pool. + thread_pool_.reset(new IoServiceThreadPool(thread_io_service_, thread_pool_size_, + defer_thread_start)); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, HTTP_CLIENT_MT_STARTED) + .arg(thread_pool_size_); + } else { + // Single-threaded mode: use the caller's IOService, + // one connection per URL. + conn_pool_.reset(new ConnectionPool(io_service, 1)); + } + } + + /// @brief Destructor + /// + /// Calls stop(). + ~HttpClientImpl() { + stop(); + } + + /// @brief Check if the current thread can perform thread pool state + /// transition. + /// + /// @throw MultiThreadingInvalidOperation if the state transition is done on + /// any of the worker threads. + void checkPermissions() { + if (thread_pool_) { + thread_pool_->checkPausePermissions(); + } + } + + /// @brief Starts running the client's thread pool, if multi-threaded. + void start() { + if (thread_pool_) { + thread_pool_->run(); + } + } + + /// @brief Close all connections, and if multi-threaded, stops the client's + /// thread pool. + void stop() { + // Close all the connections. + conn_pool_->closeAll(); + + // Stop the thread pool. + if (thread_pool_) { + thread_pool_->stop(); + } + } + + /// @brief Pauses the client's thread pool. + /// + /// Suspends thread pool event processing. + /// @throw InvalidOperation if the thread pool does not exist. + void pause() { + if (!thread_pool_) { + isc_throw(InvalidOperation, "HttpClient::pause - no thread pool"); + } + + // Pause the thread pool. + thread_pool_->pause(); + } + + /// @brief Resumes running the client's thread pool. + /// + /// Resumes thread pool event processing. + /// @throw InvalidOperation if the thread pool does not exist. + void resume() { + if (!thread_pool_) { + isc_throw(InvalidOperation, "HttpClient::resume - no thread pool"); + } + + // Resume running the thread pool. + thread_pool_->run(); + } + + /// @brief Indicates if the thread pool is running. + /// + /// @return True if the thread pool exists and it is in the RUNNING state, + /// false otherwise. + bool isRunning() { + if (thread_pool_) { + return (thread_pool_->isRunning()); + } + + return (false); + } + + /// @brief Indicates if the thread pool is stopped. + /// + /// @return True if the thread pool exists and it is in the STOPPED state, + /// false otherwise. + bool isStopped() { + if (thread_pool_) { + return (thread_pool_->isStopped()); + } + + return (false); + } + + /// @brief Indicates if the thread pool is paused. + /// + /// @return True if the thread pool exists and it is in the PAUSED state, + /// false otherwise. + bool isPaused() { + if (thread_pool_) { + return (thread_pool_->isPaused()); + } + + return (false); + } + + /// @brief Fetches the internal IOService used in multi-threaded mode. + /// + /// @return A pointer to the IOService, or an empty pointer when + /// in single-threaded mode. + asiolink::IOServicePtr getThreadIOService() { + return (thread_io_service_); + }; + + /// @brief Fetches the maximum size of the thread pool. + /// + /// @return the maximum size of the thread pool. + uint16_t getThreadPoolSize() { + return (thread_pool_size_); + } + + /// @brief Fetches the number of threads in the pool. + /// + /// @return the number of running threads. + uint16_t getThreadCount() { + if (!thread_pool_) { + return (0); + } + return (thread_pool_->getThreadCount()); + } + + /// @brief Holds a pointer to the connection pool. + ConnectionPoolPtr conn_pool_; + +private: + + /// @brief Maxim number of threads in the thread pool. + size_t thread_pool_size_; + + /// @brief Pointer to private IOService used in multi-threaded mode. + asiolink::IOServicePtr thread_io_service_; + + /// @brief Pool of threads used to service connections in multi-threaded + /// mode. + IoServiceThreadPoolPtr thread_pool_; +}; + +HttpClient::HttpClient(IOService& io_service, bool multi_threading_enabled, + size_t thread_pool_size, bool defer_thread_start) { + if (!multi_threading_enabled && thread_pool_size) { + isc_throw(InvalidOperation, + "HttpClient thread_pool_size must be zero " + "when Kea core multi-threading is disabled"); + } + + impl_.reset(new HttpClientImpl(io_service, thread_pool_size, + defer_thread_start)); +} + +HttpClient::~HttpClient() { +} + +void +HttpClient::asyncSendRequest(const Url& url, + const TlsContextPtr& tls_context, + const HttpRequestPtr& request, + const HttpResponsePtr& response, + const HttpClient::RequestHandler& request_callback, + const HttpClient::RequestTimeout& request_timeout, + const HttpClient::ConnectHandler& connect_callback, + const HttpClient::HandshakeHandler& handshake_callback, + const HttpClient::CloseHandler& close_callback) { + if (!url.isValid()) { + isc_throw(HttpClientError, "invalid URL specified for the HTTP client"); + } + + if ((url.getScheme() == Url::Scheme::HTTPS) && !tls_context) { + isc_throw(HttpClientError, "HTTPS URL scheme but no TLS context"); + } + + if (!request) { + isc_throw(HttpClientError, "HTTP request must not be null"); + } + + if (!response) { + isc_throw(HttpClientError, "HTTP response must not be null"); + } + + if (!request_callback) { + isc_throw(HttpClientError, "callback for HTTP transaction must not be null"); + } + + impl_->conn_pool_->queueRequest(url, tls_context, request, response, + request_timeout.value_, + request_callback, connect_callback, + handshake_callback, close_callback); +} + +void +HttpClient::closeIfOutOfBand(int socket_fd) { + return (impl_->conn_pool_->closeIfOutOfBand(socket_fd)); +} + +void +HttpClient::start() { + impl_->start(); +} + +void +HttpClient::checkPermissions() { + impl_->checkPermissions(); +} + +void +HttpClient::pause() { + impl_->pause(); +} + +void +HttpClient::resume() { + impl_->resume(); +} + +void +HttpClient::stop() { + impl_->stop(); +} + +const IOServicePtr +HttpClient::getThreadIOService() const { + return (impl_->getThreadIOService()); +} + +uint16_t +HttpClient::getThreadPoolSize() const { + return (impl_->getThreadPoolSize()); +} + +uint16_t +HttpClient::getThreadCount() const { + return (impl_->getThreadCount()); +} + +bool +HttpClient::isRunning() { + return (impl_->isRunning()); +} + +bool +HttpClient::isStopped() { + return (impl_->isStopped()); +} + +bool +HttpClient::isPaused() { + return (impl_->isPaused()); +} + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/client.h b/src/lib/http/client.h new file mode 100644 index 0000000..bea9057 --- /dev/null +++ b/src/lib/http/client.h @@ -0,0 +1,344 @@ +// Copyright (C) 2018-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/. + +#ifndef HTTP_CLIENT_H +#define HTTP_CLIENT_H + +#include <asiolink/io_service.h> +#include <asiolink/tls_socket.h> +#include <exceptions/exceptions.h> +#include <http/url.h> +#include <http/request.h> +#include <http/response.h> +#include <boost/shared_ptr.hpp> +#include <functional> +#include <string> +#include <thread> +#include <vector> + +namespace isc { +namespace http { + +/// @brief A generic error raised by the @ref HttpClient class. +class HttpClientError : public Exception { +public: + HttpClientError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +class HttpClientImpl; + +/// @brief HTTP client class. +/// +/// This class implements an asynchronous HTTP client. The caller can schedule +/// transmission of the HTTP request using @ref HttpClient::asyncSendRequest +/// method. The caller specifies target URL for each request. The caller also +/// specifies a pointer to the @ref HttpRequest or derived class, holding a +/// request that should be transmitted to the destination. Such request must +/// be finalized, i.e. @ref HttpRequest::finalize method must be called prior +/// to sending it. The caller must also provide a pointer to the +/// @ref HttpResponse object or an object derived from it. The type of the +/// response object must match the expected content type to be returned in the +/// server's response. The last argument specified in this call is the pointer +/// to the callback function, which should be launched when the response is +/// received, an error occurs or when a timeout in the transmission is +/// signaled. +/// +/// The HTTP client supports multiple simultaneous and persistent connections +/// with different destinations. The client determines if the connection is +/// persistent by looking into the Connection header and HTTP version of the +/// request. If the connection should be persistent the client doesn't +/// close the connection after sending a request and receiving a response from +/// the server. If the client is provided with the request to be sent to the +/// particular destination, but there is an ongoing communication with this +/// destination, e.g. as a result of sending previous request, the new +/// request is queued in the FIFO queue. When the previous request completes, +/// the next request in the queue for the particular URL will be initiated. +/// +/// Furthermore, the class supports two modes of operation: single-threaded +/// and multi-threaded mode. In single-threaded mode, all IO is driven by +/// an external IOService passed into the class constructor, and ultimately +/// only a single connection per URL can be open at any given time. +/// +/// In multi-threaded mode an internal thread pool driven by a private +/// IOService instance is used to support multiple concurrent connections +/// per URL. Currently, the number of connections per URL is set to the +/// number of threads in the thread pool. +/// +/// The client tests the persistent connection for usability before sending +/// a request by trying to read from the socket (with message peeking). If +/// the socket is usable the client uses it to transmit the request. +/// +/// This classes exposes the underlying transport socket's descriptor for +/// each connection via connect, handshake and close callbacks. +/// This is done to permit the sockets to be monitored for IO readiness +/// by external code that's something other than boost::asio +/// (e.g.select() or epoll()), and would thus otherwise starve the +/// client's IOService and cause a backlog of ready event handlers. +/// +/// All errors are reported to the caller via the callback function supplied +/// to the @ref HttpClient::asyncSendRequest. The IO errors are communicated +/// via the @c boost::system::error code value. The response parsing errors +/// are returned via the 3rd parameter of the callback. +class HttpClient { +public: + /// @brief HTTP request/response timeout value. + struct RequestTimeout { + /// @brief Constructor. + /// + /// @param value Request/response timeout value in milliseconds. + explicit RequestTimeout(long value) + : value_(value) { + } + long value_; ///< Timeout value specified. + }; + + /// @brief Callback type used in call to @ref HttpClient::asyncSendRequest. + typedef std::function<void(const boost::system::error_code&, + const HttpResponsePtr&, + const std::string&)> RequestHandler; + + /// @brief Optional handler invoked when client connects to the server. + /// + /// Returned boolean value indicates whether the client should continue + /// connecting to the server (if true) or not (false). + /// It is passed the IO error code along with the native socket handle of + /// the connection's TCP socket. The passed socket descriptor may be used + /// to monitor the readiness of the events via select() or epoll(). + /// + /// @note Beware that the IO error code can be set to "in progress" + /// so a not null error code does not always mean the connect failed. + typedef std::function<bool(const boost::system::error_code&, const int)> ConnectHandler; + + /// @brief Optional handler invoked when client performs the TLS handshake + /// with the server. + /// + /// It is called when the TLS handshake completes: + /// - if the handshake succeeds it is called with error code 0 + /// - if the handshake fails it is called with error code != 0 + /// - if TLS is not enabled, it is not called at all + /// + /// Returned boolean value indicates whether the client should continue + /// connecting to the server (if true) or not (false). + /// @note The second argument is not used. + typedef std::function<bool(const boost::system::error_code&, const int)> HandshakeHandler; + + /// @brief Optional handler invoked when client closes the connection to the server. + /// + /// It is passed the native socket handler of the connection's TCP socket. + typedef std::function<void(const int)> CloseHandler; + + /// @brief Constructor. + /// + /// @param io_service IO service to be used by the HTTP client. + /// @param multi_threading_enabled The flag which indicates if MT is enabled. + /// @param thread_pool_size maximum number of threads in the thread pool. + /// A value greater than zero enables multi-threaded mode and sets the + /// maximum number of concurrent connections per URL. A value of zero + /// (default) enables single-threaded mode with one connection per URL. + /// @param defer_thread_start When true, starting of the pool threads is + /// deferred until a subsequent call to @ref start(). In this case the + /// pool's operational state after construction is STOPPED. Otherwise, + /// the thread pool threads will be created and started, with the + /// operational state being RUNNING. Applicable only when thread-pool size + /// is greater than zero. + explicit HttpClient(asiolink::IOService& io_service, + bool multi_threading_enabled, + size_t thread_pool_size = 0, + bool defer_thread_start = false); + + /// @brief Destructor. + ~HttpClient(); + + /// @brief Queues new asynchronous HTTP request for a given URL. + /// + /// The client maintains an internal connection pool which manages lists + /// of connections per URL. In single-threaded mode, each URL is limited + /// to a single connection. In multi-threaded mode, each URL may have + /// more than one open connection per URL, enabling the client to carry + /// on multiple concurrent requests per URL. + /// + /// The client will search the pool for an open, idle connection for the + /// given URL. If there are no idle connections, the client will open + /// a new connection up to the maximum number of connections allowed by the + /// thread mode. If all possible connections are busy, the request is + /// pushed on to back of a URL-specific FIFO queue of pending requests. + /// + /// If however, there is an idle connection available than a new transaction + /// for the request will be initiated immediately upon that connection. + /// + /// Note that when a connection completes a transaction, and its URL + /// queue is not empty, it will pop a pending request from the front of + /// the queue and begin a new transaction for that request. The net effect + /// is that requests are always pulled from the front of the queue unless + /// the queue is empty. + /// + /// The existing connection is tested before it is used for the new + /// transaction by attempting to read (with message peeking) from + /// the open transport socket. If the read attempt is successful, + /// the client will transmit the HTTP request to the server using + /// this connection. It is possible that the server closes the + /// connection between the connection test and sending the request. + /// In such case, an error will be returned and the caller will + /// need to try re-sending the request. + /// + /// If the connection test fails, the client will close the socket and + /// reconnect to the server prior to sending the request. + /// + /// Pointers to the request and response objects are provided as arguments + /// of this method. These pointers should have appropriate types derived + /// from the @ref HttpRequest and @ref HttpResponse classes. For example, + /// if the request has content type "application/json", a pointer to the + /// @ref HttpResponseJson should be passed. In this case, the response type + /// should be @ref HttpResponseJson. These types are used to validate both + /// the request provided by the caller and the response received from the + /// server. + /// + /// The callback function provided by the caller is invoked when the + /// transaction terminates, i.e. when the server has responded or when an + /// error occurred. The callback is expected to be exception safe, but the + /// client internally guards against exceptions thrown by the callback. + /// + /// The first argument of the callback indicates an IO error during + /// communication with the server. If the communication is successful the + /// error code of 0 is returned. However, in this case it is still possible + /// that the transaction is unsuccessful due to HTTP response parsing error, + /// e.g. invalid content type, malformed response etc. Such errors are + /// indicated via third argument. + /// + /// If message parsing was successful the second argument of the callback + /// contains a pointer to the parsed response (the same pointer as provided + /// by the caller as the argument). If parsing was unsuccessful, the null + /// pointer is returned. + /// + /// The default timeout for the transaction is set to 10 seconds + /// (10 000 ms). If the timeout occurs, the callback is invoked with the + /// error code of @c boost::asio::error::timed_out. + /// The timeout covers both the connect and the transaction phases + /// so when connecting to the server takes too long (e.g. with a + /// misconfigured firewall) the timeout is triggered. The connect + /// callback can be used to recognize this condition. + /// + /// @param url URL where the request should be send. + /// @param tls_context TLS context. + /// @param request Pointer to the object holding a request. + /// @param response Pointer to the object where response should be stored. + /// @param request_callback Pointer to the user callback function invoked + /// when transaction ends. + /// @param request_timeout Timeout for the transaction in milliseconds. + /// @param connect_callback Optional callback invoked when the client + /// connects to the server. + /// @param handshake_callback Optional callback invoked when the client + /// performs the TLS handshake with the server. + /// @param close_callback Optional callback invoked when the client + /// closes the connection to the server. + /// + /// @throw HttpClientError If invalid arguments were provided. + void asyncSendRequest(const Url& url, + const asiolink::TlsContextPtr& tls_context, + const HttpRequestPtr& request, + const HttpResponsePtr& response, + const RequestHandler& request_callback, + const RequestTimeout& request_timeout = + RequestTimeout(10000), + const ConnectHandler& connect_callback = + ConnectHandler(), + const HandshakeHandler& handshake_callback = + HandshakeHandler(), + const CloseHandler& close_callback = + CloseHandler()); + + /// @brief Check if the current thread can perform thread pool state + /// transition. + /// + /// @throw MultiThreadingInvalidOperation if the state transition is done on + /// any of the worker threads. + void checkPermissions(); + + /// @brief Starts running the client's thread pool, if multi-threaded. + void start(); + + /// @brief Pauses the client's thread pool. + /// + /// Suspends thread pool event processing. + /// @throw InvalidOperation if the thread pool does not exist. + void pause(); + + /// @brief Resumes running the client's thread pool. + /// + /// Resumes thread pool event processing. + /// @throw InvalidOperation if the thread pool does not exist. + void resume(); + + /// @brief Halts client-side IO activity. + /// + /// Closes all connections, discards any queued requests, and in + /// multi-threaded mode discards the thread-pool and the internal + /// IOService. + void stop(); + + /// @brief Closes a connection if it has an out-of-band socket event + /// + /// If the client owns a connection using the given socket and that + /// connection is currently in a transaction the method returns as this + /// indicates a normal ready event. If the connection is not in an + /// ongoing transaction, then the connection is closed. + /// + /// This is method is intended to be used to detect and clean up then + /// sockets that are marked ready outside of transactions. The most common + /// case is the other end of the socket being closed. + /// + /// @param socket_fd socket descriptor to check + void closeIfOutOfBand(int socket_fd); + + /// @brief Fetches a pointer to the internal IOService used to + /// drive the thread-pool in multi-threaded mode. + /// + /// @return pointer to the IOService instance, or an empty pointer + /// in single-threaded mode. + const asiolink::IOServicePtr getThreadIOService() const; + + /// @brief Fetches the maximum size of the thread pool. + /// + /// @return the maximum size of the thread pool. + uint16_t getThreadPoolSize() const; + + /// @brief Fetches the number of threads in the pool. + /// + /// @return the number of running threads. + uint16_t getThreadCount() const; + + /// @brief Indicates if the thread pool is running. + /// + /// @return True if the thread pool exists and it is in the RUNNING state, + /// false otherwise. + bool isRunning(); + + /// @brief Indicates if the thread pool is stopped. + /// + /// @return True if the thread pool exists and it is in the STOPPED state, + /// false otherwise. + bool isStopped(); + + /// @brief Indicates if the thread pool is paused. + /// + /// @return True if the thread pool exists and it is in the PAUSED state, + /// false otherwise. + bool isPaused(); + +private: + + /// @brief Pointer to the HTTP client implementation. + boost::shared_ptr<HttpClientImpl> impl_; +}; + +/// @brief Defines a pointer to an HttpClient instance. +typedef boost::shared_ptr<HttpClient> HttpClientPtr; + +} // end of namespace isc::http +} // end of namespace isc + +#endif diff --git a/src/lib/http/connection.cc b/src/lib/http/connection.cc new file mode 100644 index 0000000..b1e57bd --- /dev/null +++ b/src/lib/http/connection.cc @@ -0,0 +1,600 @@ +// 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 <http/connection.h> +#include <http/connection_pool.h> +#include <http/http_log.h> +#include <http/http_messages.h> +#include <boost/make_shared.hpp> +#include <functional> + +using namespace isc::asiolink; +namespace ph = std::placeholders; + +namespace { + +/// @brief Maximum size of the HTTP message that can be logged. +/// +/// The part of the HTTP message beyond this value is truncated. +constexpr size_t MAX_LOGGED_MESSAGE_SIZE = 1024; + +} + +namespace isc { +namespace http { + +HttpConnection::Transaction::Transaction(const HttpResponseCreatorPtr& response_creator, + const HttpRequestPtr& request) + : request_(request ? request : response_creator->createNewHttpRequest()), + parser_(new HttpRequestParser(*request_)), + input_buf_(), + output_buf_() { + parser_->initModel(); +} + +HttpConnection::TransactionPtr +HttpConnection::Transaction::create(const HttpResponseCreatorPtr& response_creator) { + return (boost::make_shared<Transaction>(response_creator)); +} + +HttpConnection::TransactionPtr +HttpConnection::Transaction::spawn(const HttpResponseCreatorPtr& response_creator, + const TransactionPtr& transaction) { + if (transaction) { + return (boost::make_shared<Transaction>(response_creator, + transaction->getRequest())); + } + return (create(response_creator)); +} + +void +HttpConnection:: +SocketCallback::operator()(boost::system::error_code ec, size_t length) { + if (ec.value() == boost::asio::error::operation_aborted) { + return; + } + callback_(ec, length); +} + +HttpConnection::HttpConnection(asiolink::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) + : request_timer_(io_service), + request_timeout_(request_timeout), + tls_context_(tls_context), + idle_timeout_(idle_timeout), + tcp_socket_(), + tls_socket_(), + acceptor_(acceptor), + connection_pool_(connection_pool), + response_creator_(response_creator), + acceptor_callback_(callback) { + if (!tls_context) { + tcp_socket_.reset(new asiolink::TCPSocket<SocketCallback>(io_service)); + } else { + tls_socket_.reset(new asiolink::TLSSocket<SocketCallback>(io_service, + tls_context)); + } +} + +HttpConnection::~HttpConnection() { + close(); +} + +void +HttpConnection::recordParameters(const HttpRequestPtr& request) const { + if (!request) { + // Should never happen. + return; + } + + // Record the remote address. + request->setRemote(getRemoteEndpointAddressAsText()); + + // Record TLS parameters. + if (!tls_socket_) { + return; + } + + // The connection uses HTTPS aka HTTP over TLS. + request->setTls(true); + + // Record the first commonName of the subjectName of the client + // certificate when wanted. + if (HttpRequest::recordSubject_) { + request->setSubject(tls_socket_->getTlsStream().getSubject()); + } + + // Record the first commonName of the issuerName of the client + // certificate when wanted. + if (HttpRequest::recordIssuer_) { + request->setIssuer(tls_socket_->getTlsStream().getIssuer()); + } +} + +void +HttpConnection::shutdownCallback(const boost::system::error_code&) { + tls_socket_->close(); +} + +void +HttpConnection::shutdown() { + request_timer_.cancel(); + if (tcp_socket_) { + tcp_socket_->close(); + return; + } + if (tls_socket_) { + // Create instance of the callback to close the socket. + SocketCallback cb(std::bind(&HttpConnection::shutdownCallback, + shared_from_this(), + ph::_1)); // error_code + tls_socket_->shutdown(cb); + return; + } + // Not reachable? + isc_throw(Unexpected, "internal error: unable to shutdown the socket"); +} + +void +HttpConnection::close() { + request_timer_.cancel(); + if (tcp_socket_) { + tcp_socket_->close(); + return; + } + if (tls_socket_) { + tls_socket_->close(); + return; + } + // Not reachable? + isc_throw(Unexpected, "internal error: unable to close the socket"); +} + +void +HttpConnection::shutdownConnection() { + try { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_CONNECTION_SHUTDOWN) + .arg(getRemoteEndpointAddressAsText()); + connection_pool_.shutdown(shared_from_this()); + } catch (...) { + LOG_ERROR(http_logger, HTTP_CONNECTION_SHUTDOWN_FAILED); + } +} + +void +HttpConnection::stopThisConnection() { + try { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_CONNECTION_STOP) + .arg(getRemoteEndpointAddressAsText()); + connection_pool_.stop(shared_from_this()); + } catch (...) { + LOG_ERROR(http_logger, HTTP_CONNECTION_STOP_FAILED); + } +} + +void +HttpConnection::asyncAccept() { + // Create instance of the callback. It is safe to pass the local instance + // of the callback, because the underlying boost functions make copies + // as needed. + HttpAcceptorCallback cb = std::bind(&HttpConnection::acceptorCallback, + shared_from_this(), + ph::_1); // error + try { + HttpsAcceptorPtr tls_acceptor = + boost::dynamic_pointer_cast<HttpsAcceptor>(acceptor_); + if (!tls_acceptor) { + if (!tcp_socket_) { + isc_throw(Unexpected, "internal error: TCP socket is null"); + } + acceptor_->asyncAccept(*tcp_socket_, cb); + } else { + if (!tls_socket_) { + isc_throw(Unexpected, "internal error: TLS socket is null"); + } + tls_acceptor->asyncAccept(*tls_socket_, cb); + } + } catch (const std::exception& ex) { + isc_throw(HttpConnectionError, "unable to start accepting TCP " + "connections: " << ex.what()); + } +} + +void +HttpConnection::doHandshake() { + // Skip the handshake if the socket is not a TLS one. + if (!tls_socket_) { + doRead(); + return; + } + + // Create instance of the callback. It is safe to pass the local instance + // of the callback, because the underlying boost functions make copies + // as needed. + SocketCallback cb(std::bind(&HttpConnection::handshakeCallback, + shared_from_this(), + ph::_1)); // error + try { + tls_socket_->handshake(cb); + + } catch (const std::exception& ex) { + isc_throw(HttpConnectionError, "unable to perform TLS handshake: " + << ex.what()); + } +} + +void +HttpConnection::doRead(TransactionPtr transaction) { + try { + TCPEndpoint endpoint; + + // Transaction hasn't been created if we are starting to read the + // new request. + if (!transaction) { + transaction = Transaction::create(response_creator_); + recordParameters(transaction->getRequest()); + } + + // Create instance of the callback. It is safe to pass the local instance + // of the callback, because the underlying std functions make copies + // as needed. + SocketCallback cb(std::bind(&HttpConnection::socketReadCallback, + shared_from_this(), + transaction, + ph::_1, // error + ph::_2)); //bytes_transferred + if (tcp_socket_) { + tcp_socket_->asyncReceive(static_cast<void*>(transaction->getInputBufData()), + transaction->getInputBufSize(), + 0, &endpoint, cb); + return; + } + if (tls_socket_) { + tls_socket_->asyncReceive(static_cast<void*>(transaction->getInputBufData()), + transaction->getInputBufSize(), + 0, &endpoint, cb); + return; + } + } catch (...) { + stopThisConnection(); + } +} + +void +HttpConnection::doWrite(HttpConnection::TransactionPtr transaction) { + try { + if (transaction->outputDataAvail()) { + // Create instance of the callback. It is safe to pass the local instance + // of the callback, because the underlying std functions make copies + // as needed. + SocketCallback cb(std::bind(&HttpConnection::socketWriteCallback, + shared_from_this(), + transaction, + ph::_1, // error + ph::_2)); // bytes_transferred + if (tcp_socket_) { + tcp_socket_->asyncSend(transaction->getOutputBufData(), + transaction->getOutputBufSize(), + cb); + return; + } + if (tls_socket_) { + tls_socket_->asyncSend(transaction->getOutputBufData(), + transaction->getOutputBufSize(), + cb); + return; + } + } else { + // The isPersistent() function may throw if the request hasn't + // been created, i.e. the HTTP headers weren't parsed. We catch + // this exception below and close the connection since we're + // unable to tell if the connection should remain persistent + // or not. The default is to close it. + if (!transaction->getRequest()->isPersistent()) { + stopThisConnection(); + + } else { + // The connection is persistent and we are done sending + // the previous response. Start listening for the next + // requests. + setupIdleTimer(); + doRead(); + } + } + } catch (...) { + stopThisConnection(); + } +} + +void +HttpConnection::asyncSendResponse(const ConstHttpResponsePtr& response, + TransactionPtr transaction) { + transaction->setOutputBuf(response->toString()); + doWrite(transaction); +} + + +void +HttpConnection::acceptorCallback(const boost::system::error_code& ec) { + if (!acceptor_->isOpen()) { + return; + } + + if (ec) { + stopThisConnection(); + } + + acceptor_callback_(ec); + + if (!ec) { + if (!tls_context_) { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_REQUEST_RECEIVE_START) + .arg(getRemoteEndpointAddressAsText()) + .arg(static_cast<unsigned>(request_timeout_/1000)); + } else { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_CONNECTION_HANDSHAKE_START) + .arg(getRemoteEndpointAddressAsText()) + .arg(static_cast<unsigned>(request_timeout_/1000)); + } + + setupRequestTimer(); + doHandshake(); + } +} + +void +HttpConnection::handshakeCallback(const boost::system::error_code& ec) { + if (ec) { + LOG_INFO(http_logger, HTTP_CONNECTION_HANDSHAKE_FAILED) + .arg(getRemoteEndpointAddressAsText()) + .arg(ec.message()); + stopThisConnection(); + } else { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTPS_REQUEST_RECEIVE_START) + .arg(getRemoteEndpointAddressAsText()); + + doRead(); + } +} + +void +HttpConnection::socketReadCallback(HttpConnection::TransactionPtr transaction, + boost::system::error_code ec, size_t length) { + if (ec) { + // IO service has been stopped and the connection is probably + // going to be shutting down. + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + // EWOULDBLOCK and EAGAIN are special cases. Everything else is + // treated as fatal error. + } else if ((ec.value() != boost::asio::error::try_again) && + (ec.value() != boost::asio::error::would_block)) { + stopThisConnection(); + + // We got EWOULDBLOCK or EAGAIN which indicate that we may be able to + // read something from the socket on the next attempt. Just make sure + // we don't try to read anything now in case there is any garbage + // passed in length. + } else { + length = 0; + } + } + + // Receiving is in progress, so push back the timeout. + setupRequestTimer(transaction); + + if (length != 0) { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL_DATA, + HTTP_DATA_RECEIVED) + .arg(length) + .arg(getRemoteEndpointAddressAsText()); + + transaction->getParser()->postBuffer(static_cast<void*>(transaction->getInputBufData()), + length); + transaction->getParser()->poll(); + } + + if (transaction->getParser()->needData()) { + // The parser indicates that the some part of the message being + // received is still missing, so continue to read. + doRead(transaction); + + } else { + try { + // The whole message has been received, so let's finalize it. + transaction->getRequest()->finalize(); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_CLIENT_REQUEST_RECEIVED) + .arg(getRemoteEndpointAddressAsText()); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + HTTP_CLIENT_REQUEST_RECEIVED_DETAILS) + .arg(getRemoteEndpointAddressAsText()) + .arg(transaction->getParser()->getBufferAsString(MAX_LOGGED_MESSAGE_SIZE)); + + } catch (const std::exception& ex) { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_BAD_CLIENT_REQUEST_RECEIVED) + .arg(getRemoteEndpointAddressAsText()) + .arg(ex.what()); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + HTTP_BAD_CLIENT_REQUEST_RECEIVED_DETAILS) + .arg(getRemoteEndpointAddressAsText()) + .arg(transaction->getParser()->getBufferAsString(MAX_LOGGED_MESSAGE_SIZE)); + } + + // Don't want to timeout if creation of the response takes long. + request_timer_.cancel(); + + // Create the response from the received request using the custom + // response creator. + HttpResponsePtr response = response_creator_->createHttpResponse(transaction->getRequest()); + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC, + HTTP_SERVER_RESPONSE_SEND) + .arg(response->toBriefString()) + .arg(getRemoteEndpointAddressAsText()); + + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_BASIC_DATA, + HTTP_SERVER_RESPONSE_SEND_DETAILS) + .arg(getRemoteEndpointAddressAsText()) + .arg(HttpMessageParserBase::logFormatHttpMessage(response->toString(), + MAX_LOGGED_MESSAGE_SIZE)); + + // Response created. Activate the timer again. + setupRequestTimer(transaction); + + // Start sending the response. + asyncSendResponse(response, transaction); + } +} + +void +HttpConnection::socketWriteCallback(HttpConnection::TransactionPtr transaction, + boost::system::error_code ec, size_t length) { + if (ec) { + // IO service has been stopped and the connection is probably + // going to be shutting down. + if (ec.value() == boost::asio::error::operation_aborted) { + return; + + // EWOULDBLOCK and EAGAIN are special cases. Everything else is + // treated as fatal error. + } else if ((ec.value() != boost::asio::error::try_again) && + (ec.value() != boost::asio::error::would_block)) { + stopThisConnection(); + + // We got EWOULDBLOCK or EAGAIN which indicate that we may be able to + // read something from the socket on the next attempt. + } else { + // Sending is in progress, so push back the timeout. + setupRequestTimer(transaction); + + doWrite(transaction); + } + } + + // Since each transaction has its own output buffer, it is not really + // possible that the number of bytes written is larger than the size + // of the buffer. But, let's be safe and set the length to the size + // of the buffer if that unexpected condition occurs. + if (length > transaction->getOutputBufSize()) { + length = transaction->getOutputBufSize(); + } + + if (length <= transaction->getOutputBufSize()) { + // Sending is in progress, so push back the timeout. + setupRequestTimer(transaction); + } + + // Eat the 'length' number of bytes from the output buffer and only + // leave the part of the response that hasn't been sent. + transaction->consumeOutputBuf(length); + + // Schedule the write of the unsent data. + doWrite(transaction); +} + +void +HttpConnection::setupRequestTimer(TransactionPtr transaction) { + // Pass raw pointer rather than shared_ptr to this object, + // because IntervalTimer already passes shared pointer to the + // IntervalTimerImpl to make sure that the callback remains + // valid. + request_timer_.setup(std::bind(&HttpConnection::requestTimeoutCallback, + this, transaction), + request_timeout_, IntervalTimer::ONE_SHOT); +} + +void +HttpConnection::setupIdleTimer() { + request_timer_.setup(std::bind(&HttpConnection::idleTimeoutCallback, + this), + idle_timeout_, IntervalTimer::ONE_SHOT); +} + +void +HttpConnection::requestTimeoutCallback(TransactionPtr transaction) { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_CLIENT_REQUEST_TIMEOUT_OCCURRED) + .arg(getRemoteEndpointAddressAsText()); + + // We need to differentiate the transactions between a normal response and the + // timeout. We create new transaction from the current transaction. It is + // to preserve the request we're responding to. + auto spawned_transaction = Transaction::spawn(response_creator_, transaction); + + // The new transaction inherits the request from the original transaction + // if such transaction exists. + auto request = spawned_transaction->getRequest(); + + // Depending on when the timeout occurred, the HTTP version of the request + // may or may not be available. Therefore we check if the HTTP version is + // set in the request. If it is not available, we need to create a dummy + // request with the default HTTP/1.0 version. This version will be used + // in the response. + if (request->context()->http_version_major_ == 0) { + request.reset(new HttpRequest(HttpRequest::Method::HTTP_POST, "/", + HttpVersion::HTTP_10(), + HostHttpHeader("dummy"))); + request->finalize(); + } + + // Create the timeout response. + HttpResponsePtr response = + response_creator_->createStockHttpResponse(request, + HttpStatusCode::REQUEST_TIMEOUT); + + // Send the HTTP 408 status. + asyncSendResponse(response, spawned_transaction); +} + +void +HttpConnection::idleTimeoutCallback() { + LOG_DEBUG(http_logger, isc::log::DBGLVL_TRACE_DETAIL, + HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED) + .arg(getRemoteEndpointAddressAsText()); + // In theory we should shutdown first and stop/close after but + // it is better to put the connection management responsibility + // on the client... so simply drop idle connections. + stopThisConnection(); +} + +std::string +HttpConnection::getRemoteEndpointAddressAsText() const { + try { + if (tcp_socket_) { + if (tcp_socket_->getASIOSocket().is_open()) { + return (tcp_socket_->getASIOSocket().remote_endpoint().address().to_string()); + } + } else if (tls_socket_) { + if (tls_socket_->getASIOSocket().is_open()) { + return (tls_socket_->getASIOSocket().remote_endpoint().address().to_string()); + } + } + } catch (...) { + } + return ("(unknown address)"); +} + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/connection.h b/src/lib/http/connection.h new file mode 100644 index 0000000..70109de --- /dev/null +++ b/src/lib/http/connection.h @@ -0,0 +1,432 @@ +// 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/. + +#ifndef HTTP_CONNECTION_H +#define HTTP_CONNECTION_H + +#include <asiolink/interval_timer.h> +#include <asiolink/io_service.h> +#include <http/http_acceptor.h> +#include <http/request_parser.h> +#include <http/response_creator_factory.h> +#include <boost/enable_shared_from_this.hpp> +#include <boost/system/error_code.hpp> +#include <boost/shared_ptr.hpp> +#include <array> +#include <functional> +#include <string> + +namespace isc { +namespace http { + +/// @brief Generic error reported within @ref HttpConnection class. +class HttpConnectionError : public Exception { +public: + HttpConnectionError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Forward declaration to the @ref HttpConnectionPool. +/// +/// This declaration is needed because we don't include the header file +/// declaring @ref HttpConnectionPool to avoid circular inclusion. +class HttpConnectionPool; + +class HttpConnection; +/// @brief Pointer to the @ref HttpConnection. +typedef boost::shared_ptr<HttpConnection> HttpConnectionPtr; + +/// @brief Accepts and handles a single HTTP connection. +class HttpConnection : public boost::enable_shared_from_this<HttpConnection> { +private: + + /// @brief Type of the function implementing a callback invoked by the + /// @c SocketCallback functor. + typedef std::function<void(boost::system::error_code ec, size_t length)> + SocketCallbackFunction; + + /// @brief Functor associated with the socket object. + /// + /// This functor calls a callback function specified in the constructor. + class SocketCallback { + public: + + /// @brief Constructor. + /// + /// @param socket_callback Callback to be invoked by the functor upon + /// an event associated with the socket. + SocketCallback(SocketCallbackFunction socket_callback) + : callback_(socket_callback) { + } + + /// @brief Operator called when event associated with a socket occurs. + /// + /// This operator returns immediately when received error code is + /// @c boost::system::error_code is equal to + /// @c boost::asio::error::operation_aborted, i.e. the callback is not + /// invoked. + /// + /// @param ec Error code. + /// @param length Data length. + void operator()(boost::system::error_code ec, size_t length = 0); + + private: + /// @brief Supplied callback. + SocketCallbackFunction callback_; + }; + +protected: + + class Transaction; + + /// @brief Shared pointer to the @c Transaction. + typedef boost::shared_ptr<Transaction> TransactionPtr; + + /// @brief Represents a single exchange of the HTTP messages. + /// + /// In HTTP/1.1 multiple HTTP message exchanges may be conducted + /// over the same persistent connection before the connection is + /// closed. Since ASIO handlers for these exchanges may be sometimes + /// executed out of order, there is a need to associate the states of + /// the exchanges with the appropriate ASIO handlers. This object + /// represents such state and includes: received request, request + /// parser (being in the particular state of parsing), input buffer + /// and the output buffer. + /// + /// The new @c Transaction instance is created when the connection + /// is established and the server starts receiving the HTTP request. + /// The shared pointer to the created transaction is passed between + /// the asynchronous handlers. Therefore, as long as the asynchronous + /// communication is conducted the instance of the transaction is + /// held by the IO service which runs the handlers. The transaction + /// instance exists as long as the asynchronous handlers for the + /// given request/response exchange are executed. When the server + /// responds to the client and all corresponding IO handlers are + /// invoked the transaction is automatically destroyed. + /// + /// The timeout may occur anytime during the transaction. In such + /// cases, a new transaction instance is created to send the + /// HTTP 408 (timeout) response to the client. Creation of the + /// new transaction for the timeout response is necessary because + /// there may be some asynchronous handlers scheduled by the + /// original transaction which rely on the original transaction's + /// state. The timeout response's state is held within the new + /// transaction spawned from the original transaction. + class Transaction { + public: + + /// @brief Constructor. + /// + /// @param response_creator Pointer to the response creator being + /// used for generating a response from the request. + /// @param request Pointer to the HTTP request. If the request is + /// null, the constructor creates new request instance using the + /// provided response creator. + Transaction(const HttpResponseCreatorPtr& response_creator, + const HttpRequestPtr& request = HttpRequestPtr()); + + /// @brief Creates new transaction instance. + /// + /// It is called when the HTTP server has just scheduled asynchronous + /// reading of the HTTP message. + /// + /// @param response_creator Pointer to the response creator to be passed + /// to the transaction's constructor. + /// + /// @return Pointer to the created transaction instance. + static TransactionPtr create(const HttpResponseCreatorPtr& response_creator); + + /// @brief Creates new transaction from the current transaction. + /// + /// This method creates new transaction and inherits the request + /// from the existing transaction. This is used when the timeout + /// occurs during the messages exchange. The server creates the new + /// transaction to handle the timeout but this new transaction must + /// include the request instance so as HTTP version information can + /// be inferred from it while sending the timeout response. The + /// HTTP version information should match between the request and + /// the response. + /// + /// @param response_creator Pointer to the response creator. + /// @param transaction Existing transaction from which the request + /// should be inherited. If the transaction is null, the new (dummy) + /// request is created for the new transaction. + static TransactionPtr spawn(const HttpResponseCreatorPtr& response_creator, + const TransactionPtr& transaction); + + /// @brief Returns request instance associated with the transaction. + HttpRequestPtr getRequest() const { + return (request_); + } + + /// @brief Returns parser instance associated with the transaction. + HttpRequestParserPtr getParser() const { + return (parser_); + } + + /// @brief Returns pointer to the first byte of the input buffer. + char* getInputBufData() { + return (input_buf_.data()); + } + + /// @brief Returns input buffer size. + size_t getInputBufSize() const { + return (input_buf_.size()); + } + + /// @brief Checks if the output buffer contains some data to be + /// sent. + /// + /// @return true if the output buffer contains data to be sent, + /// false otherwise. + bool outputDataAvail() const { + return (!output_buf_.empty()); + } + + /// @brief Returns pointer to the first byte of the output buffer. + const char* getOutputBufData() const { + return (output_buf_.data()); + } + + /// @brief Returns size of the output buffer. + size_t getOutputBufSize() const { + return (output_buf_.size()); + } + + /// @brief Replaces output buffer contents with new contents. + /// + /// @param response New contents for the output buffer. + void setOutputBuf(const std::string& response) { + output_buf_ = response; + } + + /// @brief Erases n bytes from the beginning of the output buffer. + /// + /// @param length Number of bytes to be erased. + void consumeOutputBuf(const size_t length) { + output_buf_.erase(0, length); + } + + private: + + /// @brief Pointer to the request received over this connection. + HttpRequestPtr request_; + + /// @brief Pointer to the HTTP request parser. + HttpRequestParserPtr parser_; + + /// @brief Buffer for received data. + std::array<char, 32768> input_buf_; + + /// @brief Buffer used for outbound data. + std::string output_buf_; + }; + +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. + HttpConnection(asiolink::IOService& io_service, + const HttpAcceptorPtr& acceptor, + const asiolink::TlsContextPtr& tls_context, + HttpConnectionPool& connection_pool, + const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback, + const long request_timeout, + const long idle_timeout); + + /// @brief Destructor. + /// + /// Closes current connection. + virtual ~HttpConnection(); + + /// @brief Asynchronously accepts new connection. + /// + /// When the connection is established successfully, the timeout timer is + /// setup and the asynchronous handshake with client is performed. + void asyncAccept(); + + /// @brief Shutdown the socket. + void shutdown(); + + /// @brief Closes the socket. + void close(); + + /// @brief Records connection parameters into the HTTP request. + /// + /// @param request Pointer to the HTTP request. + void recordParameters(const HttpRequestPtr& request) const; + + /// @brief Asynchronously performs TLS handshake. + /// + /// When the handshake is performed successfully or skipped because TLS + /// was not enabled, the asynchronous read from the socket is started. + void doHandshake(); + + /// @brief Starts asynchronous read from the socket. + /// + /// The data received over the socket are supplied to the HTTP parser until + /// the parser signals that the entire request has been received or until + /// the parser signals an error. In the former case the server creates an + /// HTTP response using supplied response creator object. + /// + /// In case of error the connection is stopped. + /// + /// @param transaction Pointer to the transaction for which the read + /// operation should be performed. It defaults to null pointer which + /// indicates that this function should create new transaction. + void doRead(TransactionPtr transaction = TransactionPtr()); + +protected: + + /// @brief Starts asynchronous write to the socket. + /// + /// The @c output_buf_ must contain the data to be sent. + /// + /// In case of error the connection is stopped. + /// + /// @param transaction Pointer to the transaction for which the write + /// operation should be performed. + void doWrite(TransactionPtr transaction); + + /// @brief Sends HTTP response asynchronously. + /// + /// Internally it calls @ref HttpConnection::doWrite to send the data. + /// + /// @param response Pointer to the HTTP response to be sent. + /// @param transaction Pointer to the transaction. + void asyncSendResponse(const ConstHttpResponsePtr& response, + TransactionPtr transaction); + + /// @brief Local callback invoked when new connection is accepted. + /// + /// It invokes external (supplied via constructor) acceptor callback. If + /// the acceptor is not opened it returns immediately. If the connection + /// is accepted successfully the @ref HttpConnection::doRead or + /// @ref HttpConnection::doHandshake is called. + /// + /// @param ec Error code. + void acceptorCallback(const boost::system::error_code& ec); + + /// @brief Local callback invoked when TLS handshake is performed. + /// + /// If the handshake is performed successfully the @ref + /// HttpConnection::doRead is called. + /// + /// @param ec Error code. + void handshakeCallback(const boost::system::error_code& ec); + + /// @brief Callback invoked when new data is received over the socket. + /// + /// This callback supplies the data to the HTTP parser and continues + /// parsing. When the parser signals end of the HTTP request the callback + /// prepares a response and starts asynchronous send over the socket. + /// + /// @param transaction Pointer to the transaction for which the callback + /// is invoked. + /// @param ec Error code. + /// @param length Length of the received data. + void socketReadCallback(TransactionPtr transaction, + boost::system::error_code ec, + size_t length); + + /// @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(TransactionPtr transaction, + boost::system::error_code ec, + size_t length); + + /// @brief Callback invoked when TLS shutdown is performed. + /// + /// The TLS socket is unconditionally closed but the callback is called + /// only when the peer has answered so the connection should be + /// explicitly closed in all cases, i.e. do not rely on this handler. + /// + /// @param ec Error code (ignored). + void shutdownCallback(const boost::system::error_code& ec); + + /// @brief Reset timer for detecting request timeouts. + /// + /// @param transaction Pointer to the transaction to be guarded by the timeout. + void setupRequestTimer(TransactionPtr transaction = TransactionPtr()); + + /// @brief Reset timer for detecting idle timeout in persistent connections. + void setupIdleTimer(); + + /// @brief Callback invoked when the HTTP Request Timeout occurs. + /// + /// This callback creates HTTP response with Request Timeout error code + /// and sends it to the client. + /// + /// @param transaction Pointer to the transaction for which timeout occurs. + void requestTimeoutCallback(TransactionPtr transaction); + + void idleTimeoutCallback(); + + /// @brief Shuts down current connection. + /// + /// Copied from the next method @ref stopThisConnection + void shutdownConnection(); + + /// @brief Stops current connection. + void stopThisConnection(); + + /// @brief returns remote address in textual form + std::string getRemoteEndpointAddressAsText() const; + + /// @brief Timer used to detect Request Timeout. + asiolink::IntervalTimer request_timer_; + + /// @brief Configured Request Timeout in milliseconds. + long request_timeout_; + + /// @brief TLS context. + asiolink::TlsContextPtr tls_context_; + + /// @brief Timeout after which the persistent HTTP connection is shut + /// down by the server. + long idle_timeout_; + + /// @brief TCP socket used by this connection. + std::unique_ptr<asiolink::TCPSocket<SocketCallback> > tcp_socket_; + + /// @brief TLS socket used by this connection. + std::unique_ptr<asiolink::TLSSocket<SocketCallback> > tls_socket_; + + /// @brief Pointer to the TCP acceptor used to accept new connections. + HttpAcceptorPtr acceptor_; + + /// @brief Connection pool holding this connection. + HttpConnectionPool& connection_pool_; + + /// @brief Pointer to the @ref HttpResponseCreator object used to create + /// HTTP responses. + HttpResponseCreatorPtr response_creator_; + + /// @brief External TCP acceptor callback. + HttpAcceptorCallback acceptor_callback_; +}; + +} // end of namespace isc::http +} // end of namespace isc + +#endif diff --git a/src/lib/http/connection_pool.cc b/src/lib/http/connection_pool.cc new file mode 100644 index 0000000..de92eb2 --- /dev/null +++ b/src/lib/http/connection_pool.cc @@ -0,0 +1,74 @@ +// 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 <http/connection_pool.h> +#include <util/multi_threading_mgr.h> + +namespace isc { +namespace http { + +void +HttpConnectionPool::start(const HttpConnectionPtr& connection) { + if (util::MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + connections_.insert(connections_.end(), connection); + } else { + connections_.insert(connections_.end(), connection); + } + + connection->asyncAccept(); +} + +void +HttpConnectionPool::stop(const HttpConnectionPtr& connection) { + if (util::MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + connections_.remove(connection); + } else { + connections_.remove(connection); + } + + connection->close(); +} + +void +HttpConnectionPool::shutdown(const HttpConnectionPtr& connection) { + if (util::MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + connections_.remove(connection); + } else { + connections_.remove(connection); + } + + connection->shutdown(); +} + +void +HttpConnectionPool::stopAll() { + if (util::MultiThreadingMgr::instance().getMode()) { + std::lock_guard<std::mutex> lk(mutex_); + stopAllInternal(); + } else { + stopAllInternal(); + } +} + +void +HttpConnectionPool::stopAllInternal() { + for (auto connection = connections_.begin(); + connection != connections_.end(); + ++connection) { + (*connection)->close(); + } + + connections_.clear(); +} + +} +} diff --git a/src/lib/http/connection_pool.h b/src/lib/http/connection_pool.h new file mode 100644 index 0000000..a9110bc --- /dev/null +++ b/src/lib/http/connection_pool.h @@ -0,0 +1,78 @@ +// 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 HTTP_CONNECTION_POOL_H +#define HTTP_CONNECTION_POOL_H + +#include <http/connection.h> + +#include <list> +#include <mutex> + +namespace isc { +namespace http { + +/// @brief Pool of active HTTP connections. +/// +/// The HTTP server is designed to handle many connections simultaneously. +/// The communication between the client and the server may take long time +/// and the server must be able to react on other events while the communication +/// with the clients is in progress. Thus, the server must track active +/// connections and gracefully close them when needed. An obvious case when the +/// connections must be terminated by the server is when the shutdown signal +/// is received. +/// +/// This object is a simple container for the server connections which provides +/// means to terminate them on request. +class HttpConnectionPool { +public: + + /// @brief Start new connection. + /// + /// The connection is inserted to the pool and the + /// @ref HttpConnection::asyncAccept is invoked. + /// + /// @param connection Pointer to the new connection. + void start(const HttpConnectionPtr& connection); + + /// @brief Removes a connection from the pool and shutdown it. + /// + /// Shutdown is specific to TLS and is a first part of graceful close (note it is + /// NOT the same as TCP shutdown system call). + /// + /// @note if the TLS connection stalls e.g. the peer does not try I/O + /// on it the connection has to be explicitly stopped. + /// + /// @param connection Pointer to the connection. + void shutdown(const HttpConnectionPtr& connection); + + /// @brief Removes a connection from the pool and stops it. + /// + /// @param connection Pointer to the connection. + void stop(const HttpConnectionPtr& connection); + + /// @brief Stops all connections and removes them from the pool. + void stopAll(); + +protected: + + /// @brief Stops all connections and removes them from the pool. + /// + /// Must be called from with a thread-safe context. + void stopAllInternal(); + + /// @brief Set of connections. + std::list<HttpConnectionPtr> connections_; + + /// @brief Mutex to protect the internal state. + std::mutex mutex_; +}; + +} +} + +#endif + diff --git a/src/lib/http/date_time.cc b/src/lib/http/date_time.cc new file mode 100644 index 0000000..cd7824a --- /dev/null +++ b/src/lib/http/date_time.cc @@ -0,0 +1,158 @@ +// 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/date_time.h> +#include <boost/date_time/time_facet.hpp> +#include <boost/date_time/local_time/local_time.hpp> +#include <locale> +#include <sstream> + +using namespace boost::local_time; +using namespace boost::posix_time; + +namespace isc { +namespace http { + +HttpDateTime::HttpDateTime() + : time_(boost::posix_time::second_clock::universal_time()) { +} + +HttpDateTime::HttpDateTime(const boost::posix_time::ptime& t) + : time_(t) { +} + +std::string +HttpDateTime::rfc1123Format() const { + return (toString("%a, %d %b %Y %H:%M:%S GMT", "RFC 1123")); +} + +std::string +HttpDateTime::rfc850Format() const { + return (toString("%A, %d-%b-%y %H:%M:%S GMT", "RFC 850")); +} + +std::string +HttpDateTime::asctimeFormat() const { + return (toString("%a %b %e %H:%M:%S %Y", "asctime")); +} + +HttpDateTime +HttpDateTime::fromRfc1123(const std::string& time_string) { + return (HttpDateTime(fromString(time_string, + "%a, %d %b %Y %H:%M:%S %ZP", + "RFC 1123"))); +} + +HttpDateTime +HttpDateTime::fromRfc850(const std::string& time_string) { + return (HttpDateTime(fromString(time_string, + "%A, %d-%b-%y %H:%M:%S %ZP", + "RFC 850"))); +} + +HttpDateTime +HttpDateTime::fromAsctime(const std::string& time_string) { + // The asctime() puts space instead of leading 0 in days of + // month. The %e # formatter of time_input_facet doesn't deal + // with this. To deal with this, we make a copy of the string + // holding formatted time and replace a space preceding day + // number with 0. Thanks to this workaround we can use the + // %d formatter which seems to work fine. This has a side + // effect of accepting timestamps such as Sun Nov 06 08:49:37 1994, + // but it should be ok to be liberal in this case. + std::string time_string_copy(time_string); + boost::replace_all(time_string_copy, " ", " 0"); + return (HttpDateTime(fromString(time_string_copy, + "%a %b %d %H:%M:%S %Y", + "asctime", + false))); +} + +HttpDateTime +HttpDateTime::fromAny(const std::string& time_string) { + HttpDateTime date_time; + // Try to parse as a timestamp specified in RFC 1123 format. + try { + date_time = fromRfc1123(time_string); + return (date_time); + } catch (...) { + // Ignore errors, simply try different format. + } + + // Try to parse as a timestamp specified in RFC 850 format. + try { + date_time = fromRfc850(time_string); + return (date_time); + } catch (...) { + // Ignore errors, simply try different format. + } + + // Try to parse as a timestamp output by asctime() function. + try { + date_time = fromAsctime(time_string); + } catch (...) { + isc_throw(HttpTimeConversionError, + "unsupported time format of the '" << time_string + << "'"); + } + + return (date_time); + +} + +std::string +HttpDateTime::toString(const std::string& format, + const std::string& method_name) const { + std::ostringstream s; + // Create raw pointer. The output stream will take responsibility for + // deleting the object. + time_facet* df(new time_facet(format.c_str())); + s.imbue(std::locale(std::locale::classic(), df)); + + // Convert time value to a string. + s << time_; + if (s.fail()) { + isc_throw(HttpTimeConversionError, "unable to convert " + << "time value of '" << time_ << "'" + << " to " << method_name << " format"); + } + return (s.str()); +} + + +ptime +HttpDateTime::fromString(const std::string& time_string, + const std::string& format, + const std::string& method_name, + const bool zone_check) { + std::istringstream s(time_string); + // Create raw pointer. The input stream will take responsibility for + // deleting the object. + time_input_facet* tif(new time_input_facet(format)); + s.imbue(std::locale(std::locale::classic(), tif)); + + time_zone_ptr zone(new posix_time_zone("GMT")); + local_date_time ldt = local_microsec_clock::local_time(zone); + + // Parse the time value. The stream will not automatically detect whether + // the zone is GMT. We need to check it on our own. + s >> ldt; + if (s.fail() || + (zone_check && (!ldt.zone() || + ldt.zone()->std_zone_abbrev() != "GMT"))) { + isc_throw(HttpTimeConversionError, "unable to parse " + << method_name << " time value of '" + << time_string << "'"); + } + + return (ldt.local_time()); +} + + +} // namespace http +} // namespace isc diff --git a/src/lib/http/date_time.h b/src/lib/http/date_time.h new file mode 100644 index 0000000..ba96ba3 --- /dev/null +++ b/src/lib/http/date_time.h @@ -0,0 +1,161 @@ +// 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_DATE_TIME_H +#define HTTP_DATE_TIME_H + +#include <exceptions/exceptions.h> +#include <boost/date_time/posix_time/posix_time.hpp> +#include <string> + +namespace isc { +namespace http { + +/// @brief Exception thrown when there is an error during time conversion. +/// +/// The most common reason for this exception is that the unsupported time +/// format was used as an input to the time parsing functions. +class HttpTimeConversionError : public Exception { +public: + HttpTimeConversionError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief This class parses and generates time values used in HTTP. +/// +/// The HTTP protocol have historically allowed 3 different date/time formats +/// (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html). These are: +/// - Sun, 06 Nov 1994 08:49:37 GMT +/// - Sunday, 06-Nov-94 08:49:37 GMT +/// - Sun Nov 6 08:49:37 1994 +/// +/// The first format is preferred but implementations must also support +/// remaining two obsolete formats for compatibility. This class implements +/// parsers and generators for all three formats. It uses @c boost::posix_time +/// to represent time and date. It uses @c boost::date_time::time_facet +/// and @c boost::date_time::time_input_facet to generate and parse the +/// timestamps. +class HttpDateTime { +public: + + /// @brief Default constructor. + /// + /// Sets current universal time as time value. + /// Time resolution is to seconds (i.e no fractional seconds). + HttpDateTime(); + + /// @brief Construct from @c boost::posix_time::ptime object. + /// + /// @param t time value to be set. + explicit HttpDateTime(const boost::posix_time::ptime& t); + + /// @brief Returns time encapsulated by this class. + /// + /// @return @c boost::posix_time::ptime value encapsulated by the instance + /// of this class. + boost::posix_time::ptime getPtime() const { + return (time_); + } + + /// @brief Returns time value formatted as specified in RFC 1123. + /// + /// @return A string containing time value formatted as + /// Sun, 06 Nov 1994 08:49:37 GMT. + std::string rfc1123Format() const; + + /// @brief Returns time value formatted as specified in RFC 850. + /// + /// @return A string containing time value formatted as + /// Sunday, 06-Nov-94 08:49:37 GMT. + std::string rfc850Format() const; + + /// @brief Returns time value formatted as output of ANSI C's + /// asctime(). + /// + /// @return A string containing time value formatted as + /// Sun Nov 6 08:49:37 1994. + std::string asctimeFormat() const; + + /// @brief Creates an instance from a string containing time value + /// formatted as specified in RFC 1123. + /// + /// @param time_string Input string holding formatted time value. + /// @return Instance of @ref HttpDateTime. + /// @throw HttpTimeConversionError if provided timestamp has invalid + /// format. + static HttpDateTime fromRfc1123(const std::string& time_string); + + /// @brief Creates an instance from a string containing time value + /// formatted as specified in RFC 850. + /// + /// @param time_string Input string holding formatted time value. + /// @return Instance of @ref HttpDateTime. + /// @throw HttpTimeConversionError if provided timestamp has invalid + /// format. + static HttpDateTime fromRfc850(const std::string& time_string); + + /// @brief Creates an instance from a string containing time value + /// formatted as output from asctime() function. + /// + /// @param time_string Input string holding formatted time value. + /// @return Instance of @ref HttpDateTime. + /// @throw HttpTimeConversionError if provided timestamp has invalid + /// format. + static HttpDateTime fromAsctime(const std::string& time_string); + + /// @brief Creates an instance from a string containing time value + /// formatted in one of the supported formats. + /// + /// This method will detect the format of the time value and parse it. + /// It tries parsing the value in the following order: + /// - a format specified in RFC 1123, + /// - a format specified in RFC 850, + /// - a format of asctime output. + /// + /// @param time_string Input string holding formatted time value. + /// @return Instance of @ref HttpDateTime. + /// @throw HttpTimeConversionError if provided value doesn't match any + /// of the supported formats. + static HttpDateTime fromAny(const std::string& time_string); + +private: + + /// @brief Generic method formatting a time value to a specified format. + //// + /// @param format Time format as accepted by the + /// @c boost::date_time::time_facet. + std::string toString(const std::string& format, + const std::string& method_name) const; + + /// @brief Generic method parsing time value and converting it to the + /// instance of @c boost::posix_time::ptime. + /// + /// @param time_string Input string holding formatted time value. + /// @param format Time format as accepted by the + /// @c boost::date_time::time_input_facet. + /// @param method_name Name of the expected format to appear in the error + /// message if parsing fails, e.g. RFC 1123, RFC 850 or asctime. + /// @param zone_check Indicates if the time zone name should be validated + /// during parsing. This should be set to false for the formats which + /// lack time zones (e.g. asctime). + /// + /// @return Instance of the @ref boost::posix_time::ptime created from the + /// input string. + /// @throw HttpTimeConversionError if provided value doesn't match the + /// specified format. + static boost::posix_time::ptime + fromString(const std::string& time_string, const std::string& format, + const std::string& method_name, const bool zone_check = true); + + /// @brief Time value encapsulated by this class instance. + boost::posix_time::ptime time_; + +}; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/header_context.h b/src/lib/http/header_context.h new file mode 100644 index 0000000..623e263 --- /dev/null +++ b/src/lib/http/header_context.h @@ -0,0 +1,49 @@ +// 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/. + +#ifndef HTTP_HEADER_CONTEXT_H +#define HTTP_HEADER_CONTEXT_H + +#include <boost/lexical_cast.hpp> +#include <cstdint> +#include <string> + +namespace isc { +namespace http { + +/// @brief HTTP header context. +struct HttpHeaderContext { + std::string name_; + std::string value_; + + /// @brief Constructor. + /// + /// Sets header name and value to empty strings. + HttpHeaderContext() + : name_(), value_() { + } + + /// @brief Constructor. + /// + /// @param name Header name. + /// @param value Header value. + HttpHeaderContext(const std::string& name, const std::string& value) + : name_(name), value_(value) { + } + + /// @brief Constructor. + /// + /// @param name Header name. + /// @param value Numeric value for the header. + HttpHeaderContext(const std::string& name, const int64_t value) + : name_(name), value_(boost::lexical_cast<std::string>(value)) { + } +}; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/http.dox b/src/lib/http/http.dox new file mode 100644 index 0000000..c6e4933 --- /dev/null +++ b/src/lib/http/http.dox @@ -0,0 +1,24 @@ +// 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/. + +/** + @page libhttp libkea-http - Kea HTTP Library + +@section httpMTConsiderations Multi-Threading Consideration for HTTP Library + +By default this library is not thread safe, for instance HTTP listeners +and HTTP messages are not thread safe. Exceptions are: + + - HTTP client is Kea thread safe (i.e. is thread safe when the + multi-threading mode is true). + + - date time is thread safe (mainly because its encapsulated POSIX time + is private and read-only, or because all methods are instance const methods + or class methods). + + - URL is thread safe (all public methods are const methods). + +*/ diff --git a/src/lib/http/http_acceptor.h b/src/lib/http/http_acceptor.h new file mode 100644 index 0000000..eb6f55d --- /dev/null +++ b/src/lib/http/http_acceptor.h @@ -0,0 +1,39 @@ +// 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 HTTP_ACCEPTOR_H +#define HTTP_ACCEPTOR_H + +#include <asiolink/tls_acceptor.h> +#include <boost/shared_ptr.hpp> +#include <boost/system/system_error.hpp> +#include <functional> + +namespace isc { +namespace http { + +/// @brief Type of the callback for the TCP acceptor used in this library. +typedef std::function<void(const boost::system::error_code&)> +HttpAcceptorCallback; + +/// @brief Type of the TCP acceptor used in this library. +typedef asiolink::TCPAcceptor<HttpAcceptorCallback> HttpAcceptor; + +/// @brief Type of the TLS acceptor used in this library. +/// +/// @note It is a derived type of HttpAcceptor. +typedef asiolink::TLSAcceptor<HttpAcceptorCallback> HttpsAcceptor; + +/// @brief Type of shared pointer to TCP acceptors. +typedef boost::shared_ptr<HttpAcceptor> HttpAcceptorPtr; + +/// @brief Type of shared pointer to TLS acceptors. +typedef boost::shared_ptr<HttpsAcceptor> HttpsAcceptorPtr; + +} // end of namespace isc::http +} // end of namespace isc + +#endif diff --git a/src/lib/http/http_header.cc b/src/lib/http/http_header.cc new file mode 100644 index 0000000..a543e66 --- /dev/null +++ b/src/lib/http/http_header.cc @@ -0,0 +1,56 @@ +// 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 <exceptions/exceptions.h> +#include <http/http_header.h> +#include <util/strutil.h> +#include <boost/lexical_cast.hpp> + +namespace isc { +namespace http { + +HttpHeader::HttpHeader(const std::string& header_name, + const std::string& header_value) + : header_name_(header_name), header_value_(header_value) { +} + +uint64_t +HttpHeader::getUint64Value() const { + try { + return (boost::lexical_cast<uint64_t>(header_value_)); + + } catch (const boost::bad_lexical_cast& ex) { + isc_throw(BadValue, header_name_ << " HTTP header value " + << header_value_ << " is not a valid number"); + } +} + +std::string +HttpHeader::getLowerCaseName() const { + std::string ln = header_name_; + util::str::lowercase(ln); + return (ln); +} + +std::string +HttpHeader::getLowerCaseValue() const { + std::string lc = header_value_; + util::str::lowercase(lc); + return (lc); +} + +bool +HttpHeader::isValueEqual(const std::string& v) const { + std::string lcv = v; + util::str::lowercase(lcv); + return (lcv == getLowerCaseValue()); +} + + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/http_header.h b/src/lib/http/http_header.h new file mode 100644 index 0000000..35d4c17 --- /dev/null +++ b/src/lib/http/http_header.h @@ -0,0 +1,86 @@ +// 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/. + +#ifndef HTTP_HEADER_H +#define HTTP_HEADER_H + +#include <boost/shared_ptr.hpp> +#include <string> + +namespace isc { +namespace http { + +/// @brief Represents HTTP header including a header name and value. +/// +/// It includes methods for retrieving header name and value in lower case +/// and for case insensitive comparison of header values. +class HttpHeader { +public: + + /// @brief Constructor. + /// + /// @param header_name Header name. + /// @param header_value Header value. + explicit HttpHeader(const std::string& header_name, + const std::string& header_value = ""); + + /// @brief Returns header name. + std::string getName() const { + return (header_name_); + } + + /// @brief Returns header value. + std::string getValue() const { + return (header_value_); + } + + /// @brief Returns header value as unsigned integer. + /// + /// @throw BadValue if the header value is not a valid number. + uint64_t getUint64Value() const; + + /// @brief Returns lower case header name. + std::string getLowerCaseName() const; + + /// @brief Returns lower case header value. + std::string getLowerCaseValue() const; + + /// @brief Case insensitive comparison of header value. + /// + /// @param v Value to be compared. + /// + /// @return true if header value is equal, false otherwise. + bool isValueEqual(const std::string& v) const; + +private: + + std::string header_name_; ///< Header name. + std::string header_value_; ///< Header value. +}; + +/// @brief Pointer to the @c HttpHeader class. +typedef boost::shared_ptr<HttpHeader> HttpHeaderPtr; + +/// @brief Represents HTTP Host header. +class HostHttpHeader : public HttpHeader { +public: + + /// @brief Constructor. + /// + /// @param header_value Host header value. The default is empty + /// string. + explicit HostHttpHeader(const std::string& header_value = "") + : HttpHeader("Host", header_value) { + } +}; + +/// @brief Pointer to the HTTP host header. +typedef boost::shared_ptr<HostHttpHeader> HostHttpHeaderPtr; + +} // end of namespace isc::http +} // end of namespace isc + +#endif // HTTP_HEADER_H diff --git a/src/lib/http/http_log.cc b/src/lib/http/http_log.cc new file mode 100644 index 0000000..8e1994d --- /dev/null +++ b/src/lib/http/http_log.cc @@ -0,0 +1,21 @@ +// Copyright (C) 2016-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/. + +/// Defines the logger used by the libkea-http library. + +#include <config.h> + +#include <http/http_log.h> + +namespace isc { +namespace http { + +/// @brief Defines the logger used within libkea-http library. +isc::log::Logger http_logger("http"); + +} // namespace http +} // namespace isc + diff --git a/src/lib/http/http_log.h b/src/lib/http/http_log.h new file mode 100644 index 0000000..0b7d8ad --- /dev/null +++ b/src/lib/http/http_log.h @@ -0,0 +1,23 @@ +// Copyright (C) 2016-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 HTTP_LOG_H +#define HTTP_LOG_H + +#include <log/logger_support.h> +#include <log/macros.h> +#include <http/http_messages.h> + +namespace isc { +namespace http { + +/// Define the logger used within libkea-http library. +extern isc::log::Logger http_logger; + +} // namespace http +} // namespace isc + +#endif // HTTP_LOG_H diff --git a/src/lib/http/http_message.cc b/src/lib/http/http_message.cc new file mode 100644 index 0000000..ce012ca --- /dev/null +++ b/src/lib/http/http_message.cc @@ -0,0 +1,108 @@ +// 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/http_message.h> + +namespace isc { +namespace http { + +HttpMessage::HttpMessage(const HttpMessage::Direction& direction) + : direction_(direction), required_versions_(), + http_version_(HttpVersion::HTTP_10()), required_headers_(), + created_(false), finalized_(false), headers_() { +} + +HttpMessage::~HttpMessage() { +} + +void +HttpMessage::requireHttpVersion(const HttpVersion& version) { + required_versions_.insert(version); +} + +void +HttpMessage::requireHeader(const std::string& header_name) { + // Empty value denotes that the header is required but no specific + // value is expected. + HttpHeaderPtr hdr(new HttpHeader(header_name)); + required_headers_[hdr->getLowerCaseName()] = hdr; +} + +void +HttpMessage::requireHeaderValue(const std::string& header_name, + const std::string& header_value) { + HttpHeaderPtr hdr(new HttpHeader(header_name, header_value)); + required_headers_[hdr->getLowerCaseName()] = hdr; +} + +bool +HttpMessage::requiresBody() const { + // If Content-Length is required the body must exist too. There may + // be probably some cases when Content-Length is not provided but + // the body is provided. But, probably not in our use cases. + // Use lower case header name because this is how it is indexed in + // the storage. + return (required_headers_.find("content-length") != required_headers_.end()); +} + +HttpVersion +HttpMessage::getHttpVersion() const { + checkCreated(); + return (http_version_); +} + +HttpHeaderPtr +HttpMessage::getHeader(const std::string& header_name) const { + checkCreated(); + + HttpHeader hdr(header_name); + auto header_it = headers_.find(hdr.getLowerCaseName()); + if (header_it != headers_.end()) { + return (header_it->second); + } + + isc_throw(HttpMessageNonExistingHeader, header_name << " HTTP header" + " not found in the request"); +} + +std::string +HttpMessage::getHeaderValue(const std::string& header_name) const { + return (getHeader(header_name)->getValue()); +} + +uint64_t +HttpMessage::getHeaderValueAsUint64(const std::string& header_name) const { + try { + return (getHeader(header_name)->getUint64Value()); + + } catch (const std::exception& ex) { + // The specified header does exist, but the value is not a number. + isc_throw(HttpMessageError, ex.what()); + } +} + +void +HttpMessage::checkCreated() const { + if (!created_) { + isc_throw(HttpMessageError, "unable to retrieve values of HTTP" + " message because the HttpMessage::create() must be" + " called first. This is a programmatic error"); + } +} + +void +HttpMessage::checkFinalized() const { + if (!finalized_) { + isc_throw(HttpMessageError, "unable to retrieve body of HTTP" + " message because the HttpMessage::finalize() must be" + " called first. This is a programmatic error"); + } +} + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/http_message.h b/src/lib/http/http_message.h new file mode 100644 index 0000000..3634c80 --- /dev/null +++ b/src/lib/http/http_message.h @@ -0,0 +1,265 @@ +// 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/. + +#ifndef HTTP_MESSAGE_H +#define HTTP_MESSAGE_H + +#include <exceptions/exceptions.h> +#include <http/http_header.h> +#include <http/http_types.h> +#include <map> +#include <set> +#include <cstdint> +#include <string> + +namespace isc { +namespace http { + +/// @brief Generic exception thrown by @ref HttpMessage class. +class HttpMessageError : public Exception { +public: + HttpMessageError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Exception thrown when attempt is made to retrieve a +/// non-existing header. +class HttpMessageNonExistingHeader : public HttpMessageError { +public: + HttpMessageNonExistingHeader(const char* file, size_t line, + const char* what) : + HttpMessageError(file, line, what) { }; +}; + + +/// @brief Base class for @ref HttpRequest and @ref HttpResponse. +/// +/// This abstract class provides a common functionality for the HTTP +/// requests and responses. Each such message can be marked as outbound +/// or inbound. An HTTP inbound request is the one received by the server +/// and HTTP inbound response is the response received by the client. +/// Conversely, an HTTP outbound request is the request created by the +/// client and HTTP outbound response is the response created by the +/// server. There are differences in how the inbound and outbound +/// messages are created. The inbound messages are received over the +/// TCP sockets and parsed by the parsers. The parsed information is +/// stored in a context, i.e. structure holding raw information and +/// associated with the given @c HttpMessage instance. Once the message +/// is parsed and all required information is stored in the context, +/// the @c create method is called to validate and fetch information +/// from the context into the message. The @c finalize method is called +/// to commit the HTTP message body into the message. +/// +/// The outbound message is created locally from the known data, e.g. +/// HTTP version number, URI, method etc. The headers can be then +/// appended to the message via the context. In order to use this message +/// the @c finalize method must be called to commit this information. +/// Them, @c toString method can be called to generate the message in +/// the textual form, which can be transferred via TCP socket. +class HttpMessage { +public: + + /// @brief Specifies the direction of the HTTP message. + enum Direction { + INBOUND, + OUTBOUND + }; + + /// @brief Constructor. + /// + /// @param direction Direction of the message (inbound or outbound). + explicit HttpMessage(const Direction& direction); + + /// @brief Destructor. + virtual ~HttpMessage(); + + /// @brief Returns HTTP message direction. + Direction getDirection() const { + return (direction_); + } + + /// @brief Sets direction for the HTTP message. + /// + /// This is mostly useful in unit testing. + /// + /// @param direction New direction of the HTTP message. + void setDirection(const Direction& direction) { + direction_ = direction; + } + + /// @brief Specifies HTTP version allowed. + /// + /// Allowed HTTP versions must be specified prior to calling @ref create + /// method. If no version is specified, all versions are allowed. + /// + /// @param version Version number allowed for the request. + void requireHttpVersion(const HttpVersion& version); + + /// @brief Specifies a required HTTP header for the HTTP message. + /// + /// Required headers must be specified prior to calling @ref create method. + /// The specified header must exist in the received HTTP request. This puts + /// no requirement on the header value. + /// + /// @param header_name Required header name. + void requireHeader(const std::string& header_name); + + /// @brief Specifies a required value of a header in the message. + /// + /// Required header values must be specified prior to calling @ref create + /// method. The specified header must exist and its value must be equal to + /// the value specified as second parameter. + /// + /// @param header_name HTTP header name. + /// @param header_value HTTP header value. + void requireHeaderValue(const std::string& header_name, + const std::string& header_value); + + /// @brief Checks if the body is required for the HTTP message. + /// + /// Current implementation simply checks if the "Content-Length" header + /// is required. + /// + /// @return true if the body is required, false otherwise. + bool requiresBody() const; + + /// @brief Reads parsed message from the context, validates the message and + /// stores parsed information. + /// + /// This method must be called before retrieving parsed data using accessors. + /// This method doesn't parse the HTTP request body. + virtual void create() = 0; + + /// @brief Complete parsing HTTP message or creating an HTTP outbound message. + /// + /// This method is used in two situations: when a message has been received + /// into a context and may be fully parsed (including the body) or when the + /// data for the creation of the outbound message have been stored in a context + /// and the message can be now created from the context. + /// + /// This method should call @c create method if it hasn't been called yet and + /// then read the message body from the context and interpret it. If the body + /// doesn't adhere to the requirements for the message (in particular, when the + /// content type of the body is invalid) an exception should be thrown. + virtual void finalize() = 0; + + /// @brief Reset the state of the object. + virtual void reset() = 0; + + /// @brief Returns HTTP version number (major and minor). + HttpVersion getHttpVersion() const; + + /// @brief Returns object encapsulating HTTP header. + /// + /// @param header_name HTTP header name. + /// + /// @return Non-null pointer to the header. + /// @throw HttpMessageNonExistingHeader if header with the specified name + /// doesn't exist. + /// @throw HttpMessageError if the request hasn't been created. + HttpHeaderPtr getHeader(const std::string& header_name) const; + + /// @brief Returns a value of the specified HTTP header. + /// + /// @param header_name Name of the HTTP header. + /// + /// @throw HttpMessageError if the header doesn't exist. + std::string getHeaderValue(const std::string& header_name) const; + + /// @brief Returns a value of the specified HTTP header as number. + /// + /// @param header_name Name of the HTTP header. + /// + /// @throw HttpMessageError if the header doesn't exist or if the + /// header value is not number. + uint64_t getHeaderValueAsUint64(const std::string& header_name) const; + + /// @brief Returns HTTP message body as string. + virtual std::string getBody() const = 0; + + /// @brief Returns HTTP message as text. + /// + /// This method is called to generate the outbound HTTP message. Make + /// sure to call @c finalize prior to calling this method. + virtual std::string toString() const = 0; + + /// @brief Checks if the message has been successfully finalized. + /// + /// The message gets finalized on successful call to @c finalize. + /// + /// @return true if the message has been finalized, false otherwise. + bool isFinalized() const { + return (finalized_); + } + +protected: + + /// @brief Checks if the @ref create was called. + /// + /// @throw HttpMessageError if @ref create wasn't called. + void checkCreated() const; + + /// @brief Checks if the @ref finalize was called. + /// + /// @throw HttpMessageError if @ref finalize wasn't called. + void checkFinalized() const; + + /// @brief Checks if the set is empty or the specified element belongs + /// to this set. + /// + /// This is a convenience method used by the class to verify that the + /// given HTTP method belongs to "required methods", HTTP version belongs + /// to "required versions" etc. + /// + /// @param element Reference to the element. + /// @param element_set Reference to the set of elements. + /// @tparam T Element type, @ref HttpVersion etc. + /// + /// @return true if the element set is empty or if the element belongs + /// to the set. + template<typename T> + bool inRequiredSet(const T& element, + const std::set<T>& element_set) const { + return (element_set.empty() || element_set.count(element) > 0); + } + + /// @brief Message direction (inbound or outbound). + Direction direction_; + + /// @brief Set of required HTTP versions. + /// + /// If the set is empty, all versions are allowed. + std::set<HttpVersion> required_versions_; + + /// @brief HTTP version numbers. + HttpVersion http_version_; + + /// @brief Map of HTTP headers indexed by lower case header names. + typedef std::map<std::string, HttpHeaderPtr> HttpHeaderMap; + + /// @brief Map holding required HTTP headers. + /// + /// The key of this map specifies the lower case HTTP header name. + /// If the value of the HTTP header is empty, the header is required + /// but the value of the header is not checked. If the value is + /// non-empty, the value in the HTTP request must be equal (case + /// insensitive) to the value in the map. + HttpHeaderMap required_headers_; + + /// @brief Flag indicating whether @ref create was called. + bool created_; + + /// @brief Flag indicating whether @ref finalize was called. + bool finalized_; + + /// @brief Parsed HTTP headers. + HttpHeaderMap headers_; +}; + +} // end of namespace isc::http +} // end of namespace isc + +#endif // HTTP_MESSAGE_H diff --git a/src/lib/http/http_message_parser_base.cc b/src/lib/http/http_message_parser_base.cc new file mode 100644 index 0000000..000e343 --- /dev/null +++ b/src/lib/http/http_message_parser_base.cc @@ -0,0 +1,307 @@ +// Copyright (C) 2017-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/http_message_parser_base.h> +#include <functional> +#include <sstream> + +using namespace isc::util; + +namespace isc { +namespace http { + +const int HttpMessageParserBase::HTTP_PARSE_OK_ST; +const int HttpMessageParserBase::HTTP_PARSE_FAILED_ST; + +const int HttpMessageParserBase::DATA_READ_OK_EVT; +const int HttpMessageParserBase::NEED_MORE_DATA_EVT; +const int HttpMessageParserBase::MORE_DATA_PROVIDED_EVT; +const int HttpMessageParserBase::HTTP_PARSE_OK_EVT; +const int HttpMessageParserBase::HTTP_PARSE_FAILED_EVT; + + +HttpMessageParserBase::HttpMessageParserBase(HttpMessage& message) + : StateModel(), message_(message), buffer_(), buffer_pos_(0), + error_message_() { +} + +void +HttpMessageParserBase::poll() { + try { + // Run the parser until it runs out of input data or until + // parsing completes. + do { + getState(getCurrState())->run(); + + } while (!isModelDone() && (getNextEvent() != NOP_EVT) && + (getNextEvent() != NEED_MORE_DATA_EVT)); + } catch (const std::exception& ex) { + abortModel(ex.what()); + } +} + +bool +HttpMessageParserBase::needData() const { + return ((getNextEvent() == NEED_MORE_DATA_EVT) || + (getNextEvent() == START_EVT)); +} + +bool +HttpMessageParserBase::httpParseOk() const { + return ((getNextEvent() == END_EVT) && + (getLastEvent() == HTTP_PARSE_OK_EVT)); +} + +void +HttpMessageParserBase::postBuffer(const void* buf, const size_t buf_size) { + if (buf_size > 0) { + // The next event is NEED_MORE_DATA_EVT when the parser wants to + // signal that more data is needed. This method is called to supply + // more data and thus it should change the next event to + // MORE_DATA_PROVIDED_EVT. + if (getNextEvent() == NEED_MORE_DATA_EVT) { + transition(getCurrState(), MORE_DATA_PROVIDED_EVT); + } + buffer_.insert(buffer_.end(), static_cast<const char*>(buf), + static_cast<const char*>(buf) + buf_size); + } +} + +std::string +HttpMessageParserBase::getBufferAsString(const size_t limit) const { + std::string message(buffer_.begin(), buffer_.end()); + return (logFormatHttpMessage(message, limit)); +} + +std::string +HttpMessageParserBase::logFormatHttpMessage(const std::string& message, + const size_t limit) { + if ((limit > 0) && !message.empty()) { + if (limit < message.size()) { + std::ostringstream s; + s << message.substr(0, limit) + << ".........\n(truncating HTTP message larger than " + << limit << " characters)\n"; + return (s.str()); + } + } + + // Return original message if it is empty or does not exceed the + // limit. + return (message); +} + + +void +HttpMessageParserBase::defineEvents() { + StateModel::defineEvents(); + + // Define HTTP parser specific events. + defineEvent(DATA_READ_OK_EVT, "DATA_READ_OK_EVT"); + defineEvent(NEED_MORE_DATA_EVT, "NEED_MORE_DATA_EVT"); + defineEvent(MORE_DATA_PROVIDED_EVT, "MORE_DATA_PROVIDED_EVT"); + defineEvent(HTTP_PARSE_OK_EVT, "HTTP_PARSE_OK_EVT"); + defineEvent(HTTP_PARSE_FAILED_EVT, "HTTP_PARSE_FAILED_EVT"); +} + +void +HttpMessageParserBase::verifyEvents() { + StateModel::verifyEvents(); + + getEvent(DATA_READ_OK_EVT); + getEvent(NEED_MORE_DATA_EVT); + getEvent(MORE_DATA_PROVIDED_EVT); + getEvent(HTTP_PARSE_OK_EVT); + getEvent(HTTP_PARSE_FAILED_EVT); +} + +void +HttpMessageParserBase::defineStates() { + // Call parent class implementation first. + StateModel::defineStates(); + + defineState(HTTP_PARSE_OK_ST, "HTTP_PARSE_OK_ST", + std::bind(&HttpMessageParserBase::parseEndedHandler, this)); + + defineState(HTTP_PARSE_FAILED_ST, "HTTP_PARSE_FAILED_ST", + std::bind(&HttpMessageParserBase::parseEndedHandler, this)); +} + +void +HttpMessageParserBase::stateWithReadHandler(const std::string& handler_name, + std::function<void(const char c)> + after_read_logic) { + std::string bytes; + getNextFromBuffer(bytes); + // Do nothing if we reached the end of buffer. + if (getNextEvent() != NEED_MORE_DATA_EVT) { + switch(getNextEvent()) { + case DATA_READ_OK_EVT: + case MORE_DATA_PROVIDED_EVT: + after_read_logic(bytes[0]); + break; + default: + invalidEventError(handler_name, getNextEvent()); + } + } +} + +void +HttpMessageParserBase::stateWithMultiReadHandler(const std::string& handler_name, + std::function<void(const std::string&)> + after_read_logic) { + std::string bytes; + getNextFromBuffer(bytes, 0); + // Do nothing if we reached the end of buffer. + if (getNextEvent() != NEED_MORE_DATA_EVT) { + switch(getNextEvent()) { + case DATA_READ_OK_EVT: + case MORE_DATA_PROVIDED_EVT: + after_read_logic(bytes); + break; + default: + invalidEventError(handler_name, getNextEvent()); + } + } +} + +void +HttpMessageParserBase::parseFailure(const std::string& error_msg) { + error_message_ = error_msg + " : " + getContextStr(); + transition(HTTP_PARSE_FAILED_ST, HTTP_PARSE_FAILED_EVT); +} + +void +HttpMessageParserBase::onModelFailure(const std::string& explanation) { + if (error_message_.empty()) { + error_message_ = explanation; + } +} + +void +HttpMessageParserBase::getNextFromBuffer(std::string& bytes, const size_t limit) { + unsigned int ev = getNextEvent(); + bytes = "\0"; + // The caller should always provide additional data when the + // NEED_MORE_DATA_EVT occurs. If the next event is still + // NEED_MORE_DATA_EVT it indicates that the caller hasn't provided + // the data. + if (ev == NEED_MORE_DATA_EVT) { + isc_throw(HttpParseError, + "HTTP request parser requires new data to progress, but no data" + " have been provided. The transaction is aborted to avoid" + " a deadlock. This is a Kea HTTP server logic error!"); + + } else { + // Try to retrieve characters from the buffer. + const bool data_exist = popNextFromBuffer(bytes, limit); + if (!data_exist) { + // There is no more data so it is really not possible that we're + // at MORE_DATA_PROVIDED_EVT. + if (ev == MORE_DATA_PROVIDED_EVT) { + isc_throw(HttpParseError, + "HTTP server state indicates that new data have been" + " provided to be parsed, but the transaction buffer" + " contains no new data. This is a Kea HTTP server logic" + " error!"); + + } else { + // If there is no more data we should set NEED_MORE_DATA_EVT + // event to indicate that new data should be provided. + postNextEvent(NEED_MORE_DATA_EVT); + } + } + } +} + +void +HttpMessageParserBase::invalidEventError(const std::string& handler_name, + const unsigned int event) { + isc_throw(HttpParseError, handler_name << ": invalid event " + << getEventLabel(static_cast<int>(event))); +} + +void +HttpMessageParserBase::parseEndedHandler() { + switch(getNextEvent()) { + case HTTP_PARSE_OK_EVT: + message_.finalize(); + transition(END_ST, END_EVT); + break; + case HTTP_PARSE_FAILED_EVT: + abortModel("HTTP message parsing failed"); + break; + + default: + invalidEventError("parseEndedHandler", getNextEvent()); + } +} + +bool +HttpMessageParserBase::popNextFromBuffer(std::string& next, const size_t limit) { + // If there are any characters in the buffer, pop next. + if (buffer_pos_ < buffer_.size()) { + next = buffer_.substr(buffer_pos_, limit == 0 ? std::string::npos : limit); + + if (limit > 0) { + buffer_pos_ += limit; + } + + if ((buffer_pos_ > buffer_.size()) || (limit == 0)) { + buffer_pos_ = buffer_.size(); + } + return (true); + } + return (false); +} + +bool +HttpMessageParserBase::isChar(const char c) const { + // was (c >= 0) && (c <= 127) + return (c >= 0); +} + +bool +HttpMessageParserBase::isCtl(const char c) const { + return (((c >= 0) && (c <= 31)) || (c == 127)); +} + +bool +HttpMessageParserBase::isSpecial(const char c) const { + switch (c) { + case '(': + case ')': + case '<': + case '>': + case '@': + case ',': + case ';': + case ':': + case '\\': + case '"': + case '/': + case '[': + case ']': + case '?': + case '=': + case '{': + case '}': + case ' ': + case '\t': + return true; + + default: + ; + } + + return false; +} + + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/http_message_parser_base.h b/src/lib/http/http_message_parser_base.h new file mode 100644 index 0000000..01f9831 --- /dev/null +++ b/src/lib/http/http_message_parser_base.h @@ -0,0 +1,316 @@ +// Copyright (C) 2017-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/. + +#ifndef HTTP_MESSAGE_PARSER_BASE_H +#define HTTP_MESSAGE_PARSER_BASE_H + +#include <exceptions/exceptions.h> +#include <http/http_message.h> +#include <util/state_model.h> +#include <functional> +#include <string> + +namespace isc { +namespace http { + +/// @brief Exception thrown when an error during parsing HTTP message +/// has occurred. +/// +/// The most common errors are due to receiving malformed requests. +class HttpParseError : public Exception { +public: + HttpParseError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief Base class for the HTTP message parsers. +/// +/// This is a base class for @c HttpRequestParser and @c HttpResponseParser +/// classes. It provides common states, events and functionality for processing +/// the received HTTP messages. +/// +/// This class must not be used directly. Instead, an instance of the +/// derived class should be used. +/// +/// HTTP uses TCP as a transport which is asynchronous in nature, i.e. the +/// HTTP message is received in chunks and multiple TCP connections can be +/// established at the same time. Multiplexing between these connections +/// requires providing a separate state machine per connection to "remember" +/// the state of each transaction when the parser is waiting for asynchronous +/// data to be delivered. While the parser is waiting for the data, it can +/// parse requests received over other connections. This class provides means +/// for parsing partial data received over the specific connection and +/// interrupting data parsing to switch to a different context. +/// +/// A new method @ref HttpMessageParserBase::poll has been created to run the +/// parser's state machine as long as there are unparsed data in the parser's +/// internal buffer. This method returns control to the caller when the parser +/// runs out of data in this buffer. The caller must feed the buffer by calling +/// @ref HttpMessageParserBase::postBuffer and then run +/// @ref HttpMessageParserBase::poll again. +/// +/// In case, the caller provides more data than indicated by the "Content-Length" +/// header the parser will return from @c poll() after parsing the data which +/// constitute the HTTP request and not parse the extraneous data. The caller +/// should test the @ref HttpMessageParserBase::needData and +/// @ref HttpMessageParserBase::httpParseOk to determine whether parsing has +/// completed. +/// +/// The @ref util::StateModel::runModel must not be used to run the parser +/// state machine, thus it is made private method. +class HttpMessageParserBase : public util::StateModel { +public: + + /// @name States supported by the HttpMessageParserBase. + /// + //@{ + + /// @brief Parsing successfully completed. + static const int HTTP_PARSE_OK_ST = SM_DERIVED_STATE_MIN + 1000; + + /// @brief Parsing failed. + static const int HTTP_PARSE_FAILED_ST = SM_DERIVED_STATE_MIN + 1001; + + //@} + + /// @name Events used during HTTP message parsing. + /// + //@{ + + /// @brief Chunk of data successfully read and parsed. + static const int DATA_READ_OK_EVT = SM_DERIVED_EVENT_MIN + 1; + + /// @brief Unable to proceed with parsing until new data is provided. + static const int NEED_MORE_DATA_EVT = SM_DERIVED_EVENT_MIN + 2; + + /// @brief New data provided and parsing should continue. + static const int MORE_DATA_PROVIDED_EVT = SM_DERIVED_EVENT_MIN + 3; + + /// @brief Parsing HTTP request successful. + static const int HTTP_PARSE_OK_EVT = SM_DERIVED_EVENT_MIN + 1000; + + /// @brief Parsing HTTP request failed. + static const int HTTP_PARSE_FAILED_EVT = SM_DERIVED_EVENT_MIN + 1001; + + //@} + + /// @brief Constructor. + /// + /// @param message Reference to the HTTP request or response message. + HttpMessageParserBase(HttpMessage& message); + + /// @brief Run the parser as long as the amount of data is sufficient. + /// + /// The data to be parsed should be provided by calling + /// @ref HttpMessageParserBase::postBuffer. When the parser reaches the end + /// of the data buffer the @ref HttpMessageParserBase::poll sets the next + /// event to @ref NEED_MORE_DATA_EVT and returns. The caller should then invoke + /// @ref HttpMessageParserBase::postBuffer again to provide more data to the + /// parser, and call @ref HttpMessageParserBase::poll to continue parsing. + /// + /// This method also returns when parsing completes or fails. The last + /// event can be examined to check whether parsing was successful or not. + void poll(); + + /// @brief Returns true if the parser needs more data to continue. + /// + /// @return true if the next event is NEED_MORE_DATA_EVT. + bool needData() const; + + /// @brief Returns true if the message has been parsed successfully. + bool httpParseOk() const; + + /// @brief Returns error message. + std::string getErrorMessage() const { + return (error_message_); + } + + /// @brief Provides more input data to the parser. + /// + /// This method must be called prior to calling @ref HttpMessageParserBase::poll + /// to deliver data to be parsed. HTTP messages are received over TCP and + /// multiple reads may be necessary to retrieve the entire request. There is + /// no need to accumulate the entire request to start parsing it. A chunk + /// of data can be provided to the parser using this method and parsed right + /// away using @ref HttpMessageParserBase::poll. + /// + /// @param buf A pointer to the buffer holding the data. + /// @param buf_size Size of the data within the buffer. + void postBuffer(const void* buf, const size_t buf_size); + + /// @brief Returns parser's input buffer as string. + /// + /// @param limit Maximum length of the buffer to be output. If the limit is 0, + /// the length of the output is unlimited. + /// @return Textual representation of the input buffer. + std::string getBufferAsString(const size_t limit = 0) const; + + /// @brief Formats provided HTTP message for logging. + /// + /// This method is useful in cases when there is a need to log a HTTP message + /// (as text). If the @c limit is specified the message output is limited to + /// this size. If the @c limit is set to 0 (default), the whole message is + /// output. The @c getBufferAsString method calls this method internally. + /// + /// @param message HTTP message to be logged. + /// @param limit Maximum length of the buffer to be output. If the limit is 0, + /// the length of the output is unlimited. + /// @return HTTP message formatted for logging. + static std::string logFormatHttpMessage(const std::string& message, + const size_t limit = 0); + +private: + + /// @brief Make @ref runModel private to make sure that the caller uses + /// @ref poll method instead. + using StateModel::runModel; + +protected: + + /// @brief Define events used by the parser. + virtual void defineEvents(); + + /// @brief Verifies events used by the parser. + virtual void verifyEvents(); + + /// @brief Defines states of the parser. + virtual void defineStates(); + + /// @brief Generic parser handler which reads a single byte of data and + /// parses it using specified callback function. + /// + /// This generic handler is used in most of the parser states to parse a + /// single byte of input data. If there is no more data it simply returns. + /// Otherwise, if the next event is DATA_READ_OK_EVT or + /// MORE_DATA_PROVIDED_EVT, it calls the provided callback function to + /// parse the new byte of data. For all other states it throws an exception. + /// + /// @param handler_name Name of the handler function which called this + /// method. + /// @param after_read_logic Callback function to parse the byte of data. + /// This callback function implements state specific logic. + /// + /// @throw HttpRequestParserError when invalid event occurred. + void stateWithReadHandler(const std::string& handler_name, + std::function<void(const char c)> + after_read_logic); + + /// @brief Generic parser handler which reads multiple bytes of data and + /// parses it using specified callback function. + /// + /// This handler is mostly used for parsing body of the HTTP message, + /// where we don't validate the content read. Reading multiple bytes + /// is the most efficient. If there is no more data it simply returns. + /// Otherwise, if the next event is DATA_READ_OK_EVT or + /// MORE_DATA_PROVIDED_EVT, it calls the provided callback function to + /// parse the new byte of data. + /// + /// @param handler_name Name of the handler function which called this + /// method. + /// @param after_read_logic Callback function to parse multiple bytes of + /// data. This callback function implements state specific logic. + /// + /// @throw HttpRequestParserError when invalid event occurred. + void stateWithMultiReadHandler(const std::string& handler_name, + std::function<void(const std::string&)> + after_read_logic); + + /// @brief Transition parser to failure state. + /// + /// This method transitions the parser to @ref HTTP_PARSE_FAILED_ST and + /// sets next event to HTTP_PARSE_FAILED_EVT. + /// + /// @param error_msg Error message explaining the failure. + void parseFailure(const std::string& error_msg); + + /// @brief A method called when parsing fails. + /// + /// @param explanation Error message explaining the reason for parsing + /// failure. + virtual void onModelFailure(const std::string& explanation); + + /// @brief Retrieves next bytes of data from the buffer. + /// + /// During normal operation, when there is no more data in the buffer, + /// the parser sets NEED_MORE_DATA_EVT as next event to signal the need for + /// calling @ref HttpMessageParserBase::postBuffer. + /// + /// @param [out] bytes Reference to the variable where read data should be stored. + /// @param limit Maximum number of bytes to be read. + /// + /// @throw HttpMessageParserBaseError If current event is already set to + /// NEED_MORE_DATA_EVT or MORE_DATA_PROVIDED_EVT. In the former case, it + /// indicates that the caller failed to provide new data using + /// @ref HttpMessageParserBase::postBuffer. The latter case is highly unlikely + /// as it indicates that no new data were provided but the state of the + /// parser was changed from NEED_MORE_DATA_EVT or the data were provided + /// but the data buffer is empty. In both cases, it is an internal server + /// error. + void getNextFromBuffer(std::string& bytes, const size_t limit = 1); + + /// @brief This method is called when invalid event occurred in a particular + /// parser state. + /// + /// This method simply throws @ref HttpParseError informing about invalid + /// event occurring for the particular parser state. The error message + /// includes the name of the handler in which the exception has been + /// thrown. It also includes the event which caused the exception. + /// + /// @param handler_name Name of the handler in which the exception is + /// thrown. + /// @param event An event which caused the exception. + /// + /// @throw HttpMessageParserBaseError. + void invalidEventError(const std::string& handler_name, + const unsigned int event); + + /// @brief Handler for HTTP_PARSE_OK_ST and HTTP_PARSE_FAILED_ST. + /// + /// If parsing is successful, it calls @ref HttpRequest::create to validate + /// the HTTP request. In both cases it transitions the parser to the END_ST. + void parseEndedHandler(); + + /// @brief Tries to read next byte from buffer. + /// + /// @param [out] next A reference to the variable where read data should be + /// stored. + /// @param limit Maximum number of characters to be read. + /// + /// @return true if data was successfully read, false otherwise. + bool popNextFromBuffer(std::string& next, const size_t limit = 1); + + /// @brief Checks if specified value is a character. + /// + /// @return true, if specified value is a character. + bool isChar(const char c) const; + + /// @brief Checks if specified value is a control value. + /// + /// @return true, if specified value is a control value. + bool isCtl(const char c) const; + + /// @brief Checks if specified value is a special character. + /// + /// @return true, if specified value is a special character. + bool isSpecial(const char c) const; + + /// @brief Reference to the parsed HTTP message. + HttpMessage& message_; + + /// @brief Internal buffer from which parser reads data. + std::string buffer_; + + /// @brief Position of the next character to read from the buffer. + size_t buffer_pos_; + + /// @brief Error message set by @ref onModelFailure. + std::string error_message_; +}; + +} // end of namespace isc::http +} // end of namespace isc + +#endif diff --git a/src/lib/http/http_messages.cc b/src/lib/http/http_messages.cc new file mode 100644 index 0000000..292d2d8 --- /dev/null +++ b/src/lib/http/http_messages.cc @@ -0,0 +1,77 @@ +// File created from ../../../src/lib/http/http_messages.mes + +#include <cstddef> +#include <log/message_types.h> +#include <log/message_initializer.h> + +namespace isc { +namespace http { + +extern const isc::log::MessageID HTTPS_REQUEST_RECEIVE_START = "HTTPS_REQUEST_RECEIVE_START"; +extern const isc::log::MessageID HTTP_BAD_CLIENT_REQUEST_RECEIVED = "HTTP_BAD_CLIENT_REQUEST_RECEIVED"; +extern const isc::log::MessageID HTTP_BAD_CLIENT_REQUEST_RECEIVED_DETAILS = "HTTP_BAD_CLIENT_REQUEST_RECEIVED_DETAILS"; +extern const isc::log::MessageID HTTP_BAD_SERVER_RESPONSE_RECEIVED = "HTTP_BAD_SERVER_RESPONSE_RECEIVED"; +extern const isc::log::MessageID HTTP_BAD_SERVER_RESPONSE_RECEIVED_DETAILS = "HTTP_BAD_SERVER_RESPONSE_RECEIVED_DETAILS"; +extern const isc::log::MessageID HTTP_CLIENT_MT_STARTED = "HTTP_CLIENT_MT_STARTED"; +extern const isc::log::MessageID HTTP_CLIENT_QUEUE_SIZE_GROWING = "HTTP_CLIENT_QUEUE_SIZE_GROWING"; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_RECEIVED = "HTTP_CLIENT_REQUEST_RECEIVED"; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_RECEIVED_DETAILS = "HTTP_CLIENT_REQUEST_RECEIVED_DETAILS"; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_SEND = "HTTP_CLIENT_REQUEST_SEND"; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_SEND_DETAILS = "HTTP_CLIENT_REQUEST_SEND_DETAILS"; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_TIMEOUT_OCCURRED = "HTTP_CLIENT_REQUEST_TIMEOUT_OCCURRED"; +extern const isc::log::MessageID HTTP_CONNECTION_CLOSE_CALLBACK_FAILED = "HTTP_CONNECTION_CLOSE_CALLBACK_FAILED"; +extern const isc::log::MessageID HTTP_CONNECTION_HANDSHAKE_FAILED = "HTTP_CONNECTION_HANDSHAKE_FAILED"; +extern const isc::log::MessageID HTTP_CONNECTION_HANDSHAKE_START = "HTTP_CONNECTION_HANDSHAKE_START"; +extern const isc::log::MessageID HTTP_CONNECTION_SHUTDOWN = "HTTP_CONNECTION_SHUTDOWN"; +extern const isc::log::MessageID HTTP_CONNECTION_SHUTDOWN_FAILED = "HTTP_CONNECTION_SHUTDOWN_FAILED"; +extern const isc::log::MessageID HTTP_CONNECTION_STOP = "HTTP_CONNECTION_STOP"; +extern const isc::log::MessageID HTTP_CONNECTION_STOP_FAILED = "HTTP_CONNECTION_STOP_FAILED"; +extern const isc::log::MessageID HTTP_DATA_RECEIVED = "HTTP_DATA_RECEIVED"; +extern const isc::log::MessageID HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED = "HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED"; +extern const isc::log::MessageID HTTP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED = "HTTP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED"; +extern const isc::log::MessageID HTTP_REQUEST_RECEIVE_START = "HTTP_REQUEST_RECEIVE_START"; +extern const isc::log::MessageID HTTP_SERVER_RESPONSE_RECEIVED = "HTTP_SERVER_RESPONSE_RECEIVED"; +extern const isc::log::MessageID HTTP_SERVER_RESPONSE_RECEIVED_DETAILS = "HTTP_SERVER_RESPONSE_RECEIVED_DETAILS"; +extern const isc::log::MessageID HTTP_SERVER_RESPONSE_SEND = "HTTP_SERVER_RESPONSE_SEND"; +extern const isc::log::MessageID HTTP_SERVER_RESPONSE_SEND_DETAILS = "HTTP_SERVER_RESPONSE_SEND_DETAILS"; + +} // namespace http +} // namespace isc + +namespace { + +const char* values[] = { + "HTTPS_REQUEST_RECEIVE_START", "start receiving request from %1", + "HTTP_BAD_CLIENT_REQUEST_RECEIVED", "bad request received from %1: %2", + "HTTP_BAD_CLIENT_REQUEST_RECEIVED_DETAILS", "detailed information about bad request received from %1:\n%2", + "HTTP_BAD_SERVER_RESPONSE_RECEIVED", "bad response received when communicating with %1: %2", + "HTTP_BAD_SERVER_RESPONSE_RECEIVED_DETAILS", "detailed information about bad response received from %1:\n%2", + "HTTP_CLIENT_MT_STARTED", "HttpClient has been started in multi-threaded mode running %1 threads", + "HTTP_CLIENT_QUEUE_SIZE_GROWING", "queue for URL: %1, now has %2 entries and may be growing too quickly", + "HTTP_CLIENT_REQUEST_RECEIVED", "received HTTP request from %1", + "HTTP_CLIENT_REQUEST_RECEIVED_DETAILS", "detailed information about well-formed request received from %1:\n%2", + "HTTP_CLIENT_REQUEST_SEND", "sending HTTP request %1 to %2", + "HTTP_CLIENT_REQUEST_SEND_DETAILS", "detailed information about request sent to %1:\n%2", + "HTTP_CLIENT_REQUEST_TIMEOUT_OCCURRED", "HTTP request timeout occurred when communicating with %1", + "HTTP_CONNECTION_CLOSE_CALLBACK_FAILED", "Connection close callback threw an exception", + "HTTP_CONNECTION_HANDSHAKE_FAILED", "TLS handshake with %1 failed with %2", + "HTTP_CONNECTION_HANDSHAKE_START", "start TLS handshake with %1 with timeout %2", + "HTTP_CONNECTION_SHUTDOWN", "shutting down HTTP connection from %1", + "HTTP_CONNECTION_SHUTDOWN_FAILED", "shutting down HTTP connection failed", + "HTTP_CONNECTION_STOP", "stopping HTTP connection from %1", + "HTTP_CONNECTION_STOP_FAILED", "stopping HTTP connection failed", + "HTTP_DATA_RECEIVED", "received %1 bytes from %2", + "HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED", "closing persistent connection with %1 as a result of a timeout", + "HTTP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED", "premature connection timeout occurred: in transaction ? %1, transid: %2, current_transid: %3", + "HTTP_REQUEST_RECEIVE_START", "start receiving request from %1 with timeout %2", + "HTTP_SERVER_RESPONSE_RECEIVED", "received HTTP response from %1", + "HTTP_SERVER_RESPONSE_RECEIVED_DETAILS", "detailed information about well-formed response received from %1:\n%2", + "HTTP_SERVER_RESPONSE_SEND", "sending HTTP response %1 to %2", + "HTTP_SERVER_RESPONSE_SEND_DETAILS", "detailed information about response sent to %1:\n%2", + NULL +}; + +const isc::log::MessageInitializer initializer(values); + +} // Anonymous namespace + diff --git a/src/lib/http/http_messages.h b/src/lib/http/http_messages.h new file mode 100644 index 0000000..4ac3030 --- /dev/null +++ b/src/lib/http/http_messages.h @@ -0,0 +1,42 @@ +// File created from ../../../src/lib/http/http_messages.mes + +#ifndef HTTP_MESSAGES_H +#define HTTP_MESSAGES_H + +#include <log/message_types.h> + +namespace isc { +namespace http { + +extern const isc::log::MessageID HTTPS_REQUEST_RECEIVE_START; +extern const isc::log::MessageID HTTP_BAD_CLIENT_REQUEST_RECEIVED; +extern const isc::log::MessageID HTTP_BAD_CLIENT_REQUEST_RECEIVED_DETAILS; +extern const isc::log::MessageID HTTP_BAD_SERVER_RESPONSE_RECEIVED; +extern const isc::log::MessageID HTTP_BAD_SERVER_RESPONSE_RECEIVED_DETAILS; +extern const isc::log::MessageID HTTP_CLIENT_MT_STARTED; +extern const isc::log::MessageID HTTP_CLIENT_QUEUE_SIZE_GROWING; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_RECEIVED; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_RECEIVED_DETAILS; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_SEND; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_SEND_DETAILS; +extern const isc::log::MessageID HTTP_CLIENT_REQUEST_TIMEOUT_OCCURRED; +extern const isc::log::MessageID HTTP_CONNECTION_CLOSE_CALLBACK_FAILED; +extern const isc::log::MessageID HTTP_CONNECTION_HANDSHAKE_FAILED; +extern const isc::log::MessageID HTTP_CONNECTION_HANDSHAKE_START; +extern const isc::log::MessageID HTTP_CONNECTION_SHUTDOWN; +extern const isc::log::MessageID HTTP_CONNECTION_SHUTDOWN_FAILED; +extern const isc::log::MessageID HTTP_CONNECTION_STOP; +extern const isc::log::MessageID HTTP_CONNECTION_STOP_FAILED; +extern const isc::log::MessageID HTTP_DATA_RECEIVED; +extern const isc::log::MessageID HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED; +extern const isc::log::MessageID HTTP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED; +extern const isc::log::MessageID HTTP_REQUEST_RECEIVE_START; +extern const isc::log::MessageID HTTP_SERVER_RESPONSE_RECEIVED; +extern const isc::log::MessageID HTTP_SERVER_RESPONSE_RECEIVED_DETAILS; +extern const isc::log::MessageID HTTP_SERVER_RESPONSE_SEND; +extern const isc::log::MessageID HTTP_SERVER_RESPONSE_SEND_DETAILS; + +} // namespace http +} // namespace isc + +#endif // HTTP_MESSAGES_H diff --git a/src/lib/http/http_messages.mes b/src/lib/http/http_messages.mes new file mode 100644 index 0000000..625c5cb --- /dev/null +++ b/src/lib/http/http_messages.mes @@ -0,0 +1,164 @@ +# Copyright (C) 2016-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/. + +$NAMESPACE isc::http + +% HTTPS_REQUEST_RECEIVE_START start receiving request from %1 +This debug message is issued when the server starts receiving new request +over the established connection. The argument specifies the address +of the remote endpoint. + +% HTTP_BAD_CLIENT_REQUEST_RECEIVED bad request received from %1: %2 +This debug message is issued when an HTTP client sends malformed request to +the server. This includes HTTP requests using unexpected content types, +including malformed JSON etc. The first argument specifies an address of +the remote endpoint which sent the request. The second argument provides +a detailed error message. + +% HTTP_BAD_CLIENT_REQUEST_RECEIVED_DETAILS detailed information about bad request received from %1:\n%2 +This debug message is issued when an HTTP client sends malformed request to +the server. It includes detailed information about the received request +rejected by the server. The first argument specifies an address of +the remote endpoint which sent the request. The second argument provides +a request in the textual format. The request is truncated by the logger +if it is too large to be printed. + +% HTTP_BAD_SERVER_RESPONSE_RECEIVED bad response received when communicating with %1: %2 +This debug message is issued when an HTTP client fails to receive a response +from the server or when this response is malformed. The first argument +specifies the server URL. The second argument provides a detailed error +message. + +% HTTP_BAD_SERVER_RESPONSE_RECEIVED_DETAILS detailed information about bad response received from %1:\n%2 +This debug message is issued when an HTTP client receives malformed response +from the server. The first argument specifies an URL of the server. The +second argument provides a response in the textual format. The request is +truncated by the logger if it is too large to be printed. + +% HTTP_CLIENT_MT_STARTED HttpClient has been started in multi-threaded mode running %1 threads +This debug message is issued when a multi-threaded HTTP client instance has +been created. The argument specifies the maximum number of threads. + +% HTTP_CLIENT_QUEUE_SIZE_GROWING queue for URL: %1, now has %2 entries and may be growing too quickly +This warning message is issued when the queue of pending requests for the +given URL appears to be growing more quickly than the requests can be handled. +It will be emitted periodically as long as the queue size continues to grow. +This may occur with a surge of client traffic creating a momentary backlog +which then subsides as the surge subsides. If it happens continually then +it most likely indicates a deployment configuration that cannot sustain the +client load. + +% HTTP_CLIENT_REQUEST_RECEIVED received HTTP request from %1 +This debug message is issued when the server finished receiving a HTTP +request from the remote endpoint. The address of the remote endpoint is +specified as an argument. + +% HTTP_CLIENT_REQUEST_RECEIVED_DETAILS detailed information about well-formed request received from %1:\n%2 +This debug message is issued when the HTTP server receives a well-formed +request. It includes detailed information about the received request. The +first argument specifies an address of the remote endpoint which sent the +request. The second argument provides the request in the textual format. +The request is truncated by the logger if it is too large to be printed. + +% HTTP_CLIENT_REQUEST_SEND sending HTTP request %1 to %2 +This debug message is issued when the client is starting to send a HTTP +request to a server. The first argument holds basic information +about the request (HTTP version number and status code). The second +argument specifies a URL of the server. + +% HTTP_CLIENT_REQUEST_SEND_DETAILS detailed information about request sent to %1:\n%2 +This debug message is issued right before the client sends an HTTP request +to the server. It includes detailed information about the request. The +first argument specifies an URL of the server to which the request is +being sent. The second argument provides the request in the textual form. +The request is truncated by the logger if it is too large to be printed. + +% HTTP_CLIENT_REQUEST_TIMEOUT_OCCURRED HTTP request timeout occurred when communicating with %1 +This debug message is issued when the HTTP request timeout has occurred and +the server is going to send a response with Http Request timeout status +code. + +% HTTP_CONNECTION_CLOSE_CALLBACK_FAILED Connection close callback threw an exception +This is an error message emitted when the close connection callback +registered on the connection failed unexpectedly. This is a programmatic +error that should be submitted as a bug. + +% HTTP_CONNECTION_HANDSHAKE_FAILED TLS handshake with %1 failed with %2 +This information message is issued when the TLS handshake failed at the +server side. The client address and the error message are displayed. + +% HTTP_CONNECTION_HANDSHAKE_START start TLS handshake with %1 with timeout %2 +This debug message is issued when the server starts the TLS handshake +with the remote endpoint. The first argument specifies the address +of the remote endpoint. The second argument specifies request timeout in +seconds. + +% HTTP_CONNECTION_SHUTDOWN shutting down HTTP connection from %1 +This debug message is issued when one of the HTTP connections is shut down. +The connection can be stopped as a result of an error or after the +successful message exchange with a client. + +% HTTP_CONNECTION_SHUTDOWN_FAILED shutting down HTTP connection failed +This error message is issued when an error occurred during shutting down +a HTTP connection with a client. + +% HTTP_CONNECTION_STOP stopping HTTP connection from %1 +This debug message is issued when one of the HTTP connections is stopped. +The connection can be stopped as a result of an error or after the +successful message exchange with a client. + +% HTTP_CONNECTION_STOP_FAILED stopping HTTP connection failed +This error message is issued when an error occurred during closing a +HTTP connection with a client. + +% HTTP_DATA_RECEIVED received %1 bytes from %2 +This debug message is issued when the server receives a chunk of data from +the remote endpoint. This may include the whole request or only a part +of the request. The first argument specifies the amount of received data. +The second argument specifies an address of the remote endpoint which +produced the data. + +% HTTP_IDLE_CONNECTION_TIMEOUT_OCCURRED closing persistent connection with %1 as a result of a timeout +This debug message is issued when the persistent HTTP connection is being +closed as a result of being idle. + +% HTTP_PREMATURE_CONNECTION_TIMEOUT_OCCURRED premature connection timeout occurred: in transaction ? %1, transid: %2, current_transid: %3 +This warning message is issued when unexpected timeout occurred during the +transaction. This is proven to occur when the system clock is moved manually +or as a result of synchronization with a time server. Any ongoing transactions +will be interrupted. New transactions should be conducted normally. + +% HTTP_REQUEST_RECEIVE_START start receiving request from %1 with timeout %2 +This debug message is issued when the server starts receiving new request +over the established connection. The first argument specifies the address +of the remote endpoint. The second argument specifies request timeout in +seconds. + +% HTTP_SERVER_RESPONSE_RECEIVED received HTTP response from %1 +This debug message is issued when the client finished receiving an HTTP +response from the server. The URL of the server is specified as an argument. + +% HTTP_SERVER_RESPONSE_RECEIVED_DETAILS detailed information about well-formed response received from %1:\n%2 +This debug message is issued when the HTTP client receives a well-formed +response from the server. It includes detailed information about the +received response. The first argument specifies a URL of the server which +sent the response. The second argument provides the response in the textual +format. The response is truncated by the logger if it is too large to +be printed. + +% HTTP_SERVER_RESPONSE_SEND sending HTTP response %1 to %2 +This debug message is issued when the server is starting to send a HTTP +response to a remote endpoint. The first argument holds basic information +about the response (HTTP version number and status code). The second +argument specifies an address of the remote endpoint. + +% HTTP_SERVER_RESPONSE_SEND_DETAILS detailed information about response sent to %1:\n%2 +This debug message is issued right before the server sends a HTTP response +to the client. It includes detailed information about the response. The +first argument specifies an address of the remote endpoint to which the +response is being sent. The second argument provides a response in the +textual form. The response is truncated by the logger if it is too large +to be printed. diff --git a/src/lib/http/http_types.h b/src/lib/http/http_types.h new file mode 100644 index 0000000..68d1c09 --- /dev/null +++ b/src/lib/http/http_types.h @@ -0,0 +1,76 @@ +// 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/. + +#ifndef HTTP_TYPES_H +#define HTTP_TYPES_H + +namespace isc { +namespace http { + +/// @brief HTTP protocol version. +struct HttpVersion { + unsigned major_; ///< Major HTTP version. + unsigned minor_; ///< Minor HTTP version. + + /// @brief Constructor. + /// + /// @param major Major HTTP version. + /// @param minor Minor HTTP version. + explicit HttpVersion(const unsigned major, const unsigned minor) + : major_(major), minor_(minor) { + } + + /// @brief Operator less. + /// + /// @param rhs Version to compare to. + bool operator<(const HttpVersion& rhs) const { + return ((major_ < rhs.major_) || + ((major_ == rhs.major_) && (minor_ < rhs.minor_))); + } + + /// @brief Operator equal. + /// + /// @param rhs Version to compare to. + bool operator==(const HttpVersion& rhs) const { + return ((major_ == rhs.major_) && (minor_ == rhs.minor_)); + } + + /// @brief Operator not equal. + /// + /// @param rhs Version to compare to. + bool operator!=(const HttpVersion& rhs) const { + return (!operator==(rhs)); + } + + /// @name Methods returning @c HttpVersion object encapsulating typical + /// HTTP version numbers. + //@{ + + /// @brief HTTP version 1.0. + static const HttpVersion& HTTP_10() { + static HttpVersion ver(1, 0); + return (ver); + }; + + /// @brief HTTP version 1.1. + static const HttpVersion& HTTP_11() { + static HttpVersion ver(1, 1); + return (ver); + } + + /// @brief HTTP version 2.0. + static const HttpVersion& HTTP_20() { + static HttpVersion ver(2, 0); + return (ver); + } + + //@} +}; + +} // end of namespace isc::http +} // end of namespace isc + +#endif diff --git a/src/lib/http/listener.cc b/src/lib/http/listener.cc new file mode 100644 index 0000000..2e6d2e1 --- /dev/null +++ b/src/lib/http/listener.cc @@ -0,0 +1,56 @@ +// 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 <http/listener.h> +#include <http/listener_impl.h> + +using namespace isc::asiolink; + +namespace isc { +namespace http { + +HttpListener::HttpListener(IOService& io_service, + const asiolink::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) + : impl_(new HttpListenerImpl(io_service, server_address, server_port, + tls_context, creator_factory, + request_timeout.value_, + idle_timeout.value_)) { +} + +HttpListener::~HttpListener() { + stop(); +} + +IOAddress +HttpListener::getLocalAddress() const { + return (impl_->getEndpoint().getAddress()); +} + +uint16_t +HttpListener::getLocalPort() const { + return (impl_->getEndpoint().getPort()); +} + +void +HttpListener::start() { + impl_->start(); +} + +void +HttpListener::stop() { + impl_->stop(); +} + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/listener.h b/src/lib/http/listener.h new file mode 100644 index 0000000..8965f29 --- /dev/null +++ b/src/lib/http/listener.h @@ -0,0 +1,147 @@ +// 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 HTTP_LISTENER_H +#define HTTP_LISTENER_H + +#include <asiolink/io_address.h> +#include <asiolink/io_service.h> +#include <asiolink/crypto_tls.h> +#include <exceptions/exceptions.h> +#include <http/response_creator_factory.h> +#include <boost/shared_ptr.hpp> +#include <cstdint> + +namespace isc { +namespace http { + +/// @brief A generic error raised by the @ref HttpListener class. +class HttpListenerError : public Exception { +public: + HttpListenerError(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) { }; +}; + +/// @brief HttpListener implementation. +class HttpListenerImpl; + +/// @brief HTTP listener. +/// +/// This class is an entry point to the use of HTTP services in Kea. +/// It creates a transport acceptor service on the specified address and +/// port and listens to the incoming HTTP connections. The constructor +/// receives a pointer to the implementation of the +/// @ref HttpResponseCreatorFactory, which is used by the @ref HttpListener +/// to create/retrieve an instance of the @ref HttpResponseCreator when the +/// new HTTP response needs to be generated. The @ref HttpResponseCreator +/// creates an object derived from the @ref HttpResponse class, encapsulating +/// a HTTP response following some specific rules, e.g. having +/// "application/json" content type. +/// +/// When the listener is started it creates an instance of a @ref HttpConnection +/// and stores them in the pool of active connections. The @ref HttpConnection +/// is responsible for managing the next connection received and receiving the +/// HTTP request and sending appropriate response. The listener can handle +/// many HTTP connections simultaneously. +/// +/// When the @ref HttpListener::stop is invoked, all active connections are +/// closed and the listener stops accepting new connections. +class HttpListener { +public: + + /// @brief HTTP request timeout value. + struct RequestTimeout { + /// @brief Constructor. + /// + /// @param value Request timeout value in milliseconds. + explicit RequestTimeout(long value) + : value_(value) { + } + long value_; ///< Request timeout value specified. + }; + + /// @brief Idle connection timeout. + struct IdleTimeout { + /// @brief Constructor. + /// + /// @param value Connection idle timeout value in milliseconds. + explicit IdleTimeout(long value) + : value_(value) { + } + long value_; ///< Connection idle timeout value specified. + }; + + /// @brief Constructor. + /// + /// This constructor creates new server endpoint using the specified IP + /// address and port. It also validates other specified parameters. + /// + /// This constructor does not start accepting new connections! To start + /// accepting connections run @ref HttpListener::start. + /// + /// @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. + HttpListener(asiolink::IOService& io_service, + const asiolink::IOAddress& server_address, + const unsigned short server_port, + const asiolink::TlsContextPtr& tls_context, + const HttpResponseCreatorFactoryPtr& creator_factory, + const RequestTimeout& request_timeout, + const IdleTimeout& idle_timeout); + + /// @brief Destructor. + /// + /// Stops all active connections and closes transport acceptor service. + ~HttpListener(); + + /// @brief Returns local address on which server is listening. + asiolink::IOAddress getLocalAddress() const; + + /// @brief Returns local port on which server is listening. + uint16_t getLocalPort() const; + + /// @brief Starts accepting new connections. + /// + /// This method starts accepting and handling new HTTP connections on + /// the IP address and port number specified in the constructor. + /// + /// If the method is invoked successfully, it must not be invoked again + /// until @ref HttpListener::stop is called. + /// + /// @throw HttpListenerError if an error occurred. + void start(); + + /// @brief Stops all active connections and shuts down the service. + void stop(); + +protected: + + /// @brief Pointer to the implementation of the @ref HttpListener. + boost::shared_ptr<HttpListenerImpl> impl_; +}; + +/// @brief Pointer to the @ref HttpListener. +typedef boost::shared_ptr<HttpListener> HttpListenerPtr; + +/// @brief Pointer to the const @ref HttpListener. +typedef boost::shared_ptr<const HttpListener> ConstHttpListenerPtr; + +} // end of namespace isc::http +} // end of namespace isc + +#endif diff --git a/src/lib/http/listener_impl.cc b/src/lib/http/listener_impl.cc new file mode 100644 index 0000000..fdcbdd0 --- /dev/null +++ b/src/lib/http/listener_impl.cc @@ -0,0 +1,125 @@ +// Copyright (C) 2019-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 <http/connection_pool.h> +#include <http/listener.h> +#include <http/listener_impl.h> + +using namespace isc::asiolink; +namespace ph = std::placeholders; + +namespace isc { +namespace http { + +HttpListenerImpl::HttpListenerImpl(IOService& io_service, + const asiolink::IOAddress& server_address, + const unsigned short server_port, + const TlsContextPtr& tls_context, + const HttpResponseCreatorFactoryPtr& creator_factory, + const long request_timeout, + const long idle_timeout) + : io_service_(io_service), tls_context_(tls_context), acceptor_(), + endpoint_(), connections_(), + creator_factory_(creator_factory), + request_timeout_(request_timeout), idle_timeout_(idle_timeout) { + // Create the TCP or TLS acceptor. + if (!tls_context) { + acceptor_.reset(new HttpAcceptor(io_service)); + } else { + acceptor_.reset(new HttpsAcceptor(io_service)); + } + + // Try creating an endpoint. This may cause exceptions. + try { + endpoint_.reset(new TCPEndpoint(server_address, server_port)); + + } catch (...) { + isc_throw(HttpListenerError, "unable to create TCP endpoint for " + << server_address << ":" << server_port); + } + + // The factory must not be null. + if (!creator_factory_) { + isc_throw(HttpListenerError, "HttpResponseCreatorFactory must not" + " be null"); + } + + // Request timeout is signed and must be greater than 0. + if (request_timeout_ <= 0) { + isc_throw(HttpListenerError, "Invalid desired HTTP request timeout " + << request_timeout_); + } + + // Idle persistent connection timeout is signed and must be greater than 0. + if (idle_timeout_ <= 0) { + isc_throw(HttpListenerError, "Invalid desired HTTP idle persistent connection" + " timeout " << idle_timeout_); + } +} + +const TCPEndpoint& +HttpListenerImpl::getEndpoint() const { + return (*endpoint_); +} + +void +HttpListenerImpl::start() { + try { + acceptor_->open(*endpoint_); + acceptor_->setOption(HttpAcceptor::ReuseAddress(true)); + acceptor_->bind(*endpoint_); + acceptor_->listen(); + + } catch (const boost::system::system_error& ex) { + stop(); + isc_throw(HttpListenerError, "unable to setup TCP acceptor for " + "listening to the incoming HTTP requests: " << ex.what()); + } + + accept(); +} + +void +HttpListenerImpl::stop() { + connections_.stopAll(); + acceptor_->close(); +} + +void +HttpListenerImpl::accept() { + // In some cases we may need HttpResponseCreator instance per connection. + // But, the factory may also return the same instance each time. It + // depends on the use case. + HttpResponseCreatorPtr response_creator = creator_factory_->create(); + HttpAcceptorCallback acceptor_callback = + std::bind(&HttpListenerImpl::acceptHandler, this, ph::_1); + HttpConnectionPtr conn = createConnection(response_creator, + acceptor_callback); + // Add this new connection to the pool. + connections_.start(conn); +} + +void +HttpListenerImpl::acceptHandler(const boost::system::error_code&) { + // The new connection has arrived. Set the acceptor to continue + // accepting new connections. + accept(); +} + +HttpConnectionPtr +HttpListenerImpl::createConnection(const HttpResponseCreatorPtr& response_creator, + const HttpAcceptorCallback& callback) { + HttpConnectionPtr + conn(new HttpConnection(io_service_, acceptor_, tls_context_, + connections_, response_creator, callback, + request_timeout_, idle_timeout_)); + return (conn); +} + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/listener_impl.h b/src/lib/http/listener_impl.h new file mode 100644 index 0000000..4ad1960 --- /dev/null +++ b/src/lib/http/listener_impl.h @@ -0,0 +1,136 @@ +// Copyright (C) 2019-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 HTTP_LISTENER_IMPL_H +#define HTTP_LISTENER_IMPL_H + +#include <asiolink/io_service.h> +#include <asiolink/io_address.h> +#include <asiolink/tcp_endpoint.h> +#include <http/connection.h> +#include <http/connection_pool.h> +#include <http/response_creator_factory.h> +#include <boost/scoped_ptr.hpp> + +namespace isc { +namespace http { + +/// @brief Implementation of the @ref HttpListener. +class HttpListenerImpl { +public: + + /// @brief Constructor. + /// + /// This constructor creates new server endpoint using the specified IP + /// address and port. It also validates other specified parameters. + /// + /// This constructor does not start accepting new connections! To start + /// accepting connections run @ref HttpListener::start. + /// + /// @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. + HttpListenerImpl(asiolink::IOService& io_service, + const asiolink::IOAddress& server_address, + const unsigned short server_port, + const asiolink::TlsContextPtr& tls_context, + const HttpResponseCreatorFactoryPtr& creator_factory, + const long request_timeout, + const long idle_timeout); + + /// @brief Virtual destructor. + virtual ~HttpListenerImpl() { + } + + /// @brief Returns reference to the current listener endpoint. + const asiolink::TCPEndpoint& getEndpoint() const; + + /// @brief Starts accepting new connections. + /// + /// This method starts accepting and handling new HTTP connections on + /// the IP address and port number specified in the constructor. + /// + /// If the method is invoked successfully, it must not be invoked again + /// until @ref HttpListener::stop is called. + /// + /// @throw HttpListenerError if an error occurred. + void start(); + + /// @brief Stops all active connections and shuts down the service. + void stop(); + +protected: + + /// @brief Creates @ref HttpConnection instance and adds it to the + /// pool of active connections. + /// + /// The next accepted connection will be handled by this instance. + void accept(); + + /// @brief Callback invoked when the new connection is accepted. + /// + /// It calls @c HttpListener::accept to create new @c HttpConnection + /// instance. + /// + /// @param ec Error code passed to the handler. This is currently ignored. + void acceptHandler(const boost::system::error_code& ec); + + /// @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); + + /// @brief Reference to the IO service. + asiolink::IOService& io_service_; + + /// @brief TLS context. + asiolink::TlsContextPtr tls_context_; + + /// @brief Acceptor instance. + HttpAcceptorPtr acceptor_; + + /// @brief Pointer to the endpoint representing IP address and port on + /// which the service is running. + boost::scoped_ptr<asiolink::TCPEndpoint> endpoint_; + + /// @brief Pool of active connections. + HttpConnectionPool connections_; + + /// @brief Pointer to the @ref HttpResponseCreatorFactory. + HttpResponseCreatorFactoryPtr creator_factory_; + + /// @brief Timeout for HTTP Request Timeout desired. + long request_timeout_; + + /// @brief Timeout after which idle persistent connection is closed by + /// the server. + long idle_timeout_; +}; + + +} // end of namespace isc::http +} // end of namespace isc + +#endif // HTTP_LISTENER_IMPL_H diff --git a/src/lib/http/post_request.cc b/src/lib/http/post_request.cc new file mode 100644 index 0000000..1658985 --- /dev/null +++ b/src/lib/http/post_request.cc @@ -0,0 +1,33 @@ +// Copyright (C) 2016-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/post_request.h> + +namespace isc { +namespace http { + +PostHttpRequest::PostHttpRequest() + : HttpRequest() { + requireHttpMethod(Method::HTTP_POST); + requireHeader("Content-Length"); + requireHeader("Content-Type"); +} + +PostHttpRequest::PostHttpRequest(const Method& method, const std::string& uri, + const HttpVersion& version, + const HostHttpHeader& host_header, + const BasicHttpAuthPtr& basic_auth) + : HttpRequest(method, uri, version, host_header, basic_auth) { + requireHttpMethod(Method::HTTP_POST); + requireHeader("Content-Length"); + requireHeader("Content-Type"); +} + + +} // namespace http +} // namespace isc diff --git a/src/lib/http/post_request.h b/src/lib/http/post_request.h new file mode 100644 index 0000000..095fdab --- /dev/null +++ b/src/lib/http/post_request.h @@ -0,0 +1,53 @@ +// Copyright (C) 2016-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/. + +#ifndef HTTP_POST_REQUEST_H +#define HTTP_POST_REQUEST_H + +#include <http/request.h> +#include <boost/shared_ptr.hpp> + +namespace isc { +namespace http { + +class PostHttpRequest; + +/// @brief Pointer to @ref PostHttpRequest. +typedef boost::shared_ptr<PostHttpRequest> PostHttpRequestPtr; +/// @brief Pointer to const @ref PostHttpRequest. +typedef boost::shared_ptr<const PostHttpRequest> ConstPostHttpRequestPtr; + +/// @brief Represents HTTP POST request. +/// +/// Instructs the parent class to require: +/// - HTTP POST message type, +/// - Content-Length header, +/// - Content-Type header. +class PostHttpRequest : public HttpRequest { +public: + + /// @brief Constructor for inbound HTTP request. + PostHttpRequest(); + + /// @brief Constructor for outbound HTTP request. + /// + /// @param method HTTP method, e.g. POST. + /// @param uri URI. + /// @param version HTTP version. + /// @param host_header Host header to be included in the request. The default + /// is the empty Host header. + /// @param basic_auth Basic HTTP authentication credential. The default + /// is no authentication. + PostHttpRequest(const Method& method, const std::string& uri, const HttpVersion& version, + const HostHttpHeader& host_header = HostHttpHeader(), + const BasicHttpAuthPtr& basic_auth = BasicHttpAuthPtr()); +}; + + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/post_request_json.cc b/src/lib/http/post_request_json.cc new file mode 100644 index 0000000..909a901 --- /dev/null +++ b/src/lib/http/post_request_json.cc @@ -0,0 +1,102 @@ +// 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/post_request_json.h> + +using namespace isc::data; + +namespace isc { +namespace http { + +PostHttpRequestJson::PostHttpRequestJson() + : PostHttpRequest(), json_() { + requireHeaderValue("Content-Type", "application/json"); +} + +PostHttpRequestJson::PostHttpRequestJson(const Method& method, const std::string& uri, + const HttpVersion& version, + const HostHttpHeader& host_header, + const BasicHttpAuthPtr& basic_auth) + : PostHttpRequest(method, uri, version, host_header, basic_auth) { + requireHeaderValue("Content-Type", "application/json"); + context()->headers_.push_back(HttpHeaderContext("Content-Type", "application/json")); +} + + +void +PostHttpRequestJson::finalize() { + if (!created_) { + create(); + } + + // Parse JSON body and store. + parseBodyAsJson(); + finalized_ = true; +} + +void +PostHttpRequestJson::reset() { + PostHttpRequest::reset(); + json_.reset(); +} + +ConstElementPtr +PostHttpRequestJson::getBodyAsJson() const { + checkFinalized(); + return (json_); +} + +void +PostHttpRequestJson::setBodyAsJson(const data::ConstElementPtr& body) { + if (body) { + context_->body_ = body->str(); + json_ = body; + + } else { + context_->body_.clear(); + } +} + +ConstElementPtr +PostHttpRequestJson::getJsonElement(const std::string& element_name) const { + try { + ConstElementPtr body = getBodyAsJson(); + if (body) { + const std::map<std::string, ConstElementPtr>& map_value = body->mapValue(); + auto map_element = map_value.find(element_name); + if (map_element != map_value.end()) { + return (map_element->second); + } + } + + } catch (const std::exception& ex) { + isc_throw(HttpRequestJsonError, "unable to get JSON element " + << element_name << ": " << ex.what()); + } + return (ConstElementPtr()); +} + +void +PostHttpRequestJson::parseBodyAsJson() { + try { + // Only parse the body if it hasn't been parsed yet. + if (!json_ && !context_->body_.empty()) { + ElementPtr json = Element::fromJSON(context_->body_); + if (!remote_.empty() && (json->getType() == Element::map)) { + json->set("remote-address", Element::create(remote_)); + } + json_ = json; + } + } catch (const std::exception& ex) { + isc_throw(HttpRequestJsonError, "unable to parse the body of the HTTP" + " request: " << ex.what()); + } +} + +} // namespace http +} // namespace isc diff --git a/src/lib/http/post_request_json.h b/src/lib/http/post_request_json.h new file mode 100644 index 0000000..7a2faba --- /dev/null +++ b/src/lib/http/post_request_json.h @@ -0,0 +1,110 @@ +// Copyright (C) 2016-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/. + +#ifndef HTTP_POST_REQUEST_JSON_H +#define HTTP_POST_REQUEST_JSON_H + +#include <cc/data.h> +#include <exceptions/exceptions.h> +#include <http/post_request.h> +#include <boost/shared_ptr.hpp> +#include <string> + +namespace isc { +namespace http { + +/// @brief Exception thrown when body of the HTTP message is not JSON. +class HttpRequestJsonError : public HttpRequestError { +public: + HttpRequestJsonError(const char* file, size_t line, const char* what) : + HttpRequestError(file, line, what) { }; +}; + +class PostHttpRequestJson; + +/// @brief Pointer to @ref PostHttpRequestJson. +typedef boost::shared_ptr<PostHttpRequestJson> PostHttpRequestJsonPtr; + +/// @brief Represents HTTP POST request with JSON body. +/// +/// In addition to the requirements specified by the @ref PostHttpRequest +/// this class requires that the "Content-Type" is "application/json". +/// +/// This class provides methods to parse and retrieve JSON data structures. +class PostHttpRequestJson : public PostHttpRequest { +public: + + /// @brief Constructor for inbound HTTP request. + explicit PostHttpRequestJson(); + + /// @brief Constructor for outbound HTTP request. + /// + /// This constructor adds "Content-Type" header with the value of + /// "application/json" to the context. + /// + /// @param method HTTP method, e.g. POST. + /// @param uri URI. + /// @param version HTTP version. + /// @param host_header Host header to be included in the request. The default + /// is the empty Host header. + /// @param basic_auth Basic HTTP authentication credential. The default + /// is no authentication. + explicit PostHttpRequestJson(const Method& method, const std::string& uri, + const HttpVersion& version, + const HostHttpHeader& host_header = HostHttpHeader(), + const BasicHttpAuthPtr& basic_auth = BasicHttpAuthPtr()); + + /// @brief Complete parsing of the HTTP request. + /// + /// This method parses the JSON body into the structure of + /// @ref data::ConstElementPtr objects. + virtual void finalize(); + + /// @brief Reset the state of the object. + virtual void reset(); + + /// @brief Retrieves JSON body. + /// + /// @return Pointer to the root element of the JSON structure. + /// @throw HttpRequestJsonError if an error occurred. + data::ConstElementPtr getBodyAsJson() const; + + /// @brief Sets JSON body for an outbound message. + /// + /// Note that this method copies the pointer to the body, rather than + /// the entire data structure. Thus, the original object should not be + /// modified after this method is called. If the specified pointer is + /// null, the empty body is set. + /// + /// @param body JSON structure to be used as a body. + void setBodyAsJson(const data::ConstElementPtr& body); + + /// @brief Retrieves a single JSON element. + /// + /// The element must be at top level of the JSON structure. + /// + /// @param element_name Element name. + /// + /// @return Pointer to the specified element or NULL if such element + /// doesn't exist. + /// @throw HttpRequestJsonError if an error occurred. + data::ConstElementPtr getJsonElement(const std::string& element_name) const; + +protected: + + /// @brief Interprets body as JSON, which can be later retrieved using + /// data element objects. + void parseBodyAsJson(); + + /// @brief Pointer to the parsed JSON body. + data::ConstElementPtr json_; + +}; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/request.cc b/src/lib/http/request.cc new file mode 100644 index 0000000..f753fea --- /dev/null +++ b/src/lib/http/request.cc @@ -0,0 +1,285 @@ +// 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/request.h> +#include <boost/algorithm/string.hpp> +#include <boost/lexical_cast.hpp> +#include <sstream> + +namespace { + +/// @brief New line (CRLF). +const std::string crlf = "\r\n"; + +} + +namespace isc { +namespace http { + +bool HttpRequest::recordSubject_ = false; + +bool HttpRequest::recordIssuer_ = false; + +bool HttpRequest::recordBasicAuth_ = false; + +HttpRequest::HttpRequest() + : HttpMessage(INBOUND), required_methods_(), + method_(Method::HTTP_METHOD_UNKNOWN), + context_(new HttpRequestContext()), + remote_(""), tls_(false), subject_(""), issuer_(""), + basic_auth_(""), custom_("") { +} + +HttpRequest::HttpRequest(const Method& method, + const std::string& uri, + const HttpVersion& version, + const HostHttpHeader& host_header, + const BasicHttpAuthPtr& basic_auth) + : HttpMessage(OUTBOUND), required_methods_(), + method_(Method::HTTP_METHOD_UNKNOWN), + context_(new HttpRequestContext()), + remote_(""), tls_(false), subject_(""), issuer_(""), + basic_auth_(""), custom_("") { + context()->method_ = methodToString(method); + context()->uri_ = uri; + context()->http_version_major_ = version.major_; + context()->http_version_minor_ = version.minor_; + // The Host header is mandatory in HTTP/1.1 and should be placed before + // any other headers. We also include it for HTTP/1.0 as it doesn't + // harm to include it. + context()->headers_.push_back(HttpHeaderContext(host_header.getName(), + host_header.getValue())); + if (basic_auth) { + context()->headers_.push_back(BasicAuthHttpHeaderContext(*basic_auth)); + } +} + +void +HttpRequest::requireHttpMethod(const HttpRequest::Method& method) { + required_methods_.insert(method); +} + +void +HttpRequest::create() { + try { + // The RequestParser doesn't validate the method name. Thus, this + // may throw an exception. But, we're fine with lower case names, + // e.g. get, post etc. + method_ = methodFromString(context_->method_); + + // Check if the method is allowed for this request. + if (!inRequiredSet(method_, required_methods_)) { + isc_throw(BadValue, "use of HTTP " << methodToString(method_) + << " not allowed"); + } + + http_version_.major_ = context_->http_version_major_; + http_version_.minor_ = context_->http_version_minor_; + + // Check if the HTTP version is allowed for this request. + if (!inRequiredSet(http_version_, required_versions_)) { + isc_throw(BadValue, "use of HTTP version " + << http_version_.major_ << "." + << http_version_.minor_ + << " not allowed"); + } + + // Copy headers from the context. + for (auto header = context_->headers_.begin(); + header != context_->headers_.end(); + ++header) { + HttpHeaderPtr hdr(new HttpHeader(header->name_, header->value_)); + headers_[hdr->getLowerCaseName()] = hdr; + } + + if (getDirection() == HttpMessage::OUTBOUND) { + HttpHeaderPtr hdr(new HttpHeader("Content-Length", + boost::lexical_cast<std::string>(context_->body_.length()))); + headers_["content-length"] = hdr; + } + + // Iterate over required headers and check that they exist + // in the HTTP request. + for (auto req_header = required_headers_.begin(); + req_header != required_headers_.end(); + ++req_header) { + auto header = headers_.find(req_header->first); + if (header == headers_.end()) { + isc_throw(BadValue, "required header " << req_header->first + << " not found in the HTTP request"); + } else if (!req_header->second->getValue().empty() && + !header->second->isValueEqual(req_header->second->getValue())) { + // If specific value is required for the header, check + // that the value in the HTTP request matches it. + isc_throw(BadValue, "required header's " << header->first + << " value is " << req_header->second->getValue() + << ", but " << header->second->getValue() << " was found"); + } + } + + } catch (const std::exception& ex) { + // Reset the state of the object if we failed at any point. + reset(); + isc_throw(HttpRequestError, ex.what()); + } + + // All ok. + created_ = true; +} + +void +HttpRequest::finalize() { + if (!created_) { + create(); + } + + // Copy the body from the context. Derive classes may further + // interpret the body contents, e.g. against the Content-Type. + finalized_ = true; +} + +void +HttpRequest::reset() { + created_ = false; + finalized_ = false; + method_ = HttpRequest::Method::HTTP_METHOD_UNKNOWN; + headers_.clear(); +} + +HttpRequest::Method +HttpRequest::getMethod() const { + checkCreated(); + return (method_); +} + +std::string +HttpRequest::getUri() const { + checkCreated(); + return (context_->uri_); +} + +std::string +HttpRequest::getBody() const { + checkFinalized(); + return (context_->body_); +} + +std::string +HttpRequest::toBriefString() const { + checkFinalized(); + + std::ostringstream s; + s << methodToString(getMethod()) << " " << getUri() << " HTTP/" << + getHttpVersion().major_ << "." << getHttpVersion().minor_; + return (s.str()); +} + +std::string +HttpRequest::toString() const { + checkFinalized(); + + std::ostringstream s; + // HTTP method, URI and version number. + s << toBriefString() << crlf; + + // Host header must go first. + HttpHeaderPtr host_header; + try { + host_header = getHeader("Host"); + if (host_header) { + s << host_header->getName() << ": " << host_header->getValue() << crlf; + } + + } catch (...) { + // impossible condition + } + + // Add all other headers. + for (auto header_it = headers_.cbegin(); header_it != headers_.cend(); + ++header_it) { + if (header_it->second->getName() != "Host") { + s << header_it->second->getName() << ": " << header_it->second->getValue() + << crlf; + } + } + + s << crlf; + + s << getBody(); + + return (s.str()); +} + +bool +HttpRequest::isPersistent() const { + HttpHeaderPtr conn; + + try { + conn = getHeader("connection"); + + } catch (...) { + // If there is an exception, it means that the header was not found. + } + + std::string conn_value; + if (conn) { + conn_value = conn->getLowerCaseValue(); + } + + HttpVersion ver = getHttpVersion(); + + return (((ver == HttpVersion::HTTP_10()) && (conn_value == "keep-alive")) || + ((HttpVersion::HTTP_10() < ver) && (conn_value.empty() || (conn_value != "close")))); +} + +HttpRequest::Method +HttpRequest::methodFromString(std::string method) const { + boost::to_upper(method); + if (method == "GET") { + return (Method::HTTP_GET); + } else if (method == "POST") { + return (Method::HTTP_POST); + } else if (method == "HEAD") { + return (Method::HTTP_HEAD); + } else if (method == "PUT") { + return (Method::HTTP_PUT); + } else if (method == "DELETE") { + return (Method::HTTP_DELETE); + } else if (method == "OPTIONS") { + return (Method::HTTP_OPTIONS); + } else if (method == "CONNECT") { + return (Method::HTTP_CONNECT); + } else { + isc_throw(HttpRequestError, "unknown HTTP method " << method); + } +} + +std::string +HttpRequest::methodToString(const HttpRequest::Method& method) const { + switch (method) { + case Method::HTTP_GET: + return ("GET"); + case Method::HTTP_POST: + return ("POST"); + case Method::HTTP_HEAD: + return ("HEAD"); + case Method::HTTP_PUT: + return ("PUT"); + case Method::HTTP_DELETE: + return ("DELETE"); + case Method::HTTP_OPTIONS: + return ("OPTIONS"); + case Method::HTTP_CONNECT: + return ("CONNECT"); + default: + return ("unknown HTTP method"); + } +} + +} +} diff --git a/src/lib/http/request.h b/src/lib/http/request.h new file mode 100644 index 0000000..014e6e4 --- /dev/null +++ b/src/lib/http/request.h @@ -0,0 +1,312 @@ +// 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/. + +#ifndef HTTP_REQUEST_H +#define HTTP_REQUEST_H + +#include <hooks/callout_handle_associate.h> +#include <http/basic_auth.h> +#include <http/http_message.h> +#include <http/request_context.h> +#include <boost/shared_ptr.hpp> +#include <cstdint> + +namespace isc { +namespace http { + +/// @brief Generic exception thrown by @ref HttpRequest class. +class HttpRequestError : public HttpMessageError { +public: + HttpRequestError(const char* file, size_t line, const char* what) : + HttpMessageError(file, line, what) { }; +}; + +class HttpRequest; + +/// @brief Pointer to the @ref HttpRequest object. +typedef boost::shared_ptr<HttpRequest> HttpRequestPtr; + +/// @brief Represents HTTP request message. +/// +/// This derivation of the @c HttpMessage class is specialized to represent +/// HTTP requests. This class provides two constructors for creating an inbound +/// and outbound request instance respectively. This class is associated with +/// an instance of the @c HttpRequestContext, which is used to provide request +/// specific values, such as: HTTP method, version, URI and headers. +/// +/// The derivations of this class provide specializations and specify the +/// HTTP methods, versions and headers supported/required in the specific use +/// cases. For example, the @c PostHttpRequest class derives from @c HttpRequest +/// and it requires that request uses POST method. The @c PostHttpRequestJson, +/// which derives from @c PostHttpRequest requires that the POST message +/// includes body holding a JSON structure and provides methods to parse the +/// JSON body. +/// +/// Objects of this class is used to record some parameters for access control: +/// - the remote address +/// - the use of TLS +/// - the first commonName of the SubjectName of the client certificate +/// - the first commonName of the IssuerName of the client certificate +/// - the user ID of the basic HTTP authentication +/// - a custom value +/// +/// Callouts are associated to the request. +class HttpRequest : public HttpMessage, public hooks::CalloutHandleAssociate { +public: + + /// @brief HTTP methods. + enum class Method { + HTTP_GET, + HTTP_POST, + HTTP_HEAD, + HTTP_PUT, + HTTP_DELETE, + HTTP_OPTIONS, + HTTP_CONNECT, + HTTP_METHOD_UNKNOWN + }; + + /// @brief Constructor for inbound HTTP request. + HttpRequest(); + + /// @brief Constructor for outbound HTTP request. + /// + /// The constructor always includes Host header in the request, regardless + /// of the HTTP version used. + /// + /// @param method HTTP method, e.g. POST. + /// @param uri URI. + /// @param version HTTP version. + /// @param host_header Host header to be included in the request. The default + /// is the empty Host header. + /// @param basic_auth Basic HTTP authentication credential. The default + /// is no authentication. + HttpRequest(const Method& method, const std::string& uri, const HttpVersion& version, + const HostHttpHeader& host_header = HostHttpHeader(), + const BasicHttpAuthPtr& basic_auth = BasicHttpAuthPtr()); + + /// @brief Returns pointer to the @ref HttpRequestContext. + /// + /// The context holds intermediate data for creating a request. The request + /// parser stores parsed raw data in the context. When parsing is finished, + /// the data are validated and committed into the @c HttpRequest. + /// + /// @return Pointer to the underlying @ref HttpRequestContext. + const HttpRequestContextPtr& context() const { + return (context_); + } + + /// @brief Returns remote address. + /// + /// @return remote address from HTTP connection + /// getRemote method. + std::string getRemote() const { + return (remote_); + } + + /// @brief Set remote address. + /// + /// @param remote Remote end-point address in textual form. + void setRemote(const std::string& remote) { + remote_ = remote; + } + + /// @brief Specifies an HTTP method allowed for the request. + /// + /// Allowed methods must be specified prior to calling @ref create method. + /// If no method is specified, all methods are supported. + /// + /// @param method HTTP method allowed for the request. + void requireHttpMethod(const HttpRequest::Method& method); + + /// @brief Commits information held in the context into the request. + /// + /// This function reads HTTP method, version and headers from the context + /// and validates their values. For the outbound messages, it automatically + /// appends Content-Length header to the request, based on the length of the + /// request body. + /// + /// @throw HttpRequestError if the parsed request doesn't meet the specified + /// requirements for it. + virtual void create(); + + /// @brief Completes creation of the HTTP request. + /// + /// This method marks the message as finalized. The outbound request may now be + /// sent over the TCP socket. The information from the inbound message may be + /// read, including the request body. + virtual void finalize(); + + /// @brief Reset the state of the object. + virtual void reset(); + + /// @brief Returns HTTP method of the request. + Method getMethod() const; + + /// @brief Returns HTTP request URI. + std::string getUri() const; + + + /// @brief Returns HTTP message body as string. + std::string getBody() const; + + /// @brief Returns HTTP method, URI and HTTP version as a string. + std::string toBriefString() const; + + /// @brief Returns HTTP message as string. + /// + /// This method is called to generate the outbound HTTP message. Make + /// sure to call @c finalize prior to calling this method. + virtual std::string toString() const; + + /// @brief Checks if the client has requested persistent connection. + /// + /// For the HTTP/1.0 case, the connection is persistent if the client has + /// included Connection: keep-alive header. For the HTTP/1.1 case, the + /// connection is assumed to be persistent unless Connection: close header + /// has been included. + /// + /// @return true if the client has requested persistent connection, false + /// otherwise. + bool isPersistent() const; + + /// Access control parameters: get/set methods. + + /// @brief Returns recorded TLS usage. + /// + /// @return recorded TLS usage. + bool getTls() const { + return (tls_); + } + + /// @brief Set (record) TLS usage. + /// + /// @param tls the TLS usage. + void setTls(bool tls) { + tls_ = tls; + } + + /// @brief Returns recorded subject name. + /// + /// @return recorded subject name. + std::string getSubject() const { + return (subject_); + } + + /// @brief Set (record) subject name. + /// + /// @param subject the subject name. + void setSubject(const std::string& subject) { + subject_ = subject; + } + + /// @brief Returns recorded issuer name. + /// + /// @return recorded issuer name. + std::string getIssuer() const { + return (issuer_); + } + + /// @brief Set (record) issuer name. + /// + /// @param issuer the issuer name. + void setIssuer(const std::string& issuer) { + issuer_ = issuer; + } + + /// @brief Returns recorded basic auth. + /// + /// @return recorded basic auth. + std::string getBasicAuth() const { + return (basic_auth_); + } + + /// @brief Set (record) basic auth. + /// + /// @param basic_auth the basic auth. + void setBasicAuth(const std::string& basic_auth) { + basic_auth_ = basic_auth; + } + + /// @brief Returns recorded custom name. + /// + /// @return recorded custom name. + std::string getCustom() const { + return (custom_); + } + + /// @brief Set (record) custom name. + /// + /// @param custom the custom name. + void setCustom(const std::string& custom) { + custom_ = custom; + } + + /// Access control parameters: Flags which indicate what information to record. + /// Remote address and TLS usage are always recorded. + + /// @brief Record subject name. + static bool recordSubject_; + + /// @brief Record issuer name. + static bool recordIssuer_; + + /// @brief Record basic auth. + static bool recordBasicAuth_; + +protected: + + /// @brief Converts HTTP method specified in textual format to @ref Method. + /// + /// @param method HTTP method specified in the textual format. This value + /// is case insensitive. + /// + /// @return HTTP method as enum. + /// @throw HttpRequestError if unknown method specified. + Method methodFromString(std::string method) const; + + /// @brief Converts HTTP method to string. + /// + /// @param method HTTP method specified as enum. + /// + /// @return HTTP method as string. + std::string methodToString(const HttpRequest::Method& method) const; + + /// @brief Set of required HTTP methods. + /// + /// If the set is empty, all methods are allowed. + std::set<Method> required_methods_; + + /// @brief HTTP method of the request. + Method method_; + + /// @brief Pointer to the @ref HttpRequestContext holding parsed + /// data. + HttpRequestContextPtr context_; + + /// @brief Remote address. + std::string remote_; + + /// @brief TLS usage. + bool tls_; + + /// @brief Subject name. + std::string subject_; + + /// @brief Issuer name. + std::string issuer_; + + /// @brief Basic auth. + std::string basic_auth_; + + /// @brief Custom name. + std::string custom_; +}; + +} +} + +#endif diff --git a/src/lib/http/request_context.h b/src/lib/http/request_context.h new file mode 100644 index 0000000..bcbacda --- /dev/null +++ b/src/lib/http/request_context.h @@ -0,0 +1,44 @@ +// Copyright (C) 2016 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_CONTEXT_H +#define HTTP_REQUEST_CONTEXT_H + +#include <http/header_context.h> +#include <boost/shared_ptr.hpp> +#include <string> +#include <vector> + +namespace isc { +namespace http { + +/// @brief HTTP request context. +/// +/// The context is used by the @ref HttpRequestParser to store parsed +/// data. This data is later used to create an instance of the +/// @ref HttpRequest or its derivation. +struct HttpRequestContext { + /// @brief HTTP request method. + std::string method_; + /// @brief HTTP request URI. + std::string uri_; + /// @brief HTTP major version number. + unsigned int http_version_major_; + /// @brief HTTP minor version number. + unsigned int http_version_minor_; + /// @brief Collection of HTTP headers. + std::vector<HttpHeaderContext> headers_; + /// @brief HTTP request body. + std::string body_; +}; + +/// @brief Pointer to the @ref HttpRequestContext. +typedef boost::shared_ptr<HttpRequestContext> HttpRequestContextPtr; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/request_parser.cc b/src/lib/http/request_parser.cc new file mode 100644 index 0000000..d774807 --- /dev/null +++ b/src/lib/http/request_parser.cc @@ -0,0 +1,458 @@ +// Copyright (C) 2016-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/request_parser.h> +#include <functional> +#include <iostream> + +using namespace isc::util; + +namespace isc { +namespace http { + +const int HttpRequestParser::RECEIVE_START_ST; +const int HttpRequestParser::HTTP_METHOD_ST; +const int HttpRequestParser::HTTP_URI_ST; +const int HttpRequestParser::HTTP_VERSION_H_ST; +const int HttpRequestParser::HTTP_VERSION_T1_ST; +const int HttpRequestParser::HTTP_VERSION_T2_ST; +const int HttpRequestParser::HTTP_VERSION_P_ST; +const int HttpRequestParser::HTTP_VERSION_SLASH_ST; +const int HttpRequestParser::HTTP_VERSION_MAJOR_START_ST; +const int HttpRequestParser::HTTP_VERSION_MAJOR_ST; +const int HttpRequestParser::HTTP_VERSION_MINOR_START_ST; +const int HttpRequestParser::HTTP_VERSION_MINOR_ST; +const int HttpRequestParser::EXPECTING_NEW_LINE1_ST; +const int HttpRequestParser::HEADER_LINE_START_ST; +const int HttpRequestParser::HEADER_LWS_ST; +const int HttpRequestParser::HEADER_NAME_ST; +const int HttpRequestParser::SPACE_BEFORE_HEADER_VALUE_ST; +const int HttpRequestParser::HEADER_VALUE_ST; +const int HttpRequestParser::EXPECTING_NEW_LINE2_ST; +const int HttpRequestParser::EXPECTING_NEW_LINE3_ST; +const int HttpRequestParser::HTTP_BODY_ST; + +HttpRequestParser::HttpRequestParser(HttpRequest& request) + : HttpMessageParserBase(request), request_(request), + context_(request_.context()) { +} + +void +HttpRequestParser::initModel() { + // Initialize dictionaries of events and states. + initDictionaries(); + + // Set the current state to starting state and enter the run loop. + setState(RECEIVE_START_ST); + + // Parsing starts from here. + postNextEvent(START_EVT); +} + +void +HttpRequestParser::defineStates() { + // Call parent class implementation first. + HttpMessageParserBase::defineStates(); + + // Define HTTP parser specific states. + defineState(RECEIVE_START_ST, "RECEIVE_START_ST", + std::bind(&HttpRequestParser::receiveStartHandler, this)); + + defineState(HTTP_METHOD_ST, "HTTP_METHOD_ST", + std::bind(&HttpRequestParser::httpMethodHandler, this)); + + defineState(HTTP_URI_ST, "HTTP_URI_ST", + std::bind(&HttpRequestParser::uriHandler, this)); + + defineState(HTTP_VERSION_H_ST, "HTTP_VERSION_H_ST", + std::bind(&HttpRequestParser::versionHTTPHandler, this, 'H', + HTTP_VERSION_T1_ST)); + + defineState(HTTP_VERSION_T1_ST, "HTTP_VERSION_T1_ST", + std::bind(&HttpRequestParser::versionHTTPHandler, this, 'T', + HTTP_VERSION_T2_ST)); + + defineState(HTTP_VERSION_T2_ST, "HTTP_VERSION_T2_ST", + std::bind(&HttpRequestParser::versionHTTPHandler, this, 'T', + HTTP_VERSION_P_ST)); + + defineState(HTTP_VERSION_P_ST, "HTTP_VERSION_P_ST", + std::bind(&HttpRequestParser::versionHTTPHandler, this, 'P', + HTTP_VERSION_SLASH_ST)); + + defineState(HTTP_VERSION_SLASH_ST, "HTTP_VERSION_SLASH_ST", + std::bind(&HttpRequestParser::versionHTTPHandler, this, '/', + HTTP_VERSION_MAJOR_ST)); + + defineState(HTTP_VERSION_MAJOR_START_ST, "HTTP_VERSION_MAJOR_START_ST", + std::bind(&HttpRequestParser::versionNumberStartHandler, this, + HTTP_VERSION_MAJOR_ST, + &context_->http_version_major_)); + + defineState(HTTP_VERSION_MAJOR_ST, "HTTP_VERSION_MAJOR_ST", + std::bind(&HttpRequestParser::versionNumberHandler, this, + '.', HTTP_VERSION_MINOR_START_ST, + &context_->http_version_major_)); + + defineState(HTTP_VERSION_MINOR_START_ST, "HTTP_VERSION_MINOR_START_ST", + std::bind(&HttpRequestParser::versionNumberStartHandler, this, + HTTP_VERSION_MINOR_ST, + &context_->http_version_minor_)); + + defineState(HTTP_VERSION_MINOR_ST, "HTTP_VERSION_MINOR_ST", + std::bind(&HttpRequestParser::versionNumberHandler, this, + '\r', EXPECTING_NEW_LINE1_ST, + &context_->http_version_minor_)); + + defineState(EXPECTING_NEW_LINE1_ST, "EXPECTING_NEW_LINE1_ST", + std::bind(&HttpRequestParser::expectingNewLineHandler, this, + HEADER_LINE_START_ST)); + + defineState(HEADER_LINE_START_ST, "HEADER_LINE_START_ST", + std::bind(&HttpRequestParser::headerLineStartHandler, this)); + + defineState(HEADER_LWS_ST, "HEADER_LWS_ST", + std::bind(&HttpRequestParser::headerLwsHandler, this)); + + defineState(HEADER_NAME_ST, "HEADER_NAME_ST", + std::bind(&HttpRequestParser::headerNameHandler, this)); + + defineState(SPACE_BEFORE_HEADER_VALUE_ST, "SPACE_BEFORE_HEADER_VALUE_ST", + std::bind(&HttpRequestParser::spaceBeforeHeaderValueHandler, this)); + + defineState(HEADER_VALUE_ST, "HEADER_VALUE_ST", + std::bind(&HttpRequestParser::headerValueHandler, this)); + + defineState(EXPECTING_NEW_LINE2_ST, "EXPECTING_NEW_LINE2", + std::bind(&HttpRequestParser::expectingNewLineHandler, this, + HEADER_LINE_START_ST)); + + defineState(EXPECTING_NEW_LINE3_ST, "EXPECTING_NEW_LINE3_ST", + std::bind(&HttpRequestParser::expectingNewLineHandler, this, + HTTP_PARSE_OK_ST)); + + defineState(HTTP_BODY_ST, "HTTP_BODY_ST", + std::bind(&HttpRequestParser::bodyHandler, this)); +} + +void +HttpRequestParser::receiveStartHandler() { + std::string bytes; + getNextFromBuffer(bytes); + if (getNextEvent() != NEED_MORE_DATA_EVT) { + switch(getNextEvent()) { + case START_EVT: + // The first byte should contain a first character of the + // HTTP method name. + if (!isChar(bytes[0]) || isCtl(bytes[0]) || isSpecial(bytes[0])) { + parseFailure("invalid first character " + std::string(1, bytes[0]) + + " in HTTP method name"); + + } else { + context_->method_.push_back(bytes[0]); + transition(HTTP_METHOD_ST, DATA_READ_OK_EVT); + } + break; + + default: + invalidEventError("receiveStartHandler", getNextEvent()); + } + } +} + +void +HttpRequestParser::httpMethodHandler() { + stateWithReadHandler("httpMethodHandler", [this](const char c) { + // Space character terminates the HTTP method name. Next thing + // is the URI. + if (c == ' ') { + transition(HTTP_URI_ST, DATA_READ_OK_EVT); + + } else if (!isChar(c) || isCtl(c) || isSpecial(c)) { + parseFailure("invalid character " + std::string(1, c) + + " in HTTP method name"); + + } else { + // Still parsing the method. Append the next character to the + // method name. + context_->method_.push_back(c); + transition(getCurrState(), DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::uriHandler() { + stateWithReadHandler("uriHandler", [this](const char c) { + // Space character terminates the URI. + if (c == ' ') { + transition(HTTP_VERSION_H_ST, DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in HTTP URI"); + + } else { + // Still parsing the URI. Append the next character to the + // method name. + context_->uri_.push_back(c); + transition(HTTP_URI_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::versionHTTPHandler(const char expected_letter, + const unsigned int next_state) { + stateWithReadHandler("versionHTTPHandler", + [this, expected_letter, next_state](const char c) { + // We're handling one of the letters: 'H', 'T' or 'P'. + if (c == expected_letter) { + // The HTTP version is specified as "HTTP/X.Y". If the current + // character is a slash we're starting to parse major HTTP version + // number. Let's reset the version numbers. + if (c == '/') { + context_->http_version_major_ = 0; + context_->http_version_minor_ = 0; + } + // In all cases, let's transition to next specified state. + transition(next_state, DATA_READ_OK_EVT); + + } else { + // Unexpected character found. Parsing fails. + parseFailure("unexpected character " + std::string(1, c) + + " in HTTP version string"); + } + }); +} + +void +HttpRequestParser::versionNumberStartHandler(const unsigned int next_state, + unsigned int* storage) { + stateWithReadHandler("versionNumberStartHandler", + [this, next_state, storage](const char c) mutable { + // HTTP version number must be a digit. + if (isdigit(c)) { + // Update the version number using new digit being parsed. + *storage = *storage * 10 + c - '0'; + transition(next_state, DATA_READ_OK_EVT); + + } else { + parseFailure("expected digit in HTTP version, found " + + std::string(1, c)); + } + }); +} + +void +HttpRequestParser::versionNumberHandler(const char following_character, + const unsigned int next_state, + unsigned int* const storage) { + stateWithReadHandler("versionNumberHandler", + [this, following_character, next_state, storage](const char c) + mutable { + // We're getting to the end of the version number, let's transition + // to next state. + if (c == following_character) { + transition(next_state, DATA_READ_OK_EVT); + + } else if (isdigit(c)) { + // Current character is a digit, so update the version number. + *storage = *storage * 10 + c - '0'; + + } else { + parseFailure("expected digit in HTTP version, found " + + std::string(1, c)); + } + }); +} + +void +HttpRequestParser::expectingNewLineHandler(const unsigned int next_state) { + stateWithReadHandler("expectingNewLineHandler", [this, next_state](const char c) { + // Only a new line character is allowed in this state. + if (c == '\n') { + // If next state is HTTP_PARSE_OK_ST it means that we're + // parsing 3rd new line in the HTTP request message. This + // terminates the HTTP request (if there is no body) or marks the + // beginning of the body. + if (next_state == HTTP_PARSE_OK_ST) { + // Whether there is a body in this message or not, we should + // parse the HTTP headers to validate it and to check if there + // is "Content-Length" specified. The "Content-Length" is + // required for parsing body. + request_.create(); + try { + // This will throw exception if there is no Content-Length. + uint64_t content_length = + request_.getHeaderValueAsUint64("Content-Length"); + if (content_length > 0) { + // There is body in this request, so let's parse it. + transition(HTTP_BODY_ST, DATA_READ_OK_EVT); + } else { + transition(HTTP_PARSE_OK_ST, HTTP_PARSE_OK_EVT); + } + } catch (const std::exception& ex) { + // There is no body in this message. If the body is required + // parsing fails. + if (request_.requiresBody()) { + parseFailure("HTTP message lacks a body"); + + } else { + // Body not required so simply terminate parsing. + transition(HTTP_PARSE_OK_ST, HTTP_PARSE_OK_EVT); + } + } + + } else { + // This is 1st or 2nd new line, so let's transition to the + // next state required by this handler. + transition(next_state, DATA_READ_OK_EVT); + } + } else { + parseFailure("expecting new line after CR, found " + + std::string(1, c)); + } + }); +} + +void +HttpRequestParser::headerLineStartHandler() { + stateWithReadHandler("headerLineStartHandler", [this](const char c) { + // If we're parsing HTTP headers and we found CR it marks the + // end of headers section. + if (c == '\r') { + transition(EXPECTING_NEW_LINE3_ST, DATA_READ_OK_EVT); + + } else if (!context_->headers_.empty() && ((c == ' ') || (c == '\t'))) { + // New line in headers section followed by space or tab is an LWS, + // a line break within header value. + transition(HEADER_LWS_ST, DATA_READ_OK_EVT); + + } else if (!isChar(c) || isCtl(c) || isSpecial(c)) { + parseFailure("invalid character " + std::string(1, c) + + " in header name"); + + } else { + // Update header name with the parse letter. + context_->headers_.push_back(HttpHeaderContext()); + context_->headers_.back().name_.push_back(c); + transition(HEADER_NAME_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::headerLwsHandler() { + stateWithReadHandler("headerLwsHandler", [this](const char c) { + if (c == '\r') { + // Found CR during parsing a header value. Next value + // should be new line. + transition(EXPECTING_NEW_LINE2_ST, DATA_READ_OK_EVT); + + } else if ((c == ' ') || (c == '\t')) { + // Space and tab is used to mark LWS. Simply swallow + // this character. + transition(getCurrState(), DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in the HTTP header " + + context_->headers_.back().name_); + + } else { + // We're parsing header value, so let's update it. + context_->headers_.back().value_.push_back(c); + transition(HEADER_VALUE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::headerNameHandler() { + stateWithReadHandler("headerNameHandler", [this](const char c) { + // Colon follows header name and it has its own state. + if (c == ':') { + transition(SPACE_BEFORE_HEADER_VALUE_ST, DATA_READ_OK_EVT); + + } else if (!isChar(c) || isCtl(c) || isSpecial(c)) { + parseFailure("invalid character " + std::string(1, c) + + " found in the HTTP header name"); + + } else { + // Parsing a header name, so update it. + context_->headers_.back().name_.push_back(c); + transition(getCurrState(), DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::spaceBeforeHeaderValueHandler() { + stateWithReadHandler("spaceBeforeHeaderValueHandler", [this](const char c) { + if (c == ' ') { + // Remove leading whitespace from the header value. + transition(getCurrState(), DATA_READ_OK_EVT); + + } else if (c == '\r') { + // If CR found during parsing header value, it marks the end + // of this value. + transition(EXPECTING_NEW_LINE2_ST, DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in the HTTP header " + + context_->headers_.back().name_); + + } else { + // Still parsing the value, so let's update it. + context_->headers_.back().value_.push_back(c); + transition(HEADER_VALUE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::headerValueHandler() { + stateWithReadHandler("headerValueHandler", [this](const char c) { + // If CR found during parsing header value, it marks the end + // of this value. + if (c == '\r') { + transition(EXPECTING_NEW_LINE2_ST, DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in the HTTP header " + + context_->headers_.back().name_); + + } else { + // Still parsing the value, so let's update it. + context_->headers_.back().value_.push_back(c); + transition(HEADER_VALUE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpRequestParser::bodyHandler() { + stateWithMultiReadHandler("bodyHandler", [this](const std::string& body) { + // We don't validate the body at this stage. Simply record the + // number of characters specified within "Content-Length". + context_->body_ += body; + size_t content_length = request_.getHeaderValueAsUint64("Content-Length"); + if (context_->body_.length() < content_length) { + transition(HTTP_BODY_ST, DATA_READ_OK_EVT); + + } else { + // If there was some extraneous data, ignore it. + if (context_->body_.length() > content_length) { + context_->body_.resize(content_length); + } + transition(HTTP_PARSE_OK_ST, HTTP_PARSE_OK_EVT); + } + }); +} + +} // namespace http +} // namespace isc diff --git a/src/lib/http/request_parser.h b/src/lib/http/request_parser.h new file mode 100644 index 0000000..4f415d0 --- /dev/null +++ b/src/lib/http/request_parser.h @@ -0,0 +1,246 @@ +// Copyright (C) 2016-2018,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 HTTP_REQUEST_PARSER_H +#define HTTP_REQUEST_PARSER_H + +#include <http/http_message_parser_base.h> +#include <http/request.h> +#include <boost/shared_ptr.hpp> + +namespace isc { +namespace http { + +class HttpRequestParser; + +/// @brief Pointer to the @ref HttpRequestParser. +typedef boost::shared_ptr<HttpRequestParser> HttpRequestParserPtr; + +/// @brief A generic parser for HTTP requests. +/// +/// This class implements a parser for HTTP requests. The parser derives from +/// @ref HttpMessageParserBase class and implements its own state machine on +/// top of it. The states of the parser reflect various parts of the HTTP +/// message being parsed, e.g. parsing HTTP method, parsing URI, parsing +/// message body etc. The descriptions of all parser states are provided +/// below together with the constants defining these states. +/// +/// The request parser validates the syntax of the received message as it +/// progresses with parsing the data. Though, it doesn't interpret the received +/// data until the whole message is parsed. In most cases we want to apply some +/// restrictions on the message content, e.g. Kea Control API requires that +/// commands are sent using HTTP POST, with a JSON command being carried in a +/// message body. The parser doesn't verify if the message meets these +/// restrictions until the whole message is parsed, i.e. stored in the +/// @ref HttpRequestContext object. This object is associated with a +/// @ref HttpRequest object (or its derivation). When the parsing is completed, +/// the @ref HttpRequest::create method is called to retrieve the data from +/// the @ref HttpRequestContext and interpret the data. In particular, the +/// @ref HttpRequest or its derivation checks if the received message meets +/// desired restrictions. +/// +/// Kea Control API uses @ref PostHttpRequestJson class (which derives from the +/// @ref HttpRequest) to interpret received request. This class requires +/// that the HTTP request uses POST method and contains the following headers: +/// - Content-Type: application/json, +/// - Content-Length +/// +/// If any of these restrictions is not met in the received message, an +/// exception will be thrown, thereby @ref HttpRequestParser will fail parsing +/// the message. +class HttpRequestParser : public HttpMessageParserBase { +public: + + /// @name States supported by the HttpRequestParser. + /// + //@{ + + /// @brief State indicating a beginning of parsing. + static const int RECEIVE_START_ST = SM_DERIVED_STATE_MIN + 1; + + /// @brief Parsing HTTP method, e.g. GET, POST etc. + static const int HTTP_METHOD_ST = SM_DERIVED_STATE_MIN + 2; + + /// @brief Parsing URI. + static const int HTTP_URI_ST = SM_DERIVED_STATE_MIN + 3; + + /// @brief Parsing letter "H" of "HTTP". + static const int HTTP_VERSION_H_ST = SM_DERIVED_STATE_MIN + 4; + + /// @brief Parsing first occurrence of "T" in "HTTP". + static const int HTTP_VERSION_T1_ST = SM_DERIVED_STATE_MIN + 5; + + /// @brief Parsing second occurrence of "T" in "HTTP". + static const int HTTP_VERSION_T2_ST = SM_DERIVED_STATE_MIN + 6; + + /// @brief Parsing letter "P" in "HTTP". + static const int HTTP_VERSION_P_ST = SM_DERIVED_STATE_MIN + 7; + + /// @brief Parsing slash character in "HTTP/Y.X" + static const int HTTP_VERSION_SLASH_ST = SM_DERIVED_STATE_MIN + 8; + + /// @brief Starting to parse major HTTP version number. + static const int HTTP_VERSION_MAJOR_START_ST = SM_DERIVED_STATE_MIN + 9; + + /// @brief Parsing major HTTP version number. + static const int HTTP_VERSION_MAJOR_ST = SM_DERIVED_STATE_MIN + 10; + + /// @brief Starting to parse minor HTTP version number. + static const int HTTP_VERSION_MINOR_START_ST = SM_DERIVED_STATE_MIN + 11; + + /// @brief Parsing minor HTTP version number. + static const int HTTP_VERSION_MINOR_ST = SM_DERIVED_STATE_MIN + 12; + + /// @brief Parsing first new line (after HTTP version number). + static const int EXPECTING_NEW_LINE1_ST = SM_DERIVED_STATE_MIN + 13; + + /// @brief Starting to parse a header line. + static const int HEADER_LINE_START_ST = SM_DERIVED_STATE_MIN + 14; + + /// @brief Parsing LWS (Linear White Space), i.e. new line with a space + /// or tab character while parsing a HTTP header. + static const int HEADER_LWS_ST = SM_DERIVED_STATE_MIN + 15; + + /// @brief Parsing header name. + static const int HEADER_NAME_ST = SM_DERIVED_STATE_MIN + 16; + + /// @brief Parsing space before header value. + static const int SPACE_BEFORE_HEADER_VALUE_ST = SM_DERIVED_STATE_MIN + 17; + + /// @brief Parsing header value. + static const int HEADER_VALUE_ST = SM_DERIVED_STATE_MIN + 18; + + /// @brief Expecting new line after parsing header value. + static const int EXPECTING_NEW_LINE2_ST = SM_DERIVED_STATE_MIN + 19; + + /// @brief Expecting second new line marking end of HTTP headers. + static const int EXPECTING_NEW_LINE3_ST = SM_DERIVED_STATE_MIN + 20; + + /// @brief Parsing body of a HTTP message. + static const int HTTP_BODY_ST = SM_DERIVED_STATE_MIN + 21; + + //@} + + + /// @brief Constructor. + /// + /// Creates new instance of the parser. + /// + /// @param request Reference to the @ref HttpRequest object or its + /// derivation that should be used to validate the parsed request and + /// to be used as a container for the parsed request. + explicit HttpRequestParser(HttpRequest& request); + + /// @brief Initialize the state model for parsing. + /// + /// This method must be called before parsing the request, i.e. before + /// calling @ref HttpRequestParser::poll. It initializes dictionaries of + /// states and events, and sets the initial model state to RECEIVE_START_ST. + void initModel(); + +private: + + /// @brief Defines states of the parser. + virtual void defineStates(); + + /// @name State handlers. + /// + //@{ + + /// @brief Handler for RECEIVE_START_ST. + void receiveStartHandler(); + + /// @brief Handler for HTTP_METHOD_ST. + void httpMethodHandler(); + + /// @brief Handler for HTTP_URI_ST. + void uriHandler(); + + /// @brief Handler for states parsing "HTTP" string within the first line + /// of the HTTP request. + /// + /// @param expected_letter One of the 'H', 'T', 'P'. + /// @param next_state A state to which the parser should transition after + /// parsing the character. + void versionHTTPHandler(const char expected_letter, + const unsigned int next_state); + + /// @brief Handler for HTTP_VERSION_MAJOR_START_ST and + /// HTTP_VERSION_MINOR_START_ST. + /// + /// This handler calculates version number using the following equation: + /// @code + /// storage = storage * 10 + c - '0'; + /// @endcode + /// + /// @param next_state State to which the parser should transition. + /// @param [out] storage Reference to a number holding current product of + /// parsing major or minor version number. + void versionNumberStartHandler(const unsigned int next_state, + unsigned int* storage); + + /// @brief Handler for HTTP_VERSION_MAJOR_ST and HTTP_VERSION_MINOR_ST. + /// + /// This handler calculates version number using the following equation: + /// @code + /// storage = storage * 10 + c - '0'; + /// @endcode + /// + /// @param following_character Character following the version number, i.e. + /// '.' for major version, \r for minor version. + /// @param next_state State to which the parser should transition. + /// @param [out] storage Pointer to a number holding current product of + /// parsing major or minor version number. + void versionNumberHandler(const char following_character, + const unsigned int next_state, + unsigned int* const storage); + + /// @brief Handler for states related to new lines. + /// + /// If the next_state is HTTP_PARSE_OK_ST it indicates that the parsed + /// value is a 3rd new line within request HTTP message. In this case the + /// handler calls @ref HttpRequest::create to validate the received message + /// (excluding body). The handler then reads the "Content-Length" header to + /// check if the request contains a body. If the "Content-Length" is greater + /// than zero, the parser transitions to HTTP_BODY_ST. If the + /// "Content-Length" doesn't exist the parser transitions to + /// HTTP_PARSE_OK_ST. + /// + /// @param next_state A state to which parser should transition. + void expectingNewLineHandler(const unsigned int next_state); + + /// @brief Handler for HEADER_LINE_START_ST. + void headerLineStartHandler(); + + /// @brief Handler for HEADER_LWS_ST. + void headerLwsHandler(); + + /// @brief Handler for HEADER_NAME_ST. + void headerNameHandler(); + + /// @brief Handler for SPACE_BEFORE_HEADER_VALUE_ST. + void spaceBeforeHeaderValueHandler(); + + /// @brief Handler for HEADER_VALUE_ST. + void headerValueHandler(); + + /// @brief Handler for HTTP_BODY_ST. + void bodyHandler(); + + //@} + + /// @brief Reference to the request object specified in the constructor. + HttpRequest& request_; + + /// @brief Pointer to the internal context of the @ref HttpRequest object. + HttpRequestContextPtr context_; +}; + +} // namespace http +} // namespace isc + +#endif // HTTP_REQUEST_PARSER_H + diff --git a/src/lib/http/response.cc b/src/lib/http/response.cc new file mode 100644 index 0000000..cdc419a --- /dev/null +++ b/src/lib/http/response.cc @@ -0,0 +1,233 @@ +// 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/response.h> +#include <boost/date_time/local_time/local_time.hpp> +#include <boost/date_time/time_facet.hpp> +#include <sstream> + +using namespace boost::local_time; +using namespace isc::http; + +namespace { + +/// @brief A map of status codes to status names. +const std::map<HttpStatusCode, std::string> status_code_to_description = { + { HttpStatusCode::OK, "OK" }, + { HttpStatusCode::CREATED, "Created" }, + { HttpStatusCode::ACCEPTED, "Accepted" }, + { HttpStatusCode::NO_CONTENT, "No Content" }, + { HttpStatusCode::MULTIPLE_CHOICES, "Multiple Choices" }, + { HttpStatusCode::MOVED_PERMANENTLY, "Moved Permanently" }, + { HttpStatusCode::MOVED_TEMPORARILY, "Moved Temporarily" }, + { HttpStatusCode::NOT_MODIFIED, "Not Modified" }, + { HttpStatusCode::BAD_REQUEST, "Bad Request" }, + { HttpStatusCode::UNAUTHORIZED, "Unauthorized" }, + { HttpStatusCode::FORBIDDEN, "Forbidden" }, + { HttpStatusCode::NOT_FOUND, "Not Found" }, + { HttpStatusCode::REQUEST_TIMEOUT, "Request Timeout" }, + { HttpStatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error" }, + { HttpStatusCode::NOT_IMPLEMENTED, "Not Implemented" }, + { HttpStatusCode::BAD_GATEWAY, "Bad Gateway" }, + { HttpStatusCode::SERVICE_UNAVAILABLE, "Service Unavailable" } +}; + +/// @brief New line (CRLF). +const std::string crlf = "\r\n"; + +} + +namespace isc { +namespace http { + +HttpResponse::HttpResponse() + : HttpMessage(INBOUND), context_(new HttpResponseContext()) { +} + +HttpResponse::HttpResponse(const HttpVersion& version, + const HttpStatusCode& status_code, + const CallSetGenericBody& generic_body) + : HttpMessage(OUTBOUND), context_(new HttpResponseContext()) { + context_->http_version_major_ = version.major_; + context_->http_version_minor_ = version.minor_; + context_->status_code_ = static_cast<unsigned int>(status_code); + + if (generic_body.set_) { + // This currently does nothing, but it is useful to have it here as + // an example how to implement it in the derived classes. + setGenericBody(status_code); + } +} + +void +HttpResponse::create() { + try { + http_version_.major_ = context_->http_version_major_; + http_version_.minor_ = context_->http_version_minor_; + + // Check if the HTTP version is allowed for this request. + if (!inRequiredSet(http_version_, required_versions_)) { + isc_throw(BadValue, "use of HTTP version " + << http_version_.major_ << "." + << http_version_.minor_ + << " not allowed"); + } + + // Copy headers from the context. + for (auto header = context_->headers_.begin(); + header != context_->headers_.end(); + ++header) { + HttpHeaderPtr hdr(new HttpHeader(header->name_, header->value_)); + headers_[hdr->getLowerCaseName()] = hdr; + } + + if (getDirection() == HttpMessage::OUTBOUND) { + HttpHeaderPtr length_header(new HttpHeader("Content-Length", boost::lexical_cast<std::string> + (context_->body_.length()))); + headers_["content-length"] = length_header; + + HttpHeaderPtr date_header(new HttpHeader("Date", getDateHeaderValue()));; + headers_["date"] = date_header; + } + + // Iterate over required headers and check that they exist + // in the HTTP response. + for (auto req_header = required_headers_.begin(); + req_header != required_headers_.end(); + ++req_header) { + auto header = headers_.find(req_header->first); + if (header == headers_.end()) { + isc_throw(BadValue, "required header " << req_header->first + << " not found in the HTTP response"); + } else if (!req_header->second->getValue().empty() && + !header->second->isValueEqual(req_header->second->getValue())) { + // If specific value is required for the header, check + // that the value in the HTTP response matches it. + isc_throw(BadValue, "required header's " << header->first + << " value is " << req_header->second->getValue() + << ", but " << header->second->getValue() << " was found"); + } + } + + } catch (const std::exception& ex) { + // Reset the state of the object if we failed at any point. + reset(); + isc_throw(HttpResponseError, ex.what()); + } + + // All ok. + created_ = true; +} + +void +HttpResponse::finalize() { + if (!created_) { + create(); + } + + finalized_ = true; +} + +void +HttpResponse::reset() { + created_ = false; + finalized_ = false; + headers_.clear(); +} + +HttpStatusCode +HttpResponse::getStatusCode() const { + checkCreated(); + return (static_cast<HttpStatusCode>(context_->status_code_)); +} + +std::string +HttpResponse::getStatusPhrase() const { + checkCreated(); + return (context_->phrase_); +} + +std::string +HttpResponse::getBody() const { + checkFinalized(); + return (context_->body_); +} + +bool +HttpResponse::isClientError(const HttpStatusCode& status_code) { + // Client errors have status codes of 4XX. + uint16_t c = statusCodeToNumber(status_code); + return ((c >= 400) && (c < 500)); +} + +bool +HttpResponse::isServerError(const HttpStatusCode& status_code) { + // Server errors have status codes of 5XX. + uint16_t c = statusCodeToNumber(status_code); + return ((c >= 500) && (c < 600)); +} + +std::string +HttpResponse::statusCodeToString(const HttpStatusCode& status_code) { + auto status_code_it = status_code_to_description.find(status_code); + if (status_code_it == status_code_to_description.end()) { + isc_throw(HttpResponseError, "internal server error: no HTTP status" + " description for the given status code " + << static_cast<uint16_t>(status_code)); + } + return (status_code_it->second); +} + +uint16_t +HttpResponse::statusCodeToNumber(const HttpStatusCode& status_code) { + return (static_cast<uint16_t>(status_code)); +} + +std::string +HttpResponse::getDateHeaderValue() const { + // This returns current time in the recommended format. + HttpDateTime date_time; + return (date_time.rfc1123Format()); +} + +std::string +HttpResponse::toBriefString() const { + checkFinalized(); + + std::ostringstream s; + // HTTP version number and status code. + s << "HTTP/" << http_version_.major_ << "." << http_version_.minor_; + s << " " << context_->status_code_; + s << " " << statusCodeToString(static_cast<HttpStatusCode>(context_->status_code_)); + return (s.str()); +} + +std::string +HttpResponse::toString() const { + + std::ostringstream s; + // HTTP version number and status code. + s << toBriefString() << crlf; + + for (auto header_it = headers_.cbegin(); header_it != headers_.cend(); + ++header_it) { + s << header_it->second->getName() << ": " << header_it->second->getValue() + << crlf; + } + + s << crlf; + + // Include message body. + s << getBody(); + + return (s.str()); +} + +} // namespace http +} // namespace isc diff --git a/src/lib/http/response.h b/src/lib/http/response.h new file mode 100644 index 0000000..734d92f --- /dev/null +++ b/src/lib/http/response.h @@ -0,0 +1,247 @@ +// 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/. + +#ifndef HTTP_RESPONSE_H +#define HTTP_RESPONSE_H + +#include <cc/data.h> +#include <http/header_context.h> +#include <http/http_message.h> +#include <http/response_context.h> +#include <boost/lexical_cast.hpp> +#include <boost/shared_ptr.hpp> +#include <string> +#include <vector> + +namespace isc { +namespace http { + +/// @brief Generic exception thrown by @ref HttpResponse class. +class HttpResponseError : public HttpMessageError { +public: + HttpResponseError(const char* file, size_t line, const char* what) : + HttpMessageError(file, line, what) { }; +}; + +/// @brief HTTP status codes (cf RFC 2068) +enum class HttpStatusCode : std::uint16_t { + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NO_CONTENT = 204, + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + MOVED_TEMPORARILY = 302, + NOT_MODIFIED = 304, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + FORBIDDEN = 403, + NOT_FOUND = 404, + REQUEST_TIMEOUT = 408, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503 +}; + +/// @brief Encapsulates the boolean value indicating if the @ref HttpResponse +/// constructor should call its @c setGenericBody method during construction. +struct CallSetGenericBody { + + /// @brief Constructor. + /// + /// @param set A boolean value indicating if the method should be called + /// or not. + explicit CallSetGenericBody(const bool set) + : set_(set) { + } + + /// @brief Returns encapsulated true. + static const CallSetGenericBody& yes() { + static CallSetGenericBody yes(true); + return (yes); + } + + /// @brief Returns encapsulated false. + static const CallSetGenericBody& no() { + static CallSetGenericBody no(false); + return (no); + } + + /// @brief A storage for the boolean flag. + bool set_; +}; + +class HttpResponse; + +/// @brief Pointer to the @ref HttpResponse object. +typedef boost::shared_ptr<HttpResponse> HttpResponsePtr; + +/// @brief Pointer to the const @ref HttpResponse object. +typedef boost::shared_ptr<const HttpResponse> ConstHttpResponsePtr; + +/// @brief Represents HTTP response message. +/// +/// This derivation of the @c HttpMessage class is specialized to represent +/// HTTP responses. This class provides two constructors for creating an inbound +/// and outbound response instance respectively. This class is associated with +/// an instance of the @c HttpResponseContext, which is used to provide response +/// specific values, such as HTTP status and headers. +/// +/// The derivations of this class provide specializations and specify the HTTP +/// versions and headers supported/required in the specific use cases. For example, +/// the @c HttpResponseJson class derives from the @c HttpResponse and it requires +/// that response includes a body in the JSON format. +class HttpResponse : public HttpMessage { +public: + + /// @brief Constructor for the inbound HTTP response. + explicit HttpResponse(); + + + /// @brief Constructor for outbound HTTP response. + /// + /// Creates basic instance of the object. It sets the HTTP version and the + /// status code to be included in the response. + /// + /// @param version HTTP version. + /// @param status_code HTTP status code. + /// @param generic_body Indicates if the constructor should call + /// @c setGenericBody to create a generic content for the given + /// status code. This should be set to "no" when the constructor is + /// called by the derived class which provides its own implementation + /// of the @c setGenericBody method. + explicit HttpResponse(const HttpVersion& version, + const HttpStatusCode& status_code, + const CallSetGenericBody& generic_body = + CallSetGenericBody::yes()); + + /// @brief Returns pointer to the @ref HttpResponseContext. + /// + /// The context holds intermediate data for creating a response. The response + /// parser stores parsed raw data in the context. When parsing is finished, + /// the data are validated and committed into the @c HttpResponse. + /// + /// @return Pointer to the underlying @ref HttpResponseContext. + const HttpResponseContextPtr& context() const { + return (context_); + } + + /// @brief Commits information held in the context into the response. + /// + /// This function reads HTTP version, status code and headers from the + /// context and validates their values. For the outbound messages, it + /// automatically appends Content-Length and Date headers to the response. + /// The Content-Length is set to the body size. The Date is set to the + /// current date and time. + virtual void create(); + + /// @brief Completes creation of the HTTP response. + /// + /// This method marks the response as finalized. The outbound response may now + /// be sent over the TCP socket. The information from the inbound message may + /// be read, including the response body. + virtual void finalize(); + + /// @brief Reset the state of the object. + virtual void reset(); + + /// @brief Returns HTTP status code. + HttpStatusCode getStatusCode() const; + + /// @brief Returns HTTP status phrase. + std::string getStatusPhrase() const; + + /// @brief Returns HTTP response body as string. + virtual std::string getBody() const; + + /// @brief Retrieves a single JSON element. + /// + /// The element must be at top level of the JSON structure. + /// + /// @param element_name Element name. + /// + /// @return Pointer to the specified element or NULL if such element + /// doesn't exist. + /// @throw HttpResponseJsonError if an error occurred. + data::ConstElementPtr getJsonElement(const std::string& element_name) const; + + /// @brief Checks if the status code indicates client error. + /// + /// @param status_code HTTP status code. + /// @return true if the status code indicates client error. + static bool isClientError(const HttpStatusCode& status_code); + + /// @brief Checks if the status code indicates server error. + /// + /// @param status_code HTTP status code. + /// @return true if the status code indicates server error. + static bool isServerError(const HttpStatusCode& status_code); + + /// @brief Convenience method converting status code to numeric value. + /// + /// @param status_code Status code represented as enum. + /// @return Numeric representation of the status code. + static uint16_t statusCodeToNumber(const HttpStatusCode& status_code); + + /// @brief Converts status code to string. + /// + /// @param status_code HTTP status code. + /// @return Textual representation of the status code. + static std::string statusCodeToString(const HttpStatusCode& status_code); + + /// @brief Returns HTTP version and HTTP status as a string. + std::string toBriefString() const; + + /// @brief Returns HTTP response as string. + /// + /// This method is called to generate the outbound HTTP response. Make + /// sure to call @c finalize prior to calling this method. + virtual std::string toString() const; + + /// @brief Returns current time formatted as required by RFC 1123. + /// + /// This method is virtual so as it can be overridden in unit tests + /// to return a "predictable" value of time, e.g. constant value. + /// + /// @return Current time formatted as required by RFC 1123. + virtual std::string getDateHeaderValue() const; + +private: + + /// @brief Sets generic body for the given status code. + /// + /// Most of the classes derived from @ref HttpResponse will expect + /// a certain content type. Depending on the content type used they + /// will use different body formats for error messages. For example, + /// a response using text/html will use HTML within the response + /// body. The application/json will use JSON body etc. There is a + /// need to implement class specific way of generating the body + /// for error messages. Thus, each derivation of this class is + /// required to implement class specific @ref setGenericBody function + /// which should be called in the class constructor. + /// + /// This is also the case for this class, though the implementation + /// of @c setGenericBody is currently no-op. + /// + /// Note that this class can't be declared virtual because it is + /// meant to be called from the class constructor. + /// + /// @param status_code Status code for which the body should be + /// generated. + void setGenericBody(const HttpStatusCode& /*status_code*/) { }; + +protected: + + /// @brief Pointer to the @ref HttpResponseContext holding parsed + /// data. + HttpResponseContextPtr context_; +}; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/response_context.h b/src/lib/http/response_context.h new file mode 100644 index 0000000..a821477 --- /dev/null +++ b/src/lib/http/response_context.h @@ -0,0 +1,44 @@ +// 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/. + +#ifndef HTTP_RESPONSE_CONTEXT_H +#define HTTP_RESPONSE_CONTEXT_H + +#include <http/header_context.h> +#include <boost/shared_ptr.hpp> +#include <string> +#include <vector> + +namespace isc { +namespace http { + +/// @brief HTTP response context. +/// +/// This context is used by the @c HttpResponseParser to store parsed +/// data. This data is later used to create an instance of the +/// @c HttpResponse or its derivation. +struct HttpResponseContext { + /// @brief HTTP major version number. + unsigned int http_version_major_; + /// @brief HTTP minor version number. + unsigned int http_version_minor_; + /// @brief HTTP status code. + unsigned int status_code_; + /// @brief HTTP status phrase. + std::string phrase_; + /// @brief Collection of HTTP headers. + std::vector<HttpHeaderContext> headers_; + /// @brief HTTP request body. + std::string body_; +}; + +/// @brief Pointer to the @ref HttpResponseContext. +typedef boost::shared_ptr<HttpResponseContext> HttpResponseContextPtr; + +} // end of namespace http +} // end of namespace isc + +#endif // endif HTTP_RESPONSE_CONTEXT_H diff --git a/src/lib/http/response_creator.cc b/src/lib/http/response_creator.cc new file mode 100644 index 0000000..a060f6a --- /dev/null +++ b/src/lib/http/response_creator.cc @@ -0,0 +1,33 @@ +// Copyright (C) 2016-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/response_creator.h> + +namespace isc { +namespace http { + +HttpResponsePtr +HttpResponseCreator::createHttpResponse(HttpRequestPtr request) { + // This should never happen. This method must only be called with a + // non null request, so we consider it unlikely internal server error. + if (!request) { + isc_throw(HttpResponseError, "internal server error: HTTP request is null"); + } + + // If not finalized, the request parsing failed. Generate HTTP 400. + if (!request->isFinalized()) { + return (createStockHttpResponse(request, HttpStatusCode::BAD_REQUEST)); + } + + // Message has been successfully parsed. Create implementation specific + // response to this request. + return (createDynamicHttpResponse(request)); +} + +} +} diff --git a/src/lib/http/response_creator.h b/src/lib/http/response_creator.h new file mode 100644 index 0000000..ac212fe --- /dev/null +++ b/src/lib/http/response_creator.h @@ -0,0 +1,113 @@ +// Copyright (C) 2016-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/. + +#ifndef HTTP_RESPONSE_CREATOR_H +#define HTTP_RESPONSE_CREATOR_H + +#include <http/request.h> +#include <http/response.h> +#include <boost/shared_ptr.hpp> + +namespace isc { +namespace http { + +class HttpResponseCreator; + +/// @brief Pointer to the @ref HttpResponseCreator object. +typedef boost::shared_ptr<HttpResponseCreator> HttpResponseCreatorPtr; + +/// @brief Specifies an interface for classes creating HTTP responses +/// from HTTP requests. +/// +/// HTTP is designed to carry various content types. Most commonly +/// this is text/html. In Kea, the application/json content type is used +/// to carry control commands in JSON format. The libkea-http library is +/// meant to be generic and provide means for transferring different types +/// of content, depending on the use case. +/// +/// This abstract class specifies a common interface for generating HTTP +/// responses from HTTP requests using specific content type and being +/// used in some specific context. Kea modules providing HTTP services need to +/// implement their specific derivations of the @ref HttpResponseCreator +/// class. These derivations use classes derived from @ref HttpRequest as +/// an input and classes derived from @ref HttpResponse as an output of +/// @c createHttpResponse method. +class HttpResponseCreator { +public: + + /// @brief Destructor. + /// + /// Classes with virtual functions need virtual destructors. + virtual ~HttpResponseCreator() { }; + + /// @brief Create HTTP response from HTTP request received. + /// + /// This class implements a generic logic for creating a HTTP response. + /// Derived classes do not override this method. They merely implement + /// the methods it calls. + /// + /// The request processing may generally fail at one of the two stages: + /// parsing or interpretation of the parsed request. During the former + /// stage the request's syntax is checked, i.e. HTTP version, URI, + /// headers etc. During the latter stage the HTTP server checks if the + /// request is valid within the specific context, e.g. valid HTTP version + /// used, expected content type etc. + /// + /// In the @ref HttpRequest terms, the request has gone through the + /// first stage if it is "finalized" (see @ref HttpRequest::finalize). + /// This method accepts instances of both finalized and not finalized + /// requests. If the request isn't finalized it indicates that + /// the request parsing has failed. In such case, this method calls + /// @c createStockBadRequest to generate a response with HTTP 400 status + /// code. If the request is finalized, this method calls + /// @c createDynamicHttpResponse to generate implementation specific + /// response to the received request. + /// + /// This method is marked virtual final to prevent derived classes from + /// overriding this method. Instead, the derived classes must implement + /// protected methods which this method calls. + /// + /// @param request Pointer to an object representing HTTP request. + /// @return Pointer to the object encapsulating generated HTTP response. + /// @throw HttpResponseError if request is a NULL pointer. + virtual HttpResponsePtr + createHttpResponse(HttpRequestPtr request) final; + + /// @brief Create a new request. + /// + /// This method creates an instance of the @ref HttpRequest or derived + /// class. The type of the object is compatible with the instance of + /// the @ref HttpResponseCreator implementation which creates it, i.e. + /// can be used as an argument in the call to @ref createHttpResponse. + /// + /// @return Pointer to the new instance of the @ref HttpRequest. + virtual HttpRequestPtr + createNewHttpRequest() const = 0; + + /// @brief Creates implementation specific HTTP response. + /// + /// @param request Pointer to an object representing HTTP request. + /// @param status_code Status code of the response. + /// @return Pointer to an object representing HTTP response. + virtual HttpResponsePtr + createStockHttpResponse(const HttpRequestPtr& request, + const HttpStatusCode& status_code) const = 0; + +protected: + + /// @brief Creates implementation specific HTTP response. + /// + /// @param request Pointer to an object representing HTTP request. + /// @return Pointer to an object representing HTTP response. + virtual HttpResponsePtr + createDynamicHttpResponse(HttpRequestPtr request) = 0; + +}; + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/response_creator_factory.h b/src/lib/http/response_creator_factory.h new file mode 100644 index 0000000..c2fc917 --- /dev/null +++ b/src/lib/http/response_creator_factory.h @@ -0,0 +1,59 @@ +// 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/. + +#ifndef HTTP_RESPONSE_CREATOR_FACTORY_H +#define HTTP_RESPONSE_CREATOR_FACTORY_H + +#include <http/response_creator.h> +#include <boost/shared_ptr.hpp> + +namespace isc { +namespace http { + +/// @brief Specifies the interface for implementing custom factory classes +/// used to create instances of @ref HttpResponseCreator. +/// +/// The @ref HttpResponseCreator defines an interface for the classes used +/// to generate HTTP responses. Such classes are defined outside of this +/// library and they are specific to the needs of the particular module. +/// In some cases it may be desired to create new instance of the +/// @ref HttpResponseCreator implementation for every request processed. +/// The @ref HttpResponseCreatorFactory is an interface to the "factory" +/// class which generates canned @ref HttpResponseCreator instances. The +/// pointer to the factory class is passed to the @ref HttpListener and +/// the listener propagates it down to other classes. These classes call +/// @ref HttpResponseCreatorFactory::create to retrieve an instance of the +/// appropriate @ref HttpResponseCreator, which is in turn used to generate +/// HTTP response. +/// +/// Note that an implementation of the @ref HttpResponseCreatorFactory::create +/// may always return the same instance of the @ref HttpResponseCreator +/// if creating new instance for each request is not required or undesired. +class HttpResponseCreatorFactory { +public: + + /// @brief Virtual destructor. + virtual ~HttpResponseCreatorFactory() { } + + /// @brief Returns an instance of the @ref HttpResponseCreator. + /// + /// The implementation may create new instance every time this method + /// is called, or it may always return the same instance. + /// + /// @return Pointer to the instance of the @ref HttpResponseCreator to + /// be used to generate HTTP response. + virtual HttpResponseCreatorPtr create() const = 0; + +}; + +/// @brief Pointer to the @ref HttpResponseCreatorFactory. +typedef boost::shared_ptr<HttpResponseCreatorFactory> +HttpResponseCreatorFactoryPtr; + +} // end of namespace isc::http +} // end of namespace isc + +#endif diff --git a/src/lib/http/response_json.cc b/src/lib/http/response_json.cc new file mode 100644 index 0000000..7543d76 --- /dev/null +++ b/src/lib/http/response_json.cc @@ -0,0 +1,122 @@ +// 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/response_json.h> +#include <map> + +using namespace isc::data; + +namespace isc { +namespace http { + +HttpResponseJson::HttpResponseJson() + : HttpResponse() { + context()->headers_.push_back(HttpHeaderContext("Content-Type", "application/json")); +} + + +HttpResponseJson::HttpResponseJson(const HttpVersion& version, + const HttpStatusCode& status_code, + const CallSetGenericBody& generic_body) + : HttpResponse(version, status_code, CallSetGenericBody::no()) { + context()->headers_.push_back(HttpHeaderContext("Content-Type", "application/json")); + // This class provides its own implementation of the setGenericBody. + // We call it here unless the derived class calls this constructor + // from its own constructor and indicates that we shouldn't set the + // generic content in the body. + if (generic_body.set_) { + setGenericBody(status_code); + } +} + +void +HttpResponseJson::setGenericBody(const HttpStatusCode& status_code) { + // Only generate the content for the client or server errors. For + // other status codes (status OK in particular) the content should + // be created using setBodyAsJson or setBody. + if (isClientError(status_code) || isServerError(status_code)) { + std::map<std::string, ConstElementPtr> map_elements; + map_elements["result"] = + ConstElementPtr(new IntElement(statusCodeToNumber(status_code))); + map_elements["text"] = + ConstElementPtr(new StringElement(statusCodeToString(status_code))); + auto body = Element::createMap(); + body->setValue(map_elements); + setBodyAsJson(body); + } +} + +void +HttpResponseJson::finalize() { + if (!created_) { + create(); + } + + // Parse JSON body and store. + parseBodyAsJson(); + finalized_ = true; +} + +void +HttpResponseJson::reset() { + HttpResponse::reset(); + json_.reset(); +} + +ConstElementPtr +HttpResponseJson::getBodyAsJson() const { + checkFinalized(); + return (json_); +} + +void +HttpResponseJson::setBodyAsJson(const ConstElementPtr& json_body) { + if (json_body) { + context()->body_ = json_body->str(); + + } else { + context()->body_.clear(); + } + + json_ = json_body; +} + +ConstElementPtr +HttpResponseJson::getJsonElement(const std::string& element_name) const { + try { + ConstElementPtr body = getBodyAsJson(); + if (body) { + const std::map<std::string, ConstElementPtr>& map_value = body->mapValue(); + auto map_element = map_value.find(element_name); + if (map_element != map_value.end()) { + return (map_element->second); + } + } + + } catch (const std::exception& ex) { + isc_throw(HttpResponseJsonError, "unable to get JSON element " + << element_name << ": " << ex.what()); + } + return (ConstElementPtr()); +} + +void +HttpResponseJson::parseBodyAsJson() { + try { + // Only parse the body if it hasn't been parsed yet. + if (!json_ && !context_->body_.empty()) { + json_ = Element::fromJSON(context_->body_); + } + } catch (const std::exception& ex) { + isc_throw(HttpResponseJsonError, "unable to parse the body of the HTTP" + " response: " << ex.what()); + } +} + +} // namespace http +} // namespace isc diff --git a/src/lib/http/response_json.h b/src/lib/http/response_json.h new file mode 100644 index 0000000..d8ed06e --- /dev/null +++ b/src/lib/http/response_json.h @@ -0,0 +1,114 @@ +// 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_JSON_H +#define HTTP_RESPONSE_JSON_H + +#include <cc/data.h> +#include <exceptions/exceptions.h> +#include <http/response.h> + +namespace isc { +namespace http { + +/// @brief Exception thrown when body of the HTTP message is not JSON. +class HttpResponseJsonError : public HttpResponseError { +public: + HttpResponseJsonError(const char* file, size_t line, const char* what) : + HttpResponseError(file, line, what) { }; +}; + +class HttpResponseJson; + +/// @brief Pointer to the @ref HttpResponseJson object. +typedef boost::shared_ptr<HttpResponseJson> HttpResponseJsonPtr; + +/// @brief Represents HTTP response with JSON content. +/// +/// This is a specialization of the @ref HttpResponse class which +/// includes "Content-Type" equal to "application/json". It also provides +/// methods to create JSON content within HTTP responses. +class HttpResponseJson : public HttpResponse { +public: + + /// @brief Constructor for the inbound HTTP response. + explicit HttpResponseJson(); + + /// @brief Constructor for the outbound HTTP response. + /// + /// @param version HTTP version. + /// @param status_code HTTP status code. + /// @param generic_body Indicates if the constructor should call + /// @c setGenericBody to create a generic content for the given + /// status code. This should be set to "no" when the constructor is + /// called by the derived class which provides its own implementation + /// of the @c setGenericBody method. + explicit HttpResponseJson(const HttpVersion& version, + const HttpStatusCode& status_code, + const CallSetGenericBody& generic_body = + CallSetGenericBody::yes()); + + /// @brief Completes creation of the HTTP response. + /// + /// This method marks the response as finalized. The JSON structure is + /// created and can be used to retrieve the parsed data. If this is the + /// outbound message, it can be transmitted over the wire as the body + /// for the message is now committed. + virtual void finalize(); + + /// @brief Reset the state of the object. + virtual void reset(); + + /// @brief Retrieves JSON body. + /// + /// @return Pointer to the root element of the JSON structure. + /// @throw HttpRequestJsonError if an error occurred. + data::ConstElementPtr getBodyAsJson() const; + + /// @brief Generates JSON content from the data structures represented + /// as @ref data::ConstElementPtr. + /// + /// @param json_body A data structure representing JSON content. + void setBodyAsJson(const data::ConstElementPtr& json_body); + + /// @brief Retrieves a single JSON element. + /// + /// The element must be at top level of the JSON structure. + /// + /// @param element_name Element name. + /// + /// @return Pointer to the specified element or NULL if such element + /// doesn't exist. + /// @throw HttpRequestJsonError if an error occurred. + data::ConstElementPtr getJsonElement(const std::string& element_name) const; + +private: + + /// @brief Sets generic body for the given status code. + /// + /// This method generates JSON content for the HTTP client and server + /// errors. The generated JSON structure is a map containing "result" + /// value holding HTTP status code (e.g. 400) and the "text" string + /// holding a status code description. + /// + /// @param status_code Status code for which the body should be + /// generated. + void setGenericBody(const HttpStatusCode& status_code); + +protected: + + /// @brief Interprets body as JSON, which can be later retrieved using + /// data element objects. + void parseBodyAsJson(); + + /// @brief Pointer to the parsed JSON body. + data::ConstElementPtr json_; +}; + +} // end of namespace isc::http +} // end of namespace isc + +#endif diff --git a/src/lib/http/response_parser.cc b/src/lib/http/response_parser.cc new file mode 100644 index 0000000..b2ab797 --- /dev/null +++ b/src/lib/http/response_parser.cc @@ -0,0 +1,461 @@ +// Copyright (C) 2017-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/response_parser.h> +#include <functional> + +using namespace isc::util; + +namespace isc { +namespace http { + +const int HttpResponseParser::RECEIVE_START_ST; +const int HttpResponseParser::HTTP_VERSION_H_ST; +const int HttpResponseParser::HTTP_VERSION_T1_ST; +const int HttpResponseParser::HTTP_VERSION_T2_ST; +const int HttpResponseParser::HTTP_VERSION_P_ST; +const int HttpResponseParser::HTTP_VERSION_SLASH_ST; +const int HttpResponseParser::HTTP_VERSION_MAJOR_START_ST; +const int HttpResponseParser::HTTP_VERSION_MAJOR_ST; +const int HttpResponseParser::HTTP_VERSION_MINOR_START_ST; +const int HttpResponseParser::HTTP_VERSION_MINOR_ST; +const int HttpResponseParser::HTTP_STATUS_CODE_START_ST; +const int HttpResponseParser::HTTP_STATUS_CODE_ST; +const int HttpResponseParser::HTTP_PHRASE_START_ST; +const int HttpResponseParser::HTTP_PHRASE_ST; +const int HttpResponseParser::EXPECTING_NEW_LINE1_ST; +const int HttpResponseParser::HEADER_LINE_START_ST; +const int HttpResponseParser::HEADER_LWS_ST; +const int HttpResponseParser::HEADER_NAME_ST; +const int HttpResponseParser::SPACE_BEFORE_HEADER_VALUE_ST; +const int HttpResponseParser::HEADER_VALUE_ST; +const int HttpResponseParser::EXPECTING_NEW_LINE2_ST; +const int HttpResponseParser::EXPECTING_NEW_LINE3_ST; +const int HttpResponseParser::HTTP_BODY_ST; + +HttpResponseParser::HttpResponseParser(HttpResponse& response) + : HttpMessageParserBase(response), response_(response), + context_(response.context()) { +} + +void +HttpResponseParser::initModel() { + // Initialize dictionaries of events and states. + initDictionaries(); + + // Set the current state to starting state and enter the run loop. + setState(RECEIVE_START_ST); + + // Parsing starts from here. + postNextEvent(START_EVT); +} + +void +HttpResponseParser::defineStates() { + // Call parent class implementation first. + HttpMessageParserBase::defineStates(); + + // Define HTTP parser specific states. + defineState(RECEIVE_START_ST, "RECEIVE_START_ST", + std::bind(&HttpResponseParser::receiveStartHandler, this)); + + defineState(HTTP_VERSION_T1_ST, "HTTP_VERSION_T1_ST", + std::bind(&HttpResponseParser::versionHTTPHandler, this, 'T', + HTTP_VERSION_T2_ST)); + + defineState(HTTP_VERSION_T2_ST, "HTTP_VERSION_T2_ST", + std::bind(&HttpResponseParser::versionHTTPHandler, this, 'T', + HTTP_VERSION_P_ST)); + + defineState(HTTP_VERSION_P_ST, "HTTP_VERSION_P_ST", + std::bind(&HttpResponseParser::versionHTTPHandler, this, 'P', + HTTP_VERSION_SLASH_ST)); + + defineState(HTTP_VERSION_SLASH_ST, "HTTP_VERSION_SLASH_ST", + std::bind(&HttpResponseParser::versionHTTPHandler, this, '/', + HTTP_VERSION_MAJOR_ST)); + + defineState(HTTP_VERSION_MAJOR_START_ST, "HTTP_VERSION_MAJOR_START_ST", + std::bind(&HttpResponseParser::numberStartHandler, this, + HTTP_VERSION_MAJOR_ST, + "HTTP version", + &context_->http_version_major_)); + + defineState(HTTP_VERSION_MAJOR_ST, "HTTP_VERSION_MAJOR_ST", + std::bind(&HttpResponseParser::numberHandler, this, + '.', HTTP_VERSION_MINOR_START_ST, + "HTTP version", + &context_->http_version_major_)); + + defineState(HTTP_VERSION_MINOR_START_ST, "HTTP_VERSION_MINOR_START_ST", + std::bind(&HttpResponseParser::numberStartHandler, this, + HTTP_VERSION_MINOR_ST, + "HTTP version", + &context_->http_version_minor_)); + + defineState(HTTP_VERSION_MINOR_ST, "HTTP_VERSION_MINOR_ST", + std::bind(&HttpResponseParser::numberHandler, this, + ' ', HTTP_STATUS_CODE_START_ST, + "HTTP version", + &context_->http_version_minor_)); + + defineState(HTTP_STATUS_CODE_START_ST, "HTTP_STATUS_CODE_START_ST", + std::bind(&HttpResponseParser::numberStartHandler, this, + HTTP_STATUS_CODE_ST, + "HTTP status code", + &context_->status_code_)); + + defineState(HTTP_STATUS_CODE_ST, "HTTP_STATUS_CODE_ST", + std::bind(&HttpResponseParser::numberHandler, this, + ' ', HTTP_PHRASE_START_ST, + "HTTP status code", + &context_->status_code_)); + + defineState(HTTP_PHRASE_START_ST, "HTTP_PHRASE_START_ST", + std::bind(&HttpResponseParser::phraseStartHandler, this)); + + defineState(HTTP_PHRASE_ST, "HTTP_PHRASE_ST", + std::bind(&HttpResponseParser::phraseHandler, this)); + + defineState(EXPECTING_NEW_LINE1_ST, "EXPECTING_NEW_LINE1_ST", + std::bind(&HttpResponseParser::expectingNewLineHandler, this, + HEADER_LINE_START_ST)); + + defineState(HEADER_LINE_START_ST, "HEADER_LINE_START_ST", + std::bind(&HttpResponseParser::headerLineStartHandler, this)); + + defineState(HEADER_LWS_ST, "HEADER_LWS_ST", + std::bind(&HttpResponseParser::headerLwsHandler, this)); + + defineState(HEADER_NAME_ST, "HEADER_NAME_ST", + std::bind(&HttpResponseParser::headerNameHandler, this)); + + defineState(SPACE_BEFORE_HEADER_VALUE_ST, "SPACE_BEFORE_HEADER_VALUE_ST", + std::bind(&HttpResponseParser::spaceBeforeHeaderValueHandler, this)); + + defineState(HEADER_VALUE_ST, "HEADER_VALUE_ST", + std::bind(&HttpResponseParser::headerValueHandler, this)); + + defineState(EXPECTING_NEW_LINE2_ST, "EXPECTING_NEW_LINE2", + std::bind(&HttpResponseParser::expectingNewLineHandler, this, + HEADER_LINE_START_ST)); + + defineState(EXPECTING_NEW_LINE3_ST, "EXPECTING_NEW_LINE3_ST", + std::bind(&HttpResponseParser::expectingNewLineHandler, this, + HTTP_PARSE_OK_ST)); + + defineState(HTTP_BODY_ST, "HTTP_BODY_ST", + std::bind(&HttpResponseParser::bodyHandler, this)); +} + +void +HttpResponseParser::receiveStartHandler() { + std::string bytes; + getNextFromBuffer(bytes); + if (getNextEvent() != NEED_MORE_DATA_EVT) { + switch(getNextEvent()) { + case START_EVT: + if (bytes[0] == 'H') { + transition(HTTP_VERSION_T1_ST, DATA_READ_OK_EVT); + + } else { + parseFailure("unexpected first character " + std::string(1, bytes[0]) + + ": expected \'H\'"); + } + break; + + default: + invalidEventError("receiveStartHandler", getNextEvent()); + } + } +} + +void +HttpResponseParser::versionHTTPHandler(const char expected_letter, + const unsigned int next_state) { + stateWithReadHandler("versionHTTPHandler", + [this, expected_letter, next_state](const char c) { + // We're handling one of the letters: 'H', 'T' or 'P'. + if (c == expected_letter) { + // The HTTP version is specified as "HTTP/X.Y". If the current + // character is a slash we're starting to parse major HTTP version + // number. Let's reset the version numbers. + if (c == '/') { + context_->http_version_major_ = 0; + context_->http_version_minor_ = 0; + } + // In all cases, let's transition to next specified state. + transition(next_state, DATA_READ_OK_EVT); + + } else { + // Unexpected character found. Parsing fails. + parseFailure("unexpected character " + std::string(1, c) + + " in HTTP version string"); + } + }); +} + +void +HttpResponseParser::numberStartHandler(const unsigned int next_state, + const std::string& number_name, + unsigned int* storage) { + stateWithReadHandler("numberStartHandler", + [this, next_state, number_name, storage](const char c) mutable { + // HTTP version number must be a digit. + if (isdigit(c)) { + // Update the version number using new digit being parsed. + *storage = *storage * 10 + c - '0'; + transition(next_state, DATA_READ_OK_EVT); + + } else { + parseFailure("expected digit in " + number_name + ", found " + + std::string(1, c)); + } + }); +} + +void +HttpResponseParser::numberHandler(const char following_character, + const unsigned int next_state, + const std::string& number_name, + unsigned int* const storage) { + stateWithReadHandler("numberHandler", + [this, following_character, number_name, next_state, storage](const char c) + mutable { + // We're getting to the end of the version number, let's transition + // to next state. + if (c == following_character) { + transition(next_state, DATA_READ_OK_EVT); + + } else if (isdigit(c)) { + // Current character is a digit, so update the version number. + *storage = *storage * 10 + c - '0'; + + } else { + parseFailure("expected digit in " + number_name + ", found " + + std::string(1, c)); + } + }); +} + +void +HttpResponseParser::phraseStartHandler() { + stateWithReadHandler("phraseStartHandler", [this](const char c) { + if (!isChar(c) || isCtl(c)) { + parseFailure("invalid first character " + std::string(1, c) + + " in HTTP phrase"); + } else { + context_->phrase_.push_back(c); + transition(HTTP_PHRASE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpResponseParser::phraseHandler() { + stateWithReadHandler("phraseHandler", [this](const char c) { + if (c == '\r') { + transition(EXPECTING_NEW_LINE1_ST, DATA_READ_OK_EVT); + + } else if (!isChar(c) || isCtl(c)) { + parseFailure("invalid character " + std::string(1, c) + + " in HTTP phrase"); + + } else { + context_->phrase_.push_back(c); + transition(HTTP_PHRASE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpResponseParser::expectingNewLineHandler(const unsigned int next_state) { + stateWithReadHandler("expectingNewLineHandler", [this, next_state](const char c) { + // Only a new line character is allowed in this state. + if (c == '\n') { + // If next state is HTTP_PARSE_OK_ST it means that we're + // parsing 3rd new line in the HTTP request message. This + // terminates the HTTP request (if there is no body) or marks the + // beginning of the body. + if (next_state == HTTP_PARSE_OK_ST) { + // Whether there is a body in this message or not, we should + // parse the HTTP headers to validate it and to check if there + // is "Content-Length" specified. The "Content-Length" is + // required for parsing body. + response_.create(); + try { + // This will throw exception if there is no Content-Length. + uint64_t content_length = + response_.getHeaderValueAsUint64("Content-Length"); + if (content_length > 0) { + // There is body in this request, so let's parse it. + transition(HTTP_BODY_ST, DATA_READ_OK_EVT); + } else { + transition(HTTP_PARSE_OK_ST, HTTP_PARSE_OK_EVT); + } + } catch (const std::exception& ex) { + // There is no body in this message. If the body is required + // parsing fails. + if (response_.requiresBody()) { + parseFailure("HTTP message lacks a body"); + + } else { + // Body not required so simply terminate parsing. + transition(HTTP_PARSE_OK_ST, HTTP_PARSE_OK_EVT); + } + } + + } else { + // This is 1st or 2nd new line, so let's transition to the + // next state required by this handler. + transition(next_state, DATA_READ_OK_EVT); + } + } else { + parseFailure("expecting new line after CR, found " + + std::string(1, c)); + } + }); +} + +void +HttpResponseParser::headerLineStartHandler() { + stateWithReadHandler("headerLineStartHandler", [this](const char c) { + // If we're parsing HTTP headers and we found CR it marks the + // end of headers section. + if (c == '\r') { + transition(EXPECTING_NEW_LINE3_ST, DATA_READ_OK_EVT); + + } else if (!context_->headers_.empty() && ((c == ' ') || (c == '\t'))) { + // New line in headers section followed by space or tab is an LWS, + // a line break within header value. + transition(HEADER_LWS_ST, DATA_READ_OK_EVT); + + } else if (!isChar(c) || isCtl(c) || isSpecial(c)) { + parseFailure("invalid character " + std::string(1, c) + + " in header name"); + + } else { + // Update header name with the parsed letter. + context_->headers_.push_back(HttpHeaderContext()); + context_->headers_.back().name_.push_back(c); + transition(HEADER_NAME_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpResponseParser::headerLwsHandler() { + stateWithReadHandler("headerLwsHandler", [this](const char c) { + if (c == '\r') { + // Found CR during parsing a header value. Next value + // should be new line. + transition(EXPECTING_NEW_LINE2_ST, DATA_READ_OK_EVT); + + } else if ((c == ' ') || (c == '\t')) { + // Space and tab is used to mark LWS. Simply swallow + // this character. + transition(getCurrState(), DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in the HTTP header " + + context_->headers_.back().name_); + + } else { + // We're parsing header value, so let's update it. + context_->headers_.back().value_.push_back(c); + transition(HEADER_VALUE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpResponseParser::headerNameHandler() { + stateWithReadHandler("headerNameHandler", [this](const char c) { + // Colon follows header name and it has its own state. + if (c == ':') { + transition(SPACE_BEFORE_HEADER_VALUE_ST, DATA_READ_OK_EVT); + + } else if (!isChar(c) || isCtl(c) || isSpecial(c)) { + parseFailure("invalid character " + std::string(1, c) + + " found in the HTTP header name"); + + } else { + // Parsing a header name, so update it. + context_->headers_.back().name_.push_back(c); + transition(getCurrState(), DATA_READ_OK_EVT); + } + }); +} + +void +HttpResponseParser::spaceBeforeHeaderValueHandler() { + stateWithReadHandler("spaceBeforeHeaderValueHandler", [this](const char c) { + if (c == ' ') { + // Remove leading whitespace from the header value. + transition(getCurrState(), DATA_READ_OK_EVT); + + } else if (c == '\r') { + // If CR found during parsing header value, it marks the end + // of this value. + transition(EXPECTING_NEW_LINE2_ST, DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in the HTTP header " + + context_->headers_.back().name_); + + } else { + // Still parsing the value, so let's update it. + context_->headers_.back().value_.push_back(c); + transition(HEADER_VALUE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpResponseParser::headerValueHandler() { + stateWithReadHandler("headerValueHandler", [this](const char c) { + // If CR found during parsing header value, it marks the end + // of this value. + if (c == '\r') { + transition(EXPECTING_NEW_LINE2_ST, DATA_READ_OK_EVT); + + } else if (isCtl(c)) { + parseFailure("control character found in the HTTP header " + + context_->headers_.back().name_); + + } else { + // Still parsing the value, so let's update it. + context_->headers_.back().value_.push_back(c); + transition(HEADER_VALUE_ST, DATA_READ_OK_EVT); + } + }); +} + +void +HttpResponseParser::bodyHandler() { + stateWithMultiReadHandler("bodyHandler", [this](const std::string& body) { + // We don't validate the body at this stage. Simply record the + // number of characters specified within "Content-Length". + context_->body_ += body; + size_t content_length = response_.getHeaderValueAsUint64("Content-Length"); + if (context_->body_.length() < content_length) { + transition(HTTP_BODY_ST, DATA_READ_OK_EVT); + + } else { + // If there was some extraneous data, ignore it. + if (context_->body_.length() > content_length) { + context_->body_.resize(content_length); + } + transition(HTTP_PARSE_OK_ST, HTTP_PARSE_OK_EVT); + } + }); +} + + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/response_parser.h b/src/lib/http/response_parser.h new file mode 100644 index 0000000..fb83b13 --- /dev/null +++ b/src/lib/http/response_parser.h @@ -0,0 +1,243 @@ +// 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/. + +#ifndef HTTP_RESPONSE_PARSER_H +#define HTTP_RESPONSE_PARSER_H + +#include <http/http_message_parser_base.h> +#include <http/response.h> +#include <boost/shared_ptr.hpp> + +namespace isc { +namespace http { + +class HttpResponseParser; + +/// @brief Pointer to the @ref HttpResponseParser. +typedef boost::shared_ptr<HttpResponseParser> HttpResponseParserPtr; + +/// @brief A generic parser for HTTP responses. +/// +/// This class implements a parser for HTTP responses. The parser derives +/// from the @ref HttpMessageParserBase class and implements its own state +/// machine on top of it. The states of the parser reflect various parts of +/// the HTTP message being parsed, e.g. parsing HTTP version, status code, +/// message body etc. The descriptions of all parser states are provided +/// below together with the constants defining these states. +/// +/// The response parser validates the syntax of the received message as it +/// progresses with parsing the data. Though, it doesn't interpret the +/// received data until the whole message is parsed. In most cases we want +/// to apply some restrictions on the message content, e.g. responses to +/// control commands include JSON body. The parser doesn't verify if the +/// message meets such restrictions until the whole message is parsed, +/// i.e. stored in the @ref HttpResponseContext object. This object is +/// associated with @ref HttpResponse object (or its derivation). When +/// the parsing is completed, the @ref HttpResponse::create method is +/// called to retrieve and interpret the data from the context. In +/// particular, the @ref HttpResponse or its derivation checks if the +/// received message meets the desired restrictions. +class HttpResponseParser : public HttpMessageParserBase { +public: + + /// @name States supported by the HttpResponseParser. + /// + //@{ + + /// @brief State indicating a beginning of parsing. + static const int RECEIVE_START_ST = SM_DERIVED_STATE_MIN + 101; + + /// @brief Parsing letter "H" of "HTTP". + static const int HTTP_VERSION_H_ST = SM_DERIVED_STATE_MIN + 102; + + /// @brief Parsing first occurrence of "T" in "HTTP". + static const int HTTP_VERSION_T1_ST = SM_DERIVED_STATE_MIN + 103; + + /// @brief Parsing second occurrence of "T" in "HTTP". + static const int HTTP_VERSION_T2_ST = SM_DERIVED_STATE_MIN + 104; + + /// @brief Parsing letter "P" in "HTTP". + static const int HTTP_VERSION_P_ST = SM_DERIVED_STATE_MIN + 105; + + /// @brief Parsing slash character in "HTTP/Y.X" + static const int HTTP_VERSION_SLASH_ST = SM_DERIVED_STATE_MIN + 106; + + /// @brief Starting to parse major HTTP version number. + static const int HTTP_VERSION_MAJOR_START_ST = SM_DERIVED_STATE_MIN + 107; + + /// @brief Parsing major HTTP version number. + static const int HTTP_VERSION_MAJOR_ST = SM_DERIVED_STATE_MIN + 108; + + /// @brief Starting to parse minor HTTP version number. + static const int HTTP_VERSION_MINOR_START_ST = SM_DERIVED_STATE_MIN + 109; + + /// @brief Parsing minor HTTP version number. + static const int HTTP_VERSION_MINOR_ST = SM_DERIVED_STATE_MIN + 110; + + /// @brief Starting to parse HTTP status code. + static const int HTTP_STATUS_CODE_START_ST = SM_DERIVED_STATE_MIN + 111; + + /// @brief Parsing HTTP status code. + static const int HTTP_STATUS_CODE_ST = SM_DERIVED_STATE_MIN + 112; + + /// @brief Starting to parse HTTP status phrase. + static const int HTTP_PHRASE_START_ST = SM_DERIVED_STATE_MIN + 113; + + /// @brief Parsing HTTP status phrase. + static const int HTTP_PHRASE_ST = SM_DERIVED_STATE_MIN + 114; + + /// @brief Parsing first new line (after HTTP status phrase). + static const int EXPECTING_NEW_LINE1_ST = SM_DERIVED_STATE_MIN + 115; + + static const int HEADER_LINE_START_ST = SM_DERIVED_STATE_MIN + 116; + + /// @brief Parsing LWS (Linear White Space), i.e. new line with a space + /// or tab character while parsing a HTTP header. + static const int HEADER_LWS_ST = SM_DERIVED_STATE_MIN + 117; + + /// @brief Parsing header name. + static const int HEADER_NAME_ST = SM_DERIVED_STATE_MIN + 118; + + /// @brief Parsing space before header value. + static const int SPACE_BEFORE_HEADER_VALUE_ST = SM_DERIVED_STATE_MIN + 119; + + /// @brief Parsing header value. + static const int HEADER_VALUE_ST = SM_DERIVED_STATE_MIN + 120; + + /// @brief Expecting new line after parsing header value. + static const int EXPECTING_NEW_LINE2_ST = SM_DERIVED_STATE_MIN + 121; + + /// @brief Expecting second new line marking end of HTTP headers. + static const int EXPECTING_NEW_LINE3_ST = SM_DERIVED_STATE_MIN + 122; + + /// @brief Parsing body of a HTTP message. + static const int HTTP_BODY_ST = SM_DERIVED_STATE_MIN + 123; + + //@} + + /// @brief Constructor. + /// + /// Creates new instance of the parser. + /// + /// @param response Reference to the @ref HttpResponse object or its + /// derivation that should be used to validate the parsed response and + /// to be used as a container for the parsed response. + explicit HttpResponseParser(HttpResponse& response); + + /// @brief Initialize the state model for parsing. + /// + /// This method must be called before parsing the response, i.e. before + /// calling @ref HttpResponseParser::poll. It initializes dictionaries of + /// states and events, and sets the initial model state to RECEIVE_START_ST. + void initModel(); + +private: + + /// @brief Defines states of the parser. + virtual void defineStates(); + + /// @name State handlers. + /// + //@{ + + /// @brief Handler for RECEIVE_START_ST. + void receiveStartHandler(); + + /// @brief Handler for states parsing "HTTP" string within the first line + /// of the HTTP request. + /// + /// @param expected_letter One of the 'H', 'T', 'P'. + /// @param next_state A state to which the parser should transition after + /// parsing the character. + void versionHTTPHandler(const char expected_letter, + const unsigned int next_state); + + /// @brief Handler for states in which parser begins to read numeric values. + /// + /// This handler calculates version number using the following equation: + /// @code + /// storage = storage * 10 + c - '0'; + /// @endcode + /// + /// @param next_state State to which the parser should transition. + /// @param number_name Name of the parsed numeric value, e.g. HTTP version or + /// HTTP status code (used for error logging). + /// @param [out] storage Reference to a number holding current product of + /// parsing major or minor version number. + void numberStartHandler(const unsigned int next_state, + const std::string& number_name, + unsigned int* const storage); + + /// @brief Handler for states in which parser reads numeric values. + /// + /// This handler calculates version number using the following equation: + /// @code + /// storage = storage * 10 + c - '0'; + /// @endcode + /// + /// @param following_character Character following the version number, i.e. + /// '.' for major version, \r for minor version. + /// @param next_state State to which the parser should transition. + /// @param number_name Name of the parsed numeric value, e.g. HTTP version or + /// HTTP status code (used for error logging). + /// @param [out] storage Pointer to a number holding current product of + /// parsing major or minor version number. + void numberHandler(const char following_character, + const unsigned int next_state, + const std::string& number_name, + unsigned int* const storage); + + /// @brief Handler for HTTP_PHRASE_START_ST. + void phraseStartHandler(); + + /// @brief Handler for HTTP_PHRASE_ST. + void phraseHandler(); + + /// @brief Handler for states related to new lines. + /// + /// If the next_state is HTTP_PARSE_OK_ST it indicates that the parsed + /// value is a 3rd new line within request HTTP message. In this case the + /// handler calls @ref HttpRequest::create to validate the received message + /// (excluding body). The handler then reads the "Content-Length" header to + /// check if the request contains a body. If the "Content-Length" is greater + /// than zero, the parser transitions to HTTP_BODY_ST. If the + /// "Content-Length" doesn't exist the parser transitions to + /// HTTP_PARSE_OK_ST. + /// + /// @param next_state A state to which parser should transition. + void expectingNewLineHandler(const unsigned int next_state); + + /// @brief Handler for HEADER_LINE_START_ST. + void headerLineStartHandler(); + + /// @brief Handler for HEADER_LWS_ST. + void headerLwsHandler(); + + /// @brief Handler for HEADER_NAME_ST. + void headerNameHandler(); + + /// @brief Handler for SPACE_BEFORE_HEADER_VALUE_ST. + void spaceBeforeHeaderValueHandler(); + + /// @brief Handler for HEADER_VALUE_ST. + void headerValueHandler(); + + /// @brief Handler for HTTP_BODY_ST. + void bodyHandler(); + + //@} + + /// @brief Reference to the response object specified in the constructor. + HttpResponse& response_; + + /// @brief Pointer to the internal context of the @ref HttpResponse object. + HttpResponseContextPtr context_; +}; + +} // end of namespace isc::http +} // end of namespace isc + +#endif 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"); +} + +} diff --git a/src/lib/http/url.cc b/src/lib/http/url.cc new file mode 100644 index 0000000..427b11a --- /dev/null +++ b/src/lib/http/url.cc @@ -0,0 +1,223 @@ +// 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 <exceptions/exceptions.h> +#include <http/url.h> +#include <boost/lexical_cast.hpp> +#include <sstream> + +#include <iostream> + +namespace isc { +namespace http { + +Url::Url(const std::string& url) + : url_(url), valid_(false), error_message_(), scheme_(Url::HTTPS), + hostname_(), port_(0), path_() { + parse(); +} + +bool +Url::operator<(const Url& url) const { + return (url_ < url.rawUrl()); +} + +Url::Scheme +Url::getScheme() const { + checkValid(); + return (scheme_); +} + +std::string +Url::getHostname() const { + checkValid(); + return (hostname_); +} + +std::string +Url::getStrippedHostname() const { + std::string hostname = getHostname(); + if ((hostname.length() >= 2) && (hostname.at(0) == '[')) { + return (hostname.substr(1, hostname.length() - 2)); + } + + return (hostname); +} + +unsigned +Url::getPort() const { + checkValid(); + return (port_); +} + +std::string +Url::getPath() const { + checkValid(); + return (path_); +} + +std::string +Url::toText() const { + std::ostringstream s; + s << (getScheme() == HTTP ? "http" : "https"); + s << "://" << getHostname(); + + if (getPort() != 0) { + s << ":" << getPort(); + } + + s << getPath(); + + return (s.str()); +} + +void +Url::checkValid() const { + if (!isValid()) { + isc_throw(InvalidOperation, "invalid URL " << url_ << ": " << error_message_); + } +} + +void +Url::parse() { + valid_ = false; + error_message_.clear(); + scheme_ = Url::HTTPS; + hostname_.clear(); + port_ = 0; + path_.clear(); + + std::ostringstream error; + + // Retrieve scheme + size_t offset = url_.find(":"); + if ((offset == 0) || (offset == std::string::npos)) { + error << "url " << url_ << " lacks http or https scheme"; + error_message_ = error.str(); + return; + } + + // Validate scheme. + std::string scheme = url_.substr(0, offset); + if (scheme == "http") { + scheme_ = Url::HTTP; + + } else if (scheme == "https") { + scheme_ = Url::HTTPS; + + } else { + error << "invalid scheme " << scheme << " in " << url_; + error_message_ = error.str(); + return; + } + + // Colon and two slashes should follow the scheme + if (url_.substr(offset, 3) != "://") { + error << "expected :// after scheme in " << url_; + error_message_ = error.str(); + return; + } + + // Move forward to hostname. + offset += 3; + if (offset >= url_.length()) { + error << "hostname missing in " << url_; + error_message_ = error.str(); + return; + } + + size_t offset2 = 0; + + // IPv6 address is specified within [ ]. + if (url_.at(offset) == '[') { + offset2 = url_.find(']', offset); + if (offset2 == std::string::npos) { + error << "expected ] after IPv6 address in " << url_; + error_message_ = error.str(); + return; + + } else if (offset2 == offset + 1) { + error << "expected IPv6 address within [] in " << url_; + error_message_ = error.str(); + return; + } + + // Move one character beyond the ]. + ++offset2; + + } else { + // There is a normal hostname or IPv4 address. It is terminated + // by the colon (for port number), a slash (if no port number) or + // goes up to the end of the URL. + offset2 = url_.find(":", offset); + if (offset2 == std::string::npos) { + offset2 = url_.find("/", offset); + if (offset2 == std::string::npos) { + // No port number and no slash. + offset2 = url_.length(); + } + } + } + + // Extract the hostname. + hostname_ = url_.substr(offset, offset2 - offset); + + // If there is no port number and no path, simply return and mark the + // URL as valid. + if (offset2 == url_.length()) { + valid_ = true; + return; + } + + // If there is a port number, we need to read it and convert to + // numeric value. + if (url_.at(offset2) == ':') { + if (offset2 == url_.length() - 1) { + error << "expected port number after : in " << url_; + error_message_ = error.str(); + return; + } + // Move to the port number. + ++offset2; + + // Port number may be terminated by a slash or by the end of URL. + size_t slash_offset = url_.find('/', offset2); + std::string port_str; + if (slash_offset == std::string::npos) { + port_str = url_.substr(offset2); + } else { + port_str = url_.substr(offset2, slash_offset - offset2); + } + + try { + // Try to convert the port number to numeric value. + port_ = boost::lexical_cast<unsigned>(port_str); + + } catch (...) { + error << "invalid port number " << port_str << " in " << url_; + error_message_ = error.str(); + return; + } + + // Go to the end of the port section. + offset2 = slash_offset; + } + + // If there is anything left in the URL, we consider it a path. + if (offset2 != std::string::npos) { + path_ = url_.substr(offset2); + if (path_.empty()) { + path_ = "/"; + } + } + + valid_ = true; +} + +} // end of namespace isc::http +} // end of namespace isc diff --git a/src/lib/http/url.h b/src/lib/http/url.h new file mode 100644 index 0000000..d6ec9e2 --- /dev/null +++ b/src/lib/http/url.h @@ -0,0 +1,131 @@ +// 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/. + +#ifndef KEA_URL_H +#define KEA_URL_H + +#include <asiolink/io_address.h> +#include <string> + +namespace isc { +namespace http { + +/// @brief Represents an URL. +/// +/// It parses the provided URL and allows for retrieving the parts +/// of it after parsing. +class Url { +public: + + /// @brief Scheme: https or http. + enum Scheme { + HTTP, + HTTPS + }; + + /// @brief Constructor. + /// + /// Parses provided URL. + /// + /// @param url URL. + explicit Url(const std::string& url); + + /// @brief compares URLs lexically. + /// + /// Both URLs are compared as text. + /// + /// @param url URL to be compared with + /// @return true if the other operator is larger (in lexical sense) + bool operator<(const Url& url) const; + + /// @brief Checks if the URL is valid. + /// + /// @return true if the URL is valid, false otherwise. + bool isValid() const { + return (valid_); + } + + /// @brief Returns parsing error message. + std::string getErrorMessage() const { + return (error_message_); + } + + /// @brief Returns parsed scheme. + /// + /// @throw InvalidOperation if URL is invalid. + Scheme getScheme() const; + + /// @brief Returns hostname stripped from [ ] characters surrounding + /// IPv6 address. + /// + /// @throw InvalidOperation if URL is invalid. + std::string getStrippedHostname() const; + + /// @brief Returns port number. + /// + /// @return Port number or 0 if URL doesn't contain port number. + /// @throw InvalidOperation if URL is invalid. + unsigned getPort() const; + + /// @brief Returns path. + /// + /// @return URL path + /// @throw InvalidOperation if URL is invalid. + std::string getPath() const; + + /// @brief Returns textual representation of the URL. + /// + /// @return Text version of the URL. + std::string toText() const; + + /// @brief Returns the raw, unparsed URL string. + /// + /// @return Unparsed URL string. + const std::string& rawUrl() const { + return (url_); + } + +private: + /// @brief Returns hostname. + /// + /// @throw InvalidOperation if URL is invalid. + std::string getHostname() const; + + /// @brief Returns boolean value indicating if the URL is valid. + void checkValid() const; + + /// @brief Parses URL. + /// + /// This method doesn't throw an exception. Call @c isValid to see + /// if the URL is valid. + void parse(); + + /// @brief Holds specified URL. + std::string url_; + + /// @brief A flag indicating if the URL is valid. + bool valid_; + + /// @brief Holds error message after parsing. + std::string error_message_; + + /// @brief Parsed scheme. + Scheme scheme_; + + /// @brief Parsed hostname. + std::string hostname_; + + /// @brief Parsed port number. + unsigned port_; + + /// @brief Parsed path. + std::string path_; +}; + +} // end of namespace isc::http +} // end of namespace isc + +#endif // endif |