You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by ga...@apache.org on 2019/07/17 19:46:56 UTC
[trafficserver] branch master updated: Plugin reload
This is an automated email from the ASF dual-hosted git repository.
gancho pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 1eca389 Plugin reload
1eca389 is described below
commit 1eca389c5d8f908743d92b742d286b48b396a3a9
Author: Gancho Tenev <ga...@apache.org>
AuthorDate: Fri Mar 1 14:33:09 2019 -0800
Plugin reload
Reloading plugin allows new versions of a plugin code to be loaded
and executed and old versions to be unloaded without restarting
the traffic server process.
More info in doc/developer-guide/plugins/reloading-plugins.en.rst
---
.../api/functions/TSVConnCreate.en.rst | 32 +
doc/developer-guide/design-documents/index.en.rst | 28 +
.../design-documents/reloading-plugins.en.rst | 178 ++++++
doc/developer-guide/index.en.rst | 3 +-
include/ts/InkAPIPrivateIOCore.h | 3 +-
include/tscore/ts_file.h | 67 ++-
proxy/ReverseProxy.cc | 4 +-
proxy/http/HttpTransact.cc | 4 +-
proxy/http/HttpTransact.h | 5 +-
proxy/http/remap/Makefile.am | 87 +++
proxy/http/remap/PluginDso.cc | 266 +++++++++
proxy/http/remap/PluginDso.h | 104 ++++
proxy/http/remap/PluginFactory.cc | 264 +++++++++
proxy/http/remap/PluginFactory.h | 119 ++++
proxy/http/remap/RemapConfig.cc | 201 ++-----
proxy/http/remap/RemapPluginInfo.cc | 256 ++++++--
proxy/http/remap/RemapPluginInfo.h | 62 +-
proxy/http/remap/RemapPlugins.cc | 18 +-
proxy/http/remap/RemapPlugins.h | 7 +-
proxy/http/remap/UrlMapping.cc | 50 +-
proxy/http/remap/UrlMapping.h | 15 +-
proxy/http/remap/UrlRewrite.cc | 4 +
proxy/http/remap/UrlRewrite.h | 4 +
proxy/http/remap/unit-tests/plugin_misc_cb.cc | 106 ++++
.../unit-tests/plugin_missing_deleteinstance.cc | 57 ++
.../remap/unit-tests/plugin_missing_doremap.cc | 45 ++
proxy/http/remap/unit-tests/plugin_missing_init.cc | 45 ++
.../remap/unit-tests/plugin_missing_newinstance.cc | 56 ++
proxy/http/remap/unit-tests/plugin_required_cb.cc | 51 ++
.../http/remap/unit-tests/plugin_testing_calls.cc | 130 ++++
.../http/remap/unit-tests/plugin_testing_common.cc | 39 ++
.../http/remap/unit-tests/plugin_testing_common.h | 95 +++
proxy/http/remap/unit-tests/test_PluginDso.cc | 395 +++++++++++++
proxy/http/remap/unit-tests/test_PluginFactory.cc | 657 +++++++++++++++++++++
proxy/http/remap/unit-tests/test_RemapPlugin.cc | 433 ++++++++++++++
src/traffic_server/InkAPI.cc | 45 +-
src/tscore/ts_file.cc | 210 +++++++
src/tscore/unit_tests/test_ts_file.cc | 193 ++++++
38 files changed, 4058 insertions(+), 280 deletions(-)
diff --git a/doc/developer-guide/api/functions/TSVConnCreate.en.rst b/doc/developer-guide/api/functions/TSVConnCreate.en.rst
new file mode 100644
index 0000000..1e0cd50
--- /dev/null
+++ b/doc/developer-guide/api/functions/TSVConnCreate.en.rst
@@ -0,0 +1,32 @@
+.. Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed
+ with this work for additional information regarding copyright
+ ownership. The ASF licenses this file to you under the Apache
+ License, Version 2.0 (the "License"); you may not use this file
+ except in compliance with the License. You may obtain a copy of
+ the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied. See the License for the specific language governing
+ permissions and limitations under the License.
+
+.. include:: ../../../common.defs
+
+.. default-domain:: c
+
+TSVConnCreate
+*************
+
+Synopsis
+========
+
+`#include <ts/ts.h>`
+
+.. function:: TSCont TSVConnCreate(TSEventFunc funcp, TSMutex mutexp)
+
+Description
+===========
diff --git a/doc/developer-guide/design-documents/index.en.rst b/doc/developer-guide/design-documents/index.en.rst
new file mode 100644
index 0000000..5d18ac0
--- /dev/null
+++ b/doc/developer-guide/design-documents/index.en.rst
@@ -0,0 +1,28 @@
+.. Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+.. include:: ../../common.defs
+
+.. _developer-design-documents:
+
+Design Documents
+****************
+
+.. toctree::
+ :maxdepth: 1
+
+ reloading-plugins.en
diff --git a/doc/developer-guide/design-documents/reloading-plugins.en.rst b/doc/developer-guide/design-documents/reloading-plugins.en.rst
new file mode 100644
index 0000000..c3cf61d
--- /dev/null
+++ b/doc/developer-guide/design-documents/reloading-plugins.en.rst
@@ -0,0 +1,178 @@
+.. Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+.. include:: ../../common.defs
+
+.. _developer-plugins-reloading-plugins:
+
+Reloading Plugins
+*****************
+
+Reloading plugin allows new versions of a plugin code to be loaded and executed and old versions to be unloaded without
+restarting the traffic server process.
+
+Plugins are Dynamic Shared Objects (DSO), new versions of the plugins are currently loaded by using a traffic server
+configuration reload, i.e.::
+
+ traffic_ctl config reload
+
+Although this feature should be transparent to there plugin developers, the following are some design considerations
+and implementation details.
+
+
+Design Considerations
+=====================
+
+1. The mechanism of the plugin reload should be transparent to the plugin developers, plugin developers should be
+ concerned only with properly initializing and cleaning up after the plugin and its instances.
+
+2. With the current traffic server implementation new version plugin (re)load is only triggered by a configuration
+ (re)load hence naturally the configuration should be always coupled with the set of plugins it loaded.
+
+3. Due to its asynchronouse nature traffic server should allow running different newer and older versions of the same plugin at the same time.
+
+4. Old plugin versions should be unloaded after the traffic server process no longer needs them after reload.
+
+5. Running different versions of the configuration and plugin versions at the same time requires maintaining
+ a "current active set" to be used by new transactions, new continuations, etc. and also multiple "previous inactive" sets as well.
+
+6. The result of the plugin reloading should be consistent across operating systems, file systems, dynamic loader
+ implementations.
+
+
+Currently only loading of "remap" plugins (`remap.config`) is supported but (re)loading of "global" plugins
+(`plugin.config`) could use same ideas and reuse some of the classes below.
+
+
+Consistent (re)loading behavior
+-------------------------------
+
+The following are some of the problems noticed during the initial experimentation:
+
+ a. There is an internal reference counting of the DSOs implemented inside the dynamic loader.
+ If an older version of the plugin DSO is still loaded then loading of a newer version of the DSO by using
+ the same filename does not load the new version.
+
+ b. If the filename used by the dynamic loader reference counting contains symbolic links the results are not
+ consistent across different operating/file systems and dynamic loader implementations.
+
+The following possible solutions were considered:
+
+ a. maintaining different plugin filenames for each version - this would put unnecessary burden on the
+ configuration management tools
+
+ b. experiments with Linux specific `dlmopen <http://man7.org/linux/man-pages/man3/dlopen.3.html>`_ yielded
+ good results but it was not available on all supported platforms
+
+
+A less efficient but more reliable solution was chosen - DSO files are temporarily copied to and consequently
+loaded from a runtime location and the copies is kept until plugin is unloaded.
+
+Each configuration / plugin reload would use a different runtime location, ``ATSUuid`` is used to create unique
+runtime directories.
+
+
+Reference counting against DSOs
+-------------------------------
+
+During the initial analysis a common sense solution was considered - to add instrumentation around handling
+of registered hooks in order to unload plugins safely. This would be more involved and not sufficient since hooks
+are not the only mechanism that relies on the plugin DSO being loaded. This design / implementation proposes
+a different approach.
+
+Plugin code can be called from HTTP state machine (1) while handling HTTP transactions or (2) while calling
+event handling functions of continuations created by the plugin code.
+The plugin reload mechanism should guarantee that all necessary plugin DSOs are still loaded when those calls
+are performed.
+
+Those continuations are created by :c:func:`TSContCreate` and :c:func:`TSVConnCreate` and
+could be used for registering hooks (i.e. registered by :c:func:`TSHttpHookAdd`) or for
+scheduling events in the future (i.e. :c:func:`TSContScheduleOnPool`).
+
+
+Registering hooks always requires creating continuations from inside the plugin code and a separate
+instrumentation around handling of hooks is not necessary.
+
+There is an existing reference counting around ``UrlRewrite`` which makes sure it stays loaded until the HTTP state
+machine (the last HTTP transaction) stops using it. By making all plugins loaded by a single configuration reload
+a part of ``UrlRewrite`` (see `PluginFactory`_ below), we could guarantee the plugins are always loaded while
+in use by the HTTP transactions.
+
+
+Plugin context
+--------------
+
+Reference counting and managing different configuration and plugin sets require the continuation creation and
+destruction to know in which plugin context they are running.
+
+Traffic server API change was considered for ``TSCreateCont``, ``TSVConnCreate`` and ``TSDestroyCont`` but
+it was decided to keep things hidden from the plugin developer by using thread local plugin context which
+would be set/switched accordingly before executing the plugin code.
+
+The continuations created by the plugin will have a context member added to them which will be used by
+the reference counting and when continuations are destroyed or handle events.
+
+
+TSHttpArgs
+----------
+
+Traffic Server sessions and transactions provide a fixed array of void pointers that can be used by plugins
+to store information. To avoid collisions between plugins a plugin should first *reserve* an index in the array.
+
+Since :c:func:`TSHttpTxnArgIndexReserve` and :c:func:`TSHttpSsnArgIndexReserve` are meant to be called during plugin
+initialization we could end up "leaking" indices during plugins reload.
+Hence it is necessary to make sure only one index is allocated per "plugin identifying name", current
+:c:func:`TSHttpTxnArgIndexNameLookup` and :c:func:`TSHttpTxnArgIndexNameLookup` implementation assumes 1-1
+index-to-name relationship as well.
+
+
+PluginFactory
+-------------
+
+`PluginFactory` - creates and holds all plugin instances corresponding to a single configuration (re)load.
+
+#. Instantiates and initializes 'remap' plugins, eventually signals plugin unload/destruction, makes sure each plugin
+ version is loaded only once per configuration (re)load by maintaining a list of DSOs already loaded.
+
+#. Initializes, keeps track of all resulting plugin instances and eventually signals each instance destruction.
+
+#. Handles multiple plugin search paths.
+
+#. Sets a common runtime path for all plugins loaded in a single configuration (re)load to guarantee
+ `consistent (re)loading behavior`_.
+
+
+
+RemapPluginInfo
+---------------
+
+`RemapPluginInfo` - a class representing a 'remap' plugin, derived from `PluginDso`, and handling 'remap' plugin specific
+initialization and destruction and also sets up the right plugin context when its methods are called.
+
+
+
+PluginDso
+---------
+
+`PluginDso` - a class performing the actual DSO loading and unloading and all related initialization and
+cleanup plus related error handling. Its functionality is modularized into a separate class in hopes to
+be reused by 'global' plugins in the future.
+
+
+To make sure plugins are still loaded while their code is still in use there is reference counting done around ``PluginDso``
+which inherits ``RefCountObj`` and implements ``aqcuire()`` and ``release()`` methods which are called by ``TSCreateCont``,
+``TSVConnCreate`` and ``TSDestroyCont``.
diff --git a/doc/developer-guide/index.en.rst b/doc/developer-guide/index.en.rst
index 5deda01..d21e840 100644
--- a/doc/developer-guide/index.en.rst
+++ b/doc/developer-guide/index.en.rst
@@ -55,4 +55,5 @@ duplicate bugs is encouraged, but not required.
host-resolution-proposal.en
client-session-architecture.en
core-architecture/index.en
- layout/index.en
\ No newline at end of file
+ design-documents/index.en
+ layout/index.en
diff --git a/include/ts/InkAPIPrivateIOCore.h b/include/ts/InkAPIPrivateIOCore.h
index baebdc4..77283fd 100644
--- a/include/ts/InkAPIPrivateIOCore.h
+++ b/include/ts/InkAPIPrivateIOCore.h
@@ -43,7 +43,7 @@ public:
INKContInternal();
INKContInternal(TSEventFunc funcp, TSMutex mutexp);
- void init(TSEventFunc funcp, TSMutex mutexp);
+ void init(TSEventFunc funcp, TSMutex mutexp, void *context = 0);
virtual void destroy();
void handle_event_count(int event);
@@ -60,6 +60,7 @@ public:
int m_closed;
int m_deletable;
int m_deleted;
+ void *m_context;
// INKqa07670: Nokia memory leak bug fix
INKContInternalMagic_t m_free_magic;
};
diff --git a/include/tscore/ts_file.h b/include/tscore/ts_file.h
index b5ec194..e9bfd8b 100644
--- a/include/tscore/ts_file.h
+++ b/include/tscore/ts_file.h
@@ -104,6 +104,12 @@ namespace file
/// Get a copy of the path.
std::string string() const;
+ /// Get relative path
+ self_type relative_path();
+
+ /// Get parent path
+ path parent_path();
+
protected:
std::string _path; ///< File path.
};
@@ -120,6 +126,7 @@ namespace file
friend self_type status(const path &, std::error_code &) noexcept;
friend int file_type(const self_type &);
+ friend time_t modification_time(const file_status &fs);
friend uintmax_t file_size(const self_type &);
friend bool is_regular_file(const file_status &);
friend bool is_dir(const file_status &);
@@ -141,6 +148,9 @@ namespace file
/// Return the file type value.
int file_type(const file_status &fs);
+ /// Return modification time
+ time_t modification_time(const file_status &fs);
+
/// Check if the path is to a regular file.
bool is_regular_file(const file_status &fs);
@@ -159,7 +169,28 @@ namespace file
/// Check if file is readable.
bool is_readable(const path &s);
- /** Load the file at @a p into a @c std::string.
+ // Get directory location suitable for temporary files
+ path temp_directory_path();
+
+ // Returns current path.
+ path current_path();
+
+ // Returns return the canonicalized absolute pathname
+ path canonical(const path &p, std::error_code &ec);
+
+ // Checks if the file/directory exists
+ bool exists(const path &p);
+
+ // Create directories
+ bool create_directories(const path &p, std::error_code &ec, mode_t mode = 0775) noexcept;
+
+ // Copy files ("from" cannot be directory). @todo make it more generic
+ bool copy(const path &from, const path &to, std::error_code &ec);
+
+ // Removes files and directories recursively
+ bool remove(const path &path, std::error_code &ec);
+
+ /** Load the file at @a p into a @c std::string
*
* @param p Path to file
* @return The contents of the file.
@@ -222,6 +253,28 @@ namespace file
return *this /= std::string_view(that._path);
}
+ inline path
+ path::relative_path()
+ {
+ return (this->is_absolute()) ? path(_path.substr(sizeof(preferred_separator))) : *this;
+ }
+
+ inline path
+ path::parent_path()
+ {
+ if (this->is_absolute() && _path.substr(sizeof(preferred_separator)).empty()) {
+ // No relative path
+ return *this;
+ }
+
+ const size_t last_slash_idx = _path.find_last_of(preferred_separator);
+ if (std::string::npos != last_slash_idx) {
+ return path(_path.substr(0, last_slash_idx));
+ } else {
+ return path();
+ }
+ }
+
/** Combine two strings as file paths.
@return A @c path with the combined path.
@@ -250,6 +303,18 @@ namespace file
return path(std::move(lhs)) /= rhs;
}
+ inline bool
+ operator==(const path &lhs, const path &rhs)
+ {
+ return lhs.string() == rhs.string();
+ }
+
+ inline bool
+ operator!=(const path &lhs, const path &rhs)
+ {
+ return lhs.string() != rhs.string();
+ }
+
/* ------------------------------------------------------------------- */
} // namespace file
} // namespace ts
diff --git a/proxy/ReverseProxy.cc b/proxy/ReverseProxy.cc
index 2ab3d03..a4751a3 100644
--- a/proxy/ReverseProxy.cc
+++ b/proxy/ReverseProxy.cc
@@ -43,7 +43,8 @@
// Global Ptrs
static Ptr<ProxyMutex> reconfig_mutex;
-UrlRewrite *rewrite_table = nullptr;
+UrlRewrite *rewrite_table = nullptr;
+thread_local PluginThreadContext *pluginThreadContext = nullptr;
// Tokens for the Callback function
#define FILE_CHANGED 0
@@ -150,6 +151,7 @@ reloadUrlRewrite()
ink_assert(oldTable != nullptr);
// Release the old one
+ oldTable->pluginFactory.indicateReload();
oldTable->release();
Debug("url_rewrite", "%s", msg);
diff --git a/proxy/http/HttpTransact.cc b/proxy/http/HttpTransact.cc
index 854755b..a19fe40 100644
--- a/proxy/http/HttpTransact.cc
+++ b/proxy/http/HttpTransact.cc
@@ -3492,8 +3492,8 @@ HttpTransact::handle_response_from_server(State *s)
// plugin call
s->server_info.state = s->current.state;
- if (s->fp_tsremap_os_response) {
- s->fp_tsremap_os_response(s->remap_plugin_instance, reinterpret_cast<TSHttpTxn>(s->state_machine), s->current.state);
+ if (s->os_response_plugin_inst) {
+ s->os_response_plugin_inst->osResponse(reinterpret_cast<TSHttpTxn>(s->state_machine), s->current.state);
}
switch (s->current.state) {
diff --git a/proxy/http/HttpTransact.h b/proxy/http/HttpTransact.h
index 4889da9..a1f5b47 100644
--- a/proxy/http/HttpTransact.h
+++ b/proxy/http/HttpTransact.h
@@ -766,10 +766,9 @@ public:
CacheAuth_t www_auth_content = CACHE_AUTH_NONE;
// INK API/Remap API plugin interface
- void *remap_plugin_instance = nullptr;
void *user_args[TS_HTTP_MAX_USER_ARG];
- RemapPluginInfo::OS_Response_F *fp_tsremap_os_response = nullptr;
- HTTPStatus http_return_code = HTTP_STATUS_NONE;
+ RemapPluginInst *os_response_plugin_inst = nullptr;
+ HTTPStatus http_return_code = HTTP_STATUS_NONE;
int api_txn_active_timeout_value = -1;
int api_txn_connect_timeout_value = -1;
diff --git a/proxy/http/remap/Makefile.am b/proxy/http/remap/Makefile.am
index ddee9dc..3a10195 100644
--- a/proxy/http/remap/Makefile.am
+++ b/proxy/http/remap/Makefile.am
@@ -22,6 +22,7 @@ AM_CPPFLAGS += \
$(iocore_include_dirs) \
-I$(abs_top_srcdir)/include \
-I$(abs_top_srcdir)/lib \
+ -I$(abs_top_srcdir)/lib/records \
-I$(abs_top_srcdir)/proxy \
-I$(abs_top_srcdir)/mgmt \
-I$(abs_top_srcdir)/mgmt/utils \
@@ -39,6 +40,9 @@ libhttp_remap_a_SOURCES = \
RemapConfig.h \
RemapPluginInfo.cc \
RemapPluginInfo.h \
+ PluginDso.cc \
+ PluginFactory.cc \
+ PluginFactory.h \
RemapPlugins.cc \
RemapPlugins.h \
RemapProcessor.cc \
@@ -52,3 +56,86 @@ libhttp_remap_a_SOURCES = \
clang-tidy-local: $(libhttp_remap_a_SOURCES)
$(CXX_Clang_Tidy)
+
+TESTS = $(check_PROGRAMS)
+check_PROGRAMS = test_PluginDso test_PluginFactory test_RemapPluginInfo
+
+test_PluginDso_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DPLUGIN_DSO_TESTS
+EXTRA_test_PluginDso_DEPENDENCIES = unit-tests/plugin_v1.la
+test_PluginDso_LDADD = $(OPENSSL_LIBS)
+test_PluginDso_LDFLAGS = $(AM_LDFLAGS) -L$(top_builddir)/src/tscore/.libs -ltscore
+test_PluginDso_SOURCES = \
+ unit-tests/test_PluginDso.cc \
+ unit-tests/plugin_testing_common.cc \
+ PluginDso.cc
+
+test_PluginFactory_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DPLUGIN_DSO_TESTS
+EXTRA_test_PluginFactory_DEPENDENCIES = unit-tests/plugin_v1.la
+test_PluginFactory_LDADD = $(OPENSSL_LIBS)
+test_PluginFactory_LDFLAGS = $(AM_LDFLAGS) -L$(top_builddir)/src/tscore/.libs -ltscore
+test_PluginFactory_SOURCES = \
+ unit-tests/test_PluginFactory.cc \
+ unit-tests/plugin_testing_common.cc \
+ PluginFactory.cc \
+ PluginDso.cc \
+ RemapPluginInfo.cc
+
+test_RemapPluginInfo_CPPFLAGS = $(AM_CPPFLAGS) -I$(abs_top_srcdir)/tests/include -DPLUGIN_DSO_TESTS
+ EXTRA_test_RemapPluginInfo_DEPENDENCIES = \
+ unit-tests/plugin_missing_init.la \
+ unit-tests/plugin_missing_doremap.la \
+ unit-tests/plugin_missing_deleteinstance.la \
+ unit-tests/plugin_required_cb.la \
+ unit-tests/plugin_missing_newinstance.la \
+ unit-tests/plugin_testing_calls.la
+test_RemapPluginInfo_LDADD = \
+ $(OPENSSL_LIBS)
+test_RemapPluginInfo_LDFLAGS = $(AM_LDFLAGS) -L$(top_builddir)/src/tscore/.libs -ltscore
+test_RemapPluginInfo_SOURCES = \
+ unit-tests/plugin_testing_common.cc \
+ unit-tests/plugin_testing_calls.cc \
+ unit-tests/test_RemapPlugin.cc \
+ PluginDso.cc \
+ RemapPluginInfo.cc
+
+
+DSO_LDFLAGS = \
+ -module \
+ -shared \
+ -avoid-version \
+ -export-symbols-regex '^(TSRemapInit|TSRemapDone|TSRemapDoRemap|TSRemapNewInstance|TSRemapDeleteInstance|TSRemapOSResponse|TSRemapConfigReload|TSPluginInit|pluginDsoVersionTest|getPluginDebugObjectTest)$$'
+
+# Build plugins for unit testing the plugin (re)load.
+pkglib_LTLIBRARIES = \
+ unit-tests/plugin_v1.la \
+ unit-tests/plugin_v2.la \
+ unit-tests/plugin_required_cb.la \
+ unit-tests/plugin_missing_deleteinstance.la \
+ unit-tests/plugin_missing_doremap.la \
+ unit-tests/plugin_missing_init.la \
+ unit-tests/plugin_missing_newinstance.la \
+ unit-tests/plugin_testing_calls.la
+unit_tests_plugin_v1_la_SOURCES = unit-tests/plugin_misc_cb.cc
+unit_tests_plugin_v1_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_v1_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_v2_la_SOURCES = unit-tests/plugin_misc_cb.cc
+unit_tests_plugin_v2_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_v2_la_CPPFLAGS = -DPLUGINDSOVER=2 $(AM_CPPFLAGS)
+unit_tests_plugin_required_cb_la_SOURCES = unit-tests/plugin_required_cb.cc
+unit_tests_plugin_required_cb_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_required_cb_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_missing_deleteinstance_la_SOURCES = unit-tests/plugin_missing_deleteinstance.cc
+unit_tests_plugin_missing_deleteinstance_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_missing_deleteinstance_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_missing_doremap_la_SOURCES = unit-tests/plugin_missing_doremap.cc
+unit_tests_plugin_missing_doremap_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_missing_doremap_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_missing_init_la_SOURCES = unit-tests/plugin_missing_init.cc
+unit_tests_plugin_missing_init_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_missing_init_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_missing_newinstance_la_SOURCES = unit-tests/plugin_missing_newinstance.cc
+unit_tests_plugin_missing_newinstance_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_missing_newinstance_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
+unit_tests_plugin_testing_calls_la_SOURCES = unit-tests/plugin_testing_calls.cc unit-tests/plugin_testing_common.cc
+unit_tests_plugin_testing_calls_la_LDFLAGS = $(DSO_LDFLAGS)
+unit_tests_plugin_testing_calls_la_CPPFLAGS = -DPLUGINDSOVER=1 $(AM_CPPFLAGS)
diff --git a/proxy/http/remap/PluginDso.cc b/proxy/http/remap/PluginDso.cc
new file mode 100644
index 0000000..24490fc
--- /dev/null
+++ b/proxy/http/remap/PluginDso.cc
@@ -0,0 +1,266 @@
+/** @file
+
+ A class that deals with plugin Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#include "PluginDso.h"
+#ifdef PLUGIN_DSO_TESTS
+#include "unit-tests/plugin_testing_common.h"
+#else
+#include "tscore/Diags.h"
+#endif
+
+PluginDso::PluginDso(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath)
+ : _configPath(configPath), _effectivePath(effectivePath), _runtimePath(runtimePath)
+{
+}
+
+PluginDso::~PluginDso()
+{
+ std::string error;
+ (void)unload(error);
+}
+
+bool
+PluginDso::load(std::string &error)
+{
+ /* Clear all errors */
+ error.clear();
+ _errorCode.clear();
+ bool result = true;
+
+ if (isLoaded()) {
+ error.append("plugin already loaded");
+ return false;
+ }
+
+ Debug(_tag, "plugin '%s' started loading DSO", _configPath.c_str());
+
+ /* Find plugin DSO looking through the search dirs */
+ if (_effectivePath.empty()) {
+ error.append("empty effective path");
+ result = false;
+ } else {
+ Debug(_tag, "plugin '%s' effective path: %s", _configPath.c_str(), _effectivePath.c_str());
+
+ /* Copy the installed plugin DSO to a runtime directory */
+ std::error_code ec;
+ if (!copy(_effectivePath, _runtimePath, ec)) {
+ std::string temp_error;
+ temp_error.append("failed to create a copy: ").append(strerror(ec.value()));
+ error.assign(temp_error);
+ result = false;
+ } else {
+ Debug(_tag, "plugin '%s' runtime path: %s", _configPath.c_str(), _runtimePath.c_str());
+
+ /* Save the time for later checking if DSO got modified in consecutive DSO reloads */
+ std::error_code ec;
+ fs::file_status fs = fs::status(_effectivePath, ec);
+ _mtime = fs::modification_time(fs);
+ Debug(_tag, "plugin '%s' mоdification time %ld", _configPath.c_str(), _mtime);
+
+ /* Now attemt to load the plugin DSO */
+ if ((_dlh = dlopen(_runtimePath.c_str(), RTLD_NOW)) == nullptr) {
+#if defined(freebsd) || defined(openbsd)
+ char *err = (char *)dlerror();
+#else
+ char *err = dlerror();
+#endif
+ error.append(err ? err : "Unknown dlopen() error");
+ _dlh = nullptr; /* mark that the constructor failed. */
+
+ clean(error);
+ result = false;
+
+ Error("plugin '%s' failed to load: %s", _configPath.c_str(), error.c_str());
+ }
+ }
+
+ /* Remove the runtime DSO copy even if we succeed loading to avoid leftovers after crashes */
+ if (_preventiveCleaning) {
+ clean(error);
+ }
+ }
+ Debug(_tag, "plugin '%s' finished loading DSO", _configPath.c_str());
+
+ return result;
+}
+
+/**
+ * @brief unload plugin DSO
+ *
+ * @param error - error messages in case of failure.
+ * @return true - success, false - failure during unload.
+ */
+bool
+PluginDso::unload(std::string &error)
+{
+ /* clean errors */
+ error.clear();
+ bool result = false;
+
+ if (isLoaded()) {
+ result = (0 == dlclose(_dlh));
+ _dlh = nullptr;
+ if (true == result) {
+ clean(error);
+ } else {
+ error.append("failed to unload plugin");
+ }
+ } else {
+ error.append("no plugin loaded");
+ result = false;
+ }
+
+ return result;
+}
+
+/**
+ * @brief returns the address of a symbol in the plugin DSO
+ *
+ * @param symbol symbol name
+ * @param address reference to the address to be returned to the caller
+ * @param error error messages in case of symbol is not found
+ * @return true if success, false could not find the symbol (symbol can be nullptr itself)
+ */
+bool
+PluginDso::getSymbol(const char *symbol, void *&address, std::string &error) const
+{
+ /* Clear the errors */
+ dlerror();
+ error.clear();
+
+ address = dlsym(_dlh, symbol);
+ char *err = dlerror();
+
+ if (nullptr == address && nullptr != err) {
+ /* symbol really cannot be found */
+ error.assign(err);
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * @brief shows if the DSO corresponding to this effective path has already been loaded.
+ * @return true - loaded, false - not loaded
+ */
+bool
+PluginDso::isLoaded()
+{
+ return nullptr != _dlh;
+}
+
+/**
+ * @brief full path to the first plugin found in the search path which will be used to be loaded.
+ *
+ * @return full path to the plugin DSO.
+ */
+const fs::path &
+PluginDso::effectivePath() const
+{
+ return _effectivePath;
+}
+
+/**
+ * @brief full path to the runtime location of the plugin DSO actually loaded.
+ *
+ * @return full path to the runtime plugin DSO.
+ */
+
+const fs::path &
+PluginDso::runtimePath() const
+{
+ return _runtimePath;
+}
+
+/**
+ * @brief DSO modification time at the moment of DSO load.
+ *
+ * @return modification time.
+ */
+
+time_t
+PluginDso::modTime() const
+{
+ return _mtime;
+}
+
+/**
+ * @brief clean files created by the plugin instance and handle errors
+ *
+ * @param error a human readable error message if something goes wrong
+ * @ return void
+ */
+void
+PluginDso::clean(std::string &error)
+{
+ if (false == remove(_runtimePath, _errorCode)) {
+ error.append("failed to remove runtime copy: ").append(_errorCode.message());
+ }
+}
+
+void
+PluginDso::acquire()
+{
+ this->refcount_inc();
+ Debug(_tag, "plugin DSO acquire (ref-count:%d, dso-addr:%p)", this->refcount(), this);
+}
+
+void
+PluginDso::release()
+{
+ Debug(_tag, "plugin DSO release (ref-count:%d, dso-addr:%p)", this->refcount() - 1, this);
+ if (0 == this->refcount_dec()) {
+ Debug(_tag, "unloading plugin DSO '%s' (dso-addr:%p)", _configPath.c_str(), this);
+ _list.erase(this);
+ delete this;
+ }
+}
+
+void
+PluginDso::incInstanceCount()
+{
+ _instanceCount.refcount_inc();
+ Debug(_tag, "instance count (inst-count:%d, dso-addr:%p)", _instanceCount.refcount(), this);
+}
+
+void
+PluginDso::decInstanceCount()
+{
+ _instanceCount.refcount_dec();
+ Debug(_tag, "instance count (inst-count:%d, dso-addr:%p)", _instanceCount.refcount(), this);
+}
+
+int
+PluginDso::instanceCount()
+{
+ return _instanceCount.refcount();
+}
+
+PluginDso::PluginList PluginDso::_list;
diff --git a/proxy/http/remap/PluginDso.h b/proxy/http/remap/PluginDso.h
new file mode 100644
index 0000000..4554c6b
--- /dev/null
+++ b/proxy/http/remap/PluginDso.h
@@ -0,0 +1,104 @@
+/** @file
+
+ Header file for a class that deals with plugin Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#pragma once
+
+#include <dlfcn.h>
+#include <vector>
+#include <ctime>
+
+#include "tscore/ts_file.h"
+namespace fs = ts::file;
+
+#include "tscore/Ptr.h"
+#include "tscpp/util/IntrusiveDList.h"
+
+class PluginThreadContext : public RefCountObj
+{
+public:
+ virtual void acquire() = 0;
+ virtual void release() = 0;
+ static constexpr const char *const _tag = "plugin_context"; /** @brief log tag used by this class */
+};
+
+class PluginDso : public PluginThreadContext
+{
+ friend class PluginFactory;
+
+public:
+ PluginDso(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath);
+ virtual ~PluginDso();
+
+ /* DSO Load, unload, get symbols from DSO */
+ virtual bool load(std::string &error);
+ virtual bool unload(std::string &error);
+ bool isLoaded();
+ bool getSymbol(const char *symbol, void *&address, std::string &error) const;
+
+ /* Accessors for effective and runtime paths */
+ const fs::path &effectivePath() const;
+ const fs::path &runtimePath() const;
+ time_t modTime() const;
+
+ /* List used by the plugin factory */
+ using self_type = PluginDso; ///< Self reference type.
+ self_type *_next = nullptr;
+ self_type *_prev = nullptr;
+ using Linkage = ts::IntrusiveLinkage<self_type>;
+ using PluginList = ts::IntrusiveDList<PluginDso::Linkage>;
+
+ /* Methods to be called when processing a list of plugins, to overloaded by the remap or the global plugins correspondingly */
+ virtual void indicateReload() = 0;
+ virtual bool init(std::string &error) = 0;
+ virtual void done() = 0;
+
+ void acquire();
+ void release();
+
+ void incInstanceCount();
+ void decInstanceCount();
+ int instanceCount();
+
+protected:
+ void clean(std::string &error);
+
+ fs::path _configPath; /** @brief the name specified in the config file */
+ fs::path _effectivePath; /** @brief the plugin installation path which was used to load DSO */
+ fs::path _runtimePath; /** @brief the plugin runtime path where the plugin was copied to be loaded */
+
+ void *_dlh = nullptr; /** @brief dlopen handler used internally in this class, used as flag for loaded vs unloaded (nullptr) */
+ std::error_code _errorCode; /** @brief used in filesystem calls */
+
+ static constexpr const char *const _tag = "plugin_dso"; /** @brief log tag used by this class */
+ time_t _mtime = 0; /* @brief modification time of the DSO's file, used for checking */
+ bool _preventiveCleaning = true;
+
+ static PluginList _list; /** @brief a global list of plugins, usually maintained by a plugin factory or plugin instance itself */
+ RefCountObj _instanceCount; /** @brief used for properly calling "done" and "indicate config reload" methods by the factory */
+};
diff --git a/proxy/http/remap/PluginFactory.cc b/proxy/http/remap/PluginFactory.cc
new file mode 100644
index 0000000..c6b1c8a
--- /dev/null
+++ b/proxy/http/remap/PluginFactory.cc
@@ -0,0 +1,264 @@
+/** @file
+
+ Functionality allowing to load all plugins from a single config reload.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ */
+
+#include "RemapPluginInfo.h"
+#include "PluginFactory.h"
+#ifdef PLUGIN_DSO_TESTS
+#include "unit-tests/plugin_testing_common.h"
+#else
+#include "tscore/Diags.h"
+#endif
+
+#include <algorithm> /* std::swap */
+
+RemapPluginInst::RemapPluginInst(RemapPluginInfo &plugin) : _plugin(plugin)
+{
+ _plugin.acquire();
+ _plugin.incInstanceCount();
+}
+
+RemapPluginInst::~RemapPluginInst()
+{
+ _plugin.decInstanceCount();
+ _plugin.release();
+}
+
+bool
+RemapPluginInst::init(int argc, char **argv, std::string &error)
+{
+ bool result = false;
+ result = _plugin.initInstance(argc, argv, &_instance, error);
+
+ return result;
+}
+
+void
+RemapPluginInst::done()
+{
+ _plugin.doneInstance(_instance);
+}
+
+TSRemapStatus
+RemapPluginInst::doRemap(TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+ return _plugin.doRemap(_instance, rh, rri);
+}
+
+void
+RemapPluginInst::osResponse(TSHttpTxn rh, int os_response_type)
+{
+ _plugin.osResponse(_instance, rh, os_response_type);
+}
+
+PluginFactory::PluginFactory()
+{
+ _uuid = new ATSUuid();
+ if (nullptr != _uuid) {
+ _uuid->initialize(TS_UUID_V4);
+ if (!_uuid->valid()) {
+ /* Destroy and mark failure */
+ delete _uuid;
+ _uuid = nullptr;
+ }
+ }
+
+ Debug(_tag, "created plugin factory %s", getUuid());
+}
+
+PluginFactory::~PluginFactory()
+{
+ _instList.apply([](RemapPluginInst *pluginInst) -> void { delete pluginInst; });
+ _instList.clear();
+
+ fs::remove(_runtimeDir, _ec);
+
+ Debug(_tag, "destroyed plugin factory %s", getUuid());
+ delete _uuid;
+}
+
+PluginFactory &
+PluginFactory::addSearchDir(const fs::path &searchDir)
+{
+ _searchDirs.push_back(searchDir);
+ Debug(_tag, "added plugin search dir %s", searchDir.c_str());
+ return *this;
+}
+
+PluginFactory &
+PluginFactory::setRuntimeDir(const fs::path &runtimeDir)
+{
+ _runtimeDir = runtimeDir / fs::path(getUuid());
+ Debug(_tag, "set plugin runtime dir %s", runtimeDir.c_str());
+ return *this;
+}
+
+const char *
+PluginFactory::getUuid()
+{
+ return _uuid ? _uuid->getString() : "uknown";
+}
+
+/**
+ * @brief Loads, initializes and return a valid Remap Plugin instance.
+ *
+ * @param configPath plugin path as specified in the plugin
+ * @param argc number of parameters passed to the plugin during instance initialization
+ * @param argv parameters passed to the plugin during instance initialization
+ * @param context Plugin context is used from continuations to guarantee correct reference counting against the plugin.
+ * @param error human readable message if something goes wrong, empty otherwise
+ * @return pointer to a plugin instance, nullptr if failure
+ */
+RemapPluginInst *
+PluginFactory::getRemapPlugin(const fs::path &configPath, int argc, char **argv, std::string &error)
+{
+ /* Discover the effective path by looking into the search dirs */
+ fs::path effectivePath = getEffectivePath(configPath);
+ if (effectivePath.empty()) {
+ error.assign("failed to find plugin '").append(configPath.string()).append("'");
+ return nullptr;
+ }
+
+ /* Only one plugin with this effective path can be loaded by a plugin factory */
+ RemapPluginInfo *plugin = dynamic_cast<RemapPluginInfo *>(findByEffectivePath(effectivePath));
+ RemapPluginInst *inst = nullptr;
+
+ if (nullptr == plugin) {
+ /* The plugin requested have not been loaded yet. */
+ Debug(_tag, "plugin '%s' has not been loaded yet, loading as remap plugin", configPath.c_str());
+
+ fs::path runtimePath;
+ runtimePath /= _runtimeDir;
+ runtimePath /= effectivePath.relative_path();
+
+ fs::path parent = runtimePath.parent_path();
+ if (!fs::create_directories(parent, _ec)) {
+ error.assign("failed to create plugin runtime dir");
+ return nullptr;
+ }
+
+ plugin = new RemapPluginInfo(configPath, effectivePath, runtimePath);
+ if (nullptr != plugin) {
+ if (plugin->load(error)) {
+ _list.append(plugin);
+
+ if (plugin->init(error)) {
+ inst = new RemapPluginInst(*plugin);
+ inst->init(argc, argv, error);
+ _instList.append(inst);
+ }
+
+ if (_preventiveCleaning) {
+ clean(error);
+ }
+ } else {
+ return nullptr;
+ }
+ }
+ } else {
+ Debug(_tag, "plugin '%s' has already been loaded", configPath.c_str());
+ inst = new RemapPluginInst(*plugin);
+ inst->init(argc, argv, error);
+ _instList.append(inst);
+ }
+
+ return inst;
+}
+
+/**
+ * @brief full path to the first plugin found in the search path which will be used to be copied to runtime location and loaded.
+ *
+ * @param configPath path specified in the config file, it can be relative path.
+ * @return full path to the plugin.
+ */
+fs::path
+PluginFactory::getEffectivePath(const fs::path &configPath)
+{
+ if (configPath.is_absolute()) {
+ if (fs::exists(configPath)) {
+ return fs::canonical(configPath.string(), _ec);
+ } else {
+ return fs::path();
+ }
+ }
+
+ fs::path path;
+
+ for (auto dir : _searchDirs) {
+ fs::path candidatePath = dir / configPath;
+ if (fs::exists(candidatePath)) {
+ path = fs::canonical(candidatePath, _ec);
+ break;
+ }
+ }
+
+ return path;
+}
+
+/**
+ * @brief Find a plugin by path from our linked plugin list by using plugin effective (canonical) path
+ *
+ * @param path effective (caninical) path
+ * @return plugin found or nullptr if not found
+ */
+PluginDso *
+PluginFactory::findByEffectivePath(const fs::path &path)
+{
+ struct stat sb;
+ time_t mtime = 0;
+ if (0 == stat(path.c_str(), &sb)) {
+ mtime = sb.st_mtime;
+ }
+ auto spot = std::find_if(_list.begin(), _list.end(), [&](PluginDso const &plugin) -> bool {
+ return (0 == path.string().compare(plugin.effectivePath().string()) && (mtime == plugin.modTime()));
+ });
+ return spot == _list.end() ? nullptr : static_cast<PluginDso *>(spot);
+}
+
+/**
+ * @brief Tell all plugins (that so wish) that remap.config is being reloaded
+ *
+ * This method would be useful only in case configs are reloaded independently from
+ * factory/plugins instantiation and initialization.
+ */
+void
+PluginFactory::indicateReload()
+{
+ Debug(_tag, "indicated config reload to factory '%s'", getUuid());
+
+ _instList.apply([](RemapPluginInst &pluginInst) -> void { pluginInst.done(); });
+
+ _list.apply([](PluginDso &plugin) -> void {
+ if (1 == plugin.instanceCount()) {
+ plugin.done();
+ } else {
+ plugin.indicateReload();
+ }
+ });
+}
+
+void
+PluginFactory::clean(std::string &error)
+{
+ fs::remove(_runtimeDir, _ec);
+}
diff --git a/proxy/http/remap/PluginFactory.h b/proxy/http/remap/PluginFactory.h
new file mode 100644
index 0000000..1cb0661
--- /dev/null
+++ b/proxy/http/remap/PluginFactory.h
@@ -0,0 +1,119 @@
+/** @file
+
+ Functionality allowing to load all plugins from a single config reload (header).
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#pragma once
+
+#include <vector>
+
+#include "tscore/Ptr.h"
+#include "PluginDso.h"
+#include "RemapPluginInfo.h"
+
+#include "tscore/Ptr.h"
+#include "tscpp/util/IntrusiveDList.h"
+
+#include "tscore/ink_uuid.h"
+#include "ts/apidefs.h"
+
+/**
+ * @brief Bundles plugin info + plugin instance data to be used always together.
+ */
+class RemapPluginInst
+{
+public:
+ RemapPluginInst() = delete;
+ RemapPluginInst(RemapPluginInst &) = delete;
+ RemapPluginInst(RemapPluginInfo &plugin);
+ ~RemapPluginInst();
+
+ /* Used by the PluginFactory */
+ bool init(int argc, char **argv, std::string &error);
+ void done();
+
+ /* Used by the traffic server core while processing requests */
+ TSRemapStatus doRemap(TSHttpTxn rh, TSRemapRequestInfo *rri);
+ void osResponse(TSHttpTxn rh, int os_response_type);
+
+ /* List used by the plugin factory */
+ using self_type = RemapPluginInst; ///< Self reference type.
+ self_type *_next = nullptr;
+ self_type *_prev = nullptr;
+ using Linkage = ts::IntrusiveLinkage<self_type>;
+
+ /* Plugin instance = the plugin info + the data returned by the init callback */
+ RemapPluginInfo &_plugin;
+ void *_instance = nullptr;
+};
+
+/**
+ * @brief loads plugins, instantiates and keep track of plugin instances created by this factory.
+ *
+ * - Handles looking through search directories to determine final plugin canonical file name to be used (called here effective
+ * path).
+ * - Makes sure we load each DSO only once per effective path.
+ * - Keeps track of all loaded remap plugins and their instances.
+ * - Maitains the notion of plugin runtime paths and makes sure every factory instance uses different runtime paths for its plugins.
+ * - Makes sure plugin DSOs are loaded for the lifetime of the PluginFactory.
+ *
+ * Each plugin factory instance corresponds to a config reload, each new config file set is meant to use a new factory instance.
+ * A notion of runtime directory is maintained to make sure the DSO library files are not erased or modified while the library are
+ * loaded in memory and make sure if the library file is overriden with a new DSO file that the new overriding plugin's
+ * functionality will be loaded with the next factory, it also handles some problems noticed on different OSes in handling
+ * filesystem links and different dl library implementations.
+ *
+ * @note This is meant to unify the way global and remap plugins are (re)loaded (global plugin support is not implemented yet).
+ */
+class PluginFactory
+{
+ using PluginInstList = ts::IntrusiveDList<RemapPluginInst::Linkage>;
+ PluginDso::PluginList &_list = PluginDso::_list;
+
+public:
+ PluginFactory();
+ virtual ~PluginFactory();
+
+ PluginFactory &setRuntimeDir(const fs::path &runtimeDir);
+ PluginFactory &addSearchDir(const fs::path &searchDir);
+
+ RemapPluginInst *getRemapPlugin(const fs::path &configPath, int argc, char **argv, std::string &error);
+
+ virtual const char *getUuid();
+ void clean(std::string &error);
+
+ void indicateReload();
+
+protected:
+ PluginDso *findByEffectivePath(const fs::path &path);
+ fs::path getEffectivePath(const fs::path &configPath);
+
+ std::vector<fs::path> _searchDirs; /** @brief ordered list of search paths where we look for plugins */
+ fs::path _runtimeDir; /** @brief the path where we would create a temporary copies of the plugins to load */
+
+ PluginInstList _instList;
+
+ ATSUuid *_uuid = nullptr;
+ std::error_code _ec;
+ bool _preventiveCleaning = true;
+
+ static constexpr const char *const _tag = "plugin_factory"; /** @brief log tag used by this class */
+};
diff --git a/proxy/http/remap/RemapConfig.cc b/proxy/http/remap/RemapConfig.cc
index 8d5948f..bf33265 100644
--- a/proxy/http/remap/RemapConfig.cc
+++ b/proxy/http/remap/RemapConfig.cc
@@ -32,6 +32,7 @@
#include "tscore/ink_file.h"
#include "tscore/Tokenizer.h"
#include "IPAllow.h"
+#include "PluginFactory.h"
#define modulePrefix "[ReverseProxy]"
@@ -708,23 +709,29 @@ remap_check_option(const char **argv, int argc, unsigned long findmode, int *_re
return ret_flags;
}
-int
+/**
+ * @brief loads a remap plugin
+ *
+ * @pparam mp url mapping
+ * @pparam errbuf error buffer
+ * @pparam errbufsize size of the error buffer
+ * @pparam jump_to_argc
+ * @pparam plugin_found_at
+ * @return success - true, failure - false
+ */
+bool
remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, int errbufsize, int jump_to_argc,
- int *plugin_found_at)
+ int *plugin_found_at, UrlRewrite *rewrite)
{
- TSRemapInterface ri;
- struct stat stat_buf;
- RemapPluginInfo *pi;
- char *c, *err, tmpbuf[2048], default_path[PATH_NAME_MAX];
+ char *c, *err;
const char *new_argv[1024];
- char *parv[1024];
- int idx = 0;
-
+ char *pargv[1024];
+ int idx = 0;
+ int parc = 0;
*plugin_found_at = 0;
- memset(parv, 0, sizeof(parv));
+ memset(pargv, 0, sizeof(pargv));
memset(new_argv, 0, sizeof(new_argv));
- tmpbuf[0] = 0;
ink_assert((unsigned)argc < countof(new_argv));
@@ -737,135 +744,40 @@ remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, in
}
argv = &new_argv[0];
if (!remap_check_option(argv, argc, REMAP_OPTFLG_PLUGIN, &idx)) {
- return -1;
+ return false;
}
} else {
if (unlikely(!mp || (remap_check_option(argv, argc, REMAP_OPTFLG_PLUGIN, &idx) & REMAP_OPTFLG_PLUGIN) == 0)) {
snprintf(errbuf, errbufsize, "Can't find remap plugin keyword or \"url_mapping\" is nullptr");
- return -1; /* incorrect input data - almost impossible case */
+ return false; /* incorrect input data - almost impossible case */
}
}
if (unlikely((c = (char *)strchr(argv[idx], (int)'=')) == nullptr || !(*(++c)))) {
snprintf(errbuf, errbufsize, "Can't find remap plugin file name in \"@%s\"", argv[idx]);
- return -2; /* incorrect input data */
- }
-
- if (stat(c, &stat_buf) != 0) {
- ats_scoped_str plugin_default_path(RecConfigReadPluginDir());
-
- // Try with the plugin path instead
- if (strlen(c) + strlen(plugin_default_path) > (PATH_NAME_MAX - 1)) {
- Debug("remap_plugin", "way too large a path specified for remap plugin");
- return -3;
- }
-
- snprintf(default_path, PATH_NAME_MAX, "%s/%s", static_cast<char *>(plugin_default_path), c);
- Debug("remap_plugin", "attempting to stat default plugin path: %s", default_path);
-
- if (stat(default_path, &stat_buf) == 0) {
- Debug("remap_plugin", "stat successful on %s using that", default_path);
- c = &default_path[0];
- } else {
- snprintf(errbuf, errbufsize, "Can't find remap plugin file \"%s\"", c);
- return -3;
- }
+ return false; /* incorrect input data */
}
Debug("remap_plugin", "using path %s for plugin", c);
- if ((pi = RemapPluginInfo::find_by_path(c)) == nullptr) {
- pi = new RemapPluginInfo(ts::file::path(c));
- RemapPluginInfo::add_to_list(pi);
- Debug("remap_plugin", "New remap plugin info created for \"%s\"", c);
-
- {
- uint32_t elevate_access = 0;
- REC_ReadConfigInteger(elevate_access, "proxy.config.plugin.load_elevated");
- ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0);
-
- if ((pi->dl_handle = dlopen(c, RTLD_NOW)) == nullptr) {
-#if defined(freebsd) || defined(openbsd)
- err = (char *)dlerror();
-#else
- err = dlerror();
-#endif
- snprintf(errbuf, errbufsize, "Can't load plugin \"%s\" - %s", c, err ? err : "Unknown dlopen() error");
- return -4;
- }
- pi->init_cb = reinterpret_cast<RemapPluginInfo::Init_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_INIT));
- pi->config_reload_cb = reinterpret_cast<RemapPluginInfo::Reload_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_CONFIG_RELOAD));
- pi->done_cb = reinterpret_cast<RemapPluginInfo::Done_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_DONE));
- pi->new_instance_cb =
- reinterpret_cast<RemapPluginInfo::New_Instance_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_NEW_INSTANCE));
- pi->delete_instance_cb =
- reinterpret_cast<RemapPluginInfo::Delete_Instance_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_DELETE_INSTANCE));
- pi->do_remap_cb = reinterpret_cast<RemapPluginInfo::Do_Remap_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_DO_REMAP));
- pi->os_response_cb = reinterpret_cast<RemapPluginInfo::OS_Response_F *>(dlsym(pi->dl_handle, TSREMAP_FUNCNAME_OS_RESPONSE));
-
- int retcode = 0;
- if (!pi->init_cb) {
- snprintf(errbuf, errbufsize, R"(Can't find "%s" function in remap plugin "%s")", TSREMAP_FUNCNAME_INIT, c);
- retcode = -10;
- } else if (!pi->new_instance_cb && pi->delete_instance_cb) {
- snprintf(errbuf, errbufsize,
- R"(Can't find "%s" function in remap plugin "%s" which is required if "%s" function exists)",
- TSREMAP_FUNCNAME_NEW_INSTANCE, c, TSREMAP_FUNCNAME_DELETE_INSTANCE);
- retcode = -11;
- } else if (!pi->do_remap_cb) {
- snprintf(errbuf, errbufsize, R"(Can't find "%s" function in remap plugin "%s")", TSREMAP_FUNCNAME_DO_REMAP, c);
- retcode = -12;
- } else if (pi->new_instance_cb && !pi->delete_instance_cb) {
- snprintf(errbuf, errbufsize,
- R"(Can't find "%s" function in remap plugin "%s" which is required if "%s" function exists)",
- TSREMAP_FUNCNAME_DELETE_INSTANCE, c, TSREMAP_FUNCNAME_NEW_INSTANCE);
- retcode = -13;
- }
- if (retcode) {
- if (errbuf && errbufsize > 0) {
- Debug("remap_plugin", "%s", errbuf);
- }
- dlclose(pi->dl_handle);
- pi->dl_handle = nullptr;
- return retcode;
- }
- memset(&ri, 0, sizeof(ri));
- ri.size = sizeof(ri);
- ri.tsremap_version = TSREMAP_VERSION;
-
- if (pi->init_cb(&ri, tmpbuf, sizeof(tmpbuf) - 1) != TS_SUCCESS) {
- snprintf(errbuf, errbufsize, "Failed to initialize plugin \"%s\": %s", pi->path.c_str(),
- tmpbuf[0] ? tmpbuf : "Unknown plugin error");
- return -5;
- }
- } // done elevating access
- Debug("remap_plugin", "Remap plugin \"%s\" - initialization completed", c);
- }
-
- if (!pi->dl_handle) {
- snprintf(errbuf, errbufsize, "Can't load plugin \"%s\"", c);
- return -6;
- }
-
+ /* Prepare remap plugin parameters from the config */
if ((err = mp->fromURL.string_get(nullptr)) == nullptr) {
snprintf(errbuf, errbufsize, "Can't load fromURL from URL class");
- return -7;
+ return false;
}
-
- int parc = 0;
- parv[parc++] = ats_strdup(err);
+ pargv[parc++] = ats_strdup(err);
ats_free(err);
if ((err = mp->toURL.string_get(nullptr)) == nullptr) {
snprintf(errbuf, errbufsize, "Can't load toURL from URL class");
- return -7;
+ return false;
}
- parv[parc++] = ats_strdup(err);
+ pargv[parc++] = ats_strdup(err);
ats_free(err);
bool plugin_encountered = false;
// how many plugin parameters we have for this remapping
- for (idx = 0; idx < argc && parc < (int)(countof(parv) - 1); idx++) {
+ for (idx = 0; idx < argc && parc < static_cast<int>(countof(pargv) - 1); idx++) {
if (plugin_encountered && !strncasecmp("plugin=", argv[idx], 7) && argv[idx][7]) {
*plugin_found_at = idx;
break; // if there is another plugin, lets deal with that later
@@ -876,7 +788,7 @@ remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, in
}
if (!strncasecmp("pparam=", argv[idx], 7) && argv[idx][7]) {
- parv[parc++] = const_cast<char *>(&(argv[idx][7]));
+ pargv[parc++] = const_cast<char *>(&(argv[idx][7]));
}
}
@@ -885,43 +797,32 @@ remap_load_plugin(const char **argv, int argc, url_mapping *mp, char *errbuf, in
Debug("url_rewrite", "Argument %d: %s", k, argv[k]);
}
- Debug("url_rewrite", "Viewing parsed plugin parameters for %s: [%d]", pi->path.c_str(), *plugin_found_at);
+ Debug("url_rewrite", "Viewing parsed plugin parameters for %s: [%d]", c, *plugin_found_at);
for (int k = 0; k < parc; k++) {
- Debug("url_rewrite", "Argument %d: %s", k, parv[k]);
+ Debug("url_rewrite", "Argument %d: %s", k, pargv[k]);
}
- Debug("remap_plugin", "creating new plugin instance");
-
- void *ih = nullptr;
- TSReturnCode res = TS_SUCCESS;
- if (pi->new_instance_cb) {
-#if (!defined(kfreebsd) && defined(freebsd)) || defined(darwin)
- optreset = 1;
-#endif
-#if defined(__GLIBC__)
- optind = 0;
-#else
- optind = 1;
-#endif
- opterr = 0;
- optarg = nullptr;
-
- res = pi->new_instance_cb(parc, parv, &ih, tmpbuf, sizeof(tmpbuf) - 1);
- }
-
- Debug("remap_plugin", "done creating new plugin instance");
+ RemapPluginInst *pi = nullptr;
+ std::string error;
+ {
+ uint32_t elevate_access = 0;
+ REC_ReadConfigInteger(elevate_access, "proxy.config.plugin.load_elevated");
+ ElevateAccess access(elevate_access ? ElevateAccess::FILE_PRIVILEGE : 0);
- ats_free(parv[0]); // fromURL
- ats_free(parv[1]); // toURL
+ pi = rewrite->pluginFactory.getRemapPlugin(ts::file::path(const_cast<const char *>(c)), parc, pargv, error);
+ } // done elevating access
- if (res != TS_SUCCESS) {
- snprintf(errbuf, errbufsize, "Failed to create instance for plugin \"%s\": %s", c, tmpbuf[0] ? tmpbuf : "Unknown plugin error");
- return -8;
+ bool result = true;
+ if (nullptr == pi) {
+ snprintf(errbuf, errbufsize, "%s", error.c_str());
+ } else {
+ mp->add_plugin_instance(pi);
}
- mp->add_plugin(pi, ih);
+ ats_free(pargv[0]); // fromURL
+ ats_free(pargv[1]); // toURL
- return 0;
+ return result;
}
/** will process the regex mapping configuration and create objects in
output argument reg_map. It assumes existing data in reg_map is
@@ -1368,8 +1269,8 @@ remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti)
int jump_to_argc = 0;
// this loads the first plugin
- if (remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), 0,
- &plugin_found_at)) {
+ if (!remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), 0, &plugin_found_at,
+ bti->rewrite)) {
Debug("remap_plugin", "Remap plugin load error - %s", errStrBuf[0] ? errStrBuf : "Unknown error");
errStr = errStrBuf;
goto MAP_ERROR;
@@ -1377,8 +1278,8 @@ remap_parse_config_bti(const char *path, BUILD_TABLE_INFO *bti)
// this loads any subsequent plugins (if present)
while (plugin_found_at) {
jump_to_argc += plugin_found_at;
- if (remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), jump_to_argc,
- &plugin_found_at)) {
+ if (!remap_load_plugin((const char **)bti->argv, bti->argc, new_mapping, errStrBuf, sizeof(errStrBuf), jump_to_argc,
+ &plugin_found_at, bti->rewrite)) {
Debug("remap_plugin", "Remap plugin load error - %s", errStrBuf[0] ? errStrBuf : "Unknown error");
errStr = errStrBuf;
goto MAP_ERROR;
@@ -1421,7 +1322,7 @@ remap_parse_config(const char *path, UrlRewrite *rewrite)
// If this happens to be a config reload, the list of loaded remap plugins is non-empty, and we
// can signal all these plugins that a reload has begun.
- RemapPluginInfo::indicate_reload();
+ rewrite->pluginFactory.indicateReload();
bti.rewrite = rewrite;
return remap_parse_config_bti(path, &bti);
}
diff --git a/proxy/http/remap/RemapPluginInfo.cc b/proxy/http/remap/RemapPluginInfo.cc
index db6dfe5..8a1c00e 100644
--- a/proxy/http/remap/RemapPluginInfo.cc
+++ b/proxy/http/remap/RemapPluginInfo.cc
@@ -19,62 +19,252 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
*/
+#include <unistd.h>
+
#include "RemapPluginInfo.h"
#include "tscore/ink_string.h"
#include "tscore/ink_memory.h"
+#include "tscore/ink_apidefs.h"
+
+#include "RemapPluginInfo.h"
+#ifdef PLUGIN_DSO_TESTS
+#include "unit-tests/plugin_testing_common.h"
+#else
+#include "tscore/Diags.h"
+#endif
+
+/**
+ * @brief helper function that returns the function address from the plugin DSO
+ *
+ * There can be valid defined DSO symbols that are NULL
+ * but when it comes to functions we can assume that
+ * if not defined we can return nullptr and a valid address if the are defined.
+ * @param symbol function symbol name
+ * @param error error messages in case of symbol is not found
+ * @return function address or nullptr if not found.
+ */
+template <class T>
+T *
+RemapPluginInfo::getFunctionSymbol(const char *symbol)
+{
+ std::string error; /* ignore the error, return nullptr if symbol not defined */
+ void *address = nullptr;
+ getSymbol(symbol, address, error);
+ return reinterpret_cast<T *>(address);
+}
+
+std::string
+RemapPluginInfo::missingRequiredSymbolError(const std::string &pluginName, const char *required, const char *requiring)
+{
+ std::string error;
+ error.assign("plugin ").append(pluginName).append(" missing required function ").append(required);
+ if (requiring) {
+ error.append(" if ").append(requiring).append(" is defined");
+ }
+ return error;
+}
+
+RemapPluginInfo::RemapPluginInfo(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath)
+ : PluginDso(configPath, effectivePath, runtimePath)
+{
+}
+
+bool
+RemapPluginInfo::load(std::string &error)
+{
+ error.clear();
+
+ if (!PluginDso::load(error)) {
+ return false;
+ }
+
+ init_cb = getFunctionSymbol<Init_F>(TSREMAP_FUNCNAME_INIT);
+ config_reload_cb = getFunctionSymbol<Reload_F>(TSREMAP_FUNCNAME_CONFIG_RELOAD);
+ done_cb = getFunctionSymbol<Done_F>(TSREMAP_FUNCNAME_DONE);
+ new_instance_cb = getFunctionSymbol<New_Instance_F>(TSREMAP_FUNCNAME_NEW_INSTANCE);
+ delete_instance_cb = getFunctionSymbol<Delete_Instance_F>(TSREMAP_FUNCNAME_DELETE_INSTANCE);
+ do_remap_cb = getFunctionSymbol<Do_Remap_F>(TSREMAP_FUNCNAME_DO_REMAP);
+ os_response_cb = getFunctionSymbol<OS_Response_F>(TSREMAP_FUNCNAME_OS_RESPONSE);
+
+ /* Validate if the callback TSREMAP functions are specified correctly in the plugin. */
+ bool valid = true;
+ if (!init_cb) {
+ error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_INIT);
+ valid = false;
+ } else if (!do_remap_cb) {
+ error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_DO_REMAP);
+ valid = false;
+ } else if (!new_instance_cb && delete_instance_cb) {
+ error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_NEW_INSTANCE, TSREMAP_FUNCNAME_DELETE_INSTANCE);
+ valid = false;
+ } else if (new_instance_cb && !delete_instance_cb) {
+ error = missingRequiredSymbolError(_configPath.string(), TSREMAP_FUNCNAME_DELETE_INSTANCE, TSREMAP_FUNCNAME_NEW_INSTANCE);
+ valid = false;
+ }
+
+ if (valid) {
+ Debug(_tag, "plugin '%s' callbacks validated", _configPath.c_str());
+ } else {
+ Error("plugin '%s' callbacks validation failed: %s", _configPath.c_str(), error.c_str());
+ }
+ return valid;
+}
+
+/* Initialize plugin (required). */
+bool
+RemapPluginInfo::init(std::string &error)
+{
+ TSRemapInterface ri;
+ bool result = true;
-RemapPluginInfo::List RemapPluginInfo::g_list;
+ Debug(_tag, "started initializing plugin '%s'", _configPath.c_str());
-RemapPluginInfo::RemapPluginInfo(ts::file::path &&library_path) : path(std::move(library_path)) {}
+ /* A buffer to get the error from the plugin instance init function, be defensive here. */
+ char tmpbuf[2048];
+ ink_zero(tmpbuf);
-RemapPluginInfo::~RemapPluginInfo()
+ ink_zero(ri);
+ ri.size = sizeof(ri);
+ ri.tsremap_version = TSREMAP_VERSION;
+
+ setPluginContext();
+
+ if (init_cb && init_cb(&ri, tmpbuf, sizeof(tmpbuf) - 1) != TS_SUCCESS) {
+ error.assign("failed to initialize plugin ")
+ .append(_configPath.string())
+ .append(": ")
+ .append(tmpbuf[0] ? tmpbuf : "Unknown plugin error");
+ result = false;
+ }
+
+ resetPluginContext();
+
+ Debug(_tag, "finished initializing plugin '%s'", _configPath.c_str());
+
+ return result;
+}
+
+/* Called when plugin is unloaded (optional). */
+void
+RemapPluginInfo::done()
{
- if (dl_handle) {
- dlclose(dl_handle);
+ if (done_cb) {
+ done_cb();
}
}
-//
-// Find a plugin by path from our linked list
-//
-RemapPluginInfo *
-RemapPluginInfo::find_by_path(std::string_view library_path)
+bool
+RemapPluginInfo::initInstance(int argc, char **argv, void **ih, std::string &error)
{
- auto spot = std::find_if(g_list.begin(), g_list.end(),
- [&](self_type const &info) -> bool { return 0 == library_path.compare(info.path.view()); });
- return spot == g_list.end() ? nullptr : static_cast<self_type *>(spot);
+ TSReturnCode res = TS_SUCCESS;
+ bool result = true;
+
+ Debug(_tag, "started initializing instance of plugin '%s'", _configPath.c_str());
+
+ /* A buffer to get the error from the plugin instance init function, be defensive here. */
+ char tmpbuf[2048];
+ ink_zero(tmpbuf);
+
+ if (new_instance_cb) {
+#if defined(freebsd) || defined(darwin)
+ optreset = 1;
+#endif
+#if defined(__GLIBC__)
+ optind = 0;
+#else
+ optind = 1;
+#endif
+ opterr = 0;
+ optarg = nullptr;
+
+ setPluginContext();
+
+ res = new_instance_cb(argc, argv, ih, tmpbuf, sizeof(tmpbuf) - 1);
+
+ resetPluginContext();
+
+ if (TS_SUCCESS != res) {
+ error.assign("failed to create instance for plugin ")
+ .append(_configPath.string())
+ .append(": ")
+ .append(tmpbuf[0] ? tmpbuf : "Unknown plugin error");
+ result = false;
+ }
+ }
+
+ Debug(_tag, "finished initializing instance of plugin '%s'", _configPath.c_str());
+
+ return result;
}
-//
-// Add a plugin to the linked list
-//
void
-RemapPluginInfo::add_to_list(RemapPluginInfo *pi)
+RemapPluginInfo::doneInstance(void *ih)
{
- g_list.append(pi);
+ setPluginContext();
+
+ if (delete_instance_cb) {
+ delete_instance_cb(ih);
+ }
+
+ resetPluginContext();
+}
+
+TSRemapStatus
+RemapPluginInfo::doRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+ TSRemapStatus result = TSREMAP_NO_REMAP;
+
+ setPluginContext();
+
+ if (do_remap_cb) {
+ result = do_remap_cb(ih, rh, rri);
+ }
+
+ resetPluginContext();
+
+ return result;
}
-//
-// Remove and delete all plugins from a list.
-//
void
-RemapPluginInfo::delete_list()
+RemapPluginInfo::osResponse(void *ih, TSHttpTxn rh, int os_response_type)
{
- g_list.apply([](self_type *info) -> void { delete info; });
- g_list.clear();
+ setPluginContext();
+
+ if (os_response_cb) {
+ os_response_cb(ih, rh, os_response_type);
+ }
+
+ resetPluginContext();
}
-//
-// Tell all plugins (that so wish) that remap.config is being reloaded
-//
+RemapPluginInfo::~RemapPluginInfo() {}
+
void
-RemapPluginInfo::indicate_reload()
+RemapPluginInfo::indicateReload()
{
- g_list.apply([](self_type *info) -> void {
- if (info->config_reload_cb) {
- info->config_reload_cb();
- }
- });
+ setPluginContext();
+
+ if (config_reload_cb) {
+ config_reload_cb();
+ }
+
+ resetPluginContext();
+}
+
+inline void
+RemapPluginInfo::setPluginContext()
+{
+ _tempContext = pluginThreadContext;
+ pluginThreadContext = this;
+ Debug(_tag, "change plugin context from dso-addr:%p to dso-addr:%p", pluginThreadContext, _tempContext);
+}
+
+inline void
+RemapPluginInfo::resetPluginContext()
+{
+ Debug(_tag, "change plugin context from dso-addr:%p to dso-addr:%p (restore)", this, pluginThreadContext);
+ pluginThreadContext = _tempContext;
}
diff --git a/proxy/http/remap/RemapPluginInfo.h b/proxy/http/remap/RemapPluginInfo.h
index 7802b5c..cc5941d 100644
--- a/proxy/http/remap/RemapPluginInfo.h
+++ b/proxy/http/remap/RemapPluginInfo.h
@@ -19,14 +19,22 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
*/
#pragma once
+
+#include <string>
+#include <tuple>
+
#include "tscore/ink_platform.h"
-#include "tscpp/util/IntrusiveDList.h"
-#include "tscore/ts_file.h"
#include "ts/apidefs.h"
#include "ts/remap.h"
+#include "PluginDso.h"
+
+class url_mapping;
+
+extern thread_local PluginThreadContext *pluginThreadContext;
static constexpr const char *const TSREMAP_FUNCNAME_INIT = "TSRemapInit";
static constexpr const char *const TSREMAP_FUNCNAME_CONFIG_RELOAD = "TSRemapConfigReload";
@@ -36,14 +44,13 @@ static constexpr const char *const TSREMAP_FUNCNAME_DELETE_INSTANCE = "TSRemapDe
static constexpr const char *const TSREMAP_FUNCNAME_DO_REMAP = "TSRemapDoRemap";
static constexpr const char *const TSREMAP_FUNCNAME_OS_RESPONSE = "TSRemapOSResponse";
-/** Information for a remap plugin.
- * This stores the name of the library file and the callback entry points.
+/**
+ * Holds information for a remap plugin, remap specific callback entry points for plugin init/done and instance init/done, do_remap,
+ * origin server response,
*/
-class RemapPluginInfo
+class RemapPluginInfo : public PluginDso
{
public:
- using self_type = RemapPluginInfo; ///< Self reference type.
-
/// Initialization function, called on library load.
using Init_F = TSReturnCode(TSRemapInterface *api_info, char *errbuf, int errbuf_size);
/// Reload function, called to inform the plugin of a configuration reload.
@@ -59,12 +66,6 @@ public:
/// I have no idea what this is for.
using OS_Response_F = void(void *ih, TSHttpTxn rh, int os_response_type);
- self_type *_next = nullptr;
- self_type *_prev = nullptr;
- using Linkage = ts::IntrusiveLinkage<self_type>;
- using List = ts::IntrusiveDList<Linkage>;
-
- ts::file::path path;
void *dl_handle = nullptr; /* "handle" for the dynamic library */
Init_F *init_cb = nullptr;
Reload_F *config_reload_cb = nullptr;
@@ -74,16 +75,37 @@ public:
Do_Remap_F *do_remap_cb = nullptr;
OS_Response_F *os_response_cb = nullptr;
- explicit RemapPluginInfo(ts::file::path &&library_path);
+ RemapPluginInfo(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath);
~RemapPluginInfo();
- static self_type *find_by_path(std::string_view library_path);
- static void add_to_list(self_type *pi);
- static void delete_list();
- static void indicate_reload();
+ /* Overload to add / execute remap plugin specific tasks during the plugin loading */
+ virtual bool load(std::string &error);
+
+ /* Used by the factory to invoke callbacks during plugin load, init and unload */
+ virtual bool init(std::string &error);
+ virtual void done(void);
+
+ /* Used by the facility that handles remap plugin instances to invoke callbacks per plugin instance */
+ bool initInstance(int argc, char **argv, void **ih, std::string &error);
+ void doneInstance(void *ih);
+
+ /* Used by the other parts of the traffic server core while handling requests */
+ TSRemapStatus doRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri);
+ void osResponse(void *ih, TSHttpTxn rh, int os_response_type);
+
+ /* Used by traffic server core to indicate configuration reload */
+ virtual void indicateReload();
+
+protected:
+ /* Utility to be used only with unit testing */
+ std::string missingRequiredSymbolError(const std::string &pluginName, const char *required, const char *requiring = nullptr);
+ template <class T> T *getFunctionSymbol(const char *symbol);
+ void setPluginContext();
+ void resetPluginContext();
+
+ static constexpr const char *const _tag = "plugin_remap"; /** @brief log tag used by this class */
- /// Singleton list of remap plugin info instances.
- static List g_list;
+ PluginThreadContext *_tempContext = nullptr;
};
/**
diff --git a/proxy/http/remap/RemapPlugins.cc b/proxy/http/remap/RemapPlugins.cc
index 6a66445..3e69c43 100644
--- a/proxy/http/remap/RemapPlugins.cc
+++ b/proxy/http/remap/RemapPlugins.cc
@@ -19,6 +19,7 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
*/
#include "RemapPlugins.h"
@@ -26,16 +27,14 @@
ClassAllocator<RemapPlugins> pluginAllocator("RemapPluginsAlloc");
TSRemapStatus
-RemapPlugins::run_plugin(RemapPluginInfo *plugin)
+RemapPlugins::run_plugin(RemapPluginInst *plugin)
{
ink_assert(_s);
TSRemapStatus plugin_retcode;
TSRemapRequestInfo rri;
- url_mapping *map = _s->url_map.getMapping();
- URL *map_from = _s->url_map.getFromURL();
- URL *map_to = _s->url_map.getToURL();
- void *ih = map->get_instance(_cur);
+ URL *map_from = _s->url_map.getFromURL();
+ URL *map_to = _s->url_map.getToURL();
// This is the equivalent of TSHttpTxnClientReqGet(), which every remap plugin would
// have to call.
@@ -51,11 +50,10 @@ RemapPlugins::run_plugin(RemapPluginInfo *plugin)
// Prepare State for the future
if (_cur == 0) {
- _s->fp_tsremap_os_response = plugin->os_response_cb;
- _s->remap_plugin_instance = ih;
+ _s->os_response_plugin_inst = plugin;
}
- plugin_retcode = plugin->do_remap_cb(ih, reinterpret_cast<TSHttpTxn>(_s->state_machine), &rri);
+ plugin_retcode = plugin->doRemap(reinterpret_cast<TSHttpTxn>(_s->state_machine), &rri);
// TODO: Deal with negative return codes here
if (plugin_retcode < 0) {
plugin_retcode = TSREMAP_NO_REMAP;
@@ -82,7 +80,7 @@ bool
RemapPlugins::run_single_remap()
{
url_mapping *map = _s->url_map.getMapping();
- RemapPluginInfo *plugin = map->get_plugin(_cur); // get the nth plugin in our list of plugins
+ RemapPluginInst *plugin = map->get_plugin_instance(_cur); // get the nth plugin in our list of plugins
TSRemapStatus plugin_retcode = TSREMAP_NO_REMAP;
bool zret = true; // default - last iteration.
Debug("url_rewrite", "running single remap rule id %d for the %d%s time", map->map_id, _cur,
@@ -109,7 +107,7 @@ RemapPlugins::run_single_remap()
if (TSREMAP_NO_REMAP_STOP == plugin_retcode || TSREMAP_DID_REMAP_STOP == plugin_retcode) {
Debug("url_rewrite", "breaking remap plugin chain since last plugin said we should stop after %d rewrites", _rewritten);
- } else if (_cur >= map->plugin_count()) {
+ } else if (_cur >= map->plugin_instance_count()) {
Debug("url_rewrite", "completed all remap plugins for rule id %d, changed by %d plugins", map->map_id, _rewritten);
} else {
Debug("url_rewrite", "completed single remap, attempting another via immediate callback");
diff --git a/proxy/http/remap/RemapPlugins.h b/proxy/http/remap/RemapPlugins.h
index 421b55c..1bec1b9 100644
--- a/proxy/http/remap/RemapPlugins.h
+++ b/proxy/http/remap/RemapPlugins.h
@@ -19,11 +19,8 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- */
-/**
- * Remap plugins class
- **/
+ */
#pragma once
@@ -68,7 +65,7 @@ struct RemapPlugins : public Continuation {
int run_remap(int event, Event *e);
bool run_single_remap();
- TSRemapStatus run_plugin(RemapPluginInfo *plugin);
+ TSRemapStatus run_plugin(RemapPluginInst *plugin);
Action action;
diff --git a/proxy/http/remap/UrlMapping.cc b/proxy/http/remap/UrlMapping.cc
index 782b3e2..61a375d 100644
--- a/proxy/http/remap/UrlMapping.cc
+++ b/proxy/http/remap/UrlMapping.cc
@@ -19,6 +19,7 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
*/
#include "tscore/ink_defs.h"
@@ -30,50 +31,25 @@
*
**/
bool
-url_mapping::add_plugin(RemapPluginInfo *i, void *ih)
+url_mapping::add_plugin_instance(RemapPluginInst *i)
{
- _plugin_list.push_back(i);
- _instance_data.push_back(ih);
-
+ _plugin_inst_list.push_back(i);
return true;
}
/**
*
**/
-RemapPluginInfo *
-url_mapping::get_plugin(std::size_t index) const
+RemapPluginInst *
+url_mapping::get_plugin_instance(std::size_t index) const
{
- Debug("url_rewrite", "get_plugin says we have %zu plugins and asking for plugin %zu", plugin_count(), index);
- if (index < _plugin_list.size()) {
- return _plugin_list[index];
+ Debug("url_rewrite", "get_plugin says we have %zu plugins and asking for plugin %zu", _plugin_inst_list.size(), index);
+ if (index < _plugin_inst_list.size()) {
+ return _plugin_inst_list[index];
}
return nullptr;
}
-void *
-url_mapping::get_instance(std::size_t index) const
-{
- if (index < _instance_data.size()) {
- return _instance_data[index];
- }
- return nullptr;
-}
-
-/**
- *
- **/
-void
-url_mapping::delete_instance(unsigned int index)
-{
- void *ih = get_instance(index);
- RemapPluginInfo *p = get_plugin(index);
-
- if (ih && p && p->delete_instance_cb) {
- p->delete_instance_cb(ih);
- }
-}
-
/**
*
**/
@@ -96,13 +72,6 @@ url_mapping::~url_mapping()
delete rc;
}
- // Delete all instance data, this gets ugly because to delete the instance data, we also
- // must know which plugin this is associated with. Hence, looping with index instead of a
- // normal iterator. ToDo: Maybe we can combine them into another container.
- for (std::size_t i = 0; i < plugin_count(); ++i) {
- delete_instance(i);
- }
-
// Delete filters
while ((afr = filter) != nullptr) {
filter = afr->next;
@@ -122,7 +91,8 @@ url_mapping::Print()
fromURL.string_get_buf(from_url_buf, (int)sizeof(from_url_buf));
toURL.string_get_buf(to_url_buf, (int)sizeof(to_url_buf));
printf("\t %s %s=> %s %s <%s> [plugins %s enabled; running with %zu plugins]\n", from_url_buf, unique ? "(unique)" : "",
- to_url_buf, homePageRedirect ? "(R)" : "", tag ? tag : "", plugin_count() > 0 ? "are" : "not", plugin_count());
+ to_url_buf, homePageRedirect ? "(R)" : "", tag ? tag : "", _plugin_inst_list.size() > 0 ? "are" : "not",
+ _plugin_inst_list.size());
}
/**
diff --git a/proxy/http/remap/UrlMapping.h b/proxy/http/remap/UrlMapping.h
index 1b55e52..c6de768 100644
--- a/proxy/http/remap/UrlMapping.h
+++ b/proxy/http/remap/UrlMapping.h
@@ -19,6 +19,7 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
*/
#pragma once
@@ -29,6 +30,7 @@
#include "AclFiltering.h"
#include "URL.h"
#include "RemapPluginInfo.h"
+#include "PluginFactory.h"
#include "tscore/Regex.h"
#include "tscore/List.h"
@@ -79,17 +81,15 @@ class url_mapping
public:
~url_mapping();
- bool add_plugin(RemapPluginInfo *i, void *ih);
- RemapPluginInfo *get_plugin(std::size_t) const;
- void *get_instance(std::size_t) const;
+ bool add_plugin_instance(RemapPluginInst *i);
+ RemapPluginInst *get_plugin_instance(std::size_t) const;
std::size_t
- plugin_count() const
+ plugin_instance_count() const
{
- return _plugin_list.size();
+ return _plugin_inst_list.size();
}
- void delete_instance(unsigned int index);
void Print();
int from_path_len = 0;
@@ -122,8 +122,7 @@ public:
};
private:
- std::vector<RemapPluginInfo *> _plugin_list;
- std::vector<void *> _instance_data;
+ std::vector<RemapPluginInst *> _plugin_inst_list;
int _rank = 0;
};
diff --git a/proxy/http/remap/UrlRewrite.cc b/proxy/http/remap/UrlRewrite.cc
index 6f2e83d..91aa3e8 100644
--- a/proxy/http/remap/UrlRewrite.cc
+++ b/proxy/http/remap/UrlRewrite.cc
@@ -19,6 +19,7 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
*/
#include "UrlRewrite.h"
@@ -79,6 +80,9 @@ UrlRewrite::load()
REC_ReadConfigInteger(reverse_proxy, "proxy.config.reverse_proxy.enabled");
+ /* Initialize the plugin factory */
+ pluginFactory.setRuntimeDir(RecConfigReadRuntimeDir()).addSearchDir(RecConfigReadPluginDir());
+
if (0 == this->BuildTable(config_file_path)) {
_valid = true;
if (is_debug_tag_set("url_rewrite")) {
diff --git a/proxy/http/remap/UrlRewrite.h b/proxy/http/remap/UrlRewrite.h
index 89661ca..84386ee 100644
--- a/proxy/http/remap/UrlRewrite.h
+++ b/proxy/http/remap/UrlRewrite.h
@@ -19,6 +19,7 @@
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+
*/
#pragma once
@@ -28,6 +29,7 @@
#include "UrlMappingPathIndex.h"
#include "HttpTransact.h"
#include "tscore/Regex.h"
+#include "PluginFactory.h"
#include <memory>
@@ -208,6 +210,8 @@ public:
int num_rules_redirect_temporary = 0;
int num_rules_forward_with_recv_port = 0;
+ PluginFactory pluginFactory;
+
private:
bool _valid = false;
diff --git a/proxy/http/remap/unit-tests/plugin_misc_cb.cc b/proxy/http/remap/unit-tests/plugin_misc_cb.cc
new file mode 100644
index 0000000..f0792fc
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_misc_cb.cc
@@ -0,0 +1,106 @@
+/** @file
+
+ A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#include "plugin_testing_common.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+PluginDebugObject debugObject;
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+ debugObject.contextInit = pluginThreadContext;
+ return TS_SUCCESS;
+}
+
+void
+TSRemapDone(void)
+{
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+ return TSREMAP_NO_REMAP;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size)
+{
+ debugObject.contextInitInstance = pluginThreadContext;
+
+ return TS_SUCCESS;
+}
+
+void
+TSRemapDeleteInstance(void *)
+{
+}
+
+void
+TSRemapOSResponse(void *ih, TSHttpTxn rh, int os_response_type)
+{
+}
+
+void
+TSPluginInit(int argc, const char *argv[])
+{
+}
+
+void
+TSRemapConfigReload(void)
+{
+}
+
+/* This is meant for test with plugins of different versions */
+int
+pluginDsoVersionTest()
+{
+#ifdef PLUGINDSOVER
+ return PLUGINDSOVER;
+#else
+ return -1;
+#endif
+}
+
+void *
+getPluginDebugObjectTest()
+{
+ return (void *)&debugObject;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc b/proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc
new file mode 100644
index 0000000..03ded2d
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_missing_deleteinstance.cc
@@ -0,0 +1,57 @@
+/** @file
+
+ A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+ return TS_SUCCESS;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+ return TSREMAP_NO_REMAP;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size)
+{
+ return TS_SUCCESS;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_missing_doremap.cc b/proxy/http/remap/unit-tests/plugin_missing_doremap.cc
new file mode 100644
index 0000000..f727f6d
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_missing_doremap.cc
@@ -0,0 +1,45 @@
+/** @file
+
+ A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+ return TS_SUCCESS;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_missing_init.cc b/proxy/http/remap/unit-tests/plugin_missing_init.cc
new file mode 100644
index 0000000..265bfa5
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_missing_init.cc
@@ -0,0 +1,45 @@
+/** @file
+
+ A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+ return TSREMAP_NO_REMAP;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_missing_newinstance.cc b/proxy/http/remap/unit-tests/plugin_missing_newinstance.cc
new file mode 100644
index 0000000..bed55d6
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_missing_newinstance.cc
@@ -0,0 +1,56 @@
+/** @file
+
+ A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+ return TS_SUCCESS;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+ return TSREMAP_NO_REMAP;
+}
+
+void
+TSRemapDeleteInstance(void *)
+{
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_required_cb.cc b/proxy/http/remap/unit-tests/plugin_required_cb.cc
new file mode 100644
index 0000000..65b8026
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_required_cb.cc
@@ -0,0 +1,51 @@
+/** @file
+
+ A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+ return TS_SUCCESS;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+ return TSREMAP_NO_REMAP;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_testing_calls.cc b/proxy/http/remap/unit-tests/plugin_testing_calls.cc
new file mode 100644
index 0000000..89c8df2
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_testing_calls.cc
@@ -0,0 +1,130 @@
+/** @file
+
+ A test plugin for testing Plugin's Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+#include "plugin_testing_common.h"
+#include <iostream>
+
+#include "../RemapPluginInfo.h"
+
+PluginDebugObject debugObject;
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+TSReturnCode
+handleInitRun(char *errbuf, int errbuf_size, int &counter)
+{
+ TSReturnCode result = TS_SUCCESS;
+
+ if (debugObject.fail) {
+ result = TS_ERROR;
+ snprintf(errbuf, errbuf_size, "%s", "Init failed");
+ }
+
+ counter++;
+
+ return result;
+}
+
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+ TSReturnCode result = handleInitRun(errbuf, errbuf_size, debugObject.initCalled);
+ return result;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size)
+{
+ TSReturnCode result = handleInitRun(errbuf, errbuf_size, debugObject.initInstanceCalled);
+
+ if (TS_SUCCESS == result) {
+ *ih = debugObject.input_ih;
+ }
+
+ debugObject.argc = argc;
+ debugObject.argv = argv;
+
+ return result;
+}
+
+void
+TSRemapDone(void)
+{
+ debugObject.doneCalled++;
+}
+
+void
+TSRemapDeleteInstance(void *ih)
+{
+ debugObject.deleteInstanceCalled++;
+ debugObject.ih = ih;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+ debugObject.doRemapCalled++;
+ return TSREMAP_NO_REMAP;
+}
+
+void
+TSRemapOSResponse(void *ih, TSHttpTxn rh, int os_response_type)
+{
+}
+
+void
+TSRemapConfigReload(void)
+{
+ debugObject.reloadConfigCalled++;
+}
+
+/* The folowing functions are meant for unit testing */
+int
+pluginDsoVersionTest()
+{
+#ifdef PLUGINDSOVER
+ return PLUGINDSOVER;
+#else
+ return -1;
+#endif
+}
+
+void *
+getPluginDebugObjectTest()
+{
+ return (void *)&debugObject;
+}
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/plugin_testing_common.cc b/proxy/http/remap/unit-tests/plugin_testing_common.cc
new file mode 100644
index 0000000..d5d08d3
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_testing_common.cc
@@ -0,0 +1,39 @@
+/** @file
+
+ A test plugin common testing functionality
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#include "plugin_testing_common.h"
+
+void
+PrintToStdErr(const char *fmt, ...)
+{
+ va_list args;
+ va_start(args, fmt);
+ vfprintf(stderr, fmt, args);
+ va_end(args);
+}
diff --git a/proxy/http/remap/unit-tests/plugin_testing_common.h b/proxy/http/remap/unit-tests/plugin_testing_common.h
new file mode 100644
index 0000000..12346ea
--- /dev/null
+++ b/proxy/http/remap/unit-tests/plugin_testing_common.h
@@ -0,0 +1,95 @@
+/** @file
+
+ A test plugin header for testing Plugin's Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#pragma once
+
+#include <map>
+#include <string>
+#include <iostream>
+
+#include <stdio.h>
+#include <stdarg.h>
+
+#include "../PluginFactory.h"
+
+extern thread_local PluginThreadContext *pluginThreadContext;
+
+class PluginDebugObject
+{
+public:
+ PluginDebugObject() { clear(); }
+
+ void
+ clear()
+ {
+ contextInit = nullptr;
+ contextInitInstance = nullptr;
+ doRemapCalled = 0;
+ initCalled = 0;
+ doneCalled = 0;
+ initInstanceCalled = 0;
+ deleteInstanceCalled = 0;
+ reloadConfigCalled = 0;
+ ih = nullptr;
+ argc = 0;
+ argv = nullptr;
+ }
+
+ /* Input fields used to set the test behavior of the plugin call-backs */
+ bool fail = false; /* tell the plugin call-back to fail for testing purposuses */
+ void *input_ih; /* the value to be returned by the plugin instance init function */
+
+ /* Output fields showing what happend during the test */
+ const PluginThreadContext *contextInit = nullptr; /* plugin initialization context */
+ const PluginThreadContext *contextInitInstance = nullptr; /* plugin instance initialization context */
+ int doRemapCalled = 0; /* mark if remap was called */
+ int initCalled = 0; /* mark if plugin init was called */
+ int doneCalled = 0; /* mark if done was called */
+ int initInstanceCalled = 0; /* mark if instance init was called */
+ int deleteInstanceCalled = 0; /* mark if delete instance was called */
+ int reloadConfigCalled = 0; /* mark if reload config was called */
+ void *ih = nullptr; /* instance handler */
+ int argc = 0; /* number of plugin instance parameters received by the plugin */
+ char **argv = nullptr; /* plugin instance parameters received by the plugin */
+};
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+typedef void *GetPluginDebugObjectFunction(void);
+GetPluginDebugObjectFunction getPluginDebugObjectTest;
+
+#define Debug(category, fmt, ...) PrintToStdErr("(%s) %s:%d:%s() " fmt "\n", category, __FILE__, __LINE__, __func__, ##__VA_ARGS__)
+#define Error(fmt, ...) PrintToStdErr("%s:%d:%s() " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__)
+void PrintToStdErr(const char *fmt, ...);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
diff --git a/proxy/http/remap/unit-tests/test_PluginDso.cc b/proxy/http/remap/unit-tests/test_PluginDso.cc
new file mode 100644
index 0000000..c31e1d6
--- /dev/null
+++ b/proxy/http/remap/unit-tests/test_PluginDso.cc
@@ -0,0 +1,395 @@
+/** @file
+
+ Unit tests for a class that deals with plugin Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#define CATCH_CONFIG_MAIN /* include main function */
+#include <catch.hpp> /* catch unit-test framework */
+#include <fstream> /* ofstream */
+
+#include "plugin_testing_common.h"
+#include "../PluginDso.h"
+
+class PluginContext;
+thread_local PluginThreadContext *pluginThreadContext;
+
+std::error_code ec;
+
+/* A temp sandbox to play with our toys used for all fun with this test-bench */
+static fs::path tmpDir = fs::canonical(fs::temp_directory_path(), ec);
+
+/* The following are dirs that are used commonly in the unit-tests */
+static fs::path sandboxDir = tmpDir / fs::path("sandbox");
+static fs::path runtimeDir = sandboxDir / fs::path("runtime");
+static fs::path searchDir = sandboxDir / fs::path("search");
+static fs::path pluginBuildDir = fs::current_path() / fs::path("unit-tests/.libs");
+
+/* The following are paths used in all scenarios in the unit tests */
+static fs::path configPath = fs::path("plugin_v1.so");
+static fs::path pluginBuildPath = pluginBuildDir / configPath;
+static fs::path effectivePath = searchDir / configPath;
+static fs::path runtimePath = runtimeDir / configPath;
+
+void
+clean()
+{
+ fs::remove(sandboxDir, ec);
+}
+
+/* Mock used only to make PluginDso concrete enough to be tested */
+class PluginDsoUnitTest : public PluginDso
+{
+public:
+ PluginDsoUnitTest(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath)
+ : PluginDso(configPath, effectivePath, runtimePath)
+ {
+ /* don't remove runtime DSO copy preventively so we can check if it was created properly */
+ _preventiveCleaning = false;
+ }
+
+ virtual void
+ indicateReload()
+ {
+ }
+ virtual bool
+ init(std::string &error)
+ {
+ return true;
+ }
+ virtual void
+ done()
+ {
+ }
+};
+
+/*
+ * The following scenario tests loading and unloading of plugins
+ */
+SCENARIO("loading plugins", "[plugin][core]")
+{
+ clean();
+ std::string error;
+
+ GIVEN("a valid plugin")
+ {
+ /* Setup the test fixture - search, runtime dirs and install a plugin with some defined callback functions */
+ CHECK(fs::create_directories(searchDir, ec));
+ CHECK(fs::create_directories(runtimeDir, ec));
+ fs::copy(pluginBuildPath, searchDir, ec);
+
+ /* Instantiate and initialize a plugin DSO instance. Make sure effective path exists, used to load */
+ CHECK(fs::exists(effectivePath));
+ PluginDsoUnitTest plugin(configPath, effectivePath, runtimePath);
+
+ WHEN("loading a valid plugin")
+ {
+ bool result = plugin.load(error);
+
+ THEN("expect it to successfully load")
+ {
+ CHECK(true == result);
+ CHECK(error.empty());
+ CHECK(effectivePath == plugin.effectivePath());
+ CHECK(runtimePath == plugin.runtimePath());
+ CHECK(fs::exists(runtimePath));
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("loading a valid plugin")
+ {
+ bool result = plugin.load(error);
+
+ THEN("expect saving the right DSO file modification time")
+ {
+ CHECK(true == result);
+ CHECK(error.empty());
+ std::error_code ec;
+ fs::file_status fs = fs::status(effectivePath, ec);
+ CHECK(plugin.modTime() == fs::modification_time(fs));
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("loading a valid plugin but missing runtime dir")
+ {
+ CHECK(fs::remove(runtimeDir, ec));
+ CHECK_FALSE(fs::exists(runtimePath));
+ bool result = plugin.load(error);
+
+ THEN("expect it to fail")
+ {
+ CHECK_FALSE(true == result);
+ CHECK("failed to create a copy: No such file or directory" == error);
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("loading a valid plugin twice in a row")
+ {
+ /* First attempt OK */
+ bool result = plugin.load(error);
+ CHECK(true == result);
+ CHECK(error.empty());
+
+ /* Second attempt */
+ result = plugin.load(error);
+
+ THEN("expect it to fail the second attempt")
+ {
+ CHECK_FALSE(true == result);
+ CHECK("plugin already loaded" == error);
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("explicitly unloading a valid but not loaded plugin")
+ {
+ /* Make sure it is not loaded, runtime DSO not present */
+ CHECK_FALSE(fs::exists(runtimePath));
+
+ /* Unload w/o loading beforehand */
+ bool result = plugin.unload(error);
+
+ THEN("expect the unload to fail")
+ {
+ CHECK(false == result);
+ CHECK_FALSE(error.empty());
+ CHECK_FALSE(fs::exists(runtimePath));
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("unloading a valid plugin twice in a row")
+ {
+ /* First attempt OK */
+ bool result = plugin.load(error);
+ CHECK(true == result);
+ CHECK(error.empty());
+ result = plugin.unload(error);
+ CHECK(true == result);
+ CHECK("" == error);
+
+ /* Second attempt */
+ result = plugin.unload(error);
+
+ THEN("expect it to fail the second attempt")
+ {
+ CHECK_FALSE(true == result);
+ CHECK("no plugin loaded" == error);
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("explicitly unloading a valid and loaded plugin")
+ {
+ /* Make sure it is not loaded, runtime DSO not present */
+ CHECK_FALSE(fs::exists(runtimePath));
+
+ /* Load and make sure it is loaded */
+ CHECK(plugin.load(error));
+ /* Effective and runtime path set */
+ CHECK(effectivePath == plugin.effectivePath());
+ CHECK(runtimePath == plugin.runtimePath());
+ /* Runtime DSO should be present */
+ CHECK(fs::exists(runtimePath));
+
+ /* Unload */
+ bool result = plugin.unload(error);
+
+ THEN("expect it to successfully unload")
+ {
+ CHECK(true == result);
+ CHECK(error.empty());
+ /* Effective and runtime path still set */
+ CHECK(effectivePath == plugin.effectivePath());
+ CHECK(runtimePath == plugin.runtimePath());
+ /* Runtime DSO should not be found anymore */
+ CHECK_FALSE(fs::exists(runtimePath));
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("implicitly unloading a valid and loaded plugin")
+ {
+ {
+ PluginDsoUnitTest localPlugin(configPath, effectivePath, runtimePath);
+
+ /* Load and make sure it is loaded */
+ CHECK(localPlugin.load(error));
+ /* Effective and runtime path set */
+ CHECK(effectivePath == localPlugin.effectivePath());
+ CHECK(runtimePath == localPlugin.runtimePath());
+ /* Runtime DSO should be present */
+ CHECK(fs::exists(runtimePath));
+
+ /* Unload by going out of scope */
+ }
+
+ THEN("expect it to successfully unload and clean after itself")
+ {
+ /* Runtime path should be removed after unloading */
+ CHECK_FALSE(fs::exists(runtimePath));
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+ }
+
+ GIVEN("a plugin instance initialized with an empty effective path")
+ {
+ std::string error;
+ PluginDsoUnitTest plugin(configPath, /* effectivePath */ fs::path(), runtimePath);
+
+ WHEN("loading the plugin")
+ {
+ bool result = plugin.load(error);
+
+ THEN("expect the load to fail")
+ {
+ CHECK_FALSE(true == result);
+ CHECK("empty effective path" == error);
+ CHECK(plugin.effectivePath().empty());
+ CHECK(0 == plugin.modTime());
+ CHECK(runtimePath == plugin.runtimePath());
+ CHECK_FALSE(fs::exists(runtimePath));
+ }
+ }
+ }
+
+ GIVEN("an invalid plugin")
+ {
+ /* Create the directory structure and install plugins */
+ CHECK(fs::create_directories(searchDir, ec));
+ CHECK(fs::create_directories(runtimeDir, ec));
+ /* Create an invalid plugin and make sure the effective path to it exists */
+ std::ofstream file(effectivePath.string());
+ file << "Invalid plugin DSO content";
+ file.close();
+ CHECK(fs::exists(effectivePath));
+
+ /* Instantiate and initialize a plugin DSO instance. */
+ std::string error;
+ PluginDsoUnitTest plugin(configPath, effectivePath, runtimePath);
+
+ WHEN("loading an invalid plugin")
+ {
+ bool result = plugin.load(error);
+
+ THEN("expect it to fail to load")
+ {
+ /* After calling load() the following should be set correctly */
+ CHECK(effectivePath == plugin.effectivePath());
+ CHECK(runtimePath == plugin.runtimePath());
+
+ /* But the load should fail and an error should be returned */
+ CHECK(false == result);
+ CHECK_FALSE(error.empty());
+
+ /* Runtime DSO should not exist since the load failed. */
+ CHECK_FALSE(fs::exists(runtimePath));
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+ }
+}
+
+/*
+ * The following scenario tests finding symbols inside the DSO.
+ */
+SCENARIO("looking for symbols inside a plugin DSO", "[plugin][core]")
+{
+ clean();
+ std::string error;
+
+ /* Setup the test fixture - search, runtime dirs and install a plugin with some defined callback functions */
+ CHECK(fs::create_directories(searchDir, ec));
+ CHECK(fs::create_directories(runtimeDir, ec));
+ fs::copy(pluginBuildDir / configPath, searchDir, ec);
+
+ /* Initialize a plugin DSO instance */
+ PluginDsoUnitTest plugin(configPath, effectivePath, runtimePath);
+
+ /* Now test away. */
+ GIVEN("plugin loaded successfully")
+ {
+ CHECK(plugin.load(error));
+
+ WHEN("looking for an existing symbol")
+ {
+ THEN("expect to find it")
+ {
+ void *s = nullptr;
+ CHECK(plugin.getSymbol("TSRemapInit", s, error));
+ CHECK(nullptr != s);
+ CHECK(error.empty());
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("looking for non-existing symbol")
+ {
+ THEN("expect not to find it and get an error")
+ {
+ void *s = nullptr;
+ CHECK_FALSE(plugin.getSymbol("NONEXISTING_SYMBOL", s, error));
+ CHECK(nullptr == s);
+ CHECK_FALSE(error.empty());
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("looking for multiple existing symbols")
+ {
+ THEN("expect to find them all")
+ {
+ std::vector<const char *> list{"TSRemapInit", "TSRemapDone", "TSRemapDoRemap", "TSRemapNewInstance",
+ "TSRemapDeleteInstance", "TSRemapOSResponse", "TSPluginInit", "pluginDsoVersionTest"};
+ for (auto symbol : list) {
+ void *s = nullptr;
+ CHECK(plugin.getSymbol(symbol, s, error));
+ CHECK(nullptr != s);
+ CHECK(error.empty());
+ }
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ /* The following version function is used only for unit-testing of the plugin factory functionality */
+ WHEN("using a symbol to call the corresponding version function")
+ {
+ THEN("expect to return the version number")
+ {
+ void *s = nullptr;
+ CHECK(plugin.getSymbol("pluginDsoVersionTest", s, error));
+ int (*version)() = reinterpret_cast<int (*)()>(s);
+ int ver = version ? version() : -1;
+ CHECK(1 == ver);
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+ }
+}
diff --git a/proxy/http/remap/unit-tests/test_PluginFactory.cc b/proxy/http/remap/unit-tests/test_PluginFactory.cc
new file mode 100644
index 0000000..c75040e
--- /dev/null
+++ b/proxy/http/remap/unit-tests/test_PluginFactory.cc
@@ -0,0 +1,657 @@
+/** @file
+
+ Unit tests for a class that deals with plugin Dynamic Shared Objects (DSO)
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#define CATCH_CONFIG_MAIN /* include main function */
+#include <catch.hpp> /* catch unit-test framework */
+#include <fstream> /* ofstream */
+#include <utime.h>
+
+#include "plugin_testing_common.h"
+#include "../PluginFactory.h"
+#include "../PluginDso.h"
+
+thread_local PluginThreadContext *pluginThreadContext;
+
+std::error_code ec;
+static void *INSTANCE_HANDLER = (void *)789;
+
+/* Mock of PluginFactory just to get consisten UUID to be able to test consistently */
+static fs::path tempComponent = fs::path("c71e2bab-90dc-4770-9535-c9304c3de38e");
+class PluginFactoryUnitTest : public PluginFactory
+{
+public:
+ PluginFactoryUnitTest(const fs::path &tempComponent)
+ {
+ _tempComponent = tempComponent;
+ _preventiveCleaning = false;
+ }
+
+protected:
+ const char *
+ getUuid()
+ {
+ return _tempComponent.c_str();
+ }
+
+ fs::path _tempComponent;
+};
+
+PluginDebugObject *
+getDebugObject(const PluginDso &plugin)
+{
+ std::string error; /* ignore the error, return nullptr if symbol not defined */
+ void *address = nullptr;
+ plugin.getSymbol("getPluginDebugObjectTest", address, error);
+ GetPluginDebugObjectFunction *getObject = reinterpret_cast<GetPluginDebugObjectFunction *>(address);
+ if (getObject) {
+ PluginDebugObject *object = reinterpret_cast<PluginDebugObject *>(getObject());
+ return object;
+ } else {
+ return nullptr;
+ }
+}
+
+/* A temp sandbox to play with our toys used for all fun with this test-bench */
+static fs::path tmpDir = fs::canonical(fs::temp_directory_path(), ec);
+
+/* The following are paths that are used commonly in the unit-tests */
+static fs::path sandboxDir = tmpDir / "sandbox";
+static fs::path runtimeRootDir = sandboxDir / "runtime";
+static fs::path runtimeDir = runtimeRootDir / tempComponent;
+static fs::path searchDir = sandboxDir / "search";
+static fs::path pluginBuildDir = fs::current_path() / "unit-tests/.libs";
+
+void
+clean()
+{
+ fs::remove(sandboxDir, ec);
+}
+
+static void
+setupConfigPathTest(const fs::path &configPath, const fs::path &pluginBuildPath, const fs::path &uuid, fs::path &effectivePath,
+ fs::path &runtimePath, time_t mtime = 0, bool append = false)
+{
+ std::string error;
+ if (!append) {
+ clean();
+ }
+
+ effectivePath = configPath.is_absolute() ? configPath : searchDir / configPath;
+ runtimePath = runtimeRootDir / uuid / effectivePath.relative_path();
+
+ /* Create the directory structure and install plugins */
+ fs::create_directories(effectivePath.parent_path(), ec);
+ fs::copy(pluginBuildPath, effectivePath, ec);
+ if (0 != mtime) {
+ struct stat sb;
+ struct utimbuf new_times;
+ stat(effectivePath.c_str(), &sb);
+ new_times.actime = sb.st_atime; /* keep atime unchanged */
+ new_times.modtime = mtime; /* set mtime to current time */
+ utime(effectivePath.c_str(), &new_times);
+ }
+
+ CHECK(fs::exists(effectivePath));
+}
+
+static PluginFactoryUnitTest *
+getFactory(const fs::path &uuid)
+{
+ /* Instantiate and initialize a plugin factory. */
+ PluginFactoryUnitTest *factory = new PluginFactoryUnitTest(uuid);
+ factory->setRuntimeDir(runtimeRootDir);
+ factory->addSearchDir(searchDir);
+ return factory;
+}
+
+static void
+teardownConfigPathTest(PluginFactoryUnitTest *factory)
+{
+ delete factory;
+ clean();
+}
+
+static void
+validateSuccessfulConfigPathTest(const RemapPluginInst *pluginInst, const std::string &error, const fs::path &effectivePath,
+ const fs::path &runtimePath)
+{
+ CHECK(nullptr != pluginInst);
+ CHECK("" == error);
+ CHECK(effectivePath == pluginInst->_plugin.effectivePath());
+ CHECK(runtimePath == pluginInst->_plugin.runtimePath());
+}
+
+SCENARIO("loading plugins", "[plugin][core]")
+{
+ fs::path effectivePath;
+ fs::path runtimePath;
+ std::string error;
+
+ GIVEN("an existing plugin")
+ {
+ fs::path pluginName = fs::path("plugin_v1.so");
+ fs::path buildPath = pluginBuildDir / pluginName;
+
+ WHEN("config using plugin file name only")
+ {
+ fs::path configPath = pluginName;
+ CHECK(configPath.is_relative()); /* make sure this is relative path - this is what we are testing */
+
+ setupConfigPathTest(configPath, buildPath, tempComponent, effectivePath, runtimePath);
+ PluginFactoryUnitTest *factory = getFactory(tempComponent);
+ RemapPluginInst *plugin = factory->getRemapPlugin(configPath, 0, nullptr, error);
+
+ THEN("expect it to successfully load") { validateSuccessfulConfigPathTest(plugin, error, effectivePath, runtimePath); }
+
+ teardownConfigPathTest(factory);
+ }
+
+ WHEN("config is using plugin relative filename")
+ {
+ fs::path configPath = fs::path("subdir") / pluginName;
+ CHECK(configPath.is_relative()); /* make sure this is relative path - this is what we are testing */
+
+ setupConfigPathTest(configPath, buildPath, tempComponent, effectivePath, runtimePath);
+ PluginFactoryUnitTest *factory = getFactory(tempComponent);
+ RemapPluginInst *plugin = factory->getRemapPlugin(configPath, 0, nullptr, error);
+
+ THEN("expect it to successfully load") { validateSuccessfulConfigPathTest(plugin, error, effectivePath, runtimePath); }
+
+ teardownConfigPathTest(factory);
+ }
+
+ WHEN("config is using plugin absolute path")
+ {
+ fs::path configPath = searchDir / "subdir" / pluginName;
+ CHECK(configPath.is_absolute()); /* make sure this is absolute path - this is what we are testing */
+
+ setupConfigPathTest(configPath, buildPath, tempComponent, effectivePath, runtimePath);
+ PluginFactoryUnitTest *factory = getFactory(tempComponent);
+ RemapPluginInst *plugin = factory->getRemapPlugin(configPath, 0, nullptr, error);
+
+ THEN("expect it to successfully load") { validateSuccessfulConfigPathTest(plugin, error, effectivePath, runtimePath); }
+
+ teardownConfigPathTest(factory);
+ }
+
+ WHEN("config using nonexisting relative plugin file name")
+ {
+ fs::path relativeExistingPath = pluginName;
+ CHECK(relativeExistingPath.is_relative());
+ fs::path relativeNonexistingPath("subdir");
+ relativeNonexistingPath /= fs::path("nonexisting_plugin.so");
+ CHECK(relativeNonexistingPath.is_relative());
+
+ setupConfigPathTest(relativeExistingPath, buildPath, tempComponent, effectivePath, runtimePath);
+ PluginFactoryUnitTest *factory = getFactory(tempComponent);
+ RemapPluginInst *plugin = factory->getRemapPlugin(relativeNonexistingPath, 0, nullptr, error);
+
+ THEN("expect it to fail with appropriate error message")
+ {
+ std::string expectedError;
+ expectedError.append("failed to find plugin '").append(relativeNonexistingPath.string()).append("'");
+ CHECK(nullptr == plugin);
+ CHECK(expectedError == error);
+ }
+
+ teardownConfigPathTest(factory);
+ }
+
+ WHEN("config using nonexisting absolute plugin file name")
+ {
+ fs::path relativeExistingPath = pluginName;
+ CHECK(relativeExistingPath.is_relative());
+ fs::path absoluteNonexistingPath = searchDir / "subdir" / "nonexisting_plugin.so";
+ CHECK(absoluteNonexistingPath.is_absolute());
+
+ setupConfigPathTest(relativeExistingPath, buildPath, tempComponent, effectivePath, runtimePath);
+ PluginFactoryUnitTest *factory = getFactory(tempComponent);
+ RemapPluginInst *plugin = factory->getRemapPlugin(absoluteNonexistingPath, 0, nullptr, error);
+
+ THEN("expect it to fail with appropriate error message")
+ {
+ std::string expectedError;
+ expectedError.append("failed to find plugin '").append(absoluteNonexistingPath.string()).append("'");
+ CHECK(nullptr == plugin);
+ CHECK(expectedError == error);
+ }
+
+ teardownConfigPathTest(factory);
+ }
+ }
+}
+
+SCENARIO("multiple search dirs + multiple or no plugins installed", "[plugin][core]")
+{
+ GIVEN("multiple search dirs specified for the plugin search")
+ {
+ /* Create the directory structure and install plugins */
+ fs::path configPath = fs::path("plugin_v1.so");
+ fs::path pluginName = fs::path("plugin_v1.so");
+ fs::path searchDir1 = sandboxDir / "search1";
+ fs::path searchDir2 = sandboxDir / "search2";
+ fs::path searchDir3 = sandboxDir / "search3";
+ std::vector<fs::path> searchDirs = {searchDir1, searchDir2, searchDir3};
+ fs::path effectivePath1 = searchDir1 / configPath;
+ fs::path effectivePath2 = searchDir2 / configPath;
+ fs::path effectivePath3 = searchDir3 / configPath;
+ fs::path runtimePath1 = runtimeDir / effectivePath1.relative_path();
+ fs::path runtimePath2 = runtimeDir / effectivePath2.relative_path();
+ fs::path runtimePath3 = runtimeDir / effectivePath3.relative_path();
+ fs::path pluginBuildPath = fs::current_path() / fs::path("unit-tests/.libs") / pluginName;
+
+ std::string error;
+
+ for (auto searchDir : searchDirs) {
+ CHECK(fs::create_directories(searchDir, ec));
+ fs::copy(pluginBuildPath, searchDir, ec);
+ }
+ CHECK(fs::create_directories(runtimeDir, ec));
+
+ /* Instantiate and initialize a plugin DSO instance. */
+ PluginFactoryUnitTest factory(tempComponent);
+ factory.setRuntimeDir(runtimeRootDir);
+ for (auto searchDir : searchDirs) {
+ factory.addSearchDir(searchDir);
+ }
+
+ CHECK(fs::exists(effectivePath1));
+ CHECK(fs::exists(effectivePath2));
+ CHECK(fs::exists(effectivePath3));
+
+ WHEN("loading an existing plugin using its absolute path but the plugin is not located in any of the search dirs")
+ {
+ /* Prepare "unregistered" directory containing a valid plugin but not registered with the factory as a search directory */
+ fs::path unregisteredDir = sandboxDir / searchDir / "unregistered";
+ CHECK(fs::create_directories(unregisteredDir, ec));
+ fs::copy(pluginBuildPath, unregisteredDir, ec);
+ fs::path abEffectivePath = unregisteredDir / pluginName;
+ fs::path absRuntimePath = runtimeDir / abEffectivePath.relative_path();
+ CHECK(abEffectivePath.is_absolute());
+ CHECK(fs::exists(abEffectivePath));
+
+ /* Now use an absolute path containing the unregistered search directory */
+ RemapPluginInst *pluginInst = factory.getRemapPlugin(abEffectivePath, 0, nullptr, error);
+
+ THEN("Expect it to successfully load")
+ {
+ CHECK(nullptr != pluginInst);
+ CHECK(error.empty());
+ CHECK(abEffectivePath == pluginInst->_plugin.effectivePath());
+ CHECK(absRuntimePath == pluginInst->_plugin.runtimePath());
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("a valid plugin is found in the first search path")
+ {
+ RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error);
+
+ THEN("Expect it to successfully load the one found in the first search dir and copy it in the runtime dir")
+ {
+ CHECK(nullptr != pluginInst);
+ CHECK(error.empty());
+ CHECK(effectivePath1 == pluginInst->_plugin.effectivePath());
+ CHECK(runtimePath1 == pluginInst->_plugin.runtimePath());
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("the first search dir is missing the plugin but the second search has it")
+ {
+ CHECK(fs::remove(effectivePath1, ec));
+ RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error);
+
+ THEN("Expect it to successfully load the one found in the second search dir")
+ {
+ CHECK(nullptr != pluginInst);
+ CHECK(error.empty());
+ CHECK(effectivePath2 == pluginInst->_plugin.effectivePath());
+ CHECK(runtimePath2 == pluginInst->_plugin.runtimePath());
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("the first and second search dir are missing the plugin but the third search has it")
+ {
+ CHECK(fs::remove(effectivePath1, ec));
+ CHECK(fs::remove(effectivePath2, ec));
+ RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error);
+
+ THEN("Expect it to successfully load the one found in the third search dir")
+ {
+ CHECK(nullptr != pluginInst);
+ CHECK(error.empty());
+ CHECK(effectivePath3 == pluginInst->_plugin.effectivePath());
+ CHECK(runtimePath3 == pluginInst->_plugin.runtimePath());
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+
+ WHEN("none of the search dirs contains a valid plugin")
+ {
+ CHECK(fs::remove(effectivePath1, ec));
+ CHECK(fs::remove(effectivePath2, ec));
+ CHECK(fs::remove(effectivePath3, ec));
+
+ THEN("expect the plugin load to fail.")
+ {
+ RemapPluginInst *pluginInst = factory.getRemapPlugin(configPath, 0, nullptr, error);
+ CHECK(nullptr == pluginInst);
+ CHECK(std::string("failed to find plugin '").append(configPath.string()).append("'") == error);
+ CHECK_FALSE(fs::exists(runtimePath1));
+ CHECK_FALSE(fs::exists(runtimePath2));
+ CHECK_FALSE(fs::exists(runtimePath3));
+ }
+ CHECK(fs::remove(sandboxDir, ec));
+ }
+ }
+}
+
+static int
+getPluginVersion(const PluginDso &plugin)
+{
+ std::string error;
+ void *s = nullptr;
+ CHECK(plugin.getSymbol("pluginDsoVersionTest", s, error));
+ int (*version)() = reinterpret_cast<int (*)()>(s);
+ return version ? version() : -1;
+}
+
+SCENARIO("loading multiple version of the same plugin at the same time", "[plugin][core]")
+{
+ static fs::path uuid_t1 = fs::path("c71e2bab-90dc-4770-9535-c9304c3de381"); /* UUID at moment t1 */
+ static fs::path uuid_t2 = fs::path("c71e2bab-90dc-4770-9535-e7304c3ee732"); /* UUID at moment t2 */
+
+ fs::path effectivePath_v1; /* expected effective path for DSO v1 */
+ fs::path effectivePath_v2; /* expected effective path for DSO v2 */
+ fs::path runtimePath_v1; /* expected runtime path for DSO v1 */
+ fs::path runtimePath_v2; /* expected runtime path for DSO v2 */
+ void *tsRemapInitSym_v1_t1 = nullptr; /* callback address from DSO v1 at moment t1 */
+ void *tsRemapInitSym_v1_t2 = nullptr; /* callback address from DSO v1 at moment t2 */
+ void *tsRemapInitSym_v2_t2 = nullptr; /* callback address from DSO v2 at moment t2 */
+
+ std::string error;
+ std::string error1;
+ std::string error2;
+
+ fs::path configName = fs::path("plugin.so"); /* use same config name for all following tests */
+ fs::path buildPath_v1 = pluginBuildDir / fs::path("plugin_v1.so"); /* DSO v1 */
+ fs::path buildPath_v2 = pluginBuildDir / fs::path("plugin_v2.so"); /* DSO v1 */
+
+ GIVEN("two different versions v1 and v2 of same plugin")
+ {
+ WHEN("(1) loading v1, (2) overwriting with v2 and then (3) reloading by using the same plugin name, "
+ "(*) v1 and v2 DSOs modification time are different (changed)")
+ {
+ /* Simulate installing plugin plugin_v1.so (ver 1) as plugin.so and loading it at some point of time t1 */
+ setupConfigPathTest(configName, buildPath_v1, uuid_t1, effectivePath_v1, runtimePath_v1, 1556825556);
+ PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+ RemapPluginInst *plugin_v1 = factory1->getRemapPlugin(configName, 0, nullptr, error1);
+ plugin_v1->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v1_t1, error);
+
+ /* Simulate installing plugin plugin_v2.so (v1) as plugin.so and loading it at some point of time t2 */
+ /* Note that during the installation plugin_v2.so (v2) is "barberically" overriding the existing plugin.so which was v1 */
+ setupConfigPathTest(configName, buildPath_v2, uuid_t2, effectivePath_v2, runtimePath_v2, 1556825557);
+ PluginFactoryUnitTest *factory2 = getFactory(uuid_t2);
+ RemapPluginInst *plugin_v2 = factory2->getRemapPlugin(configName, 0, nullptr, error2);
+
+ /* Make sure plugin.so was overriden */
+ CHECK(effectivePath_v1 == effectivePath_v2);
+
+ /* Although effective path is the same runtime paths should be different */
+ CHECK(runtimePath_v1 != runtimePath_v2);
+
+ THEN("expect both to be successfully loaded and used simultaneously")
+ {
+ /* Both loadings should succeed */
+ validateSuccessfulConfigPathTest(plugin_v1, error1, effectivePath_v1, runtimePath_v1);
+ validateSuccessfulConfigPathTest(plugin_v2, error2, effectivePath_v2, runtimePath_v2);
+
+ /* Make sure what we installed and loaded first was v1 and after the plugin reload we run v2 */
+ CHECK(1 == getPluginVersion(plugin_v1->_plugin));
+ CHECK(2 == getPluginVersion(plugin_v2->_plugin));
+
+ /* Make sure the symbols we get from the 2 loaded plugins don't yield the same callback function pointer */
+ plugin_v1->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v1_t2, error);
+ plugin_v2->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v2_t2, error);
+ CHECK(nullptr != tsRemapInitSym_v1_t2);
+ CHECK(nullptr != tsRemapInitSym_v2_t2);
+ CHECK(tsRemapInitSym_v1_t2 != tsRemapInitSym_v2_t2);
+
+ /* Make sure v1 callback functions addresses did not change for v1 after v2 was loaded */
+ CHECK(tsRemapInitSym_v1_t1 == tsRemapInitSym_v1_t2);
+ }
+
+ teardownConfigPathTest(factory1);
+ teardownConfigPathTest(factory2);
+ }
+ }
+
+ GIVEN("two different versions v1 and v2 of same plugin")
+ {
+ WHEN("(1) loading v1, (2) overwriting with v2 and then (3) reloading by using the same plugin name, "
+ "(*) v1 and v2 DSOs modification time are same (did NOT change)")
+ {
+ /* Simulate installing plugin plugin_v1.so (ver 1) as plugin.so and loading it at some point of time t1 */
+ setupConfigPathTest(configName, buildPath_v1, uuid_t1, effectivePath_v1, runtimePath_v1, 1556825556);
+ PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+ RemapPluginInst *plugin_v1 = factory1->getRemapPlugin(configName, 0, nullptr, error1);
+
+ /* Simulate installing plugin plugin_v2.so (v1) as plugin.so and loading it at some point of time t2 */
+ /* Note that during the installation plugin_v2.so (v2) is "barberically" overriding the existing plugin.so
+ which was v1, since the modification time is exactly the same the new v2 plugin would not be loaded and
+ we should get the same PluginDso address and same effective and runtime paths */
+ setupConfigPathTest(configName, buildPath_v2, uuid_t2, effectivePath_v2, runtimePath_v2, 1556825556);
+ PluginFactoryUnitTest *factory2 = getFactory(uuid_t2);
+ RemapPluginInst *plugin_v2 = factory2->getRemapPlugin(configName, 0, nullptr, error2);
+
+ /* Make sure plugin.so was overriden */
+ CHECK(effectivePath_v1 == effectivePath_v2);
+
+ THEN("expect only v1 plugin to be loaded since the timestamp has not changed")
+ {
+ /* Both getRemapPlugin() calls should succeed but only v1 plugin DSO should be used */
+ validateSuccessfulConfigPathTest(plugin_v1, error1, effectivePath_v1, runtimePath_v1);
+ validateSuccessfulConfigPathTest(plugin_v2, error2, effectivePath_v2, runtimePath_v1);
+
+ /* Make sure we ended up with the same DSO object and runtime paths should be same - no new plugin was loaded */
+ CHECK(&(plugin_v1->_plugin) == &(plugin_v2->_plugin));
+ CHECK(plugin_v1->_plugin.runtimePath() == plugin_v2->_plugin.runtimePath());
+
+ /* Make sure v2 DSO was NOT loaded both instances should return same v1 version */
+ CHECK(1 == getPluginVersion(plugin_v1->_plugin));
+ CHECK(1 == getPluginVersion(plugin_v2->_plugin));
+
+ /* Make sure the symbols we get from the 2 loaded plugins yield the same callback function pointer */
+ plugin_v1->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v1_t2, error);
+ plugin_v2->_plugin.getSymbol("TSRemapInit", tsRemapInitSym_v2_t2, error);
+ CHECK(nullptr != tsRemapInitSym_v1_t2);
+ CHECK(nullptr != tsRemapInitSym_v2_t2);
+ CHECK(tsRemapInitSym_v1_t2 == tsRemapInitSym_v2_t2);
+ }
+
+ teardownConfigPathTest(factory1);
+ teardownConfigPathTest(factory2);
+ }
+ }
+
+ /* Since factories share the list of loaded plugins to avoid unnecessary loading of unchanged plugins
+ * lets check if destroying a factory impacts plugins loaded from another factory */
+ GIVEN("configurations with and without plugins")
+ {
+ WHEN("loading a configuration without plugins and then reloading configuration with a plugin")
+ {
+ /* Simulate configuration without plugins - an unused factory */
+ PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+
+ /* Now provision and load a plugin using a second factory */
+ setupConfigPathTest(configName, buildPath_v2, uuid_t2, effectivePath_v2, runtimePath_v2, 1556825556);
+ PluginFactoryUnitTest *factory2 = getFactory(uuid_t2);
+ RemapPluginInst *plugin_v2 = factory2->getRemapPlugin(configName, 0, nullptr, error2);
+
+ THEN("the plugin from the second factory to work")
+ {
+ validateSuccessfulConfigPathTest(plugin_v2, error2, effectivePath_v2, runtimePath_v2);
+
+ /* Now delete the first factory and call a plugin from the second factory */
+ delete factory1;
+ CHECK(TSREMAP_NO_REMAP == plugin_v2->_plugin.doRemap(INSTANCE_HANDLER, nullptr, nullptr));
+ }
+
+ teardownConfigPathTest(factory2);
+ }
+ }
+}
+
+SCENARIO("notifying plugins of config reload", "[plugin][core]")
+{
+ /* use 2 copies of the same plugin to test */
+ fs::path configName1 = fs::path("plugin_testing_calls_1.so");
+ fs::path configName2 = fs::path("plugin_testing_calls_2.so");
+ fs::path buildPath = pluginBuildDir / fs::path("plugin_testing_calls.so");
+
+ static fs::path uuid_t1 = fs::path("c71e2bab-90dc-4770-9535-c9304c3de381"); /* UUID at moment t1 */
+ static fs::path uuid_t2 = fs::path("c71e2bab-90dc-4770-9535-e7304c3ee732"); /* UUID at moment t2 */
+
+ fs::path effectivePath1;
+ fs::path effectivePath2;
+ fs::path runtimePath1;
+ fs::path runtimePath2;
+
+ std::string error;
+
+ GIVEN("simple configuration with 1 plugin and 1 factory")
+ {
+ WHEN("indicating config reload")
+ {
+ /* Simulate configuration without plugins - an unused factory */
+ setupConfigPathTest(configName1, buildPath, uuid_t1, effectivePath1, runtimePath1, 1556825556);
+ PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+ RemapPluginInst *plugin1 = factory1->getRemapPlugin(configName1, 0, nullptr, error);
+
+ /* check if loaded successfully */
+ validateSuccessfulConfigPathTest(plugin1, error, effectivePath1, runtimePath1);
+
+ /* Prapare the debug object */
+ PluginDebugObject *debugObject = getDebugObject(plugin1->_plugin);
+ debugObject->clear();
+
+ THEN("expect 'done' methods to be called for plugin and the instance but not the 'reload config' methods")
+ {
+ /* Simulate reloading the config */
+ factory1->indicateReload();
+
+ /* was "done" method called? */
+ CHECK(1 == debugObject->doneCalled);
+ CHECK(1 == debugObject->deleteInstanceCalled);
+ CHECK(0 == debugObject->reloadConfigCalled);
+ }
+
+ teardownConfigPathTest(factory1);
+ }
+ }
+
+ GIVEN("configuration with 2 plugins loaded by 1 factory")
+ {
+ WHEN("indicating config reload")
+ {
+ /* Simulate configuration without plugins - an unused factory */
+ setupConfigPathTest(configName1, buildPath, uuid_t1, effectivePath1, runtimePath1, 1556825556);
+ setupConfigPathTest(configName2, buildPath, uuid_t1, effectivePath2, runtimePath2, 1556825556, /* append */ true);
+ PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+ RemapPluginInst *plugin1 = factory1->getRemapPlugin(configName1, 0, nullptr, error);
+ RemapPluginInst *plugin2 = factory1->getRemapPlugin(configName2, 0, nullptr, error);
+
+ /* check if loaded successfully */
+ validateSuccessfulConfigPathTest(plugin1, error, effectivePath1, runtimePath1);
+ validateSuccessfulConfigPathTest(plugin2, error, effectivePath2, runtimePath2);
+
+ /* Prapare the debug objects */
+ PluginDebugObject *debugObject1 = getDebugObject(plugin1->_plugin);
+ PluginDebugObject *debugObject2 = getDebugObject(plugin2->_plugin);
+ debugObject1->clear();
+ debugObject2->clear();
+
+ THEN("expect 'done' methods to be called but not the 'reload config' methods")
+ {
+ /* Simulate reloading the config */
+ factory1->indicateReload();
+
+ /* Was "done" method called? */
+ CHECK(1 == debugObject1->doneCalled);
+ CHECK(1 == debugObject1->deleteInstanceCalled);
+ CHECK(0 == debugObject1->reloadConfigCalled);
+ CHECK(1 == debugObject2->doneCalled);
+ CHECK(1 == debugObject2->deleteInstanceCalled);
+ CHECK(0 == debugObject2->reloadConfigCalled);
+ }
+
+ teardownConfigPathTest(factory1);
+ }
+ }
+
+ GIVEN("configuration with 1 plugin loaded by 2 separate factories")
+ {
+ WHEN("indicating config reload")
+ {
+ /* Simulate configuration without plugins - an unused factory */
+ setupConfigPathTest(configName1, buildPath, uuid_t1, effectivePath1, runtimePath1, 1556825556);
+ PluginFactoryUnitTest *factory1 = getFactory(uuid_t1);
+ PluginFactoryUnitTest *factory2 = getFactory(uuid_t2);
+ RemapPluginInst *plugin1 = factory1->getRemapPlugin(configName1, 0, nullptr, error);
+ RemapPluginInst *plugin2 = factory2->getRemapPlugin(configName1, 0, nullptr, error);
+
+ /* Prapare the debug objects */
+ PluginDebugObject *debugObject1 = getDebugObject(plugin1->_plugin);
+ PluginDebugObject *debugObject2 = getDebugObject(plugin2->_plugin);
+
+ THEN("expect instance 'done' to be always called, but plugin 'done' called only after destroying one factory")
+ {
+ debugObject2->clear();
+ factory2->indicateReload();
+ CHECK(0 == debugObject2->doneCalled);
+ CHECK(1 == debugObject2->deleteInstanceCalled);
+ CHECK(1 == debugObject2->reloadConfigCalled);
+
+ delete factory2;
+
+ debugObject1->clear();
+ factory1->indicateReload();
+ CHECK(1 == debugObject1->doneCalled);
+ CHECK(1 == debugObject1->deleteInstanceCalled);
+ CHECK(0 == debugObject1->reloadConfigCalled);
+
+ delete factory1;
+ }
+
+ clean();
+ }
+ }
+}
diff --git a/proxy/http/remap/unit-tests/test_RemapPlugin.cc b/proxy/http/remap/unit-tests/test_RemapPlugin.cc
new file mode 100644
index 0000000..0eedcaf
--- /dev/null
+++ b/proxy/http/remap/unit-tests/test_RemapPlugin.cc
@@ -0,0 +1,433 @@
+/** @file
+
+ Unit tests for a class that deals with remap plugins
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ @section details Details
+
+ Implements code necessary for Reverse Proxy which mostly consists of
+ general purpose hostname substitution in URLs.
+
+ */
+
+#define CATCH_CONFIG_MAIN /* include main function */
+#include <catch.hpp> /* catch unit-test framework */
+#include <fstream> /* ofstream */
+#include <string>
+
+#include "plugin_testing_common.h"
+#include "../RemapPluginInfo.h"
+
+thread_local PluginThreadContext *pluginThreadContext;
+
+static void *INSTANCE_HANDLER = (void *)789;
+std::error_code ec;
+
+/* Some plugin context pointers used for unit testing */
+// static const PluginThreadContext *PLUGIN_INIT_CONTEXT_CUR = (PluginThreadContext *)1;
+// static const PluginThreadContext *PLUGIN_INIT_CONTEXT_NEW_V1 = (PluginThreadContext *)2;
+// static const PluginThreadContext *PLUGIN_INIT_CONTEXT_NEW_V2 = (PluginThreadContext *)3;
+
+/* A temp sandbox to play with our toys used for all fun with this test-bench */
+static fs::path tmpDir = fs::canonical(fs::temp_directory_path(), ec);
+
+/* The following are paths that are used commonly in the unit-tests */
+static fs::path sandboxDir = tmpDir / "sandbox";
+static fs::path runtimeDir = sandboxDir / "runtime";
+static fs::path searchDir = sandboxDir / "search";
+static fs::path pluginBuildDir = fs::current_path() / "unit-tests/.libs";
+
+void
+clean()
+{
+ fs::remove(sandboxDir, ec);
+}
+
+/* Mock used only to make unit testing convenient to check if callbacks are really called and check errors */
+class RemapPluginUnitTest : public RemapPluginInfo
+{
+public:
+ RemapPluginUnitTest(const fs::path &configPath, const fs::path &effectivePath, const fs::path &runtimePath)
+ : RemapPluginInfo(configPath, effectivePath, runtimePath)
+ {
+ }
+ std::string
+ getError(const char *required, const char *requiring = nullptr)
+ {
+ return missingRequiredSymbolError(_configPath.string(), required, requiring);
+ }
+
+ PluginDebugObject *
+ getDebugObject()
+ {
+ std::string error; /* ignore the error, return nullptr if symbol not defined */
+ void *address = nullptr;
+ getSymbol("getPluginDebugObjectTest", address, error);
+ GetPluginDebugObjectFunction *getObject = reinterpret_cast<GetPluginDebugObjectFunction *>(address);
+ if (getObject) {
+ PluginDebugObject *object = reinterpret_cast<PluginDebugObject *>(getObject());
+ return object;
+ } else {
+ return nullptr;
+ }
+ }
+};
+
+RemapPluginUnitTest *
+setupSandBox(const fs::path configPath)
+{
+ std::string error;
+ clean();
+
+ /* Create the directory structure and install plugins */
+ CHECK(fs::create_directories(searchDir, ec));
+ fs::copy(pluginBuildDir / configPath, searchDir, ec);
+ CHECK(fs::create_directories(runtimeDir, ec));
+
+ fs::path effectivePath = searchDir / configPath;
+ fs::path runtimePath = runtimeDir / configPath;
+ fs::path pluginBuildPath = pluginBuildDir / configPath;
+
+ /* Instantiate and initialize a plugin DSO instance. */
+ RemapPluginUnitTest *plugin = new RemapPluginUnitTest(configPath, effectivePath, runtimePath);
+
+ return plugin;
+}
+
+bool
+loadPlugin(RemapPluginUnitTest *plugin, std::string &error, PluginDebugObject *&debugObject)
+{
+ bool result = plugin->load(error);
+ debugObject = plugin->getDebugObject();
+ return result;
+}
+
+void
+cleanupSandBox(RemapPluginInfo *plugin)
+{
+ delete plugin;
+ clean();
+}
+
+SCENARIO("loading remap plugins", "[plugin][core]")
+{
+ std::string error;
+ PluginDebugObject *debugObject = nullptr;
+
+ GIVEN("a plugin which has only minimum required call back functions")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_required_cb.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ WHEN("loading")
+ {
+ bool result = loadPlugin(plugin, error, debugObject);
+
+ THEN("expect it to successfully load")
+ {
+ CHECK(true == result);
+ CHECK(error.empty());
+ }
+ cleanupSandBox(plugin);
+ }
+ }
+
+ GIVEN("a plugin which is missing the plugin TSREMAP_FUNCNAME_INIT function")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_missing_init.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ WHEN("loading")
+ {
+ bool result = loadPlugin(plugin, error, debugObject);
+
+ THEN("expect it to successfully load")
+ {
+ CHECK_FALSE(result);
+ CHECK(error == plugin->getError(TSREMAP_FUNCNAME_INIT));
+ }
+ cleanupSandBox(plugin);
+ }
+ }
+
+ GIVEN("a plugin which is missing the TSREMAP_FUNCNAME_DO_REMAP function")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_missing_doremap.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ WHEN("loading")
+ {
+ bool result = loadPlugin(plugin, error, debugObject);
+
+ THEN("expect it to fail")
+ {
+ CHECK_FALSE(result);
+ CHECK(error == plugin->getError(TSREMAP_FUNCNAME_DO_REMAP));
+ }
+ cleanupSandBox(plugin);
+ }
+ }
+
+ GIVEN("a plugin which has TSREMAP_FUNCNAME_NEW_INSTANCE but is missing the TSREMAP_FUNCNAME_DELETE_INSTANCE function")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_missing_deleteinstance.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ WHEN("loading")
+ {
+ bool result = loadPlugin(plugin, error, debugObject);
+
+ THEN("expect it to fail")
+ {
+ CHECK_FALSE(result);
+ CHECK(error == plugin->getError(TSREMAP_FUNCNAME_DELETE_INSTANCE, TSREMAP_FUNCNAME_NEW_INSTANCE));
+ }
+ cleanupSandBox(plugin);
+ }
+ }
+
+ GIVEN("a plugin which has TSREMAP_FUNCNAME_DELETE_INSTANCE but is missing the TSREMAP_FUNCNAME_NEW_INSTANCE function")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_missing_newinstance.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ WHEN("loading")
+ {
+ bool result = loadPlugin(plugin, error, debugObject);
+
+ THEN("expect it to fail")
+ {
+ CHECK_FALSE(result);
+ CHECK(error == plugin->getError(TSREMAP_FUNCNAME_NEW_INSTANCE, TSREMAP_FUNCNAME_DELETE_INSTANCE));
+ }
+ cleanupSandBox(plugin);
+ }
+ }
+}
+
+void
+prepCallTest(bool toFail, PluginDebugObject *debugObject)
+{
+ debugObject->clear();
+ debugObject->fail = toFail; // Tell the mock init to succeed or succeed.
+}
+
+void
+checkCallTest(bool shouldHaveFailed, bool result, const std::string &error, std::string &expectedError, int &called)
+{
+ CHECK(1 == called); // Init was called.
+ if (shouldHaveFailed) {
+ CHECK(false == result);
+ CHECK(error == expectedError); // Appropriate error was returned.
+ } else {
+ CHECK(true == result); // Init succesfull - returned TS_SUCCESS.
+ CHECK(error.empty()); // No error was returned.
+ }
+}
+
+SCENARIO("invoking plugin init", "[plugin][core]")
+{
+ std::string error;
+ PluginDebugObject *debugObject = nullptr;
+
+ GIVEN("plugin init function")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_testing_calls.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ bool result = loadPlugin(plugin, error, debugObject);
+ CHECK(true == result);
+
+ WHEN("init succeeds")
+ {
+ prepCallTest(/* toFail */ false, debugObject);
+
+ result = plugin->init(error);
+
+ THEN("expect init to be called, success code and no error to be returned")
+ {
+ std::string expectedError;
+
+ checkCallTest(/* shouldHaveFailed */ false, result, error, expectedError, debugObject->initCalled);
+ }
+ cleanupSandBox(plugin);
+ }
+
+ WHEN("init fails")
+ {
+ prepCallTest(/* toFail */ true, debugObject);
+
+ result = plugin->init(error);
+
+ THEN("expect init to be called, failure code and an error to be returned")
+ {
+ std::string expectedError;
+ expectedError.assign("failed to initialize plugin ").append(pluginConfigPath.string()).append(": Init failed");
+
+ checkCallTest(/* shouldHaveFailed */ true, result, error, expectedError, debugObject->initCalled);
+ }
+ cleanupSandBox(plugin);
+ }
+ }
+}
+
+SCENARIO("invoking plugin instance init", "[plugin][core]")
+{
+ std::string error;
+ PluginDebugObject *debugObject = nullptr;
+ void *ih = nullptr; // Instance handler pointer.
+
+ /* a sample test set of parameters */
+ static const char *args[] = {"arg1", "arg2", "arg3"};
+ static char **ARGV = const_cast<char **>(args);
+ static char ARGC = sizeof ARGV;
+
+ GIVEN("an instance init function")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_testing_calls.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ bool result = loadPlugin(plugin, error, debugObject);
+ CHECK(true == result);
+
+ WHEN("instance init succeeds")
+ {
+ prepCallTest(/* toFail */ false, debugObject);
+ debugObject->input_ih = INSTANCE_HANDLER; /* this is what the plugin instance init will return */
+
+ result = plugin->initInstance(ARGC, ARGV, &ih, error);
+
+ THEN("expect init to be called successfully with no error and expected instance handler")
+ {
+ std::string expectedError;
+
+ checkCallTest(/* shouldHaveFailed */ false, result, error, expectedError, debugObject->initInstanceCalled);
+
+ /* Verify expected handler */
+ CHECK(INSTANCE_HANDLER == ih);
+ /* Plugin received the parameters that we passed */
+ CHECK(ARGC == debugObject->argc);
+ CHECK(ARGV == debugObject->argv);
+ for (int i = 0; i < 3; i++) {
+ CHECK(0 == strcmp(ARGV[i], debugObject->argv[i]));
+ }
+ }
+ cleanupSandBox(plugin);
+ }
+
+ WHEN("instance init fails")
+ {
+ prepCallTest(/* toFail */ true, debugObject);
+
+ result = plugin->initInstance(ARGC, ARGV, &ih, error);
+
+ THEN("expect init to be called but failed with expected error and no instance handler")
+ {
+ std::string expectedError;
+ expectedError.assign("failed to create instance for plugin ").append(pluginConfigPath.string()).append(": Init failed");
+
+ checkCallTest(/* shouldHaveFailed */ true, result, error, expectedError, debugObject->initInstanceCalled);
+
+ /* Ideally instance handler should not be touched in case of failure */
+ CHECK(nullptr == ih);
+ /* Plugin received the parameters that we passed */
+ CHECK(ARGC == debugObject->argc);
+ CHECK(ARGV == debugObject->argv);
+ for (int i = 0; i < 3; i++) {
+ CHECK(0 == strcmp(ARGV[i], debugObject->argv[i]));
+ }
+ }
+ cleanupSandBox(plugin);
+ }
+ }
+}
+
+SCENARIO("unloading the plugin", "[plugin][core]")
+{
+ std::string error;
+ PluginDebugObject *debugObject = nullptr;
+
+ GIVEN("a 'done' function")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_testing_calls.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ bool result = loadPlugin(plugin, error, debugObject);
+ CHECK(true == result);
+
+ WHEN("'done' is called")
+ {
+ debugObject->clear();
+
+ plugin->done();
+
+ THEN("expect it to run") { CHECK(1 == debugObject->doneCalled); }
+ cleanupSandBox(plugin);
+ }
+ }
+
+ GIVEN("a 'delete_instance' function")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_testing_calls.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ bool result = loadPlugin(plugin, error, debugObject);
+ CHECK(true == result);
+
+ WHEN("'delete_instance' is called")
+ {
+ debugObject->clear();
+
+ plugin->doneInstance(INSTANCE_HANDLER);
+
+ THEN("expect it to run and receive the right instance handler")
+ {
+ CHECK(1 == debugObject->deleteInstanceCalled);
+ CHECK(INSTANCE_HANDLER == debugObject->ih);
+ }
+ cleanupSandBox(plugin);
+ }
+ }
+}
+
+SCENARIO("config reload", "[plugin][core]")
+{
+ std::string error;
+ PluginDebugObject *debugObject = nullptr;
+
+ GIVEN("a 'config reload' callback function")
+ {
+ fs::path pluginConfigPath = fs::path("plugin_testing_calls.so");
+ RemapPluginUnitTest *plugin = setupSandBox(pluginConfigPath);
+
+ bool result = loadPlugin(plugin, error, debugObject);
+ CHECK(true == result);
+
+ WHEN("'config reload' is called")
+ {
+ debugObject->clear();
+
+ plugin->indicateReload();
+
+ THEN("expect it to run") { CHECK(1 == debugObject->reloadConfigCalled); }
+ cleanupSandBox(plugin);
+ }
+ }
+}
diff --git a/src/traffic_server/InkAPI.cc b/src/traffic_server/InkAPI.cc
index 9414b1d..d4d6916 100644
--- a/src/traffic_server/InkAPI.cc
+++ b/src/traffic_server/InkAPI.cc
@@ -1001,6 +1001,7 @@ INKContInternal::INKContInternal()
m_closed(1),
m_deletable(0),
m_deleted(0),
+ m_context(0),
m_free_magic(INKCONT_INTERN_MAGIC_ALIVE)
{
}
@@ -1013,18 +1014,20 @@ INKContInternal::INKContInternal(TSEventFunc funcp, TSMutex mutexp)
m_closed(1),
m_deletable(0),
m_deleted(0),
+ m_context(0),
m_free_magic(INKCONT_INTERN_MAGIC_ALIVE)
{
SET_HANDLER(&INKContInternal::handle_event);
}
void
-INKContInternal::init(TSEventFunc funcp, TSMutex mutexp)
+INKContInternal::init(TSEventFunc funcp, TSMutex mutexp, void *context)
{
SET_HANDLER(&INKContInternal::handle_event);
mutex = (ProxyMutex *)mutexp;
m_event_func = funcp;
+ m_context = context;
}
void
@@ -1095,7 +1098,11 @@ INKContInternal::handle_event(int event, void *edata)
Debug("plugin", "INKCont Deletable but not deleted %d", m_event_count);
}
} else {
- int retval = m_event_func((TSCont)this, (TSEvent)event, edata);
+ /* set the plugin context */
+ auto *previousContext = pluginThreadContext;
+ pluginThreadContext = reinterpret_cast<PluginThreadContext *>(m_context);
+ int retval = m_event_func((TSCont)this, (TSEvent)event, edata);
+ pluginThreadContext = previousContext;
if (edata && event == EVENT_INTERVAL) {
Event *e = reinterpret_cast<Event *>(edata);
if (e->period != 0) {
@@ -4371,6 +4378,8 @@ TSMgmtSourceGet(const char *var_name, TSMgmtSource *source)
//
////////////////////////////////////////////////////////////////////
+extern thread_local PluginThreadContext *pluginThreadContext;
+
TSCont
TSContCreate(TSEventFunc funcp, TSMutex mutexp)
{
@@ -4379,9 +4388,13 @@ TSContCreate(TSEventFunc funcp, TSMutex mutexp)
sdk_assert(sdk_sanity_check_mutex(mutexp) == TS_SUCCESS);
}
+ if (pluginThreadContext) {
+ pluginThreadContext->acquire();
+ }
+
INKContInternal *i = INKContAllocator.alloc();
- i->init(funcp, mutexp);
+ i->init(funcp, mutexp, pluginThreadContext);
return (TSCont)i;
}
@@ -4392,6 +4405,10 @@ TSContDestroy(TSCont contp)
INKContInternal *i = (INKContInternal *)contp;
+ if (i->m_context) {
+ reinterpret_cast<PluginThreadContext *>(i->m_context)->release();
+ }
+
i->destroy();
}
@@ -5990,6 +6007,8 @@ TSHttpTxnReenable(TSHttpTxn txnp, TSEvent event)
}
}
+TSReturnCode TSHttpArgIndexNameLookup(UserArg::Type type, const char *name, int *arg_idx, const char **description);
+
TSReturnCode
TSHttpArgIndexReserve(UserArg::Type type, const char *name, const char *description, int *ptr_idx)
{
@@ -5997,7 +6016,19 @@ TSHttpArgIndexReserve(UserArg::Type type, const char *name, const char *descript
sdk_assert(sdk_sanity_check_null_ptr(name) == TS_SUCCESS);
sdk_assert(0 <= type && type < UserArg::Type::COUNT);
- int idx = UserArgIdx[type]++;
+ int idx;
+
+ /* Since this function is meant to be called during plugin initialization we could end up "leaking" indices during plugins reload.
+ * Make sure we allocate 1 index per name, also current TSHttpArgIndexNameLookup() implementation assumes 1-1 relationship as
+ * well. */
+ const char *desc;
+ if (TS_SUCCESS == TSHttpArgIndexNameLookup(type, name, &idx, &desc)) {
+ // Found existing index.
+ *ptr_idx = idx;
+ return TS_SUCCESS;
+ }
+
+ idx = UserArgIdx[type]++;
int limit = (type == UserArg::Type::VCONN) ? TS_VCONN_MAX_USER_ARG : TS_HTTP_MAX_USER_ARG;
if (idx < limit) {
@@ -6648,11 +6679,15 @@ TSVConnCreate(TSEventFunc event_funcp, TSMutex mutexp)
// TODO: probably don't need this if memory allocations fails properly
sdk_assert(sdk_sanity_check_mutex(mutexp) == TS_SUCCESS);
+ if (pluginThreadContext) {
+ pluginThreadContext->acquire();
+ }
+
INKVConnInternal *i = INKVConnAllocator.alloc();
sdk_assert(sdk_sanity_check_null_ptr((void *)i) == TS_SUCCESS);
- i->init(event_funcp, mutexp);
+ i->init(event_funcp, mutexp, pluginThreadContext);
return reinterpret_cast<TSVConn>(i);
}
diff --git a/src/tscore/ts_file.cc b/src/tscore/ts_file.cc
index 3467176..d50fae3 100644
--- a/src/tscore/ts_file.cc
+++ b/src/tscore/ts_file.cc
@@ -20,6 +20,8 @@
#include "tscore/ts_file.h"
#include <fcntl.h>
+#include <sys/types.h>
+#include <dirent.h>
namespace ts
{
@@ -56,12 +58,220 @@ namespace file
return zret;
}
+ path
+ temp_directory_path()
+ {
+ /* ISO/IEC 9945 (POSIX): The path supplied by the first environment variable found in the list TMPDIR, TMP, TEMP, TEMPDIR.
+ * If none of these are found, "/tmp" */
+ char const *folder = nullptr;
+ if ((nullptr == (folder = getenv("TMPDIR"))) && (nullptr == (folder = getenv("TMP"))) &&
+ (nullptr == (folder = getenv("TEMPDIR")))) {
+ folder = "/tmp";
+ }
+ return path(folder);
+ }
+
+ path
+ current_path()
+ {
+ char cwd[PATH_MAX];
+ if (::getcwd(cwd, sizeof(cwd)) != NULL) {
+ return path(cwd);
+ }
+ return path();
+ }
+
+ path
+ canonical(const path &p, std::error_code &ec)
+ {
+ if (p.empty()) {
+ ec = std::error_code(EINVAL, std::system_category());
+ return path();
+ }
+
+ char buf[PATH_MAX + 1];
+ char *res = ::realpath(p.c_str(), buf);
+ if (res) {
+ ec = std::error_code();
+ return path(res);
+ }
+
+ ec = std::error_code(errno, std::system_category());
+ return path();
+ }
+
+ bool
+ exists(const path &p)
+ {
+ std::error_code ec;
+ status(p, ec);
+ return !(ec && ENOENT == ec.value());
+ }
+
+ static bool
+ do_mkdir(const path &p, std::error_code &ec, mode_t mode)
+ {
+ struct stat st;
+ if (stat(p.c_str(), &st) != 0) {
+ if (mkdir(p.c_str(), mode) != 0 && errno != EEXIST) {
+ ec = std::error_code(errno, std::system_category());
+ return false;
+ }
+ } else if (!S_ISDIR(st.st_mode)) {
+ ec = std::error_code(ENOTDIR, std::system_category());
+ return false;
+ }
+ return true;
+ }
+
+ bool
+ create_directories(const path &p, std::error_code &ec, mode_t mode) noexcept
+ {
+ if (p.empty()) {
+ ec = std::error_code(EINVAL, std::system_category());
+ return false;
+ }
+
+ bool result = false;
+ ec = std::error_code();
+
+ size_t pos = 0;
+ std::string token;
+ while ((pos = p.string().find_first_of(p.preferred_separator, pos)) != std::string::npos) {
+ token = p.string().substr(0, pos);
+ if (!token.empty()) {
+ result = do_mkdir(path(token), ec, mode);
+ }
+ pos = pos + sizeof(p.preferred_separator);
+ }
+
+ if (result) {
+ result = do_mkdir(p, ec, mode);
+ }
+ return result;
+ }
+
+ bool
+ copy(const path &from, const path &to, std::error_code &ec)
+ {
+ static int BUF_SIZE = 65536;
+ FILE *src, *dst;
+ size_t in, out;
+ char buf[BUF_SIZE];
+ int bufsize = BUF_SIZE;
+
+ if (from.empty() || to.empty()) {
+ ec = std::error_code(EINVAL, std::system_category());
+ return false;
+ }
+
+ ec = std::error_code();
+
+ std::error_code err;
+ path final_to;
+ file_status s = status(to, err);
+ if (!(err && ENOENT == err.value()) && is_dir(s)) {
+ const size_t last_slash_idx = from.string().find_last_of(from.preferred_separator);
+ std::string filename = from.string().substr(last_slash_idx + 1);
+ final_to = to / filename;
+ } else {
+ final_to = to;
+ }
+
+ if (nullptr == (src = fopen(from.c_str(), "r"))) {
+ ec = std::error_code(errno, std::system_category());
+ return false;
+ }
+ if (nullptr == (dst = fopen(final_to.c_str(), "w"))) {
+ ec = std::error_code(errno, std::system_category());
+ return false;
+ }
+
+ while (1) {
+ in = fread(buf, 1, bufsize, src);
+ if (0 == in)
+ break;
+ out = fwrite(buf, 1, in, dst);
+ if (0 == out)
+ break;
+ }
+
+ fclose(src);
+ fclose(dst);
+
+ return true;
+ }
+
+ static bool
+ remove_path(const path &p, std::error_code &ec)
+ {
+ DIR *dir;
+ struct dirent *entry;
+ bool res = true;
+ std::error_code err;
+
+ file_status s = status(p, err);
+ if (err && ENOENT == err.value()) {
+ // file/dir does not exist
+ return false;
+ } else if (is_regular_file(s)) {
+ // regular file, try to remove it!
+ if (unlink(p.c_str()) != 0) {
+ ec = std::error_code(errno, std::system_category());
+ res = false;
+ }
+ return res;
+ } else if (!is_dir(s)) {
+ // not a directory
+ ec = std::error_code(ENOTDIR, std::system_category());
+ return false;
+ }
+
+ // recursively remove nested files and directories
+ if (nullptr == (dir = opendir(p.c_str()))) {
+ ec = std::error_code(errno, std::system_category());
+ return false;
+ }
+
+ while (nullptr != (entry = readdir(dir))) {
+ if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
+ continue;
+ }
+
+ remove_path(p / entry->d_name, ec);
+ }
+
+ if (0 != rmdir(p.c_str())) {
+ ec = std::error_code(errno, std::system_category());
+ }
+
+ closedir(dir);
+ return true;
+ }
+
+ bool
+ remove(const path &p, std::error_code &ec)
+ {
+ if (p.empty()) {
+ ec = std::error_code(EINVAL, std::system_category());
+ return false;
+ }
+
+ ec = std::error_code();
+ return remove_path(p, ec);
+ } // namespace file
+
int
file_type(const file_status &fs)
{
return fs._stat.st_mode & S_IFMT;
}
+ time_t
+ modification_time(const file_status &fs)
+ {
+ return fs._stat.st_mtime;
+ }
uintmax_t
file_size(const file_status &fs)
{
diff --git a/src/tscore/unit_tests/test_ts_file.cc b/src/tscore/unit_tests/test_ts_file.cc
index 58366de..603ded3 100644
--- a/src/tscore/unit_tests/test_ts_file.cc
+++ b/src/tscore/unit_tests/test_ts_file.cc
@@ -22,6 +22,7 @@
*/
#include <iostream>
+#include <fstream> /* ofstream */
#include "tscore/ts_file.h"
#include "../../../tests/include/catch.hpp"
@@ -67,3 +68,195 @@ TEST_CASE("ts_file_io", "[libts][ts_file_io]")
REQUIRE(ec.value() == 2);
REQUIRE(ts::file::is_readable(file) == false);
}
+
+TEST_CASE("ts_file::path::parent_path", "[libts][fs_file]")
+{
+ CHECK(ts::file::path("/").parent_path() == path("/"));
+ CHECK(ts::file::path("/absolute/path/file.txt").parent_path() == ts::file::path("/absolute/path"));
+ CHECK(ts::file::path("/absolute/path/.").parent_path() == ts::file::path("/absolute/path"));
+
+ CHECK(ts::file::path("relative/path/file.txt").parent_path() == ts::file::path("relative/path"));
+ CHECK(ts::file::path("relative/path/.").parent_path() == ts::file::path("relative/path"));
+ CHECK(ts::file::path(".").parent_path() == ts::file::path(""));
+}
+
+static std::string
+setenvvar(const std::string &name, const std::string &value)
+{
+ std::string saved;
+ if (nullptr != getenv(name.c_str())) {
+ saved.assign(value);
+ }
+
+ if (!value.empty()) {
+ setenv(name.c_str(), value.c_str(), 1);
+ } else {
+ unsetenv(name.c_str());
+ }
+
+ return saved;
+}
+
+TEST_CASE("ts_file::path::temp_directory_path", "[libts][fs_file]")
+{
+ // Clean all temp dir env variables.
+ std::string s1 = setenvvar("TMPDIR", std::string());
+ std::string s2 = setenvvar("TEMPDIR", std::string());
+ std::string s3 = setenvvar("TMP", std::string());
+ std::string s;
+
+ // If nothing defined return "/tmp"
+ CHECK(ts::file::temp_directory_path() == ts::file::path("/tmp"));
+
+ // TMPDIR defined.
+ s = setenvvar("TMPDIR", "/temp_dirname1");
+ CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname1"));
+ setenvvar("TMPDIR", s);
+
+ // TEMPDIR
+ s = setenvvar("TEMPDIR", "/temp_dirname");
+ CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname"));
+ // TMP defined, it should take precedence over TEMPDIR.
+ s = setenvvar("TMP", "/temp_dirname1");
+ CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname1"));
+ // TMPDIR defined, it should take precedence over TMP.
+ s = setenvvar("TMPDIR", "/temp_dirname2");
+ CHECK(ts::file::temp_directory_path() == ts::file::path("/temp_dirname2"));
+ setenvvar("TMPDIR", s);
+ setenvvar("TMP", s);
+ setenvvar("TEMPDIR", s);
+
+ // Restore all temp dir env variables to their previous state.
+ setenvvar("TMPDIR", s1);
+ setenvvar("TEMPDIR", s2);
+ setenvvar("TMP", s3);
+}
+
+TEST_CASE("ts_file::path::create_directories", "[libts][fs_file]")
+{
+ std::error_code ec;
+ path tempdir = ts::file::temp_directory_path();
+
+ CHECK_FALSE(ts::file::create_directories(path(), ec));
+ CHECK(ec.value() == EINVAL);
+
+ path testdir1 = tempdir / "dir1";
+ CHECK(ts::file::create_directories(testdir1, ec));
+ CHECK(ts::file::exists(testdir1));
+
+ path testdir2 = testdir1 / "dir2";
+ CHECK(ts::file::create_directories(testdir1, ec));
+ CHECK(ts::file::exists(testdir1));
+
+ // Cleanup
+ CHECK(ts::file::remove(testdir1, ec));
+ CHECK_FALSE(ts::file::exists(testdir1));
+}
+
+TEST_CASE("ts_file::path::remove", "[libts][fs_file]")
+{
+ std::error_code ec;
+ path tempdir = ts::file::temp_directory_path();
+
+ CHECK_FALSE(ts::file::remove(path(), ec));
+ CHECK(ec.value() == EINVAL);
+
+ path testdir1 = tempdir / "dir1";
+ path testdir2 = testdir1 / "dir2";
+ path file1 = testdir2 / "test.txt";
+
+ // Simple creation and removal of a directory /tmp/dir1
+ CHECK(ts::file::create_directories(testdir1, ec));
+ CHECK(ts::file::exists(testdir1));
+ CHECK(ts::file::remove(testdir1, ec));
+ CHECK_FALSE(ts::file::exists(testdir1));
+
+ // Create /tmp/dir1/dir2 and remove /tmp/dir1/dir2 => /tmp/dir1 should exist
+ CHECK(ts::file::create_directories(testdir2, ec));
+ CHECK(ts::file::remove(testdir2, ec));
+ CHECK(ts::file::exists(testdir1));
+
+ // Create a file, remove it, test if exists and then attempting to remove it again should fail.
+ CHECK(ts::file::create_directories(testdir2, ec));
+ std::ofstream file(file1.string());
+ file << "Simple test file";
+ file.close();
+ CHECK(ts::file::exists(file1));
+ CHECK(ts::file::remove(file1, ec));
+ CHECK_FALSE(ts::file::exists(file1));
+ CHECK_FALSE(ts::file::remove(file1, ec));
+
+ // Clean up.
+ CHECK(ts::file::remove(testdir1, ec));
+ CHECK_FALSE(ts::file::exists(testdir1));
+}
+
+TEST_CASE("ts_file::path::canonical", "[libts][fs_file]")
+{
+ std::error_code ec;
+ path tempdir = ts::file::canonical(ts::file::temp_directory_path(), ec);
+ path testdir1 = tempdir / "dir1";
+ path testdir2 = testdir1 / "dir2";
+ path testdir3 = testdir2 / "dir3";
+ path unorthodox = testdir3 / path("..") / path("..") / "dir2";
+
+ // Invalid empty path.
+ CHECK(path() == ts::file::canonical(path(), ec));
+ CHECK(ec.value() == EINVAL);
+
+ // Fail if directory does not exist
+ CHECK(path() == ts::file::canonical(unorthodox, ec));
+ CHECK(ec.value() == ENOENT);
+
+ // Create the dir3 and test again
+ CHECK(create_directories(testdir3, ec));
+ CHECK(ts::file::exists(testdir3));
+ CHECK(ts::file::exists(testdir2));
+ CHECK(ts::file::exists(testdir1));
+ CHECK(ts::file::exists(unorthodox));
+ CHECK(ts::file::canonical(unorthodox, ec) == testdir2);
+ CHECK(ec.value() == 0);
+
+ // Cleanup
+ CHECK(ts::file::remove(testdir1, ec));
+ CHECK_FALSE(ts::file::exists(testdir1));
+}
+
+TEST_CASE("ts_file::path::copy", "[libts][fs_file]")
+{
+ std::error_code ec;
+ path tempdir = ts::file::temp_directory_path();
+ path testdir1 = tempdir / "dir1";
+ path testdir2 = testdir1 / "dir2";
+ path file1 = testdir2 / "test1.txt";
+ path file2 = testdir2 / "test2.txt";
+
+ // Invalid empty path, both to and from parameters.
+ CHECK_FALSE(ts::file::copy(path(), path(), ec));
+ CHECK(ec.value() == EINVAL);
+
+ CHECK(ts::file::create_directories(testdir2, ec));
+ std::ofstream file(file1.string());
+ file << "Simple test file";
+ file.close();
+ CHECK(ts::file::exists(file1));
+
+ // Invalid empty path, now from parameter is ok but to is empty
+ CHECK_FALSE(ts::file::copy(file1, path(), ec));
+ CHECK(ec.value() == EINVAL);
+
+ // successfull copy: "to" is directory
+ CHECK(ts::file::copy(file1, testdir2, ec));
+ CHECK(ec.value() == 0);
+
+ // successful copy: "to" is file
+ CHECK(ts::file::copy(file1, file2, ec));
+ CHECK(ec.value() == 0);
+
+ // Compare the content
+ CHECK(ts::file::load(file1, ec) == ts::file::load(file2, ec));
+
+ // Cleanup
+ CHECK(ts::file::remove(testdir1, ec));
+ CHECK_FALSE(ts::file::exists(testdir1));
+}
\ No newline at end of file