Support client connection annotation by helpers via clt_conn_id=ID. TCP client connections tagging is useful for faking various forms of connection-based "authentication" when standard HTTP authentication cannot be used. A URL rewriter or, external ACL helper may mark the "authenticated" client connection to avoid going through "authentication" steps during subsequent requests on the same connection and to share connection "authentication" information with Squid ACLs, other helpers, and logs. After this change, Squid accepts optional clt_conn_id=ID pair from a helper and associates the received ID with the client TCP connection. Squid treats the received clt_conn_id=ID pair as a regular annotation, but also keeps it across all requests on the same client connection. A helper may update the client connection ID value during subsequent requests. To send clt_conn_id=ID pair to a URL rewriter, use url_rewrite_extras with a %{clt_conn_id}note macro. Also after this patch the notes comming from helpers replaces any existing note values. This is a Measurement Factory project === modified file 'src/Notes.cc' --- src/Notes.cc 2014-04-30 09:41:25 +0000 +++ src/Notes.cc 2014-06-22 17:15:26 +0000 @@ -14,40 +14,41 @@ * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA. * */ #include "squid.h" #include "AccessLogEntry.h" #include "acl/FilledChecklist.h" #include "acl/Gadgets.h" +#include "client_side.h" #include "ConfigParser.h" #include "globals.h" #include "HttpReply.h" #include "HttpRequest.h" #include "SquidConfig.h" #include "Store.h" #include "StrList.h" #include #include Note::Value::~Value() { aclDestroyAclList(&aclList); } Note::Value::Pointer Note::addValue(const String &value) { Value::Pointer v = new Value(value); @@ -186,75 +187,111 @@ return value.size() ? value.termedBuf() : NULL; } const char * NotePairs::findFirst(const char *noteKey) const { for (std::vector::const_iterator i = entries.begin(); i != entries.end(); ++i) { if ((*i)->name.cmp(noteKey) == 0) return (*i)->value.termedBuf(); } return NULL; } void NotePairs::add(const char *key, const char *note) { entries.push_back(new NotePairs::Entry(key, note)); } void +NotePairs::remove(const char *key) +{ + std::vector::iterator i = entries.begin(); + while(i != entries.end()) { + if ((*i)->name.cmp(key) == 0) { + delete *i; + i = entries.erase(i); + } else { + ++i; + } + } +} + +void NotePairs::addStrList(const char *key, const char *values) { String strValues(values); const char *item; const char *pos = NULL; int ilen = 0; while (strListGetItem(&strValues, ',', &item, &ilen, &pos)) { String v; v.append(item, ilen); entries.push_back(new NotePairs::Entry(key, v.termedBuf())); } } bool NotePairs::hasPair(const char *key, const char *value) const { for (std::vector::const_iterator i = entries.begin(); i != entries.end(); ++i) { if ((*i)->name.cmp(key) == 0 && (*i)->value.cmp(value) == 0) return true; } return false; } void NotePairs::append(const NotePairs *src) { for (std::vector::const_iterator i = src->entries.begin(); i != src->entries.end(); ++i) { entries.push_back(new NotePairs::Entry((*i)->name.termedBuf(), (*i)->value.termedBuf())); } } void NotePairs::appendNewOnly(const NotePairs *src) { for (std::vector::const_iterator i = src->entries.begin(); i != src->entries.end(); ++i) { if (!hasPair((*i)->name.termedBuf(), (*i)->value.termedBuf())) entries.push_back(new NotePairs::Entry((*i)->name.termedBuf(), (*i)->value.termedBuf())); } } +void +NotePairs::replaceOrAdd(const NotePairs *src) +{ + for (std::vector::const_iterator i = src->entries.begin(); i != src->entries.end(); ++i) { + remove((*i)->name.termedBuf()); + } + append(src); +} + NotePairs & SyncNotes(AccessLogEntry &ale, HttpRequest &request) { // XXX: auth code only has access to HttpRequest being authenticated // so we must handle the case where HttpRequest is set without ALE being set. if (!ale.notes) { if (!request.notes) request.notes = new NotePairs; ale.notes = request.notes; } else { assert(ale.notes == request.notes); } return *ale.notes; } + +void +UpdateRequestNotes(ConnStateData *csd, HttpRequest &request, NotePairs const &helperNotes) +{ + // Tag client connection if the helper responded with clt_conn_id=ID. + if (const char *connId = helperNotes.findFirst("clt_conn_id")) { + if (csd) + csd->connectionId(connId); + } + if (!request.notes) + request.notes = new NotePairs; + request.notes->replaceOrAdd(&helperNotes); +} === modified file 'src/Notes.h' --- src/Notes.h 2014-04-30 09:41:25 +0000 +++ src/Notes.h 2014-06-22 17:15:11 +0000 @@ -119,82 +119,98 @@ * Used to store a note key/value pair. */ class Entry { public: Entry(const char *aKey, const char *aValue): name(aKey), value(aValue) {} String name; String value; MEMPROXY_CLASS(Entry); }; NotePairs() {} ~NotePairs(); /** * Append the entries of the src NotePairs list to our list. */ void append(const NotePairs *src); /** + * Replace existing list entries with the src NotePairs entries. + * Entries which do not exist in the destination set are added. + */ + void replaceOrAdd(const NotePairs *src); + + /** * Append any new entries of the src NotePairs list to our list. * Entries which already exist in the destination set are ignored. */ void appendNewOnly(const NotePairs *src); /** * Returns a comma separated list of notes with key 'noteKey'. * Use findFirst instead when a unique kv-pair is needed. */ const char *find(const char *noteKey, const char *sep = ",") const; /** * Returns the first note value for this key or an empty string. */ const char *findFirst(const char *noteKey) const; /** * Adds a note key and value to the notes list. * If the key name already exists in list, add the given value to its set * of values. */ void add(const char *key, const char *value); /** + * Remove all notes with a given key. + */ + void remove(const char *key); + + /** * Adds a note key and values strList to the notes list. * If the key name already exists in list, add the new values to its set * of values. */ void addStrList(const char *key, const char *values); /** * Return true if the key/value pair is already stored */ bool hasPair(const char *key, const char *value) const; /** * Convert NotePairs list to a string consist of "Key: Value" * entries separated by sep string. */ const char *toString(const char *sep = "\r\n") const; /** * True if there are not entries in the list */ bool empty() const {return entries.empty();} std::vector entries; ///< The key/value pair entries private: NotePairs &operator = (NotePairs const &); // Not implemented NotePairs(NotePairs const &); // Not implemented }; MEMPROXY_CLASS_INLINE(NotePairs::Entry); class AccessLogEntry; /** * Keep in sync HttpRequest and the corresponding AccessLogEntry objects */ NotePairs &SyncNotes(AccessLogEntry &ale, HttpRequest &request); +class ConnStateData; +/** + * Updates ConnStateData ids and HttpRequest notes from helpers received notes. + */ +void UpdateRequestNotes(ConnStateData *csd, HttpRequest &request, NotePairs const ¬es); #endif === modified file 'src/auth/UserRequest.cc' --- src/auth/UserRequest.cc 2014-05-22 09:12:48 +0000 +++ src/auth/UserRequest.cc 2014-06-10 15:19:29 +0000 @@ -247,44 +247,42 @@ auth_user_request->authenticate(request, conn, type); } static Auth::UserRequest::Pointer authTryGetUser(Auth::UserRequest::Pointer auth_user_request, ConnStateData * conn, HttpRequest * request) { Auth::UserRequest::Pointer res; if (auth_user_request != NULL) res = auth_user_request; else if (request != NULL && request->auth_user_request != NULL) res = request->auth_user_request; else if (conn != NULL) res = conn->getAuth(); // attach the credential notes from helper to the transaction if (request != NULL && res != NULL && res->user() != NULL) { // XXX: we have no access to the transaction / AccessLogEntry so cant SyncNotes(). // workaround by using anything already set in HttpRequest // OR use new and rely on a later Sync copying these to AccessLogEntry - if (!request->notes) - request->notes = new NotePairs; - request->notes->appendNewOnly(&res->user()->notes); + UpdateRequestNotes(conn, *request, res->user()->notes); } return res; } /* returns one of * AUTH_ACL_CHALLENGE, * AUTH_ACL_HELPER, * AUTH_ACL_CANNOT_AUTHENTICATE, * AUTH_AUTHENTICATED * * How to use: In your proxy-auth dependent acl code, use the following * construct: * int rv; * if ((rv = AuthenticateAuthenticate()) != AUTH_AUTHENTICATED) * return rv; * * when this code is reached, the request/connection is authenticated. * * if you have non-acl code, but want to force authentication, you need a === modified file 'src/cf.data.pre' --- src/cf.data.pre 2014-05-31 16:22:44 +0000 +++ src/cf.data.pre 2014-06-09 14:56:06 +0000 @@ -4561,75 +4561,83 @@ COMMENT_START OPTIONS FOR URL REWRITING ----------------------------------------------------------------------------- COMMENT_END NAME: url_rewrite_program redirect_program TYPE: wordlist LOC: Config.Program.redirect DEFAULT: none DOC_START Specify the location of the executable URL rewriter to use. Since they can perform almost any function there isn't one included. For each requested URL, the rewriter will receive on line with the format [channel-ID ] URL [ extras] - After processing the request the helper must reply using the following format: + After processing the request the helper must reply using the following + format: - [channel-ID ] result [ kv-pairs] + [channel-ID ] result [ key=value ...] The result code can be: OK status=30N url="..." Redirect the URL to the one supplied in 'url='. 'status=' is optional and contains the status code to send the client in Squids HTTP response. It must be one of the HTTP redirect status codes: 301, 302, 303, 307, 308. When no status is given Squid will use 302. OK rewrite-url="..." Rewrite the URL to the one supplied in 'rewrite-url='. The new URL is fetched directly by Squid and returned to the client as the response to its request. OK When neither of url= and rewrite-url= are sent Squid does not change the URL. ERR Do not change the URL. BH An internal error occurred in the helper, preventing a result being identified. The 'message=' key name is reserved for delivering a log message. - In the future, the interface protocol will be extended with - key=value pairs ("kv-pairs" shown above). Helper programs - should be prepared to receive and possibly ignore additional - whitespace-separated tokens on each input line. + Squid understands the following optional key=value pairs received from + URL rewriters: + clt_conn_id=ID + Associates the received ID with the client TCP connection. + The clt_conn_id=ID pair is treated as a regular annotation but + it persists across all transactions on the client connection + rather than disappearing after the current request. A helper + may update the client connection ID value during subsequent + requests by returning a new ID value. To send the connection + ID to the URL rewriter, use url_rewrite_extras: + url_rewrite_extras clt_conn_id=%{clt_conn_id}note ... When using the concurrency= option the protocol is changed by introducing a query channel tag in front of the request/response. The query channel tag is a number between 0 and concurrency-1. This value must be echoed back unchanged to Squid as the first part of the response relating to its request. WARNING: URL re-writing ability should be avoided whenever possible. Use the URL redirect form of response instead. Re-write creates a difference in the state held by the client and server. Possibly causing confusion when the server response contains snippets of its view state. Embeded URLs, response and content Location headers, etc. are not re-written by this interface. By default, a URL rewriter is not used. DOC_END NAME: url_rewrite_children redirect_children === modified file 'src/client_side.h' --- src/client_side.h 2014-06-05 14:57:58 +0000 +++ src/client_side.h 2014-06-11 14:06:45 +0000 @@ -358,40 +358,44 @@ if (!sslServerBump) sslServerBump = srvBump; else assert(sslServerBump == srvBump); } /// Fill the certAdaptParams with the required data for certificate adaptation /// and create the key for storing/retrieve the certificate to/from the cache void buildSslCertGenerationParams(Ssl::CertificateProperties &certProperties); /// Called when the client sends the first request on a bumped connection. /// Returns false if no [delayed] error should be written to the client. /// Otherwise, writes the error to the client and returns true. Also checks /// for SQUID_X509_V_ERR_DOMAIN_MISMATCH on bumped requests. bool serveDelayedError(ClientSocketContext *context); Ssl::BumpMode sslBumpMode; ///< ssl_bump decision (Ssl::bumpEnd if n/a). #else bool switchedToHttps() const { return false; } #endif + /* clt_conn_id=ID annotation access */ + const SBuf &connectionId() const { return connectionId_; } + void connectionId(const char *anId) { connectionId_ = anId; } + protected: void startDechunkingRequest(); void finishDechunkingRequest(bool withSuccess); void abortChunkedRequestBody(const err_type error); err_type handleChunkedRequestBody(size_t &putSize); void startPinnedConnectionMonitoring(); void clientPinnedConnectionRead(const CommIoCbParams &io); private: int connFinishedWithConn(int size); void clientAfterReadingRequests(); bool concurrentRequestQueueFilled() const; #if USE_AUTH /// some user details that can be used to perform authentication on this connection Auth::UserRequest::Pointer auth_; #endif HttpParser parser_; @@ -401,34 +405,36 @@ #if USE_OPENSSL bool switchedToHttps_; /// The SSL server host name appears in CONNECT request or the server ip address for the intercepted requests String sslConnectHostOrIp; ///< The SSL server host name as passed in the CONNECT request String sslCommonName; ///< CN name for SSL certificate generation String sslBumpCertKey; ///< Key to use to store/retrieve generated certificate /// HTTPS server cert. fetching state for bump-ssl-server-first Ssl::ServerBump *sslServerBump; Ssl::CertSignAlgorithm signAlgorithm; ///< The signing algorithm to use #endif /// the reason why we no longer write the response or nil const char *stoppedSending_; /// the reason why we no longer read the request or nil const char *stoppedReceiving_; AsyncCall::Pointer reader; ///< set when we are reading BodyPipe::Pointer bodyPipe; // set when we are reading request body + SBuf connectionId_; ///< clt_conn_id=ID annotation for client connection + CBDATA_CLASS2(ConnStateData); }; void setLogUri(ClientHttpRequest * http, char const *uri, bool cleanUrl = false); const char *findTrailingHTTPVersion(const char *uriAndHTTPVersion, const char *end = NULL); int varyEvaluateMatch(StoreEntry * entry, HttpRequest * req); void clientOpenListenSockets(void); void clientHttpConnectionsClose(void); void httpRequestFree(void *); #endif /* SQUID_CLIENTSIDE_H */ === modified file 'src/client_side_request.cc' --- src/client_side_request.cc 2014-06-05 14:57:58 +0000 +++ src/client_side_request.cc 2014-06-10 15:14:04 +0000 @@ -1220,46 +1220,46 @@ void clientStoreIdDoneWrapper(void *data, const HelperReply &result) { ClientRequestContext *calloutContext = (ClientRequestContext *)data; if (!calloutContext->httpStateIsValid()) return; calloutContext->clientStoreIdDone(result); } void ClientRequestContext::clientRedirectDone(const HelperReply &reply) { HttpRequest *old_request = http->request; debugs(85, 5, HERE << "'" << http->uri << "' result=" << reply); assert(redirect_state == REDIRECT_PENDING); redirect_state = REDIRECT_DONE; + UpdateRequestNotes(http->getConn(), *old_request, reply.notes); + // Put helper response Notes into the transaction state record (ALE) eventually // do it early to ensure that no matter what the outcome the notes are present. - if (http->al != NULL) { - NotePairs ¬es = SyncNotes(*http->al, *old_request); - notes.append(&reply.notes); - } + if (http->al != NULL) + (void)SyncNotes(*http->al, *old_request); switch (reply.result) { case HelperReply::Unknown: case HelperReply::TT: // Handler in redirect.cc should have already mapped Unknown // IF it contained valid entry for the old URL-rewrite helper protocol debugs(85, DBG_IMPORTANT, "ERROR: URL rewrite helper returned invalid result code. Wrong helper? " << reply); break; case HelperReply::BrokenHelper: debugs(85, DBG_IMPORTANT, "ERROR: URL rewrite helper: " << reply << ", attempt #" << (redirect_fail_count+1) << " of 2"); if (redirect_fail_count < 2) { // XXX: make this configurable ? ++redirect_fail_count; // reset state flag to try redirector again from scratch. redirect_done = false; } break; case HelperReply::Error: // no change to be done. @@ -1341,46 +1341,46 @@ if (http->getConn() != NULL && Comm::IsConnOpen(http->getConn()->clientConnection)) fd_note(http->getConn()->clientConnection->fd, http->uri); assert(http->uri); http->doCallouts(); } /** * This method handles the different replies from StoreID helper. */ void ClientRequestContext::clientStoreIdDone(const HelperReply &reply) { HttpRequest *old_request = http->request; debugs(85, 5, "'" << http->uri << "' result=" << reply); assert(store_id_state == REDIRECT_PENDING); store_id_state = REDIRECT_DONE; + UpdateRequestNotes(http->getConn(), *old_request, reply.notes); + // Put helper response Notes into the transaction state record (ALE) eventually // do it early to ensure that no matter what the outcome the notes are present. - if (http->al != NULL) { - NotePairs ¬es = SyncNotes(*http->al, *old_request); - notes.append(&reply.notes); - } + if (http->al != NULL) + (void)SyncNotes(*http->al, *old_request); switch (reply.result) { case HelperReply::Unknown: case HelperReply::TT: // Handler in redirect.cc should have already mapped Unknown // IF it contained valid entry for the old helper protocol debugs(85, DBG_IMPORTANT, "ERROR: storeID helper returned invalid result code. Wrong helper? " << reply); break; case HelperReply::BrokenHelper: debugs(85, DBG_IMPORTANT, "ERROR: storeID helper: " << reply << ", attempt #" << (store_id_fail_count+1) << " of 2"); if (store_id_fail_count < 2) { // XXX: make this configurable ? ++store_id_fail_count; // reset state flag to try StoreID again from scratch. store_id_done = false; } break; case HelperReply::Error: // no change to be done. @@ -1670,40 +1670,47 @@ * longer valid, it should call cbdataReferenceDone() so that * ClientHttpRequest's reference count goes to zero and it will get * deleted. ClientHttpRequest will then delete ClientRequestContext. * * Note that we set the _done flags here before actually starting * the callout. This is strictly for convenience. */ tos_t aclMapTOS (acl_tos * head, ACLChecklist * ch); nfmark_t aclMapNfmark (acl_nfmark * head, ACLChecklist * ch); void ClientHttpRequest::doCallouts() { assert(calloutContext); /*Save the original request for logging purposes*/ if (!calloutContext->http->al->request) { calloutContext->http->al->request = request; HTTPMSGLOCK(calloutContext->http->al->request); + + NotePairs ¬es = SyncNotes(*calloutContext->http->al, *calloutContext->http->request); + // Make the previously set client connection ID available as annotation. + if (ConnStateData *csd = calloutContext->http->getConn()) { + if (csd->connectionId().length()) + notes.add("clt_conn_id", SBuf(csd->connectionId()).c_str()); + } } if (!calloutContext->error) { // CVE-2009-0801: verify the Host: header is consistent with other known details. if (!calloutContext->host_header_verify_done) { debugs(83, 3, HERE << "Doing calloutContext->hostHeaderVerify()"); calloutContext->host_header_verify_done = true; calloutContext->hostHeaderVerify(); return; } if (!calloutContext->http_access_done) { debugs(83, 3, HERE << "Doing calloutContext->clientAccessCheck()"); calloutContext->http_access_done = true; calloutContext->clientAccessCheck(); return; } #if USE_ADAPTATION if (!calloutContext->adaptation_acl_check_done) { === modified file 'src/external_acl.cc' --- src/external_acl.cc 2014-05-15 07:32:10 +0000 +++ src/external_acl.cc 2014-06-10 15:17:50 +0000 @@ -1517,44 +1517,42 @@ ACL *acl = ACL::FindByName(AclMatchedName); assert(acl); ACLExternal *me = dynamic_cast (acl); assert (me); ACLExternal::ExternalAclLookup(checklist, me); } /// Called when an async lookup returns void ExternalACLLookup::LookupDone(void *data, void *result) { ACLFilledChecklist *checklist = Filled(static_cast(data)); checklist->extacl_entry = cbdataReference((external_acl_entry *)result); // attach the helper kv-pair to the transaction if (checklist->extacl_entry) { if (HttpRequest * req = checklist->request) { // XXX: we have no access to the transaction / AccessLogEntry so cant SyncNotes(). // workaround by using anything already set in HttpRequest // OR use new and rely on a later Sync copying these to AccessLogEntry - if (!req->notes) - req->notes = new NotePairs; - req->notes->appendNewOnly(&checklist->extacl_entry->notes); + UpdateRequestNotes(checklist->conn(), *req, checklist->extacl_entry->notes); } } checklist->resumeNonBlockingCheck(ExternalACLLookup::Instance()); } /* This registers "external" in the registry. To do dynamic definitions * of external ACL's, rather than a static prototype, have a Prototype instance * prototype in the class that defines each external acl 'class'. * Then, then the external acl instance is created, it self registers under * it's name. * Be sure that clone is fully functional for that acl class though! */ ACL::Prototype ACLExternal::RegistryProtoype(&ACLExternal::RegistryEntry_, "external"); ACLExternal ACLExternal::RegistryEntry_("external"); ACL * ACLExternal::clone() const {