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.
This patch documents the clt_conn_id key=value pair in cf.data.pre file
only for url rewriters. Because annotations are common to all helpers we
may want to make a special section at the beginning of cf.data.per for
all helpers. Suggestions are welcome.
I must also note that this patch adds an inconsistency. All annotation
key=values pairs received from helpers, accumulated to the existing key
notes values. The clt_conn_id=Id pair is always unique and replaces the
existing clt_conn_id=Id annotation pair.
We may want to make all annotations unique, or maybe implement a
configuration mechanism to define which annotations are overwriting
their previous values and which appending the new values.
This is a Measurement Factory project
Regards,
Christos
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.
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-10 15:20:30 +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 <algorithm>
#include <string>
Note::Value::~Value()
{
aclDestroyAclList(&aclList);
}
Note::Value::Pointer
Note::addValue(const String &value)
{
Value::Pointer v = new Value(value);
@@ -186,40 +187,54 @@
return value.size() ? value.termedBuf() : NULL;
}
const char *
NotePairs::findFirst(const char *noteKey) const
{
for (std::vector<NotePairs::Entry *>::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<NotePairs::Entry *>::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<NotePairs::Entry *>::const_iterator i = entries.begin(); i != entries.end(); ++i) {
if ((*i)->name.cmp(key) == 0 && (*i)->value.cmp(value) == 0)
return true;
}
@@ -241,20 +256,36 @@
if (!hasPair((*i)->name.termedBuf(), (*i)->value.termedBuf()))
entries.push_back(new NotePairs::Entry((*i)->name.termedBuf(), (*i)->value.termedBuf()));
}
}
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);
+ // The new clt_conn_id will replace any previously set value.
+ if (request.notes != NULL)
+ request.notes->remove("clt_conn_id");
+ }
+ if (!request.notes)
+ request.notes = new NotePairs;
+ request.notes->appendNewOnly(&helperNotes);
+}
=== modified file 'src/Notes.h'
--- src/Notes.h 2014-04-30 09:41:25 +0000
+++ src/Notes.h 2014-06-10 15:15:23 +0000
@@ -143,58 +143,68 @@
/**
* 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<NotePairs::Entry *> 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 <SP>] URL [<SP> extras]<NL>
- 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 <SP>] result [<SP> kv-pairs]
+ [channel-ID <SP>] result [<SP> 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<ACLExternal *> (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<ACLChecklist*>(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
{