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);

Reply via email to