adaptation_meta option This option allows Squid administrator to add custom ICAP request headers or eCAP options to Squid ICAP requests or eCAP transactions. Use it to pass custom authentication tokens and other transaction-state related meta information to an ICAP/eCAP service. The addition of a meta header is ACL-driven: adaptation_meta name value [!]aclname ... Processing for a given header name stops after the first ACL list match. Thus, it is impossible to add two headers with the same name. If no ACL lists match for a given header name, no such header is added. For example: # do not debug transactions except for those that need debugging adaptation_meta X-Debug 1 needs_debugging # log all transactions except for those that must remain secret adaptation_meta X-Log 1 !keep_secret # mark transactions from users in the "G 1" group adaptation_meta X-Authenticated-Groups "G 1" authed_as_G1 The "value" parameter may be a regular squid.conf token or a "double quoted string". Within the quoted string, use backslash (\) to escape any character, which is currently only useful for escaping backslashes and double quotes. For example, "this string has one backslash (\\) and two \"quotes\"" This is a Measurement Factory project === modified file 'src/ConfigParser.cc' --- src/ConfigParser.cc 2011-01-28 07:58:53 +0000 +++ src/ConfigParser.cc 2011-10-27 11:25:09 +0000 @@ -114,3 +114,69 @@ return t; } + +void +ConfigParser::ParseQuotedString(char **var) +{ + String sVar; + ParseQuotedString(&sVar); + *var = xstrdup(sVar.termedBuf()); +} + +void +ConfigParser::ParseQuotedString(String *var) +{ + // Get all of the remaining string + char *token = strtok(NULL, ""); + if (token == NULL) + self_destruct(); + + if (*token != '"') { + token = strtok(token, w_space); + var->reset(token); + return; + } + + char *s = token + 1; + /* scan until the end of the quoted string, unescaping " and \ */ + while (*s && *s != '"') { + if (*s == '\\') { + const char * next = s+1; // may point to 0 + memmove(s, next, strlen(next) + 1); + } + s++; + } + + if (*s != '"') { + debugs(3, DBG_CRITICAL, "ParseQuotedString: missing '\"' at the end of quoted string" ); + self_destruct(); + } + strtok(s-1, "\""); /*Reset the strtok to point after the " */ + *s = '\0'; + + var->reset(token+1); +} + +const char * +ConfigParser::QuoteString(String &var) +{ + static String quotedStr; + const char *s = var.termedBuf(); + bool needQuote = false; + + for (const char *l = s; !needQuote && *l != '\0'; l++ ) + needQuote = !isalnum(*l); + + if (!needQuote) + return s; + + quotedStr.clean(); + quotedStr.append('"'); + for (; *s != '\0'; s++) { + if(*s == '"' || *s == '\\') + quotedStr.append('\\'); + quotedStr.append(*s); + } + quotedStr.append('"'); + return quotedStr.termedBuf(); +} === modified file 'src/ConfigParser.h' --- src/ConfigParser.h 2011-08-26 16:50:49 +0000 +++ src/ConfigParser.h 2011-10-20 10:19:47 +0000 @@ -67,6 +67,9 @@ static void ParseBool(bool *var); static void ParseString(char **var); static void ParseString(String *var); + static void ParseQuotedString(char **var); + static void ParseQuotedString(String *var); + static const char *QuoteString(String &var); static void ParseWordList(wordlist **list); static char * strtokFile(); }; === modified file 'src/Makefile.am' --- src/Makefile.am 2011-10-21 23:36:47 +0000 +++ src/Makefile.am 2011-10-27 11:29:26 +0000 @@ -1004,6 +1004,7 @@ tests/testStore \ tests/testString \ tests/testURL \ + tests/testConfigParser \ $(STORE_TESTS) ## NP: required to run the above list. check_PROGRAMS only builds the binaries... @@ -3233,6 +3234,36 @@ $(REPL_OBJS) \ $(SQUID_CPPUNIT_LA) +tests_testConfigParser_SOURCES = \ + ClientInfo.h \ + mem.cc \ + MemBuf.cc \ + String.cc \ + ConfigParser.cc \ + tests/testMain.cc \ + tests/testConfigParser.cc \ + tests/testConfigParser.h \ + tests/stub_cache_cf.cc \ + tests/stub_cache_manager.cc \ + tests/stub_debug.cc \ + tests/stub_HelperChildConfig.cc \ + time.cc \ + wordlist.cc +nodist_tests_testConfigParser_SOURCES = \ + $(TESTSOURCES) +tests_testConfigParser_LDADD = \ + base/libbase.la \ + libsquid.la \ + ip/libip.la \ + $(top_builddir)/lib/libmiscutil.la \ + $(REGEXLIB) \ + $(SQUID_CPPUNIT_LIBS) \ + $(SSLLIB) \ + $(COMPAT_LIB) \ + $(XTRA_LIBS) +tests_testConfigParser_LDFLAGS = $(LIBADD_DL) +tests_testConfigParser_DEPENDENCIES = \ + $(SQUID_CPPUNIT_LA) TESTS += testHeaders === modified file 'src/acl/Gadgets.h' --- src/acl/Gadgets.h 2010-11-21 04:40:05 +0000 +++ src/acl/Gadgets.h 2011-10-27 11:25:09 +0000 @@ -36,5 +36,7 @@ extern void aclCacheMatchFlush(dlink_list * cache); /// \ingroup ACLAPI extern void dump_acl_access(StoreEntry * entry, const char *name, acl_access * head); +/// \ingroup ACLAPI +extern void dump_acl_list(StoreEntry * entry, ACLList * head); #endif /* SQUID_ACL_GADGETS_H */ === modified file 'src/adaptation/Config.cc' --- src/adaptation/Config.cc 2011-08-04 00:13:34 +0000 +++ src/adaptation/Config.cc 2011-10-27 11:25:09 +0000 @@ -37,11 +37,13 @@ #include "acl/Gadgets.h" #include "Store.h" #include "Array.h" // really Vector +#include "acl/FilledChecklist.h" #include "adaptation/Config.h" #include "adaptation/Service.h" #include "adaptation/AccessRule.h" #include "adaptation/ServiceGroups.h" #include "adaptation/History.h" +#include "HttpRequest.h" bool Adaptation::Config::Enabled = false; @@ -50,6 +52,54 @@ int Adaptation::Config::send_client_ip = false; int Adaptation::Config::send_username = false; int Adaptation::Config::use_indirect_client = true; +Adaptation::Config::MetaHeaders Adaptation::Config::metaHeaders; + + +Adaptation::Config::MetaHeader::Value::~Value() +{ + aclDestroyAclList(&aclList); +} + +Adaptation::Config::MetaHeader::Value::Pointer +Adaptation::Config::MetaHeader::addValue(const String &value) +{ + Value::Pointer v = new Value(value); + values.push_back(v); + return v; +} + +const char * +Adaptation::Config::MetaHeader::match(HttpRequest *request, HttpReply *reply) +{ + + typedef Values::iterator VLI; + ACLFilledChecklist ch(NULL, request, NULL); + if (reply) + ch.reply = HTTPMSGLOCK(reply); + + for (VLI i = values.begin(); i != values.end(); ++i ) { + const int ret= ch.fastCheck((*i)->aclList); + debugs(93, 5, HERE << "Check for header name: " << name << ": " << (*i)->value + <<", HttpRequest: " << request << " HttpReply: " << reply << " matched: " << ret); + if (ret == ACCESS_ALLOWED) + return (*i)->value.termedBuf(); + } + return NULL; +} + +Adaptation::Config::MetaHeader::Pointer +Adaptation::Config::addMetaHeader(const String &headerName) +{ + typedef MetaHeaders::iterator AMLI; + for(AMLI i = metaHeaders.begin(); i != metaHeaders.end(); ++i) { + if ((*i)->name == headerName) + return (*i); + } + + MetaHeader::Pointer meta = new MetaHeader(headerName); + metaHeaders.push_back(meta); + return meta; +} Adaptation::ServiceConfig* @@ -135,6 +185,8 @@ DetachServices(); serviceConfigs.clean(); + + FreeMetaHeader(); } void @@ -209,6 +261,64 @@ } void +Adaptation::Config::ParseMetaHeader(ConfigParser &parser) +{ + String name, value; + const char *warnFor[] = { + "Methods", + "Service", + "ISTag", + "Encapsulated", + "Opt-body-type", + "Max-Connections", + "Options-TTL", + "Date", + "Service-ID", + "Allow", + "Preview", + "Transfer-Preview", + "Transfer-Ignore", + "Transfer-Complete", + NULL + }; + ConfigParser::ParseString(&name); + ConfigParser::ParseQuotedString(&value); + + // TODO: Find a way to move this check to ICAP + for (int i = 0; warnFor[i] != NULL; i++) { + if (name.caseCmp(warnFor[i]) == 0) { + debugs(93, DBG_CRITICAL, "WARNING: meta name \"" << name <<"\" is a reserved ICAP header name"); + break; + } + } + + MetaHeader::Pointer meta = addMetaHeader(name); + MetaHeader::Value::Pointer headValue = meta->addValue(value); + aclParseAclList(parser, &headValue->aclList); +} + +void +Adaptation::Config::DumpMetaHeader(StoreEntry *entry, const char *name) +{ + typedef MetaHeaders::iterator AMLI; + for(AMLI m = metaHeaders.begin(); m != metaHeaders.end(); ++m) { + typedef MetaHeader::Values::iterator VLI; + for (VLI v =(*m)->values.begin(); v != (*m)->values.end(); ++v ) { + storeAppendPrintf(entry, "%s " SQUIDSTRINGPH " %s", + name, SQUIDSTRINGPRINT((*m)->name), ConfigParser::QuoteString((*v)->value)); + dump_acl_list(entry, (*v)->aclList); + storeAppendPrintf(entry, "\n"); + } + } +} + +void +Adaptation::Config::FreeMetaHeader() +{ + metaHeaders.clean(); +} + +void Adaptation::Config::ParseServiceSet() { Adaptation::Config::ParseServiceGroup(new ServiceSet); === modified file 'src/adaptation/Config.h' --- src/adaptation/Config.h 2011-08-03 08:30:00 +0000 +++ src/adaptation/Config.h 2011-10-27 11:25:09 +0000 @@ -2,6 +2,7 @@ #define SQUID_ADAPTATION__CONFIG_H #include "event.h" +#include "acl/Gadgets.h" #include "base/AsyncCall.h" #include "adaptation/forward.h" #include "adaptation/Elements.h" @@ -19,6 +20,9 @@ static void ParseServiceSet(void); static void ParseServiceChain(void); + static void ParseMetaHeader(ConfigParser &parser); + static void FreeMetaHeader(); + static void DumpMetaHeader(StoreEntry *, const char *); static void ParseAccess(ConfigParser &parser); static void FreeAccess(void); @@ -43,6 +47,52 @@ time_t oldest_service_failure; int service_revival_delay; + /** + * Used to store meta headers. The meta headers are custom + * ICAP request headers or ECAP options used to pass custom + * transaction-state related meta information to a service. + */ + class MetaHeader: public RefCountable { + public: + typedef RefCount Pointer; + /// Stores a value for the meta header. + class Value: public RefCountable { + public: + typedef RefCount Pointer; + String value; ///< a header value + ACLList *aclList; ///< The access list used to determine if this value is valid for a request + explicit Value(const String &aVal) : value(aVal), aclList(NULL) {} + ~Value(); + }; + typedef Vector Values; + + explicit MetaHeader(const String &aName): name(aName) {} + + /** + * Adds a value to the meta header and returns a pointer to the + * related Value object. + */ + Value::Pointer addValue(const String &value); + + /** + * Walks through the possible values list of the meta and selects + * the first value which matches the given HttpRequest and HttpReply + * or NULL if none matches. + */ + const char *match(HttpRequest *request, HttpReply *reply); + String name; ///< The meta header name + Values values; ///< The possible values list for the meta header + }; + typedef Vector MetaHeaders; + static MetaHeaders metaHeaders; ///< The list of configured meta headers + + /** + * Adds a header to the meta headers list and returns a pointer to the + * related metaHeaders object. If the header name already exists in list, + * returns a pointer to the existing object. + */ + static MetaHeader::Pointer addMetaHeader(const String &header); + typedef Vector ServiceConfigs; ServiceConfigs serviceConfigs; === modified file 'src/adaptation/ecap/XactionRep.cc' --- src/adaptation/ecap/XactionRep.cc 2011-10-10 03:27:47 +0000 +++ src/adaptation/ecap/XactionRep.cc 2011-10-20 10:19:47 +0000 @@ -81,11 +81,13 @@ return clientIpValue(); if (name == libecap::metaUserName) return usernameValue(); - if (name == Adaptation::Config::masterx_shared_name) + if (Adaptation::Config::masterx_shared_name && name == Adaptation::Config::masterx_shared_name) return masterxSharedValue(name); // TODO: metaServerIp, metaAuthenticatedUser, and metaAuthenticatedGroups - return libecap::Area(); + + // If the name is unknown, metaValue returns an emtpy area + return metaValue(name); } void @@ -101,6 +103,8 @@ if (const libecap::Area value = masterxSharedValue(name)) visitor.visit(name, value); } + + visitEachMetaHeader(visitor); // TODO: metaServerIp, metaAuthenticatedUser, and metaAuthenticatedGroups } @@ -162,6 +166,48 @@ return libecap::Area(); } +const libecap::Area +Adaptation::Ecap::XactionRep::metaValue(const libecap::Name &name) const +{ + HttpRequest *request = dynamic_cast(theCauseRep ? + theCauseRep->raw().header : theVirginRep.raw().header); + Must(request); + HttpReply *reply = dynamic_cast(theVirginRep.raw().header); + + if (name.known()) { // must check to avoid empty names matching unset cfg + typedef Adaptation::Config::MetaHeaders::iterator ACAMLI; + for(ACAMLI i = Adaptation::Config::metaHeaders.begin(); i != Adaptation::Config::metaHeaders.end(); ++i) { + if (name == (*i)->name.termedBuf()) { + if (const char *value = (*i)->match(request, reply)) + return libecap::Area::FromTempString(value); + else + return libecap::Area(); + } + } + } + + return libecap::Area(); +} + +void +Adaptation::Ecap::XactionRep::visitEachMetaHeader(libecap::NamedValueVisitor &visitor) const +{ + HttpRequest *request = dynamic_cast(theCauseRep ? + theCauseRep->raw().header : theVirginRep.raw().header); + Must(request); + HttpReply *reply = dynamic_cast(theVirginRep.raw().header); + + typedef Adaptation::Config::MetaHeaders::iterator ACAMLI; + for(ACAMLI i = Adaptation::Config::metaHeaders.begin(); i != Adaptation::Config::metaHeaders.end(); ++i) { + const char *v; + if (v = (*i)->match(request, reply)) { + const libecap::Name name((*i)->name.termedBuf()); + const libecap::Area value = libecap::Area::FromTempString(v); + visitor.visit(name, value); + } + } +} + void Adaptation::Ecap::XactionRep::start() { === modified file 'src/adaptation/ecap/XactionRep.h' --- src/adaptation/ecap/XactionRep.h 2011-03-11 22:22:13 +0000 +++ src/adaptation/ecap/XactionRep.h 2011-10-20 10:19:47 +0000 @@ -93,6 +93,10 @@ const libecap::Area clientIpValue() const; const libecap::Area usernameValue() const; const libecap::Area masterxSharedValue(const libecap::Name &name) const; + /// Return the adaptation meta header value for the given header "name" + const libecap::Area metaValue(const libecap::Name &name) const; + /// Return the adaptation meta headers and their values + void visitEachMetaHeader(libecap::NamedValueVisitor &visitor) const; private: AdapterXaction theMaster; // the actual adaptation xaction we represent === modified file 'src/adaptation/icap/ModXact.cc' --- src/adaptation/icap/ModXact.cc 2011-06-04 12:48:45 +0000 +++ src/adaptation/icap/ModXact.cc 2011-10-20 10:19:47 +0000 @@ -1414,6 +1414,19 @@ if (TheConfig.send_username && request) makeUsernameHeader(request, buf); + // Adaptation::Config::metaHeaders + typedef Adaptation::Config::MetaHeaders::iterator ACAMLI; + for(ACAMLI i = Adaptation::Config::metaHeaders.begin(); i != Adaptation::Config::metaHeaders.end(); ++i) { + HttpRequest *r = virgin.cause ? + virgin.cause : dynamic_cast(virgin.header); + Must(r); + + HttpReply *reply = dynamic_cast(virgin.header); + + if (const char *value = (*i)->match(r, reply)) + buf.Printf("%s: %s\r\n", (*i)->name.termedBuf(), value); + } + // fprintf(stderr, "%s\n", buf.content()); buf.append(ICAP::crlf, 2); // terminate ICAP header === modified file 'src/cache_cf.cc' --- src/cache_cf.cc 2011-10-04 17:28:03 +0000 +++ src/cache_cf.cc 2011-10-20 10:19:47 +0000 @@ -98,6 +98,9 @@ static void parse_adaptation_service_set_type(); static void parse_adaptation_service_chain_type(); static void parse_adaptation_access_type(); +static void parse_adaptation_meta_type(Adaptation::Config::MetaHeaders *); +static void dump_adaptation_meta_type(StoreEntry *, const char *, Adaptation::Config::MetaHeaders &); +static void free_adaptation_meta_type(Adaptation::Config::MetaHeaders *); #endif #if ICAP_CLIENT @@ -4373,6 +4376,23 @@ Adaptation::Config::ParseAccess(LegacyParser); } +static void +parse_adaptation_meta_type(Adaptation::Config::MetaHeaders *) +{ + Adaptation::Config::ParseMetaHeader(LegacyParser); +} + +static void +dump_adaptation_meta_type(StoreEntry *entry, const char *name, Adaptation::Config::MetaHeaders &) +{ + Adaptation::Config::DumpMetaHeader(entry, name); +} + +static void +free_adaptation_meta_type(Adaptation::Config::MetaHeaders *) +{ + // Nothing to do, it is released inside Adaptation::Config::freeService() +} #endif /* USE_ADAPTATION */ === modified file 'src/cf.data.depend' --- src/cf.data.depend 2011-08-10 15:54:51 +0000 +++ src/cf.data.depend 2011-10-20 10:19:47 +0000 @@ -36,6 +36,7 @@ adaptation_access_type adaptation_service_set adaptation_service_chain acl icap_service icap_class adaptation_service_set_type icap_service ecap_service adaptation_service_chain_type icap_service ecap_service +adaptation_meta_type acl icap_access_type icap_class acl icap_class_type icap_service icap_service_type === modified file 'src/cf.data.pre' --- src/cf.data.pre 2011-10-12 21:22:33 +0000 +++ src/cf.data.pre 2011-10-27 11:29:26 +0000 @@ -7012,6 +7012,41 @@ adaptation_masterx_shared_names X-Subscriber-ID DOC_END +NAME: adaptation_meta +TYPE: adaptation_meta_type +IFDEF: USE_ADAPTATION +LOC: Adaptation::Config::metaHeaders +DEFAULT: none +DOC_START + This option allows Squid administrator to add custom ICAP request + headers or eCAP options to Squid ICAP requests or eCAP transactions. + Use it to pass custom authentication tokens and other + transaction-state related meta information to an ICAP/eCAP service. + + The addition of a meta header is ACL-driven: + adaptation_meta name value [!]aclname ... + + Processing for a given header name stops after the first ACL list match. + Thus, it is impossible to add two headers with the same name. If no ACL + lists match for a given header name, no such header is added. For + example: + + # do not debug transactions except for those that need debugging + adaptation_meta X-Debug 1 needs_debugging + + # log all transactions except for those that must remain secret + adaptation_meta X-Log 1 !keep_secret + + # mark transactions from users in the "G 1" group + adaptation_meta X-Authenticated-Groups "G 1" authed_as_G1 + + The "value" parameter may be a regular squid.conf token or a "double + quoted string". Within the quoted string, use backslash (\) to escape + any character, which is currently only useful for escaping backslashes + and double quotes. For example, + "this string has one backslash (\\) and two \"quotes\"" +DOC_END + NAME: icap_retry TYPE: acl_access IFDEF: ICAP_CLIENT === added file 'src/tests/testConfigParser.cc' --- src/tests/testConfigParser.cc 1970-01-01 00:00:00 +0000 +++ src/tests/testConfigParser.cc 2011-10-27 11:25:09 +0000 @@ -0,0 +1,85 @@ +#define SQUID_UNIT_TEST 1 +#include "config.h" + +#include "testConfigParser.h" +#include "SquidString.h" +#include "Mem.h" +#include "event.h" +#include "ConfigParser.h" + +CPPUNIT_TEST_SUITE_REGISTRATION( testConfigParser); + +/* let this test link sanely */ +void +eventAdd(const char *name, EVH * func, void *arg, double when, int, bool cbdata) +{} + +void testConfigParser::setUp() +{ +} + +bool testConfigParser::doParseQuotedTest(const char *s, const char *expectInterp) +{ + char cfgline[2048]; + char cfgparam[2048]; + snprintf(cfgline, 2048, "Config %s", s); + + // Points to the start of quoted string + const char *tmp = strchr(cfgline, ' '); + + if (tmp == NULL) { + fprintf(stderr, "Invalid config line: %s\n", s); + return false; + } + // Keep the initial value on cfgparam. The ConfigParser methods will write on cfgline + strcpy(cfgparam, tmp+1); + + // Initialize parser to point to the start of quoted string + strtok(cfgline, w_space); + String unEscaped; + ConfigParser::ParseQuotedString(&unEscaped); + + const bool interpOk = (unEscaped.cmp(expectInterp) == 0); + if (!interpOk) { + printf("%25s: %s\n%25s: %s\n%25s: %s\n", + "Raw configuration", cfgparam, + "Expected interpretation", expectInterp, + "Actual interpretation", unEscaped.termedBuf()); + } + + const char *quoted = ConfigParser::QuoteString(unEscaped); + bool quotedOk = (strcmp(cfgparam, quoted)==0); + if (!quotedOk) { + printf("%25s: %s\n%25s: %s\n%25s: %s\n", + "Raw configuration", cfgparam, + "Parsed and quoted", quoted, + "parsed value was", unEscaped.termedBuf()); + } + + return quotedOk && interpOk ; +} + +void testConfigParser::testParseQuoted() +{ + // SingleToken + CPPUNIT_ASSERT(doParseQuotedTest("SingleToken", "SingleToken")); + + // This is a quoted "string" by me + CPPUNIT_ASSERT(doParseQuotedTest("\"This is a quoted \\\"string\\\" by me\"", + "This is a quoted \"string\" by me")); + + // escape sequence test: \\"\"\\" + CPPUNIT_ASSERT(doParseQuotedTest("\"escape sequence test: \\\\\\\\\\\"\\\\\\\"\\\\\\\\\\\"\"", + "escape sequence test: \\\\\"\\\"\\\\\"")); + + // \beginning and end test" + CPPUNIT_ASSERT(doParseQuotedTest("\"\\\\beginning and end test\\\"\"", + "\\beginning and end test\"")); + + // " + CPPUNIT_ASSERT(doParseQuotedTest("\"\\\"\"", "\"")); + + /* \ */ + CPPUNIT_ASSERT(doParseQuotedTest("\"\\\\\"", "\\")); +} + === added file 'src/tests/testConfigParser.h' --- src/tests/testConfigParser.h 1970-01-01 00:00:00 +0000 +++ src/tests/testConfigParser.h 2011-10-27 11:25:09 +0000 @@ -0,0 +1,24 @@ +#ifndef SQUID_SRC_TEST_CONFIG_PARSER_H +#define SQUID_SRC_TEST_CONFIG_PARSER_H + +#include + +/* + * test the ConfigParser framework + */ + +class testConfigParser : public CPPUNIT_NS::TestFixture +{ + CPPUNIT_TEST_SUITE( testConfigParser ); + CPPUNIT_TEST( testParseQuoted ); + CPPUNIT_TEST_SUITE_END(); + +public: + void setUp(); + +protected: + bool doParseQuotedTest(const char *, const char *); + void testParseQuoted(); +}; + +#endif