This patch lays the groundwork for HTTP/2.0 support in Squid:
* registers PRI method for detection of the HTTP/2.0 "magic" connection
header when expecting HTTP/1.1 traffic
* extend the request line HTTP-version's accepted by the parser to
include "HTTP/2.0".
* reject 2.0 traffic received by reverse- or forward-proxy ports and log
"error:http-2.0-not-supported" in access.log.
* reject "HTTP/2.0" version label or "PRI" method in any use other than
a complete HTTP/2.0 magic connection header.
* change delivered error page to the "Method Not Allowed" response,
indicating the PRI magic "method" is rejected by this HTTP/1.1 proxy.
* intercepted HTTP/2.0 traffic is relayed to ORIGINAL_DST or a
cache_peer transparently.
Note that HTTP/2.0 traffic relaying can be prevented by defining an ACL
for method PRI and limited by http_access controls in the same way as
method CONNECT. cache_peer_access selection can also make use of the PRI
method in ACLs to control tunneling to peers.
Also, fix bug 3371 (CONNECT payload data sent in the first packet being
dropped by Squid) as a necessary step towards relaying the HTTP/2.0
frames which are expected to be pushed in by the client immediately
after the HTTP/2.0 magic header.
Also, add URL parsing and generation support for "*" URL in
request-lines of PRI, OPTIONS and TRACE methods. This is only updating
the URL internal code for OPTIONS and TRACE, the remainder of support
necessary to cope with that URL special case according to HTTP/1.1
requirements is still missing and outside the scope of this patch.
Amos
=== modified file 'src/client_side.cc'
--- src/client_side.cc 2013-07-26 11:26:04 +0000
+++ src/client_side.cc 2013-08-02 09:49:24 +0000
@@ -81,6 +81,7 @@
#include "squid.h"
#include "acl/FilledChecklist.h"
#include "anyp/PortCfg.h"
+#include "base/StringArea.h"
#include "base/Subscription.h"
#include "base/TextException.h"
#include "CachePeer.h"
@@ -2301,6 +2302,29 @@
return parseHttpRequestAbort(csd, "error:method-not-allowed");
}
+ /* deny "PRI * HTTP/2.0" via non-intercepted ports */
+ bool isHttp20Magic = (*method_p == Http::METHOD_PRI &&
+ *http_ver == Http::ProtocolVersion(2,0) &&
+ memcmp(&hp->buf[hp->hdr_end+1], "SM\r\n\r\n", 6) ==
0);
+ if (isHttp20Magic) {
+ /* NOTE: the 'SM\r\n\r\n' string is part of the HTTP/2.0 magic
connection header
+ * even though to the HTTP/1 parser it appears to be a payload.
+ * ensure it gets removed before we start handling the real HTTP/2
frames.
+ */
+ memmove(const_cast<char*>(&hp->buf[hp->hdr_end+1]),
&hp->buf[hp->hdr_end + 6 +1], hp->bufsiz - hp->hdr_end - 6);
+ hp->bufsiz -= 6;
+
+ // we only support tunneling intercepted HTTP/2.0 traffic for now
+ if (csd->port && !csd->port->flags.isIntercepted()) {
+ hp->request_parse_status = Http::scMethodNotAllowed;
+ return parseHttpRequestAbort(csd, "error:http-2.0-not-supported");
+ }
+ } else if (*method_p == Http::METHOD_PRI || *http_ver ==
Http::ProtocolVersion(2,0)) {
+ // other uses of PRI method or HTTP/2.0 version moniker are not
supported
+ hp->request_parse_status = Http::scMethodNotAllowed;
+ return parseHttpRequestAbort(csd, "error:method-not-allowed");
+ }
+
if (*method_p == Http::METHOD_NONE) {
/* XXX need a way to say "this many character length string" */
debugs(33, DBG_IMPORTANT, "clientParseRequestMethod: Unsupported
method in request '" << hp->buf << "'");
@@ -2374,7 +2398,10 @@
*/
if (csd->transparent()) {
/* intercept or transparent mode, properly working with no failures */
- prepareTransparentURL(csd, http, url, req_hdr);
+ if (isHttp20Magic)
+ http->uri = xstrdup(url);
+ else
+ prepareTransparentURL(csd, http, url, req_hdr);
} else if (internalCheck(url)) {
/* internal URL mode */
@@ -2702,8 +2729,9 @@
/* RFC 2616 section 10.5.6 : handle unsupported HTTP major versions
cleanly. */
/* We currently only support 0.9, 1.0, 1.1 properly */
+ /* We support 2.0 experimentally */
if ( (http_ver.major == 0 && http_ver.minor != 9) ||
- (http_ver.major > 1) ) {
+ (http_ver.major > 2) ) {
clientStreamNode *node = context->getClientReplyContext();
debugs(33, 5, "Unsupported HTTP version discovered. :\n" <<
HttpParserHdrBuf(hp));
@@ -2856,10 +2884,14 @@
HTTPMSGLOCK(http->request);
clientSetKeepaliveFlag(http);
- // Let tunneling code be fully responsible for CONNECT requests
- if (http->request->method == Http::METHOD_CONNECT) {
+ // Let tunneling code be fully responsible for CONNECT and PRI requests
+ if (http->request->method == Http::METHOD_CONNECT || http->request->method
== Http::METHOD_PRI) {
context->mayUseConnection(true);
conn->flags.readMore = false;
+
+ // consume header early so that tunnel gets just the body
+ connNoteUseOfBuffer(conn, http->req_sz);
+ notedUseOfBuffer = true;
}
#if USE_SSL
=== modified file 'src/client_side_request.cc'
--- src/client_side_request.cc 2013-07-13 12:19:45 +0000
+++ src/client_side_request.cc 2013-07-15 04:15:51 +0000
@@ -555,6 +555,7 @@
{
// IP address validation for Host: failed. Admin wants to ignore them.
// NP: we do not yet handle CONNECT tunnels well, so ignore for them
+ // NP: we also ignore HTTP/2.0 PRI tunnels, but they are missing Host
header entirely.
if (!Config.onoff.hostStrictVerify && http->request->method !=
Http::METHOD_CONNECT) {
debugs(85, 3, "SECURITY ALERT: Host header forgery detected on " <<
http->getConn()->clientConnection <<
" (" << A << " does not match " << B << ") on URL: " <<
urlCanonical(http->request));
@@ -1512,7 +1513,7 @@
{
debugs(85, 4, "clientProcessRequest: " <<
RequestMethodStr(request->method) << " '" << uri << "'");
- if (request->method == Http::METHOD_CONNECT && !redirect.status) {
+ if (!redirect.status && (request->method == Http::METHOD_CONNECT ||
request->method == Http::METHOD_PRI)) {
#if USE_SSL
if (sslBumpNeeded()) {
sslBumpStart();
=== modified file 'src/http/MethodType.h'
--- src/http/MethodType.h 2012-10-27 00:13:19 +0000
+++ src/http/MethodType.h 2013-07-14 11:14:40 +0000
@@ -75,6 +75,9 @@
METHOD_UNBIND,
#endif
+ // HTTP/2.0 magic -
http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-3.5
+ METHOD_PRI,
+
// Squid extension methods
METHOD_PURGE,
METHOD_OTHER,
=== modified file 'src/tunnel.cc'
--- src/tunnel.cc 2013-06-11 08:11:30 +0000
+++ src/tunnel.cc 2013-08-02 10:03:11 +0000
@@ -33,6 +33,8 @@
#include "squid.h"
#include "acl/FilledChecklist.h"
+#include "base/CbcPointer.h"
+#include "base/StringArea.h"
#include "base/Vector.h"
#include "CachePeer.h"
#include "client_side_request.h"
@@ -99,6 +101,7 @@
bool noConnections() const;
char *url;
+ CbcPointer<ClientHttpRequest> http;
HttpRequest::Pointer request;
Comm::ConnectionList serverDestinations;
@@ -224,6 +227,7 @@
TunnelStateData::TunnelStateData() :
url(NULL),
+ http(),
request(NULL),
status_ptr(NULL),
connectRespBuf(NULL),
@@ -647,6 +651,9 @@
void
TunnelStateData::copyRead(Connection &from, IOCB *completion)
{
+ debugs(26, 8, "Payload " << from.len << " bytes from " << from.conn);
+ debugs(26, 9, "\n----------\n" << StringArea(from.buf, from.len) <<
"\n----------");
+
assert(from.len == 0);
AsyncCall::Pointer call = commCbCall(5,4, "TunnelBlindCopyReadHandler",
CommIoCbPtrFun(completion, this));
@@ -674,11 +681,23 @@
assert(!tunnelState->waitingForConnectExchange());
*tunnelState->status_ptr = Http::scOkay;
if (cbdataReferenceValid(tunnelState)) {
+
if (!tunnelState->server.len)
tunnelState->copyRead(tunnelState->server,
TunnelStateData::ReadServer);
else
tunnelState->copy(tunnelState->server.len, tunnelState->server,
tunnelState->client, TunnelStateData::WriteClientDone);
- tunnelState->copyRead(tunnelState->client,
TunnelStateData::ReadClient);
+
+ // Bug 3371: shovel any payload already pushed into ConnStateData by
the client request
+ if (tunnelState->http.valid() && tunnelState->http->getConn() &&
tunnelState->http->getConn()->in.notYetUsed) {
+ debugs(26, 9, "Tunnel PUSH Payload: \n" <<
StringArea(tunnelState->http->getConn()->in.buf,
tunnelState->http->getConn()->in.notYetUsed) << "\n----------");
+
+ // We just need to ensure the bytes from ConnStateData are in
client.buf already
+ memcpy(tunnelState->client.buf,
tunnelState->http->getConn()->in.buf,
tunnelState->http->getConn()->in.notYetUsed);
+ // NP: readClient() takes care of buffer length accounting.
+ tunnelState->readClient(tunnelState->client.buf,
tunnelState->http->getConn()->in.notYetUsed, COMM_OK, 0);
+ tunnelState->http->getConn()->in.notYetUsed = 0; // ConnStateData
buffer accounting after the shuffle.
+ } else
+ tunnelState->copyRead(tunnelState->client,
TunnelStateData::ReadClient);
}
}
@@ -708,7 +727,6 @@
{
TunnelStateData *tunnelState = (TunnelStateData *)data;
debugs(26, 3, conn << ", flag=" << flag);
- assert(tunnelState->waitingForConnectRequest());
if (flag != COMM_OK) {
*tunnelState->status_ptr = Http::scInternalServerError;
@@ -890,6 +908,7 @@
tunnelState->server.size_ptr = size_ptr;
tunnelState->status_ptr = status_ptr;
tunnelState->client.conn = http->getConn()->clientConnection;
+ tunnelState->http = http;
comm_add_close_handler(tunnelState->client.conn->fd,
tunnelClientClosed,
@@ -918,19 +937,25 @@
flags.proxying = tunnelState->request->flags.proxying;
MemBuf mb;
mb.init();
- mb.Printf("CONNECT %s HTTP/1.1\r\n", tunnelState->url);
- HttpStateData::httpBuildRequestHeader(tunnelState->request.getRaw(),
- NULL, /*
StoreEntry */
- NULL, /*
AccessLogEntry */
- &hdr_out,
- flags); /*
flags */
- packerToMemInit(&p, &mb);
- hdr_out.packInto(&p);
- hdr_out.clean();
- packerClean(&p);
- mb.append("\r\n", 2);
-
- if (tunnelState->clientExpectsConnectResponse()) {
+
+ if (tunnelState->request->method == Http::METHOD_PRI) {
+ // send full HTTP/2.0 magic connection header to server
+ mb.Printf("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
+ } else {
+ // assume CONNECT
+ mb.Printf("CONNECT %s HTTP/1.1\r\n", tunnelState->url);
+ HttpStateData::httpBuildRequestHeader(tunnelState->request.getRaw(),
NULL, NULL, &hdr_out, flags);
+ packerToMemInit(&p, &mb);
+ hdr_out.packInto(&p);
+ hdr_out.clean();
+ packerClean(&p);
+ mb.append("\r\n", 2);
+ }
+
+ debugs(11, 2, "Tunnel server REQUEST: " << tunnelState->server.conn <<
":\n----------\n" <<
+ StringArea(mb.content(), mb.contentSize()) << "\n----------");
+
+ if (tunnelState->clientExpectsConnectResponse() ||
tunnelState->request->method == Http::METHOD_PRI) {
// hack: blindly tunnel peer response (to our CONNECT request) to the
client as ours.
AsyncCall::Pointer writeCall = commCbCall(5,5,
"tunnelConnectedWriteDone",
CommIoCbPtrFun(tunnelConnectedWriteDone, tunnelState));
@@ -950,8 +975,6 @@
// 2*SQUID_TCP_SO_RCVBUF: HttpMsg::parse() zero-terminates, which uses
space.
tunnelState->connectRespBuf->init(SQUID_TCP_SO_RCVBUF,
2*SQUID_TCP_SO_RCVBUF);
tunnelState->readConnectResponse();
-
- assert(tunnelState->waitingForConnectExchange());
}
AsyncCall::Pointer timeoutCall = commCbCall(5, 4, "tunnelTimeout",
=== modified file 'src/url.cc'
--- src/url.cc 2012-12-27 17:58:29 +0000
+++ src/url.cc 2013-07-18 17:28:07 +0000
@@ -243,7 +243,7 @@
if (sscanf(url, "%[^:]:%d", host, &port) < 1)
return NULL;
- } else if ((method == Http::METHOD_OPTIONS || method ==
Http::METHOD_TRACE) &&
+ } else if ((method == Http::METHOD_OPTIONS || method == Http::METHOD_TRACE
|| method == Http::METHOD_PRI) &&
strcmp(url, "*") == 0) {
protocol = AnyP::PROTO_HTTP;
port = urlDefaultPort(protocol);
@@ -507,9 +507,22 @@
if (request->protocol == AnyP::PROTO_URN) {
snprintf(urlbuf, MAX_URL, "urn:" SQUIDSTRINGPH,
SQUIDSTRINGPRINT(request->urlpath));
- } else if (request->method.id() == Http::METHOD_CONNECT) {
- snprintf(urlbuf, MAX_URL, "%s:%d", request->GetHost(), request->port);
} else {
+
+ switch(request->method.id()) {
+ case Http::METHOD_CONNECT:
+ snprintf(urlbuf, MAX_URL, "%s:%d", request->GetHost(),
request->port);
+ break;
+
+ case Http::METHOD_OPTIONS:
+ case Http::METHOD_TRACE:
+ case Http::METHOD_PRI:
+ if (request->urlpath == "*")
+ return (request->canonical =
xstrdup(request->urlpath.termedBuf()));
+ // else fall through to default
+
+ default:
+ {
portbuf[0] = '\0';
if (request->port != urlDefaultPort(request->protocol))
@@ -523,6 +536,8 @@
request->GetHost(),
portbuf,
SQUIDSTRINGPRINT(request->urlpath));
+ }
+ }
}
return (request->canonical = xstrdup(urlbuf));
@@ -543,9 +558,24 @@
if (request->protocol == AnyP::PROTO_URN) {
snprintf(buf, MAX_URL, "urn:" SQUIDSTRINGPH,
SQUIDSTRINGPRINT(request->urlpath));
- } else if (request->method.id() == Http::METHOD_CONNECT) {
- snprintf(buf, MAX_URL, "%s:%d", request->GetHost(), request->port);
} else {
+
+ switch(request->method.id()) {
+ case Http::METHOD_CONNECT:
+ snprintf(buf, MAX_URL, "%s:%d", request->GetHost(), request->port);
+ break;
+
+ case Http::METHOD_OPTIONS:
+ case Http::METHOD_TRACE:
+ case Http::METHOD_PRI:
+ if (request->urlpath == "*") {
+ memcpy(buf, "*", 2);
+ return buf;
+ }
+ // else fall through to default
+
+ default:
+ {
portbuf[0] = '\0';
if (request->port != urlDefaultPort(request->protocol))
@@ -576,6 +606,8 @@
if (Config.onoff.strip_query_terms)
if ((t = strchr(buf, '?')))
*(++t) = '\0';
+ }
+ }
}
if (stringHasCntl(buf))
@@ -822,6 +854,10 @@
if (r->method == Http::METHOD_CONNECT)
return 1;
+ // we support HTTP/2.0 magic PRI requests
+ if (r->method == Http::METHOD_PRI)
+ return (r->urlpath == "*");
+
// we support OPTIONS and TRACE directed at us (with a 501 reply, for now)
// we also support forwarding OPTIONS and TRACE, except for the *-URI ones
if (r->method == Http::METHOD_OPTIONS || r->method == Http::METHOD_TRACE)
=== modified file 'tools/squidclient.cc'
--- tools/squidclient.cc 2013-06-03 14:05:16 +0000
+++ tools/squidclient.cc 2013-08-01 23:16:27 +0000
@@ -445,6 +445,9 @@
if (version[0] == '-' || !version[0]) {
/* HTTP/0.9, no headers, no version */
snprintf(msg, BUFSIZ, "%s %s\r\n", method, url);
+ } else if (strcmp(version, "2.0") == 0) {
+ // send HTTP/2.0 magic buffer, then wait for the server response.
+ snprintf(msg, BUFSIZ, "PRI *
HTTP/2.0\r\n\r\nSM\r\n\r\nSETTINGS-FRAME...\nDONE\r\n");
} else {
if (!xisdigit(version[0])) // not HTTP/n.n
snprintf(msg, BUFSIZ, "%s %s %s\r\n", method, url, version);