From 4b5ee347c45167231e267a88af4042cedeea87a1 Mon Sep 17 00:00:00 2001 From: Derek Dagit Date: Thu, 13 Sep 2018 10:06:24 -0500 Subject: [PATCH] Adds redirect actions settings, returns by default New config `proxy.config.http.redirect.actions` --- doc/admin-guide/files/records.config.en.rst | 55 ++++ iocore/utils/I_Machine.h | 1 + iocore/utils/Machine.cc | 13 + mgmt/RecordsConfig.cc | 3 + proxy/http/HttpConfig.cc | 137 ++++++++++ proxy/http/HttpConfig.h | 48 +++- proxy/http/HttpTransact.cc | 31 +++ tests/gold_tests/redirect/redirect.test.py | 4 +- .../redirect/redirect_actions.test.py | 243 ++++++++++++++++++ .../gold_tests/redirect/redirect_post.test.py | 5 +- 10 files changed, 533 insertions(+), 7 deletions(-) create mode 100644 tests/gold_tests/redirect/redirect_actions.test.py diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst index 9d14ccb6fe1..8c7bd763173 100644 --- a/doc/admin-guide/files/records.config.en.rst +++ b/doc/admin-guide/files/records.config.en.rst @@ -1326,6 +1326,61 @@ HTTP Redirection This setting determines the maximum size in bytes of uploaded content to be buffered for HTTP methods such as POST and PUT. +.. ts:cv:: CONFIG proxy.config.http.redirect.actions STRING routable:follow + :reloadable: + + This setting determines how redirects should be handled. The setting consists + of a comma-separated list of key-value pairs, where the keys are named IP + address ranges and the values are actions. + + The following are valid keys: + + ============= =============================================================== + Key Description + ============= =============================================================== + ``self`` Addresses of the host's interfaces + ``loopback`` IPv4 ``127.0.0.0/8`` and IPv6 ``::1`` + ``private`` IPv4 ``10.0.0.0/8`` ``100.64.0.0/10`` ``172.16.0.0/12`` ``192.168.0.0/16`` and IPv6 ``fc00::/7`` + ``multicast`` IPv4 ``224.0.0.0/4`` and IPv6 ``ff00::/8`` + ``linklocal`` IPv4 ``169.254.0.0/16`` and IPv6 ``fe80::/10`` + ``routable`` All publicly routable addresses + ``default`` All address ranges not configured specifically + ============= =============================================================== + + The following are valid values: + + ========== ================================================================== + Value Description + ========== ================================================================== + ``return`` Do not process the redirect, send it as the proxy response. + ``reject`` Do not process the redirect, send a 403 as the proxy response. + ``follow`` Internally follow the redirect up to :ts:cv:`proxy.config.http.number_of_redirections`. **Use this setting with caution!** + ========== ================================================================== + + .. warning:: Following a redirect to other than ``routable`` addresses can be + dangerous, as it allows the controller of an origin to arrange a probe the + |TS| host. Enabling these redirects makes |TS| open to third party attacks + and probing and therefore should be considered only in known safe + environments. + + For example, a setting of + ``loopback:reject,private:reject,routable:follow,default:return`` would send + ``403`` as the proxy response to loopback and private addresses, routable + addresses would be followed up to + :ts:cv:`proxy.config.http.number_of_redirections`, and redirects to all other + ranges will be sent as the proxy response. + + The action for ``self`` has the highest priority when an address would match + multiple keys, and the action for ``default`` has the lowest priority. Other + keys represent disjoint sets of addresses that will not conflict. If + duplicate keys are present in the setting, the right-most key-value pair is + used. + + The default value is ``routable:follow``, which means "follow routable + redirects, return all other redirects". Note that + :ts:cv:`proxy.config.http.number_of_redirections` must be positive also, + otherwise redirects will be returned rather than followed. + Origin Server Connect Attempts ============================== diff --git a/iocore/utils/I_Machine.h b/iocore/utils/I_Machine.h index d17c8a81150..fe4da83d870 100644 --- a/iocore/utils/I_Machine.h +++ b/iocore/utils/I_Machine.h @@ -81,6 +81,7 @@ struct Machine { static self *instance(); bool is_self(const char *name); bool is_self(const IpAddr *ipaddr); + bool is_self(struct sockaddr const *addr); void insert_id(char *id); void insert_id(IpAddr *ipaddr); diff --git a/iocore/utils/Machine.cc b/iocore/utils/Machine.cc index 0c47d28835b..dac3225a24e 100644 --- a/iocore/utils/Machine.cc +++ b/iocore/utils/Machine.cc @@ -288,6 +288,19 @@ Machine::is_self(const IpAddr *ipaddr) return ink_hash_table_lookup(machine_id_ipaddrs, string_value, &value) == 1 ? true : false; } +bool +Machine::is_self(struct sockaddr const *addr) +{ + void *value = nullptr; + char string_value[INET6_ADDRSTRLEN + 1] = {0}; + + if (addr == nullptr) { + return false; + } + ats_ip_ntop(addr, string_value, sizeof(string_value)); + return ink_hash_table_lookup(machine_id_ipaddrs, string_value, &value) == 1 ? true : false; +} + void Machine::insert_id(char *id) { diff --git a/mgmt/RecordsConfig.cc b/mgmt/RecordsConfig.cc index c4d4d3c1d34..2ef788a4e49 100644 --- a/mgmt/RecordsConfig.cc +++ b/mgmt/RecordsConfig.cc @@ -165,6 +165,7 @@ static const RecordElement RecordsConfig[] = //# 2. proxy.config.http.redirect_use_orig_cache_key: Location Header if set to 0 (default), else use original request cache key //# 3. redirection_host_no_port: do not include default port in host header during redirection //# 4. post_copy_size: The maximum POST data size TS permits to copy + //# 5. redirect.actions: How to handle redirects. //# //############################################################################## {RECT_CONFIG, "proxy.config.http.number_of_redirections", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} @@ -175,6 +176,8 @@ static const RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.http.post_copy_size", RECD_INT, "2048", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , + {RECT_CONFIG, "proxy.config.http.redirect.actions", RECD_STRING, "routable:follow", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} + , //############################################################################## //# diff --git a/proxy/http/HttpConfig.cc b/proxy/http/HttpConfig.cc index 31d1127f14d..d391207ea55 100644 --- a/proxy/http/HttpConfig.cc +++ b/proxy/http/HttpConfig.cc @@ -1216,6 +1216,7 @@ HttpConfig::startup() HttpEstablishStaticConfigByte(c.redirection_host_no_port, "proxy.config.http.redirect_host_no_port"); HttpEstablishStaticConfigLongLong(c.oride.number_of_redirections, "proxy.config.http.number_of_redirections"); HttpEstablishStaticConfigLongLong(c.post_copy_size, "proxy.config.http.post_copy_size"); + HttpEstablishStaticConfigStringAlloc(c.redirect_actions_string, "proxy.config.http.redirect.actions"); OutboundConnTrack::config_init(&c.outbound_conntrack, &c.oride.outbound_conntrack); @@ -1485,6 +1486,8 @@ HttpConfig::reconfigure() params->post_copy_size = m_master.post_copy_size; params->oride.client_cert_filename = ats_strdup(m_master.oride.client_cert_filename); params->oride.client_cert_filepath = ats_strdup(m_master.oride.client_cert_filepath); + params->redirect_actions_string = ats_strdup(m_master.redirect_actions_string); + params->redirect_actions_map = parse_redirect_actions(params->redirect_actions_string, params->redirect_actions_self_action); params->negative_caching_list = m_master.negative_caching_list; @@ -1601,3 +1604,137 @@ HttpConfig::parse_ports_list(char *ports_string) } return (ports_list); } + +//////////////////////////////////////////////////////////////// +// +// HttpConfig::parse_redirect_actions() +// +//////////////////////////////////////////////////////////////// +IpMap * +HttpConfig::parse_redirect_actions(char *input_string, RedirectEnabled::Action &self_action) +{ + using RedirectEnabled::Action; + using RedirectEnabled::AddressClass; + using RedirectEnabled::action_map; + using RedirectEnabled::address_class_map; + + if (nullptr == input_string) { + Emergency("parse_redirect_actions: The configuration value is empty."); + return nullptr; + } + Tokenizer configTokens(", "); + int n_rules = configTokens.Initialize(input_string); + std::map configMapping; + for (int i = 0; i < n_rules; i++) { + const char *rule = configTokens[i]; + Tokenizer ruleTokens(":"); + int n_mapping = ruleTokens.Initialize(rule); + if (2 != n_mapping) { + Emergency("parse_redirect_actions: Individual rules must be an address class and an action separated by a colon (:)"); + return nullptr; + } + std::string c_input(ruleTokens[0]), a_input(ruleTokens[1]); + AddressClass c = + address_class_map.find(ruleTokens[0]) != address_class_map.end() ? address_class_map[ruleTokens[0]] : AddressClass::INVALID; + Action a = action_map.find(ruleTokens[1]) != action_map.end() ? action_map[ruleTokens[1]] : Action::INVALID; + + if (AddressClass::INVALID == c) { + Emergency("parse_redirect_actions: '%.*s' is not a valid address class", static_cast(c_input.size()), c_input.data()); + return nullptr; + } else if (Action::INVALID == a) { + Emergency("parse_redirect_actions: '%.*s' is not a valid action", static_cast(a_input.size()), a_input.data()); + return nullptr; + } + configMapping[c] = a; + } + + // Ensure the default. + if (configMapping.end() == configMapping.find(AddressClass::DEFAULT)) { + configMapping[AddressClass::DEFAULT] = Action::RETURN; + } + + IpMap *ret = new IpMap(); + IpAddr min, max; + Action action = Action::INVALID; + + // Order Matters. IpAddr::mark uses Painter's Algorithm. Last one wins. + + // PRIVATE + action = configMapping.find(AddressClass::PRIVATE) != configMapping.end() ? configMapping[AddressClass::PRIVATE] : + configMapping[AddressClass::DEFAULT]; + // 10.0.0.0/8 + min.load("10.0.0.0"); + max.load("10.255.255.255"); + ret->mark(min, max, reinterpret_cast(action)); + // 100.64.0.0/10 + min.load("100.64.0.0"); + max.load("100.127.255.255"); + ret->mark(min, max, reinterpret_cast(action)); + // 172.16.0.0/12 + min.load("172.16.0.0"); + max.load("172.31.255.255"); + ret->mark(min, max, reinterpret_cast(action)); + // 192.168.0.0/16 + min.load("192.168.0.0"); + max.load("192.168.255.255"); + ret->mark(min, max, reinterpret_cast(action)); + // fc00::/7 + min.load("fc00::"); + max.load("feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + ret->mark(min, max, reinterpret_cast(action)); + + // LOOPBACK + action = configMapping.find(AddressClass::LOOPBACK) != configMapping.end() ? configMapping[AddressClass::LOOPBACK] : + configMapping[AddressClass::DEFAULT]; + min.load("127.0.0.0"); + max.load("127.255.255.255"); + ret->mark(min, max, reinterpret_cast(action)); + min.load("::1"); + max.load("::1"); + ret->mark(min, max, reinterpret_cast(action)); + + // MULTICAST + action = configMapping.find(AddressClass::MULTICAST) != configMapping.end() ? configMapping[AddressClass::MULTICAST] : + configMapping[AddressClass::DEFAULT]; + // 224.0.0.0/4 + min.load("224.0.0.0"); + max.load("239.255.255.255"); + ret->mark(min, max, reinterpret_cast(action)); + // ff00::/8 + min.load("ff00::"); + max.load("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + ret->mark(min, max, reinterpret_cast(action)); + + // LINKLOCAL + action = configMapping.find(AddressClass::LINKLOCAL) != configMapping.end() ? configMapping[AddressClass::LINKLOCAL] : + configMapping[AddressClass::DEFAULT]; + // 169.254.0.0/16 + min.load("169.254.0.0"); + max.load("169.254.255.255"); + ret->mark(min, max, reinterpret_cast(action)); + // fe80::/10 + min.load("fe80::"); + max.load("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + ret->mark(min, max, reinterpret_cast(action)); + + // SELF + // We must store the self address class separately instead of adding the addresses to our map. + // The addresses Trafficserver will use depend on configurations that are loaded here, so they are not available yet. + action = configMapping.find(AddressClass::SELF) != configMapping.end() ? configMapping[AddressClass::SELF] : + configMapping[AddressClass::DEFAULT]; + self_action = action; + + // IpMap::fill only marks things that are not already marked. + + // ROUTABLE + action = configMapping.find(AddressClass::ROUTABLE) != configMapping.end() ? configMapping[AddressClass::ROUTABLE] : + configMapping[AddressClass::DEFAULT]; + min.load("0.0.0.0"); + max.load("255.255.255.255"); + ret->fill(min, max, reinterpret_cast(action)); + min.load("::"); + max.load("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"); + ret->fill(min, max, reinterpret_cast(action)); + + return ret; +} diff --git a/proxy/http/HttpConfig.h b/proxy/http/HttpConfig.h index a6039f7d7d2..362ee77fe8a 100644 --- a/proxy/http/HttpConfig.h +++ b/proxy/http/HttpConfig.h @@ -36,6 +36,7 @@ #include #include #include +#include #ifdef HAVE_CTYPE_H #include @@ -43,6 +44,7 @@ #include "tscore/ink_platform.h" #include "tscore/ink_inet.h" +#include "tscore/IpMap.h" #include "tscore/Regex.h" #include "string_view" #include "tscore/BufferWriter.h" @@ -395,6 +397,39 @@ OptionBitSet optStrToBitset(std::string_view optConfigStr, ts::FixedBufferWriter } // namespace HttpForwarded +namespace RedirectEnabled +{ +enum class AddressClass { + INVALID = -1, + DEFAULT, + PRIVATE, + LOOPBACK, + MULTICAST, + LINKLOCAL, + ROUTABLE, + SELF, +}; + +enum class Action { + INVALID = -1, + RETURN, + REJECT, + FOLLOW, +}; + +static std::map address_class_map = { + {"default", AddressClass::DEFAULT}, {"private", AddressClass::PRIVATE}, {"loopback", AddressClass::LOOPBACK}, + {"multicast", AddressClass::MULTICAST}, {"linklocal", AddressClass::LINKLOCAL}, {"routable", AddressClass::ROUTABLE}, + {"self", AddressClass::SELF}, +}; + +static std::map action_map = { + {"return", Action::RETURN}, + {"reject", Action::REJECT}, + {"follow", Action::FOLLOW}, +}; +} // namespace RedirectEnabled + ///////////////////////////////////////////////////////////// // This is a little helper class, used by the HttpConfigParams // and State (txn) structure. It allows for certain configs @@ -823,6 +858,10 @@ struct HttpConfigParams : public ConfigInfo { MgmtInt post_copy_size = 2048; MgmtInt max_post_size = 0; + char *redirect_actions_string = nullptr; + IpMap *redirect_actions_map = nullptr; + RedirectEnabled::Action redirect_actions_self_action = RedirectEnabled::Action::INVALID; + /////////////////////////////////////////////////////////////////// // Put all MgmtByte members down here, avoids additional padding // /////////////////////////////////////////////////////////////////// @@ -896,6 +935,9 @@ class HttpConfig // parse ssl ports configuration string static HttpConfigPortRange *parse_ports_list(char *ports_str); + // parse redirect configuration string + static IpMap *parse_redirect_actions(char *redirect_actions_string, RedirectEnabled::Action &self_action); + public: static int m_id; static HttpConfigParams m_master; @@ -926,8 +968,8 @@ inline HttpConfigParams::~HttpConfigParams() ats_free(oride.cache_vary_default_other); ats_free(connect_ports_string); ats_free(reverse_proxy_no_host_redirect); + ats_free(redirect_actions_string); - if (connect_ports) { - delete connect_ports; - } + delete connect_ports; + delete redirect_actions_map; } diff --git a/proxy/http/HttpTransact.cc b/proxy/http/HttpTransact.cc index a861e6009c6..7c0e933d1fc 100644 --- a/proxy/http/HttpTransact.cc +++ b/proxy/http/HttpTransact.cc @@ -1647,6 +1647,37 @@ HttpTransact::OSDNSLookup(State *s) "IP: %s", ats_ip_ntop(&s->server_info.dst_addr.sa, addrbuf, sizeof(addrbuf))); + if (s->redirect_info.redirect_in_process) { + // If dns lookup was not successful, the code below will handle the error. + RedirectEnabled::Action action = RedirectEnabled::Action::INVALID; + if (true == Machine::instance()->is_self(s->host_db_info.ip())) { + action = s->http_config_param->redirect_actions_self_action; + } else { + ink_release_assert(s->http_config_param->redirect_actions_map != nullptr); + ink_release_assert( + s->http_config_param->redirect_actions_map->contains(s->host_db_info.ip(), reinterpret_cast(&action))); + } + switch (action) { + case RedirectEnabled::Action::FOLLOW: + TxnDebug("http_trans", "[OSDNSLookup] Invalid redirect address. Following"); + break; + case RedirectEnabled::Action::REJECT: + TxnDebug("http_trans", "[OSDNSLookup] Invalid redirect address. Rejecting."); + build_error_response(s, HTTP_STATUS_FORBIDDEN, nullptr, "request#syntax_error"); + SET_VIA_STRING(VIA_DETAIL_TUNNEL, VIA_DETAIL_TUNNEL_NO_FORWARD); + TRANSACT_RETURN(SM_ACTION_SEND_ERROR_CACHE_NOOP, nullptr); + break; + case RedirectEnabled::Action::RETURN: + TxnDebug("http_trans", "[OSDNSLookup] Configured to return on invalid redirect address."); + // fall-through + default: + // Return this 3xx to the client as-is. + TxnDebug("http_trans", "[OSDNSLookup] Invalid redirect address. Returning."); + build_response_copy(s, &s->hdr_info.server_response, &s->hdr_info.client_response, s->client_info.http_version); + TRANSACT_RETURN(SM_ACTION_INTERNAL_CACHE_NOOP, nullptr); + } + } + // so the dns lookup was a success, but the lookup succeeded on // a hostname which was expanded by the traffic server. we should // not automatically forward the request to this expanded hostname. diff --git a/tests/gold_tests/redirect/redirect.test.py b/tests/gold_tests/redirect/redirect.test.py index 330f5465921..f5c055643e6 100644 --- a/tests/gold_tests/redirect/redirect.test.py +++ b/tests/gold_tests/redirect/redirect.test.py @@ -32,12 +32,12 @@ ts.Disk.records_config.update({ 'proxy.config.diags.debug.enabled': 1, 'proxy.config.diags.debug.tags': 'http|dns|redirect', - 'proxy.config.http.redirection_enabled': 1, 'proxy.config.http.number_of_redirections': 1, 'proxy.config.http.cache.http': 0, 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), 'proxy.config.dns.resolv_conf': 'NULL', - 'proxy.config.url_remap.remap_required': 0 # need this so the domain gets a chance to be evaluated through DNS + 'proxy.config.url_remap.remap_required': 0, # need this so the domain gets a chance to be evaluated through DNS + 'proxy.config.http.redirect.actions': 'self:follow', # redirects to self are not followed by default }) Test.Setup.Copy(os.path.join(Test.Variables.AtsTestToolsDir,'tcp_client.py')) diff --git a/tests/gold_tests/redirect/redirect_actions.test.py b/tests/gold_tests/redirect/redirect_actions.test.py new file mode 100644 index 00000000000..ae4e0f44131 --- /dev/null +++ b/tests/gold_tests/redirect/redirect_actions.test.py @@ -0,0 +1,243 @@ +''' +Test redirection behavior to invalid addresses +''' +# 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. + +from enum import Enum +import re +import os +import socket +Test.Summary = ''' +Test redirection behavior to invalid addresses +''' + +Test.SkipIf(Condition.true('autest sometimes does not capture output on the last test case of a scenario')) +Test.ContinueOnFail = False + +Test.Setup.Copy(os.path.join(Test.Variables.AtsTestToolsDir,'tcp_client.py')) + +dns = Test.MakeDNServer('dns') +# This record is used in each test case to get the initial redirect response from the origin that we will handle. +dnsRecords = {'iwillredirect.test': ['127.0.0.1']} + +host = socket.gethostname() +ipv4addrs = set() +try: + ipv4addrs = set([ip for \ + (family,_,_,_,(ip,*_)) in \ + socket.getaddrinfo(host,port=None) if \ + socket.AF_INET == family]) +except socket.gaierror: + pass + +ipv6addrs = set() +try: + ipv6addrs = set(["[{0}]".format(ip.split('%')[0]) for \ + (family,_,_,_,(ip,*_)) in \ + socket.getaddrinfo(host,port=None) if \ + socket.AF_INET6 == family and 'fe80' != ip[0:4]]) # Skip link-local addresses. +except socket.gaierror: + pass + +origin = Test.MakeOriginServer('origin', ip='0.0.0.0') +ArbitraryTimestamp='12345678' + +# This is for cases when the content is actually fetched from the invalid address. +request_header = { + 'headers': ('GET / HTTP/1.1\r\n' + 'Host: *\r\n\r\n'), + 'timestamp': ArbitraryTimestamp, + 'body': ''} +response_header = { + 'headers': ('HTTP/1.1 204 No Content\r\n' + 'Connection: close\r\n\r\n'), + 'timestamp': ArbitraryTimestamp, + 'body': ''} +origin.addResponse('sessionfile.log', request_header, response_header) + +# Map scenarios to trafficserver processes. +trafficservers={} + +data_dirname = 'generated_test_data' +data_path = os.path.join(Test.TestDirectory, data_dirname) +os.makedirs(data_path, exist_ok=True) + +def normalizeForAutest(value): + ''' + autest uses "test run" names to build file and directory names, so we must transform them in case there are incompatible or + annoying characters. + This means we can also use them in URLs. + ''' + if not value: + return None + return re.sub(r'[^a-z0-9-]', '_', value, flags=re.I) + +def makeTestCase(redirectTarget, expectedAction, scenario): + ''' + Helper method that creates a "meta-test" from which autest generates a test case. + + :param redirectTarget: The target address of a redirect from origin to be handled. + :param scenario: Defines the ACL to configure and the addresses to test. + ''' + + config = ','.join(':'.join(t) for t in ((addr.name.lower(),action.name.lower()) for (addr,action) in scenario.items())) + + normRedirectTarget = normalizeForAutest(redirectTarget) + normConfig = normalizeForAutest(config) + tr = Test.AddTestRun('With_Config_{0}_Redirect_to_{1}'.format(normConfig, normRedirectTarget)) + + if trafficservers: + tr.StillRunningAfter = origin + tr.StillRunningAfter = dns + else: + tr.Processes.Default.StartBefore(origin) + tr.Processes.Default.StartBefore(dns) + + if config not in trafficservers: + trafficservers[config] = Test.MakeATSProcess('ts_{0}'.format(normConfig)) + trafficservers[config].Disk.records_config.update({ + 'proxy.config.diags.debug.enabled': 1, + 'proxy.config.diags.debug.tags': 'http|dns|redirect', + 'proxy.config.http.number_of_redirections': 1, + 'proxy.config.http.cache.http': 0, + 'proxy.config.dns.nameservers': '127.0.0.1:{0}'.format(dns.Variables.Port), + 'proxy.config.dns.resolv_conf': 'NULL', + 'proxy.config.url_remap.remap_required': 0, + 'proxy.config.http.redirect.actions': config, + 'proxy.config.http.connect_attempts_timeout': 5, + 'proxy.config.http.connect_attempts_max_retries': 0, + }) + tr.Processes.Default.StartBefore(trafficservers[config]) + else: + tr.StillRunningAfter = trafficservers[config] + + testDomain = 'testdomain{0}.test'.format(normRedirectTarget) + # The micro DNS server can't tell us whether it has a record of the domain already, so we use a dictionary to avoid duplicates. + # We remove any surrounding brackets that are common to IPv6 addresses. + if redirectTarget: + dnsRecords[testDomain] = [redirectTarget.strip('[]')] + + # A GET request parameterized on the config and on the target. + request_header = { + 'headers': ('GET /redirect?config={0}&target={1} HTTP/1.1\r\n' + 'Host: *\r\n\r\n').\ + format(normConfig, normRedirectTarget), + 'timestamp': ArbitraryTimestamp, + 'body': ''} + # Returns a redirect to the test domain for the given target & the port number for the TS of the given config. + response_header = { + 'headers': ('HTTP/1.1 307 Temporary Redirect\r\n' + 'Location: http://{0}:{1}/\r\n' + 'Connection: close\r\n\r\n').\ + format(testDomain, origin.Variables.Port), + 'timestamp': ArbitraryTimestamp, + 'body': ''} + origin.addResponse('sessionfile.log', request_header, response_header) + + # Generate the request data file. + with open(os.path.join(data_path, tr.Name), 'w') as f: + f.write(('GET /redirect?config={0}&target={1} HTTP/1.1\r\n' + 'Host: iwillredirect.test:{2}\r\n\r\n').\ + format(normConfig, normRedirectTarget, origin.Variables.Port)) + # Set the command with the appropriate URL. + tr.Processes.Default.Command = "bash -o pipefail -c 'python tcp_client.py 127.0.0.1 {0} {1} | head -n 1'".\ + format(trafficservers[config].Variables.port, os.path.join(data_dirname, tr.Name)) + tr.Processes.Default.ReturnCode = 0 + # Generate and set the 'gold file' to check stdout + goldFilePath = os.path.join(data_path, '{0}.gold'.format(tr.Name)) + with open(goldFilePath, 'w') as f: + f.write(expectedAction.value['expectedStatusLine']) + tr.Processes.Default.Streams.stdout = goldFilePath + +class AddressE(Enum): + ''' + Classes of addresses are mapped to example addresses. + ''' + Private = ('10.0.0.1', '[fc00::1]') + Loopback = (['127.1.2.3']) # [::1] is ommitted here because it is likely overwritten by Self, and there are no others in IPv6. + Multicast = ('224.1.2.3', '[ff42::]') + Linklocal = ('169.254.0.1', '[fe80::]') + Routable = ('72.30.35.10', '[2001:4998:58:1836::10]') # Do not Follow redirects to these in an automated test. + Self = ipv4addrs | ipv6addrs # Addresses of this host. + Default = None # All addresses apply, nothing in particular to test. + +class ActionE(Enum): + # Title case because 'return' is a Python keyword. + Return = {'config':'return', 'expectedStatusLine':'HTTP/1.1 307 Temporary Redirect\r\n'} + Reject = {'config':'reject', 'expectedStatusLine':'HTTP/1.1 403 Forbidden\r\n'} + Follow = {'config':'follow', 'expectedStatusLine':'HTTP/1.1 204 No Content\r\n'} + + # Added to test failure modes. + Break = {'expectedStatusLine': 'HTTP/1.1 502 Cannot find server.\r\n'} + +scenarios = [ + { + # Follow to loopback, but alternately reject/return others. + AddressE.Private: ActionE.Reject, + AddressE.Loopback: ActionE.Follow, + AddressE.Multicast: ActionE.Reject, + AddressE.Linklocal: ActionE.Return, + AddressE.Routable: ActionE.Reject, + AddressE.Self: ActionE.Return, + AddressE.Default: ActionE.Reject, + }, + + { + # Follow to loopback, but alternately reject/return others, flipped from the previous scenario. + AddressE.Private: ActionE.Return, + AddressE.Loopback: ActionE.Follow, + AddressE.Multicast: ActionE.Return, + AddressE.Linklocal: ActionE.Reject, + AddressE.Routable: ActionE.Return, + AddressE.Self: ActionE.Reject, + AddressE.Default: ActionE.Return, + }, + + { + # Return loopback, but reject everything else. + AddressE.Loopback: ActionE.Return, + AddressE.Default: ActionE.Reject, + }, + + { + # Reject loopback, but return everything else. + AddressE.Loopback: ActionE.Reject, + AddressE.Default: ActionE.Return, + }, + + { + # Return everything. + AddressE.Default: ActionE.Return, + }, + ] + +for scenario in scenarios: + for addressClass in AddressE: + if not addressClass.value: + # Default has no particular addresses to test. + continue + for address in addressClass.value: + expectedAction = scenario[addressClass] if addressClass in scenario else scenario[AddressE.Default] + makeTestCase(redirectTarget=address, expectedAction=expectedAction, scenario=scenario) + + # Test redirects to names that cannot be resolved. + makeTestCase(redirectTarget=None, expectedAction=ActionE.Break, scenario=scenario) + +dns.addRecords(records=dnsRecords) + +# Make sure this runs only after local files have been created. +Test.Setup.Copy(data_path) diff --git a/tests/gold_tests/redirect/redirect_post.test.py b/tests/gold_tests/redirect/redirect_post.test.py index f23d900910e..bc83a42b5a5 100644 --- a/tests/gold_tests/redirect/redirect_post.test.py +++ b/tests/gold_tests/redirect/redirect_post.test.py @@ -39,8 +39,9 @@ ts.Disk.records_config.update({ 'proxy.config.http.number_of_redirections': MAX_REDIRECT, 'proxy.config.http.post_copy_size' : 919430601, - 'proxy.config.http.cache.http': 0 # , - # 'proxy.config.diags.debug.enabled': 1 + 'proxy.config.http.cache.http': 0, + 'proxy.config.http.redirect.actions': 'self:follow', # redirects to self are not followed by default + # 'proxy.config.diags.debug.enabled': 1, }) redirect_request_header = {"headers": "POST /redirect1 HTTP/1.1\r\nHost: *\r\nContent-Length: 52428800\r\n\r\n", "timestamp": "5678", "body": ""}