HTTPMSGLOCK(checklist.reply);
}
+ // XXX: The two "It was denied" clauses below mishandle cases with no
+ // matching rules, violating the "If no rules within the set have matching
+ // ACLs, the header field is left as is" promise in squid.conf.
+ // TODO: Use Acl::Answer::implicit. See HttpStateData::forwardUpgrade().
if (checklist.fastCheck().allowed()) {
/* aclCheckFast returns true for allow. */
debugs(66, 7, "checklist for mangler is positive. Mangle");
} else if (NULL == hm->replacement) {
/* It was denied, and we don't have any replacement */
debugs(66, 7, "checklist denied, we have no replacement. Pass");
+ // XXX: We said "Pass", but the caller will delete on zero retval.
retval = 0;
} else {
/* It was denied, but we have a replacement. Replace the
--- /dev/null
+/*
+ * Copyright (C) 1996-2019 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#include "squid.h"
+#include "acl/Acl.h"
+#include "acl/Gadgets.h"
+#include "ConfigParser.h"
+#include "globals.h"
+#include "HttpUpgradeProtocolAccess.h"
+#include "sbuf/Stream.h"
+
+#include <algorithm>
+
+const SBuf HttpUpgradeProtocolAccess::ProtoOther("OTHER");
+
+ProtocolView::ProtocolView(const char * const start, const size_t len):
+ ProtocolView(SBuf(start, len))
+{
+}
+
+ProtocolView::ProtocolView(const SBuf &proto):
+ name(proto.substr(0, proto.find('/'))),
+ version(proto.substr(name.length()))
+{
+}
+
+std::ostream &
+operator <<(std::ostream &os, const ProtocolView &view)
+{
+ os << view.name;
+ if (!view.version.isEmpty())
+ os << view.version;
+ return os;
+}
+
+/* HttpUpgradeProtocolAccess */
+
+HttpUpgradeProtocolAccess::~HttpUpgradeProtocolAccess()
+{
+ aclDestroyAccessList(&other);
+}
+
+void
+HttpUpgradeProtocolAccess::configureGuard(ConfigParser &parser)
+{
+ const auto rawProto = parser.NextToken();
+ if (!rawProto)
+ throw TextException(ToSBuf("expected a protocol name or ", ProtoOther), Here());
+
+ if (ProtoOther.cmp(rawProto) == 0) {
+ aclParseAccessLine(cfg_directive, parser, &other);
+ return;
+ }
+
+ // To preserve ACL rules checking order, to exclude inapplicable (i.e. wrong
+ // protocol version) rules, and to keep things simple, we merge no rules.
+ acl_access *access = nullptr;
+ aclParseAccessLine(cfg_directive, parser, &access);
+ if (access)
+ namedGuards.emplace_back(rawProto, access);
+}
+
+/* HttpUpgradeProtocolAccess::NamedGuard */
+
+HttpUpgradeProtocolAccess::NamedGuard::NamedGuard(const char *rawProtocol, acl_access *acls):
+ protocol(rawProtocol),
+ proto(protocol),
+ guard(acls)
+{
+}
+
+HttpUpgradeProtocolAccess::NamedGuard::~NamedGuard() {
+ aclDestroyAccessList(&guard);
+}
+
--- /dev/null
+/*
+ * Copyright (C) 1996-2019 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#ifndef SQUID_HTTP_UPGRADE_H
+#define SQUID_HTTP_UPGRADE_H
+
+#include "acl/forward.h"
+#include "sbuf/SBuf.h"
+
+#include <map>
+
+/// a reference to a protocol name[/version] string; no 0-termination is assumed
+class ProtocolView
+{
+public:
+ ProtocolView(const char * const start, const size_t len);
+ explicit ProtocolView(const SBuf &proto);
+
+ SBuf name; ///< everything up to (but excluding) the first slash('/')
+ SBuf version; ///< everything after the name, including the slash('/')
+};
+
+std::ostream &operator <<(std::ostream &, const ProtocolView &);
+
+// HTTP is not explicit about case sensitivity of Upgrade protocol strings, but
+// there are bug reports showing different case variants used for WebSocket. We
+// conservatively preserve the received case and compare case-sensitively.
+
+/// Either b has no version restrictions or both have the same version.
+/// For example, "ws/1" is in "ws" but "ws" is not in "ws/1".
+inline bool
+vAinB(const ProtocolView &a, const ProtocolView &b)
+{
+ // Optimization: Do not assert(a.name == b.name).
+ return b.version.isEmpty() || (a.version == b.version);
+}
+
+/// Allows or blocks HTTP Upgrade protocols (see http_upgrade_request_protocols)
+class HttpUpgradeProtocolAccess
+{
+public:
+ HttpUpgradeProtocolAccess() = default;
+ ~HttpUpgradeProtocolAccess();
+ HttpUpgradeProtocolAccess(HttpUpgradeProtocolAccess &&) = delete; // no copying of any kind
+
+ /// \returns the ACLs matching the given "name[/version]" protocol (or nil)
+ const acl_access *findGuard(const SBuf &proto) const;
+
+ /// parses a single allow/deny rule
+ void configureGuard(ConfigParser&);
+
+ /// iterates over all configured rules, calling the given visitor
+ template <typename Visitor> inline void forEach(const Visitor &) const;
+
+ /// iterates over rules applicable to the given protocol, calling visitor;
+ /// breaks iteration if the visitor returns true
+ template <typename Visitor> inline void forApplicable(const ProtocolView &, const Visitor &) const;
+
+private:
+ /// a single configured access rule for an explicitly named protocol
+ class NamedGuard
+ {
+ public:
+ NamedGuard(const char *rawProtocol, acl_access*);
+ NamedGuard(const NamedGuard &&) = delete; // no copying of any kind
+ ~NamedGuard();
+
+ const SBuf protocol; ///< configured protocol name (and version)
+ const ProtocolView proto; ///< optimization: compiled this->protocol
+ acl_access *guard = nullptr; ///< configured access rule; never nil
+ };
+
+ /// maps HTTP Upgrade protocol name/version to the ACLs guarding its usage
+ typedef std::deque<NamedGuard> NamedGuards;
+
+ /// pseudonym to specify rules for "all other protocols"
+ static const SBuf ProtoOther;
+
+ /// rules governing upgrades to explicitly named protocols
+ NamedGuards namedGuards;
+
+ /// OTHER rules governing unnamed protocols
+ acl_access *other = nullptr;
+};
+
+template <typename Visitor>
+inline void
+HttpUpgradeProtocolAccess::forEach(const Visitor &visitor) const
+{
+ for (const auto &namedGuard: namedGuards)
+ visitor(namedGuard.protocol, namedGuard.guard);
+ if (other)
+ visitor(ProtoOther, other);
+}
+
+template <typename Visitor>
+inline void
+HttpUpgradeProtocolAccess::forApplicable(const ProtocolView &offer, const Visitor &visitor) const
+{
+ auto seenApplicable = false;
+ for (const auto &namedGuard: namedGuards) {
+ if (offer.name != namedGuard.proto.name)
+ continue;
+ if (vAinB(offer, namedGuard.proto) && visitor(namedGuard.protocol, namedGuard.guard))
+ return;
+ seenApplicable = true; // may already be true
+ }
+ if (!seenApplicable && other) // OTHER is applicable if named rules were not
+ (void)visitor(ProtoOther, other);
+}
+
+#endif /* SQUID_HTTP_UPGRADE_H */
+
hier_code.h \
HierarchyLogEntry.h \
$(HTCPSOURCE) \
+ HttpUpgradeProtocolAccess.cc \
+ HttpUpgradeProtocolAccess.h \
http.cc \
http.h \
HttpHeaderFieldStat.h \
cache_cf.h \
AuthReg.h \
RefreshPattern.h \
- cache_cf.cc \
CachePeer.cc \
CachePeer.h \
cache_manager.cc \
tests/stub_libstore.cc \
Transients.cc \
tests/test_http_range.cc \
+ tests/stub_cache_cf.cc \
tests/stub_external_acl.cc \
+ tests/stub_HttpUpgradeProtocolAccess.cc \
tests/stub_ipc_Forwarder.cc \
tests/stub_libdiskio.cc \
tests/stub_libeui.cc \
cache_cf.h \
AuthReg.h \
RefreshPattern.h \
- cache_cf.cc \
debug.cc \
CacheDigest.h \
tests/stub_CacheDigest.cc \
tools.h \
tools.cc \
Transients.cc \
+ tests/stub_cache_cf.cc \
+ tests/stub_HttpUpgradeProtocolAccess.cc \
tests/stub_tunnel.cc \
tests/stub_libstore.cc \
MemStore.cc \
cache_cf.h \
AuthReg.h \
RefreshPattern.h \
- cache_cf.cc \
CachePeer.cc \
CachePeer.h \
CacheDigest.h \
tools.h \
tools.cc \
Transients.cc \
+ tests/stub_cache_cf.cc \
+ tests/stub_HttpUpgradeProtocolAccess.cc \
tests/stub_tunnel.cc \
tests/stub_libstore.cc \
MemStore.cc \
cache_cf.h \
AuthReg.h \
RefreshPattern.h \
- cache_cf.cc \
CachePeer.cc \
CachePeer.h \
cache_manager.cc \
StoreMetaUnpacker.cc \
StoreSwapLogData.cc \
String.cc \
+ tests/stub_cache_cf.cc \
+ tests/stub_HttpUpgradeProtocolAccess.cc \
tests/stub_libstore.cc \
tests/CapturingStoreEntry.h \
tests/testEvent.cc \
cache_cf.h \
AuthReg.h \
RefreshPattern.h \
- cache_cf.cc \
CachePeer.cc \
CachePeer.h \
carp.h \
String.cc \
StrList.h \
StrList.cc \
+ tests/stub_cache_cf.cc \
+ tests/stub_HttpUpgradeProtocolAccess.cc \
tests/stub_libstore.cc \
tests/testEventLoop.cc \
tests/testEventLoop.h \
class HeaderManglers;
class RefreshPattern;
class RemovalPolicySettings;
+class HttpUpgradeProtocolAccess;
namespace AnyP
{
HeaderWithAclList *request_header_add;
///reply_header_add access list
HeaderWithAclList *reply_header_add;
+ /// http_upgrade_request_protocols
+ HttpUpgradeProtocolAccess *http_upgrade_request_protocols;
///note
Notes notes;
char *coredump_dir;
#include "SquidString.h"
#include "StrList.h"
-/** appends an item to the list */
void
-strListAdd(String * str, const char *item, char del)
+strListAdd(String &str, const char *item, const size_t itemSize, const char delimiter)
{
- assert(str && item);
- const auto itemSize = strlen(item);
- if (str->size()) {
- char buf[3];
- buf[0] = del;
- buf[1] = ' ';
- buf[2] = '\0';
- Must(str->canGrowBy(2));
- str->append(buf, 2);
+ if (str.size()) {
+ const char buf[] = { delimiter, ' ' };
+ const auto bufSize = sizeof(buf);
+ Must(str.canGrowBy(bufSize));
+ str.append(buf, bufSize);
}
- Must(str->canGrowBy(itemSize));
- str->append(item, itemSize);
+ Must(str.canGrowBy(itemSize));
+ str.append(item, itemSize);
+}
+
+void
+strListAdd(String *str, const char *item, const char delimiter)
+{
+ assert(str);
+ assert(item);
+ strListAdd(*str, item, strlen(item), delimiter);
+}
+
+void
+strListAdd(String &str, const SBuf &item, char delimiter)
+{
+ strListAdd(str, item.rawContent(), item.length(), delimiter);
}
/** returns true iff "m" is a member of the list */
#include "sbuf/forward.h"
+#include <iterator>
+
class String;
+/// Appends the given item to a delimiter-separated list in str.
void strListAdd(String * str, const char *item, char del);
+
+/// Appends the given item of a given size to a delimiter-separated list in str.
+void strListAdd(String &str, const char *item, const size_t itemSize, const char del = ',');
+
+/// Appends the given item to a delimiter-separated list in str.
+/// Use strListAdd(c-string) for c-string items with unknown length.
+void strListAdd(String &str, const SBuf &item, char delimiter = ',');
+
int strListIsMember(const String * str, const SBuf &item, char del);
int strListIsSubstr(const String * list, const char *s, char del);
+/// Iterates through delimiter-separated and optionally "quoted" list members.
+/// Follows HTTP #rule, including skipping OWS and empty members.
int strListGetItem(const String * str, char del, const char **item, int *ilen, const char **pos);
/// Searches for the first matching key=value pair
/// within a delimiter-separated list of items.
// not explicit: allow "aclMatchCode to Acl::Answer" conversions (for now)
Answer(const aclMatchCode aCode, int aKind = 0): code(aCode), kind(aKind) {}
- Answer(): code(ACCESS_DUNNO), kind(0) {}
+ Answer() = default;
bool operator ==(const aclMatchCode aCode) const {
return code == aCode;
/// whether Squid is uncertain about the allowed() or denied() answer
bool conflicted() const { return !allowed() && !denied(); }
- aclMatchCode code; ///< ACCESS_* code
- int kind; ///< which custom access list verb matched
+ aclMatchCode code = ACCESS_DUNNO; ///< ACCESS_* code
+
+ /// the matched custom access list verb (or zero)
+ int kind = 0;
+
+ /// whether we were computed by the "negate the last explicit action" rule
+ bool implicit = false;
};
} // namespace Acl
implicitRuleAnswer = Acl::Answer(ACCESS_DENIED);
// else we saw no rules and will respond with ACCESS_DUNNO
+ implicitRuleAnswer.implicit = true;
debugs(28, 3, HERE << this << " NO match found, last action " <<
lastAction << " so returning " << implicitRuleAnswer);
markFinished(implicitRuleAnswer, "implicit rule won");
headClone->header.delById(Http::HdrType::PROXY_AUTHENTICATE);
headClone->header.removeHopByHopEntries();
+ // TODO: modify HttpHeader::removeHopByHopEntries to accept a list of
+ // excluded hop-by-hop headers
+ if (head->header.has(Http::HdrType::UPGRADE)) {
+ const auto upgrade = head->header.getList(Http::HdrType::UPGRADE);
+ headClone->header.putStr(Http::HdrType::UPGRADE, upgrade.termedBuf());
+ }
+
// pack polished HTTP header
packHead(httpBuf, headClone.getRaw());
#include "ftp/Elements.h"
#include "globals.h"
#include "HttpHeaderTools.h"
+#include "HttpUpgradeProtocolAccess.h"
#include "icmp/IcmpConfig.h"
#include "ident/Config.h"
#include "ip/Intercept.h"
static void dump_on_unsupported_protocol(StoreEntry *entry, const char *name, acl_access *access);
static void free_on_unsupported_protocol(acl_access **access);
static void ParseAclWithAction(acl_access **access, const Acl::Answer &action, const char *desc, ACL *acl = nullptr);
+static void parse_http_upgrade_request_protocols(HttpUpgradeProtocolAccess **protoGuards);
+static void dump_http_upgrade_request_protocols(StoreEntry *entry, const char *name, HttpUpgradeProtocolAccess *protoGuards);
+static void free_http_upgrade_request_protocols(HttpUpgradeProtocolAccess **protoGuards);
/*
* LegacyParser is a parser for legacy code that uses the global
free_acl_access(access);
}
+static void
+parse_http_upgrade_request_protocols(HttpUpgradeProtocolAccess **protoGuardsPtr)
+{
+ assert(protoGuardsPtr);
+ auto &protoGuards = *protoGuardsPtr;
+ if (!protoGuards)
+ protoGuards = new HttpUpgradeProtocolAccess();
+ protoGuards->configureGuard(LegacyParser);
+}
+
+static void
+dump_http_upgrade_request_protocols(StoreEntry *entry, const char *rawName, HttpUpgradeProtocolAccess *protoGuards)
+{
+ assert(protoGuards);
+ const SBuf name(rawName);
+ protoGuards->forEach([entry,&name](const SBuf &proto, const acl_access *acls) {
+ SBufList line;
+ line.push_back(name);
+ line.push_back(proto);
+ const auto acld = acls->treeDump("", &Acl::AllowOrDeny);
+ line.insert(line.end(), acld.begin(), acld.end());
+ dump_SBufList(entry, line);
+ });
+}
+
+static void
+free_http_upgrade_request_protocols(HttpUpgradeProtocolAccess **protoGuardsPtr)
+{
+ assert(protoGuardsPtr);
+ auto &protoGuards = *protoGuardsPtr;
+ delete protoGuards;
+ protoGuards = nullptr;
+}
+
hostdomaintype cache_peer
http_header_access acl
http_header_replace
+http_upgrade_request_protocols acl
HeaderWithAclList acl
adaptation_access_type adaptation_service_set adaptation_service_chain acl icap_service icap_class
adaptation_service_set_type icap_service ecap_service
that the request body is needed. Delaying is the default behavior.
DOC_END
+NAME: http_upgrade_request_protocols
+TYPE: http_upgrade_request_protocols
+LOC: Config.http_upgrade_request_protocols
+DEFAULT: none
+DEFAULT_DOC: Upgrade header dropped, effectively blocking an upgrade attempt.
+DOC_START
+ Controls client-initiated and server-confirmed switching from HTTP to
+ another protocol (or to several protocols) using HTTP Upgrade mechanism
+ defined in RFC 7230 Section 6.7. Squid itself does not understand the
+ protocols being upgraded to and participates in the upgraded
+ communication only as a dumb TCP proxy. Admins should not allow
+ upgrading to protocols that require a more meaningful proxy
+ participation.
+
+ Usage: http_upgrade_request_protocols <protocol> allow|deny [!]acl ...
+
+ The required "protocol" parameter is either an all-caps word OTHER or an
+ explicit protocol name (e.g. "WebSocket") optionally followed by a slash
+ and a version token (e.g. "HTTP/3"). Explicit protocol names and
+ versions are case sensitive.
+
+ When an HTTP client sends an Upgrade request header, Squid iterates over
+ the client-offered protocols and, for each protocol P (with an optional
+ version V), evaluates the first non-empty set of
+ http_upgrade_request_protocols rules (if any) from the following list:
+
+ * All rules with an explicit protocol name equal to P.
+ * All rules that use OTHER instead of a protocol name.
+
+ In other words, rules using OTHER are considered for protocol P if and
+ only if there are no rules mentioning P by name.
+
+ If both of the above sets are empty, then Squid removes protocol P from
+ the Upgrade offer.
+
+ If the client sent a versioned protocol offer P/X, then explicit rules
+ referring to the same-name but different-version protocol P/Y are
+ declared inapplicable. Inapplicable rules are not evaluated (i.e. are
+ ignored). However, inapplicable rules still belong to the first set of
+ rules for P.
+
+ Within the applicable rule subset, individual rules are evaluated in
+ their configuration order. If all ACLs of an applicable "allow" rule
+ match, then the protocol offered by the client is forwarded to the next
+ hop as is. If all ACLs of an applicable "deny" rule match, then the
+ offer is dropped. If no applicable rules have matching ACLs, then the
+ offer is also dropped. The first matching rule also ends rules
+ evaluation for the offered protocol.
+
+ If all client-offered protocols are removed, then Squid forwards the
+ client request without the Upgrade header. Squid never sends an empty
+ Upgrade request header.
+
+ An Upgrade request header with a value violating HTTP syntax is dropped
+ and ignored without an attempt to use extractable individual protocol
+ offers.
+
+ Upon receiving an HTTP 101 (Switching Protocols) control message, Squid
+ checks that the server listed at least one protocol name and sent a
+ Connection:upgrade response header. Squid does not understand individual
+ protocol naming and versioning concepts enough to implement stricter
+ checks, but an admin can restrict HTTP 101 (Switching Protocols)
+ responses further using http_reply_access. Responses denied by
+ http_reply_access rules and responses flagged by the internal Upgrade
+ checks result in HTTP 502 (Bad Gateway) ERR_INVALID_RESP errors and
+ Squid-to-server connection closures.
+
+ If Squid sends an Upgrade request header, and the next hop (e.g., the
+ origin server) responds with an acceptable HTTP 101 (Switching
+ Protocols), then Squid forwards that message to the client and becomes
+ a TCP tunnel.
+
+ The presence of an Upgrade request header alone does not preclude cache
+ lookups. In other words, an Upgrade request might be satisfied from the
+ cache, using regular HTTP caching rules.
+
+ This clause only supports fast acl types.
+ See http://wiki.squid-cache.org/SquidFaq/SquidAcl for details.
+
+ Each of the following groups of configuration lines represents a
+ separate configuration example:
+
+ # never upgrade to protocol Foo; all others are OK
+ http_upgrade_request_protocols Foo deny all
+ http_upgrade_request_protocols OTHER allow all
+
+ # only allow upgrades to protocol Bar (except for its first version)
+ http_upgrade_request_protocols Bar/1 deny all
+ http_upgrade_request_protocols Bar allow all
+ http_upgrade_request_protocols OTHER deny all # this rule is optional
+
+ # only allow upgrades to protocol Baz, and only if Baz is the only offer
+ acl UpgradeHeaderHasMultipleOffers ...
+ http_upgrade_request_protocols Baz deny UpgradeHeaderHasMultipleOffers
+ http_upgrade_request_protocols Baz allow all
+DOC_END
+
NAME: server_pconn_for_nonretriable
TYPE: acl_access
DEFAULT: none
return os << pic.connection << ", request=" << pic.request;
}
+std::ostream &
+operator <<(std::ostream &os, const ConnStateData::ServerConnectionContext &scc)
+{
+ return os << scc.conn_ << ", srv_bytes=" << scc.preReadServerBytes.length();
+}
+
// pining related comm callbacks
virtual void clientPinnedConnectionClosed(const CommCloseCbParams &io);
+ /// noteTakeServerConnectionControl() callback parameter
+ class ServerConnectionContext {
+ public:
+ ServerConnectionContext(const Comm::ConnectionPointer &conn, const HttpRequest::Pointer &req, const SBuf &post101Bytes): preReadServerBytes(post101Bytes), conn_(conn) { conn_->enterOrphanage(); }
+
+ /// gives to-server connection to the new owner
+ Comm::ConnectionPointer connection() { conn_->leaveOrphanage(); return conn_; }
+
+ SBuf preReadServerBytes; ///< post-101 bytes received from the server
+
+ private:
+ friend std::ostream &operator <<(std::ostream &, const ServerConnectionContext &);
+ Comm::ConnectionPointer conn_; ///< to-server connection
+ };
+
+ /// Gives us the control of the Squid-to-server connection.
+ /// Used, for example, to initiate a TCP tunnel after protocol switching.
+ virtual void noteTakeServerConnectionControl(ServerConnectionContext) {}
+
// comm callbacks
void clientReadFtpData(const CommIoCbParams &io);
void connStateClosed(const CommCloseCbParams &io);
void clientPostHttpsAccept(ConnStateData *);
std::ostream &operator <<(std::ostream &os, const ConnStateData::PinnedIdleContext &pic);
+std::ostream &operator <<(std::ostream &, const ConnStateData::ServerConnectionContext &);
#endif /* SQUID_CLIENTSIDE_H */
*rfc931 = 0; // quick init the head. the rest does not matter.
}
-static int64_t lost_conn = 0;
Comm::Connection::~Connection()
{
if (fd >= 0) {
- debugs(5, 4, "BUG #3329: Orphan Comm::Connection: " << *this);
- debugs(5, 4, "NOTE: " << ++lost_conn << " Orphans since last started.");
+ if (flags & COMM_ORPHANED) {
+ debugs(5, 5, "closing orphan: " << *this);
+ } else {
+ static uint64_t losses = 0;
+ ++losses;
+ debugs(5, 4, "BUG #3329: Lost orphan #" << losses << ": " << *this);
+ }
close();
}
#define COMM_TRANSPARENT 0x10 // arrived via TPROXY
#define COMM_INTERCEPTION 0x20 // arrived via NAT
#define COMM_REUSEPORT 0x40 //< needs SO_REUSEPORT
+/// not registered with Comm and not owned by any connection-closing code
+#define COMM_ORPHANED 0x40
/**
* Store data about the physical and logical attributes of a connection.
*/
ConnectionPointer copyDetails() const;
+ /// close the still-open connection when its last reference is gone
+ void enterOrphanage() { flags |= COMM_ORPHANED; }
+ /// resume relying on owner(s) to initiate an explicit connection closure
+ void leaveOrphanage() { flags &= ~COMM_ORPHANED; }
+
/** Close any open socket. */
void close();
#include "HttpHeaderTools.h"
#include "HttpReply.h"
#include "HttpRequest.h"
+#include "HttpUpgradeProtocolAccess.h"
#include "log/access_log.h"
#include "MemBuf.h"
#include "MemObject.h"
cbdataReferenceDone(_peer);
+ delete upgradeHeaderOut;
+
debugs(11,5, HERE << "HttpStateData " << this << " destroyed; " << serverConnection);
}
Must(!flags.handling1xx);
flags.handling1xx = true;
- if (!request->canHandle1xx() || request->forcedBodyContinuation) {
- debugs(11, 2, "ignoring 1xx because it is " << (request->forcedBodyContinuation ? "already sent" : "not supported by client"));
- proceedAfter1xx();
- return;
- }
+ const auto statusCode = reply->sline.status();
+
+ // drop1xx() needs to handle HTTP 101 (Switching Protocols) responses
+ // specially because they indicate that the server has stopped speaking HTTP
+ Must(!flags.serverSwitchedProtocols);
+ flags.serverSwitchedProtocols = (statusCode == Http::scSwitchingProtocols);
+
+ if (statusCode == Http::scContinue && request->forcedBodyContinuation)
+ return drop1xx("we have sent it already");
+
+ if (!request->canHandle1xx())
+ return drop1xx("the client does not support it");
#if USE_HTTP_VIOLATIONS
// check whether the 1xx response forwarding is allowed by squid.conf
ch.reply = reply;
ch.syncAle(originalRequest().getRaw(), nullptr);
HTTPMSGLOCK(ch.reply);
- if (!ch.fastCheck().allowed()) { // TODO: support slow lookups?
- debugs(11, 3, HERE << "ignoring denied 1xx");
- proceedAfter1xx();
- return;
- }
+ if (!ch.fastCheck().allowed()) // TODO: support slow lookups?
+ return drop1xx("http_reply_access blocked it");
}
#endif // USE_HTTP_VIOLATIONS
+ if (flags.serverSwitchedProtocols) {
+ if (const auto reason = blockSwitchingProtocols(*reply))
+ return drop1xx(reason);
+ }
+
debugs(11, 2, HERE << "forwarding 1xx to client");
// the Sink will use this to call us back after writing 1xx to the client
// for similar reasons without a 1xx response.
}
+/// if possible, safely ignores the received 1xx control message
+/// otherwise, terminates the server connection
+void
+HttpStateData::drop1xx(const char *reason)
+{
+ if (flags.serverSwitchedProtocols) {
+ debugs(11, 2, "bad 101 because " << reason);
+ const auto err = new ErrorState(ERR_INVALID_RESP, Http::scBadGateway, request.getRaw(), fwd->al);
+ fwd->fail(err);
+ closeServer();
+ mustStop("prohibited HTTP/101 response");
+ return;
+ }
+
+ debugs(11, 2, "ignoring 1xx because " << reason);
+ proceedAfter1xx();
+}
+
+/// \retval nil if the HTTP/101 (Switching Protocols) reply should be forwarded
+/// \retval reason why an attempt to switch protocols should be stopped
+const char *
+HttpStateData::blockSwitchingProtocols(const HttpReply &reply) const
+{
+ if (!upgradeHeaderOut)
+ return "Squid offered no Upgrade at all, but server switched to a tunnel";
+
+ // See RFC 7230 section 6.7 for the corresponding MUSTs
+
+ if (!reply.header.has(Http::HdrType::UPGRADE))
+ return "server did not send an Upgrade header field";
+
+ if (!reply.header.hasListMember(Http::HdrType::CONNECTION, "upgrade", ','))
+ return "server did not send 'Connection: upgrade'";
+
+ const auto acceptedProtos = reply.header.getList(Http::HdrType::UPGRADE);
+ const char *pos = nullptr;
+ const char *accepted = nullptr;
+ int acceptedLen = 0;
+ while (strListGetItem(&acceptedProtos, ',', &accepted, &acceptedLen, &pos)) {
+ debugs(11, 5, "server accepted at least" << Raw(nullptr, accepted, acceptedLen));
+ return nullptr; // OK: let the client validate server's selection
+ }
+
+ return "server sent an essentially empty Upgrade header field";
+}
+
/// restores state and resumes processing after 1xx is ignored or forwarded
void
HttpStateData::proceedAfter1xx()
{
Must(flags.handling1xx);
+
+ if (flags.serverSwitchedProtocols) {
+ // pass server connection ownership to request->clientConnectionManager
+ ConnStateData::ServerConnectionContext scc(serverConnection, request, inBuf);
+ typedef UnaryMemFunT<ConnStateData, ConnStateData::ServerConnectionContext> MyDialer;
+ AsyncCall::Pointer call = asyncCall(11, 3, "ConnStateData::noteTakeServerConnectionControl",
+ MyDialer(request->clientConnectionManager,
+ &ConnStateData::noteTakeServerConnectionControl, scc));
+ ScheduleCallHere(call);
+ fwd->unregister(serverConnection);
+ comm_remove_close_handler(serverConnection->fd, closeHandler);
+ closeHandler = nullptr;
+ serverConnection = nullptr;
+ doneWithFwd = "switched protocols";
+ mustStop(doneWithFwd);
+ return;
+ }
+
debugs(11, 2, "continuing with " << payloadSeen << " bytes in buffer after 1xx");
CallJobHere(11, 3, this, HttpStateData, HttpStateData::processReply);
}
strConnection.clean();
}
+/// copies from-client Upgrade info into the given to-server header while
+/// honoring configuration filters and following HTTP requirements
+void
+HttpStateData::forwardUpgrade(HttpHeader &hdrOut)
+{
+ if (!Config.http_upgrade_request_protocols)
+ return; // forward nothing by default
+
+ /* RFC 7230 section 6.7 paragraph 10:
+ * A server MUST ignore an Upgrade header field that is received in
+ * an HTTP/1.0 request.
+ */
+ if (request->http_ver == Http::ProtocolVersion(1,0))
+ return;
+
+ const auto &hdrIn = request->header;
+ if (!hdrIn.has(Http::HdrType::UPGRADE))
+ return;
+ const auto upgradeIn = hdrIn.getList(Http::HdrType::UPGRADE);
+
+ String upgradeOut;
+
+ ACLFilledChecklist ch(nullptr, request.getRaw());
+ ch.al = fwd->al;
+ const char *pos = nullptr;
+ const char *offeredStr = nullptr;
+ int offeredStrLen = 0;
+ while (strListGetItem(&upgradeIn, ',', &offeredStr, &offeredStrLen, &pos)) {
+ const ProtocolView offeredProto(offeredStr, offeredStrLen);
+ debugs(11, 5, "checks all rules applicable to " << offeredProto);
+ Config.http_upgrade_request_protocols->forApplicable(offeredProto, [&ch, offeredStr, offeredStrLen, &upgradeOut] (const SBuf &cfgProto, const acl_access *guard) {
+ debugs(11, 5, "checks " << cfgProto << " rule(s)");
+ ch.changeAcl(guard);
+ const auto answer = ch.fastCheck();
+ if (answer.implicit)
+ return false; // keep looking for an explicit rule match
+ if (answer.allowed())
+ strListAdd(upgradeOut, offeredStr, offeredStrLen);
+ // else drop the offer (explicitly denied cases and ACL errors)
+ return true; // stop after an explicit rule match or an error
+ });
+ }
+
+ if (upgradeOut.size()) {
+ hdrOut.putStr(Http::HdrType::UPGRADE, upgradeOut.termedBuf());
+
+ /* RFC 7230 section 6.7 paragraph 10:
+ * When Upgrade is sent, the sender MUST also send a Connection header
+ * field that contains an "upgrade" connection option, in
+ * order to prevent Upgrade from being accidentally forwarded by
+ * intermediaries that might not implement the listed protocols.
+ *
+ * NP: Squid does not truly implement the protocol(s) in this Upgrade.
+ * For now we are treating an explicit blind tunnel as "implemented"
+ * regardless of the security implications.
+ */
+ hdrOut.putStr(Http::HdrType::CONNECTION, "upgrade");
+ }
+}
+
/**
* Decides whether a particular header may be cloned from the received Clients request
* to our outgoing fetch request.
case Http::HdrType::KEEP_ALIVE: /** \par Keep-Alive: */
case Http::HdrType::PROXY_AUTHENTICATE: /** \par Proxy-Authenticate: */
case Http::HdrType::TRAILER: /** \par Trailer: */
- case Http::HdrType::UPGRADE: /** \par Upgrade: */
case Http::HdrType::TRANSFER_ENCODING: /** \par Transfer-Encoding: */
break;
+ /// \par Upgrade is hop-by-hop but forwardUpgrade() may send a filtered one
+ case Http::HdrType::UPGRADE:
+ break;
+
/** \par OTHER headers I haven't bothered to track down yet. */
case Http::HdrType::AUTHORIZATION:
/* build and pack headers */
{
HttpHeader hdr(hoRequest);
+ forwardUpgrade(hdr);
httpBuildRequestHeader(request.getRaw(), entry, fwd->al, &hdr, flags);
if (request->flags.pinned && request->flags.connectionAuth)
else if (hdr.has(Http::HdrType::AUTHORIZATION))
request->flags.authSent = true;
+ // The late placement of this check supports reply_header_add mangling,
+ // but also complicates optimizing upgradeHeaderOut-like lookups.
+ if (hdr.has(Http::HdrType::UPGRADE)) {
+ assert(!upgradeHeaderOut);
+ upgradeHeaderOut = new String(hdr.getList(Http::HdrType::UPGRADE));
+ }
+
hdr.packInto(mb);
hdr.clean();
}
class FwdState;
class HttpHeader;
+class String;
class HttpStateData : public Client
{
bool ignoreCacheControl;
bool surrogateNoStore;
+ /// Upgrade header value sent to the origin server or cache peer.
+ String *upgradeHeaderOut = nullptr;
+
void processSurrogateControl(HttpReply *);
protected:
void processReply();
void proceedAfter1xx();
void handle1xx(HttpReply *msg);
+ void drop1xx(const char *reason);
private:
/**
void httpTimeout(const CommTimeoutCbParams ¶ms);
mb_size_t buildRequestPrefix(MemBuf * mb);
+ void forwardUpgrade(HttpHeader&);
static bool decideIfWeDoRanges (HttpRequest * orig_request);
bool peerSupportsConnectionPinning() const;
+ const char *blockSwitchingProtocols(const HttpReply&) const;
/// Parser being used at present to parse the HTTP/ICY server response.
Http1::ResponseParserPointer hp;
unsigned int front_end_https = 0; ///< send "Front-End-Https: On" header (off/on/auto=2)
bool keepalive = false; ///< whether to keep the connection persistent
bool only_if_cached = false;
- bool handling1xx = false; ///< we are ignoring or forwarding 1xx response
+
+ /// Whether we are processing an HTTP 1xx control message.
+ bool handling1xx = false;
+
+ /// Whether we received an HTTP 101 (Switching Protocols) control message.
+ /// Implies true handling1xx, but the serverSwitchedProtocols state is
+ /// permanent/final while handling of other control messages usually stops.
+ bool serverSwitchedProtocols = false;
+
bool headers_parsed = false;
/// Whether the next TCP hop is a cache_peer, including originserver
clientProcessRequest(this, parser_, context.getRaw());
}
+int
+Http::One::Server::pipelinePrefetchMax() const
+{
+ const auto context = pipeline.back();
+ const auto request = (context && context->http) ? context->http->request : nullptr;
+ if (request && request->header.has(Http::HdrType::UPGRADE))
+ return 0;
+
+ return ConnStateData::pipelinePrefetchMax();
+}
+
void
Http::One::Server::processParsedRequest(Http::StreamPointer &context)
{
const ClientHttpRequest *http = context->http;
+ // remember Upgrade header; removeHopByHopEntries() will remove it
+ String upgradeHeader;
+ const auto switching = (rep->sline.status() == Http::scSwitchingProtocols);
+ if (switching)
+ upgradeHeader = rep->header.getList(Http::HdrType::UPGRADE);
+
// apply selected clientReplyContext::buildReplyHeader() mods
// it is not clear what headers are required for control messages
rep->header.removeHopByHopEntries();
// paranoid: ContentLengthInterpreter has cleaned non-generated replies
rep->removeIrrelevantContentLength();
- rep->header.putStr(Http::HdrType::CONNECTION, "keep-alive");
+
+ if (switching && /* paranoid: */ upgradeHeader.size()) {
+ rep->header.putStr(Http::HdrType::UPGRADE, upgradeHeader.termedBuf());
+ rep->header.putStr(Http::HdrType::CONNECTION, "upgrade, keep-alive");
+ } else {
+ rep->header.putStr(Http::HdrType::CONNECTION, "keep-alive");
+ }
+
httpHdrMangleList(&rep->header, http->request, http->al, ROR_REPLY);
MemBuf *mb = rep->pack();
return true;
}
+void switchToTunnel(HttpRequest *request, const Comm::ConnectionPointer &clientConn, const Comm::ConnectionPointer &srvConn, const SBuf &preReadServerData);
+
+void
+Http::One::Server::noteTakeServerConnectionControl(ServerConnectionContext server)
+{
+ const auto context = pipeline.front();
+ assert(context);
+ const auto http = context->http;
+ assert(http);
+ assert(http->request);
+
+ stopReading();
+ Must(!writer);
+
+ switchToTunnel(http->request, clientConnection,
+ server.connection(), server.preReadServerBytes);
+}
+
ConnStateData *
Http::NewServer(MasterXactionPointer &xact)
{
virtual void processParsedRequest(Http::StreamPointer &context);
virtual void handleReply(HttpReply *rep, StoreIOBuffer receivedData);
virtual bool writeControlMsgAndCall(HttpReply *rep, AsyncCall::Pointer &call);
+ virtual int pipelinePrefetchMax() const;
virtual time_t idleTimeout() const;
+ virtual void noteTakeServerConnectionControl(ServerConnectionContext);
/* BodyPipe API */
virtual void noteMoreBodySpaceAvailable(BodyPipe::Pointer);
CBDATA_NAMESPACED_CLASS_INIT(Ssl, PeekingPeerConnector);
-void switchToTunnel(HttpRequest *request, Comm::ConnectionPointer & clientConn, Comm::ConnectionPointer &srvConn);
+void switchToTunnel(HttpRequest *request, const Comm::ConnectionPointer &clientConn, const Comm::ConnectionPointer &srvConn, const SBuf &preReadServerData);
void
Ssl::PeekingPeerConnector::cbCheckForPeekAndSpliceDone(Acl::Answer answer, void *data)
bail(new ErrorState(ERR_GATEWAY_FAILURE, Http::scInternalServerError, request.getRaw(), al));
throw TextException("from-client connection gone", Here());
}
- switchToTunnel(request.getRaw(), clientConn, serverConn);
- tunnelInsteadOfNegotiating();
+ startTunneling();
}
}
}
+void
+Ssl::PeekingPeerConnector::startTunneling()
+{
+ // switchToTunnel() drains any already buffered from-server data (rBufData)
+ fd_table[serverConn->fd].useDefaultIo();
+ // tunnelStartShoveling() drains any buffered from-client data (inBuf)
+ fd_table[clientConn->fd].useDefaultIo();
+
+ // TODO: Encapsulate this frequently repeated logic into a method.
+ const auto session = fd_table[serverConn->fd].ssl;
+ auto b = SSL_get_rbio(session.get());
+ auto srvBio = static_cast<Ssl::ServerBio*>(BIO_get_data(b));
+
+ switchToTunnel(request.getRaw(), clientConn, serverConn, srvBio->rBufData());
+ tunnelInsteadOfNegotiating();
+}
+
void
Ssl::PeekingPeerConnector::noteWantWrite()
{
/// connection manager members
void serverCertificateVerified();
+ /// Abruptly stops TLS negotiation and starts tunneling.
+ void startTunneling();
+
/// A wrapper function for checkForPeekAndSpliceDone for use with acl
static void cbCheckForPeekAndSpliceDone(Acl::Answer answer, void *data);
--- /dev/null
+/*
+ * Copyright (C) 1996-2020 The Squid Software Foundation and contributors
+ *
+ * Squid software is distributed under GPLv2+ license and includes
+ * contributions from numerous individuals and organizations.
+ * Please see the COPYING and CONTRIBUTORS files for details.
+ */
+
+#include "squid.h"
+#include "ConfigParser.h"
+
+#define STUB_API "HttpUpgradeProtocolAccess.cc"
+#include "STUB.h"
+
+#include "HttpUpgradeProtocolAccess.h"
+ProtocolView::ProtocolView(const char * const, const size_t) STUB
+ProtocolView::ProtocolView(SBuf const &) STUB
+std::ostream &operator <<(std::ostream &os, const ProtocolView &) STUB_RETVAL(os)
+HttpUpgradeProtocolAccess::~HttpUpgradeProtocolAccess() STUB
+const acl_access *HttpUpgradeProtocolAccess::findGuard(const SBuf &) const STUB_RETVAL(nullptr)
+void HttpUpgradeProtocolAccess::configureGuard(ConfigParser &) STUB
+const SBuf HttpUpgradeProtocolAccess::ProtoOther("STUB-OTHER");
+HttpUpgradeProtocolAccess::NamedGuard::~NamedGuard() STUB_NOP
+HttpUpgradeProtocolAccess::NamedGuard::NamedGuard(const char *, acl_access *): protocol("STUB-UNDEF"), proto(protocol) STUB_NOP
+
void requirePathnameExists(const char *name, const char *path) STUB_NOP
void parse_time_t(time_t * var) STUB
void ConfigParser::ParseUShort(unsigned short *var) STUB
+void ConfigParser::ParseWordList(wordlist **) STUB
void dump_acl_access(StoreEntry * entry, const char *name, acl_access * head) STUB
void dump_acl_list(StoreEntry*, ACLList*) STUB
void tunnelStart(ClientHttpRequest *) STUB
-void switchToTunnel(HttpRequest *request, Comm::ConnectionPointer &clientConn, Comm::ConnectionPointer &srvConn) STUB
+void switchToTunnel(HttpRequest *, const Comm::ConnectionPointer &, const Comm::ConnectionPointer &, const SBuf &) STUB
if (const auto p = conn->getPeer()) {
if (p->secure.encryptTransport)
return advanceDestination("secure connection to peer", conn, [this,&conn] {
- secureConnectionToPeer(conn);
- });
+ secureConnectionToPeer(conn);
+ });
}
connectedToPeer(conn);
}
}
-#if USE_OPENSSL
+/**
+ * Sets up a TCP tunnel through Squid and starts shoveling traffic.
+ * \param request the request that initiated/caused this tunnel
+ * \param clientConn the already accepted client-to-Squid TCP connection
+ * \param srvConn the already established Squid-to-server TCP connection
+ * \param preReadServerData server-sent bytes to be forwarded to the client
+ */
void
-switchToTunnel(HttpRequest *request, Comm::ConnectionPointer &clientConn, Comm::ConnectionPointer &srvConn)
+switchToTunnel(HttpRequest *request, const Comm::ConnectionPointer &clientConn, const Comm::ConnectionPointer &srvConn, const SBuf &preReadServerData)
{
Must(Comm::IsConnOpen(clientConn));
Must(Comm::IsConnOpen(srvConn));
TunnelStateData *tunnelState = new TunnelStateData(context->http);
- // tunnelStartShoveling() drains any buffered from-client data (inBuf)
- fd_table[clientConn->fd].useDefaultIo();
-
request->hier.resetPeerNotes(srvConn, tunnelState->getHost());
tunnelState->server.conn = srvConn;
else
request->prepForDirect();
- // we drain any already buffered from-server data below (rBufData)
- fd_table[srvConn->fd].useDefaultIo();
+ tunnelState->preReadServerData = preReadServerData;
- auto ssl = fd_table[srvConn->fd].ssl.get();
- assert(ssl);
- BIO *b = SSL_get_rbio(ssl);
- Ssl::ServerBio *srvBio = static_cast<Ssl::ServerBio *>(BIO_get_data(b));
- tunnelState->preReadServerData = srvBio->rBufData();
tunnelStartShoveling(tunnelState);
}
-#endif //USE_OPENSSL