trafficserver-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From bc...@apache.org
Subject [trafficserver] 01/03: Adds redirect actions settings, returns by default
Date Thu, 13 Sep 2018 16:02:46 GMT
This is an automated email from the ASF dual-hosted git repository.

bcall pushed a commit to branch 8.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 44be529383c378809d7ad5471ed2f332c3a383fc
Author: Derek Dagit <derekd@oath.com>
AuthorDate: Thu Sep 13 10:06:24 2018 -0500

    Adds redirect actions settings, returns by default
    
    New config `proxy.config.http.redirect.actions`
    
    (cherry picked from commit 7f08ad604451881c6aeb372d2fe55b8a90780ef4)
---
 doc/admin-guide/files/records.config.en.rst        |  61 ++++++
 iocore/utils/I_Machine.h                           |   1 +
 iocore/utils/Machine.cc                            |  13 ++
 mgmt/RecordsConfig.cc                              |   3 +
 proxy/http/HttpConfig.cc                           | 137 ++++++++++++
 proxy/http/HttpConfig.h                            |  47 +++-
 proxy/http/HttpTransact.cc                         |  31 +++
 tests/gold_tests/redirect/redirect.test.py         |   4 +-
 tests/gold_tests/redirect/redirect_actions.test.py | 243 +++++++++++++++++++++
 tests/gold_tests/redirect/redirect_post.test.py    |   5 +-
 10 files changed, 538 insertions(+), 7 deletions(-)

diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index de434e1..75ddb09 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -1330,6 +1330,67 @@ HTTP Redirection
    This setting enables Trafficserver to allow using original request cache key (for example,
set using a TS API) during a 3xx redirect follow.
    The default behavior (0) is to use the URL specified by Location header in the 3xx response
as the cache key.
 
+.. ts:cv:: CONFIG proxy.config.http.post_copy_size INT 2048
+   :reloadable:
+
+   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 74bd300..cf4e518 100644
--- a/iocore/utils/I_Machine.h
+++ b/iocore/utils/I_Machine.h
@@ -82,6 +82,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 0c47d28..dac3225 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 c3818b3..d61a5da 100644
--- a/mgmt/RecordsConfig.cc
+++ b/mgmt/RecordsConfig.cc
@@ -167,6 +167,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}
@@ -177,6 +178,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 1afb230..2076c99 100644
--- a/proxy/http/HttpConfig.cc
+++ b/proxy/http/HttpConfig.cc
@@ -1222,6 +1222,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");
 
   http_config_cont->handleEvent(EVENT_NONE, nullptr);
 
@@ -1488,6 +1489,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;
 
@@ -1606,3 +1609,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<AddressClass, Action> 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<int>(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<int>(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<void *>(action));
+  // 100.64.0.0/10
+  min.load("100.64.0.0");
+  max.load("100.127.255.255");
+  ret->mark(min, max, reinterpret_cast<void *>(action));
+  // 172.16.0.0/12
+  min.load("172.16.0.0");
+  max.load("172.31.255.255");
+  ret->mark(min, max, reinterpret_cast<void *>(action));
+  // 192.168.0.0/16
+  min.load("192.168.0.0");
+  max.load("192.168.255.255");
+  ret->mark(min, max, reinterpret_cast<void *>(action));
+  // fc00::/7
+  min.load("fc00::");
+  max.load("feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+  ret->mark(min, max, reinterpret_cast<void *>(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<void *>(action));
+  min.load("::1");
+  max.load("::1");
+  ret->mark(min, max, reinterpret_cast<void *>(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<void *>(action));
+  // ff00::/8
+  min.load("ff00::");
+  max.load("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+  ret->mark(min, max, reinterpret_cast<void *>(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<void *>(action));
+  // fe80::/10
+  min.load("fe80::");
+  max.load("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+  ret->mark(min, max, reinterpret_cast<void *>(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<void *>(action));
+  min.load("::");
+  max.load("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+  ret->fill(min, max, reinterpret_cast<void *>(action));
+
+  return ret;
+}
diff --git a/proxy/http/HttpConfig.h b/proxy/http/HttpConfig.h
index ad070f7..432a2bc 100644
--- a/proxy/http/HttpConfig.h
+++ b/proxy/http/HttpConfig.h
@@ -36,6 +36,7 @@
 #include <cstdlib>
 #include <cstdio>
 #include <bitset>
+#include <map>
 
 #ifdef HAVE_CTYPE_H
 #include <cctype>
@@ -395,6 +396,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<std::string, AddressClass> 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<std::string, Action> 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
@@ -827,6 +861,10 @@ public:
   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 //
   ///////////////////////////////////////////////////////////////////
@@ -898,6 +936,9 @@ public:
   // 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;
@@ -928,8 +969,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 0d4b948..46f6d53 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<void **>(&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 330f546..f5c0556 100644
--- a/tests/gold_tests/redirect/redirect.test.py
+++ b/tests/gold_tests/redirect/redirect.test.py
@@ -32,12 +32,12 @@ dns = Test.MakeDNServer("dns")
 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 0000000..ae4e0f4
--- /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 f23d900..bc83a42 100644
--- a/tests/gold_tests/redirect/redirect_post.test.py
+++ b/tests/gold_tests/redirect/redirect_post.test.py
@@ -39,8 +39,9 @@ dest_serv = Test.MakeOriginServer("dest_server")
 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": ""}


Mime
View raw message