diff --git a/ci/tsqa/files/header-rewrite.config b/ci/tsqa/files/header-rewrite.config new file mode 100644 index 00000000000..4a06c96f21f --- /dev/null +++ b/ci/tsqa/files/header-rewrite.config @@ -0,0 +1,13 @@ +cond %{READ_REQUEST_PRE_REMAP_HOOK} +cond %{PATH} /^.*addcookie$/ [AND] +add-cookie testkey testaddvalue + +cond %{READ_REQUEST_PRE_REMAP_HOOK} +cond %{PATH} /^.*rmcookie$/ [AND] +rm-cookie testkey + + +cond %{READ_REQUEST_PRE_REMAP_HOOK} +cond %{PATH} /^.*setcookie$/ [AND] +set-cookie testkey testsetvalue + diff --git a/ci/tsqa/tests/test_header_rewrite.py b/ci/tsqa/tests/test_header_rewrite.py new file mode 100644 index 00000000000..9f1bb40ae86 --- /dev/null +++ b/ci/tsqa/tests/test_header_rewrite.py @@ -0,0 +1,115 @@ +''' +Test cookie rewrite +''' +import os +import requests +import time +import logging +import random +import tsqa.test_cases +import helpers +import shutil +import SocketServer +import urllib2 + +log = logging.getLogger(__name__) + +class EchoServerHandler(SocketServer.BaseRequestHandler): + """ + A subclass of RequestHandler which will return all data received back + """ + + def handle(self): + # Receive the data in small chunks and retransmit it + while True: + data = self.request.recv(4096).strip() + if data: + log.debug('Sending data back to the client') + else: + log.debug('Client disconnected') + break + cookie = '' + if 'Cookie' in data: + cookie = data.split('Cookie: ')[1].split('\r\n')[0] + + resp = ('HTTP/1.1 200 OK\r\n' + 'Content-Length: {data_length}\r\n' + 'Content-Type: text/html; charset=UTF-8\r\n' + 'Connection: keep-alive\r\n' + '\r\n{data_string}'.format( + data_length = len(cookie), + data_string = cookie + )) + self.request.sendall(resp) + +class TestHeaderRewrite(helpers.EnvironmentCase): + ''' + Tests for header rewrite + ''' + @classmethod + def setUpEnv(cls, env): + cls.traffic_server_port = int(cls.configs['records.config']['CONFIG']['proxy.config.http.server_ports']) + + # create a socket server + cls.socket_server = tsqa.endpoint.SocketServerDaemon(EchoServerHandler) + cls.socket_server.start() + cls.socket_server.ready.wait() + + cls.configs['remap.config'].add_line( + 'map / http://127.0.0.1:%d' %(cls.socket_server.port) + ) + + # setup the plugin + cls.config_file = 'header-rewrite.config' + cls.test_config_path = helpers.tests_file_path(cls.config_file) + + cls.configs['plugin.config'].add_line('%s/header_rewrite.so %s' % ( + cls.environment.layout.plugindir, + cls.test_config_path + )) + + def test_cookie_rewrite(self): + + cookie_test_add_dict = { + '' : 'testkey=testaddvalue', + 'testkey=somevalue' : 'testkey=somevalue', + 'otherkey=testvalue' : 'otherkey=testvalue;testkey=testaddvalue', + 'testkey = "other=value"; a = a' : 'testkey = "other=value"; a = a', + 'testkeyx===' : 'testkeyx===;testkey=testaddvalue' + } + for key in cookie_test_add_dict: + opener = urllib2.build_opener() + opener.addheaders.append(('Cookie', key)) + f = opener.open("http://127.0.0.1:%d/addcookie" % (self.traffic_server_port)) + resp = f.read() + self.assertEqual(resp, cookie_test_add_dict[key]) + + cookie_test_rm_dict = { + '' : '', + ' testkey=somevalue' : '', + 'otherkey=testvalue' : 'otherkey=testvalue', + 'testkey = "other=value" ; a = a' : ' a = a', + 'otherkey=othervalue= ; testkey===' : 'otherkey=othervalue= ', + 'firstkey ="firstvalue" ; testkey = =; secondkey=\'\'' : 'firstkey ="firstvalue" ; secondkey=\'\'' + } + for key in cookie_test_rm_dict: + opener = urllib2.build_opener() + opener.addheaders.append(('Cookie', key)) + f = opener.open("http://127.0.0.1:%d/rmcookie" % (self.traffic_server_port)) + resp = f.read() + self.assertEqual(resp, cookie_test_rm_dict[key]) + + cookie_test_set_dict = { + '' : 'testkey=testsetvalue', + 'testkey=somevalue' : 'testkey=testsetvalue', + 'otherkey=testvalue' : 'otherkey=testvalue;testkey=testsetvalue', + 'testkey = "other=value"; a = a' : 'testkey = testsetvalue; a = a', + 'testkeyx===' : 'testkeyx===;testkey=testsetvalue', + 'firstkey ="firstvalue" ; testkey = =; secondkey=\'\'' : 'firstkey ="firstvalue" ; testkey = testsetvalue; secondkey=\'\'' + } + for key in cookie_test_set_dict: + opener = urllib2.build_opener() + opener.addheaders.append(('Cookie', key)) + f = opener.open("http://127.0.0.1:%d/setcookie" % (self.traffic_server_port)) + resp = f.read() + self.assertEqual(resp, cookie_test_set_dict[key]) diff --git a/doc/admin-guide/plugins/header_rewrite.en.rst b/doc/admin-guide/plugins/header_rewrite.en.rst index ada3a8dcc07..8b76e82de1c 100644 --- a/doc/admin-guide/plugins/header_rewrite.en.rst +++ b/doc/admin-guide/plugins/header_rewrite.en.rst @@ -458,6 +458,15 @@ occurs first). The following operators are available: +add-cookie +~~~~~~~~~~ +:: + + add-cookie + +Adds a new ```` cookie line with the contents ````. Note that this +operator will do nothing if a cookie pair with ```` already exists. + add-header ~~~~~~~~~~ :: @@ -512,6 +521,14 @@ rm-header Removes the header ````. +rm-cookie +~~~~~~~~~ +:: + + rm-cookie + +Removes the cookie ````. + set-config ~~~~~~~~~~ :: @@ -626,6 +643,15 @@ When invoked, and when ```` is any of ``1``, ``true``, or ``TRUE``, this operator causes |TS| to abort further request remapping. Any other value and the operator will effectively be a no-op. +set-cookie +~~~~~~~~~~ +:: + + set-cookie + +Replaces the value of cookie ```` with ````, creating the cookie +if necessary. + Operator Flags -------------- diff --git a/plugins/header_rewrite/factory.cc b/plugins/header_rewrite/factory.cc index 1bfddf7704c..b4d2e8b2818 100644 --- a/plugins/header_rewrite/factory.cc +++ b/plugins/header_rewrite/factory.cc @@ -56,6 +56,12 @@ operator_factory(const std::string &op) o = new OperatorNoOp(); } else if (op == "counter") { o = new OperatorCounter(); + } else if (op == "rm-cookie") { + o = new OperatorRMCookie(); + } else if (op == "set-cookie") { + o = new OperatorSetCookie(); + } else if (op == "add-cookie") { + o = new OperatorAddCookie(); } else if (op == "set-conn-dscp") { o = new OperatorSetConnDSCP(); } else if (op == "set-debug") { diff --git a/plugins/header_rewrite/operator.cc b/plugins/header_rewrite/operator.cc index 19db657a12b..83682102188 100644 --- a/plugins/header_rewrite/operator.cc +++ b/plugins/header_rewrite/operator.cc @@ -58,3 +58,14 @@ OperatorHeaders::initialize(Parser &p) require_resources(RSRC_CLIENT_REQUEST_HEADERS); require_resources(RSRC_CLIENT_RESPONSE_HEADERS); } + +void +OperatorCookies::initialize(Parser &p) +{ + Operator::initialize(p); + + _cookie = p.get_arg(); + + require_resources(RSRC_SERVER_REQUEST_HEADERS); + require_resources(RSRC_CLIENT_REQUEST_HEADERS); +} diff --git a/plugins/header_rewrite/operator.h b/plugins/header_rewrite/operator.h index d6190a73895..88f59c2beb2 100644 --- a/plugins/header_rewrite/operator.h +++ b/plugins/header_rewrite/operator.h @@ -83,4 +83,21 @@ class OperatorHeaders : public Operator DISALLOW_COPY_AND_ASSIGN(OperatorHeaders); }; +/////////////////////////////////////////////////////////////////////////////// +// Base class for all Cookie based Operators, this is obviously also an +// Operator interface. +// +class OperatorCookies : public Operator +{ +public: + OperatorCookies() : _cookie("") { TSDebug(PLUGIN_NAME_DBG, "Calling CTOR for OperatorCookies"); } + void initialize(Parser &p); + +protected: + std::string _cookie; + +private: + DISALLOW_COPY_AND_ASSIGN(OperatorCookies); +}; + #endif // __OPERATOR_H diff --git a/plugins/header_rewrite/operators.cc b/plugins/header_rewrite/operators.cc index 4c6fcd718d8..7b02899e629 100644 --- a/plugins/header_rewrite/operators.cc +++ b/plugins/header_rewrite/operators.cc @@ -619,6 +619,216 @@ OperatorCounter::exec(const Resources & /* ATS_UNUSED res */) const TSStatIntIncrement(_counter, 1); } +// OperatorRMCookie +void +OperatorRMCookie::exec(const Resources &res) const +{ + if (res.bufp && res.hdr_loc) { + TSDebug(PLUGIN_NAME, "OperatorRMCookie::exec() invoked on cookie %s", _cookie.c_str()); + TSMLoc field_loc; + + // Find Cookie + field_loc = TSMimeHdrFieldFind(res.bufp, res.hdr_loc, TS_MIME_FIELD_COOKIE, TS_MIME_LEN_COOKIE); + if (NULL == field_loc) { + TSDebug(PLUGIN_NAME, "OperatorRMCookie::exec, no cookie"); + return; + } + + int cookies_len = 0; + const char *cookies = TSMimeHdrFieldValueStringGet(res.bufp, res.hdr_loc, field_loc, -1, &cookies_len); + std::string updated_cookie; + if (CookieHelper::cookieModifyHelper(cookies, cookies_len, updated_cookie, CookieHelper::COOKIE_OP_DEL, _cookie) && + TS_SUCCESS == + TSMimeHdrFieldValueStringSet(res.bufp, res.hdr_loc, field_loc, -1, updated_cookie.c_str(), updated_cookie.size())) { + TSDebug(PLUGIN_NAME, "OperatorRMCookie::exec, updated_cookie = [%s]", updated_cookie.c_str()); + } + TSHandleMLocRelease(res.bufp, res.hdr_loc, field_loc); + } +} + +// OperatorAddCookie +void +OperatorAddCookie::initialize(Parser &p) +{ + OperatorCookies::initialize(p); + _value.set_value(p.get_value()); +} + +void +OperatorAddCookie::exec(const Resources &res) const +{ + std::string value; + + _value.append_value(value, res); + + if (_value.need_expansion()) { + VariableExpander ve(value); + + value = ve.expand(res); + } + + if (res.bufp && res.hdr_loc) { + TSDebug(PLUGIN_NAME, "OperatorAddCookie::exec() invoked on cookie %s", _cookie.c_str()); + TSMLoc field_loc; + + // Find Cookie + field_loc = TSMimeHdrFieldFind(res.bufp, res.hdr_loc, TS_MIME_FIELD_COOKIE, TS_MIME_LEN_COOKIE); + if (NULL == field_loc) { + TSDebug(PLUGIN_NAME, "OperatorAddCookie::exec, no cookie"); + if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(res.bufp, res.hdr_loc, TS_MIME_FIELD_COOKIE, TS_MIME_LEN_COOKIE, &field_loc)) { + value = _cookie + '=' + value; + if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(res.bufp, res.hdr_loc, field_loc, -1, value.c_str(), value.size())) { + TSDebug(PLUGIN_NAME, "Adding cookie %s", _cookie.c_str()); + TSMimeHdrFieldAppend(res.bufp, res.hdr_loc, field_loc); + } + TSHandleMLocRelease(res.bufp, res.hdr_loc, field_loc); + } + return; + } + + int cookies_len = 0; + const char *cookies = TSMimeHdrFieldValueStringGet(res.bufp, res.hdr_loc, field_loc, -1, &cookies_len); + std::string updated_cookie; + if (CookieHelper::cookieModifyHelper(cookies, cookies_len, updated_cookie, CookieHelper::COOKIE_OP_ADD, _cookie, value) && + TS_SUCCESS == + TSMimeHdrFieldValueStringSet(res.bufp, res.hdr_loc, field_loc, -1, updated_cookie.c_str(), updated_cookie.size())) { + TSDebug(PLUGIN_NAME, "OperatorAddCookie::exec, updated_cookie = [%s]", updated_cookie.c_str()); + } + } +} + +// OperatorSetCookie +void +OperatorSetCookie::initialize(Parser &p) +{ + OperatorCookies::initialize(p); + _value.set_value(p.get_value()); +} + +void +OperatorSetCookie::exec(const Resources &res) const +{ + std::string value; + + _value.append_value(value, res); + + if (_value.need_expansion()) { + VariableExpander ve(value); + + value = ve.expand(res); + } + + if (res.bufp && res.hdr_loc) { + TSDebug(PLUGIN_NAME, "OperatorSetCookie::exec() invoked on cookie %s", _cookie.c_str()); + TSMLoc field_loc; + + // Find Cookie + field_loc = TSMimeHdrFieldFind(res.bufp, res.hdr_loc, TS_MIME_FIELD_COOKIE, TS_MIME_LEN_COOKIE); + if (NULL == field_loc) { + TSDebug(PLUGIN_NAME, "OperatorSetCookie::exec, no cookie"); + if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(res.bufp, res.hdr_loc, TS_MIME_FIELD_COOKIE, TS_MIME_LEN_COOKIE, &field_loc)) { + value = _cookie + "=" + value; + if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(res.bufp, res.hdr_loc, field_loc, -1, value.c_str(), value.size())) { + TSDebug(PLUGIN_NAME, "Adding cookie %s", _cookie.c_str()); + TSMimeHdrFieldAppend(res.bufp, res.hdr_loc, field_loc); + } + TSHandleMLocRelease(res.bufp, res.hdr_loc, field_loc); + } + return; + } + + int cookies_len = 0; + const char *cookies = TSMimeHdrFieldValueStringGet(res.bufp, res.hdr_loc, field_loc, -1, &cookies_len); + std::string updated_cookie; + if (CookieHelper::cookieModifyHelper(cookies, cookies_len, updated_cookie, CookieHelper::COOKIE_OP_SET, _cookie, value) && + TS_SUCCESS == + TSMimeHdrFieldValueStringSet(res.bufp, res.hdr_loc, field_loc, -1, updated_cookie.c_str(), updated_cookie.size())) { + TSDebug(PLUGIN_NAME, "OperatorSetCookie::exec, updated_cookie = [%s]", updated_cookie.c_str()); + } + TSHandleMLocRelease(res.bufp, res.hdr_loc, field_loc); + } +} + +bool +CookieHelper::cookieModifyHelper(const char *cookies, const size_t cookies_len, std::string &updated_cookies, + const CookieHelper::CookieOp cookie_op, const std::string &cookie_key, + const std::string &cookie_value) +{ + if (0 == cookie_key.size()) { + TSDebug(PLUGIN_NAME, "CookieHelper::cookieModifyHelper, empty cookie_key"); + return false; + } + + for (size_t idx = 0; idx < cookies_len;) { + // advance any leading spaces + for (; idx < cookies_len && std::isspace(cookies[idx]); idx++) + ; + if (0 == strncmp(cookies + idx, cookie_key.c_str(), cookie_key.size())) { + size_t key_start_idx = idx; + // advance to past the name and any subsequent spaces + for (idx += cookie_key.size(); idx < cookies_len && std::isspace(cookies[idx]); idx++) + ; + if (idx < cookies_len && cookies[idx++] == '=') { + // cookie_key is found, then we don't need to add it. + if (CookieHelper::COOKIE_OP_ADD == cookie_op) { + return false; + } + for (; idx < cookies_len && std::isspace(cookies[idx]); idx++) + ; + size_t value_start_idx = idx; + for (; idx < cookies_len && cookies[idx] != ';'; idx++) + ; + // cookie value is found + size_t value_end_idx = idx; + if (CookieHelper::COOKIE_OP_SET == cookie_op) { + updated_cookies.append(cookies, value_start_idx); + updated_cookies.append(cookie_value); + updated_cookies.append(cookies + value_end_idx, cookies_len - value_end_idx); + return true; + } + + if (CookieHelper::COOKIE_OP_DEL == cookie_op) { + // +1 to skip the semi-colon after the cookie_value + updated_cookies.append(cookies, key_start_idx); + if (value_end_idx < cookies_len) { + updated_cookies.append(cookies + value_end_idx + 1, cookies_len - value_end_idx - 1); + } + // if the cookie to delete is the last pair, + // the semi-colon before this pair needs to be deleted + // this handles the case "c = b; key=value", the expected result is "c = b" + size_t last_semi_colon = updated_cookies.find_last_of(';'); + if (last_semi_colon != std::string::npos) { + size_t last_equal = updated_cookies.find_last_of('='); + if (last_equal != std::string::npos) { + if (last_equal < last_semi_colon) { + // remove the last semi colon and subsequent chars + updated_cookies = updated_cookies.substr(0, last_semi_colon); + } + } else { + // if there is no equal left in cookie, valid cookie value doesn't exist + updated_cookies = ""; + } + } + return true; + } + } + } + // find the next cookie pair followed by semi-colon + while (idx < cookies_len && cookies[idx++] != ';') + ; + } + + if (CookieHelper::COOKIE_OP_ADD == cookie_op || CookieHelper::COOKIE_OP_SET == cookie_op) { + if (0 == cookies_len) { + updated_cookies = cookie_key + '=' + cookie_value; + } else { + updated_cookies = std::string(cookies, cookies_len) + ';' + cookie_key + '=' + cookie_value; + } + return true; + } + return false; +} + // OperatorSetConnDSCP void OperatorSetConnDSCP::initialize(Parser &p) diff --git a/plugins/header_rewrite/operators.h b/plugins/header_rewrite/operators.h index b6ec68c8a66..831b7f6c818 100644 --- a/plugins/header_rewrite/operators.h +++ b/plugins/header_rewrite/operators.h @@ -229,6 +229,59 @@ class OperatorCounter : public Operator int _counter; }; +class OperatorRMCookie : public OperatorCookies +{ +public: + OperatorRMCookie() { TSDebug(PLUGIN_NAME_DBG, "Calling CTOR for OperatorRMCookie"); } +protected: + void exec(const Resources &res) const; + +private: + DISALLOW_COPY_AND_ASSIGN(OperatorRMCookie); +}; + +class OperatorAddCookie : public OperatorCookies +{ +public: + OperatorAddCookie() { TSDebug(PLUGIN_NAME_DBG, "Calling CTOR for OperatorAddCookie"); } + void initialize(Parser &p); + +protected: + void exec(const Resources &res) const; + +private: + DISALLOW_COPY_AND_ASSIGN(OperatorAddCookie); + + Value _value; +}; + +class OperatorSetCookie : public OperatorCookies +{ +public: + OperatorSetCookie() { TSDebug(PLUGIN_NAME_DBG, "Calling CTOR for OperatorSetCookie"); } + void initialize(Parser &p); + +protected: + void exec(const Resources &res) const; + +private: + DISALLOW_COPY_AND_ASSIGN(OperatorSetCookie); + + Value _value; +}; + +namespace CookieHelper +{ +enum CookieOp { COOKIE_OP_DEL, COOKIE_OP_ADD, COOKIE_OP_SET }; + +/* + * This function returns if cookies need to be changed or not. + * If the return value is true, updated_cookies would be cookies after the change. + */ +bool cookieModifyHelper(const char *cookies, const size_t cookies_len, std::string &updated_cookies, const CookieOp cookie_op, + const std::string &cookie_key, const std::string &cookie_value = std::string()); +} + class OperatorSetConnDSCP : public Operator { public: