shithub: opusfile

Download patch

ref: e63503349f3fb5d72030e9e7ea3fbc43bf743762
parent: bad75759948a72946a24831d66ef951d3e982558
author: Timothy B. Terriberry <tterribe@xiph.org>
date: Tue Nov 19 10:24:44 EST 2013

Update IPv4/IPv6 dual stack to RFC 6555.

RFC 6555 "Happy Eyeballs" has a few recommendations for
 implementing dual requests to hosts with both IPv4 and IPv6 DNS
 entries that differ slightly from how we used to do it.
This commit updates things to follow those recommendations.

--- a/src/http.c
+++ b/src/http.c
@@ -44,7 +44,8 @@
   RFC 6066: Transport Layer Security (TLS) Extensions: Extension Definitions
   RFC 6125: Representation and Verification of Domain-Based Application Service
    Identity within Internet Public Key Infrastructure Using X.509 (PKIX)
-   Certificates in the Context of Transport Layer Security (TLS)*/
+   Certificates in the Context of Transport Layer Security (TLS)
+  RFC 6555: Happy Eyeballs: Success with Dual-Stack Hosts*/
 
 typedef struct OpusParsedURL   OpusParsedURL;
 typedef struct OpusStringBuf   OpusStringBuf;
@@ -358,6 +359,11 @@
    when seeking, and time out rapidly.*/
 # define OP_NCONNS_MAX (4)
 
+/*The amount of time before we attempt to re-resolve the host.
+  This is 10 minutes, as recommended in RFC 6555 for expiring cached connection
+   results for dual-stack hosts.*/
+# define OP_RESOLVE_CACHE_TIMEOUT_MS (10*60*(opus_int32)1000)
+
 /*The number of redirections at which we give up.
   The value here is the current default in Firefox.
   RFC 2068 mandated a maximum of 5, but RFC 2616 relaxed that to "a client
@@ -831,6 +837,8 @@
     struct sockaddr_in  v4;
     struct sockaddr_in6 v6;
   }                addr;
+  /*The last time we re-resolved the host.*/
+  struct timeb     resolve_time;
   /*A buffer used to build HTTP requests.*/
   OpusStringBuf    request;
   /*A buffer used to build proxy CONNECT requests.*/
@@ -842,6 +850,10 @@
   opus_int64       content_length;
   /*The position indicator used when no connection is active.*/
   opus_int64       pos;
+  /*The host we actually connected to.*/
+  char            *connect_host;
+  /*The port we actually connected to.*/
+  unsigned         connect_port;
   /*The connection we're currently reading from.
     This can be -1 if no connection is active.*/
   int              cur_conni;
@@ -875,6 +887,7 @@
   op_sb_init(&_stream->request);
   op_sb_init(&_stream->proxy_connect);
   op_sb_init(&_stream->response);
+  _stream->connect_host=NULL;
   _stream->seekable=0;
 }
 
@@ -914,6 +927,7 @@
   op_sb_clear(&_stream->response);
   op_sb_clear(&_stream->proxy_connect);
   op_sb_clear(&_stream->request);
+  if(_stream->connect_host!=_stream->url.host)_ogg_free(_stream->connect_host);
   op_parsed_url_clear(&_stream->url);
 }
 
@@ -1866,9 +1880,9 @@
                     left to try.
                     *_addr will be set to NULL in this case.*/
 static int op_sock_connect_next(op_sock _fd,
- struct addrinfo **_addr,int _ai_family){
-  struct addrinfo *addr;
-  int err;
+ const struct addrinfo **_addr,int _ai_family){
+  const struct addrinfo *addr;
+  int                    err;
   addr=*_addr;
   for(;;){
     /*Move to the next address of the requested type.*/
@@ -1887,38 +1901,32 @@
 /*The number of address families to try connecting to simultaneously.*/
 # define OP_NPROTOS (2)
 
-static int op_http_connect(OpusHTTPStream *_stream,OpusHTTPConn *_conn,
- struct addrinfo *_addrs,struct timeb *_start_time){
-  struct addrinfo *addr;
-  struct addrinfo *addrs[OP_NPROTOS];
-  struct pollfd    fds[OP_NPROTOS];
-  int              ai_family;
-  int              nprotos;
-  int              ret;
-  int              pi;
-  int              pj;
+static int op_http_connect_impl(OpusHTTPStream *_stream,OpusHTTPConn *_conn,
+ const struct addrinfo *_addrs,struct timeb *_start_time){
+  const struct addrinfo *addr;
+  const struct addrinfo *addrs[OP_NPROTOS];
+  struct pollfd          fds[OP_NPROTOS];
+  int                    ai_family;
+  int                    nprotos;
+  int                    ret;
+  int                    pi;
+  int                    pj;
   for(pi=0;pi<OP_NPROTOS;pi++)addrs[pi]=NULL;
-  addr=_addrs;
   /*Try connecting via both IPv4 and IPv6 simultaneously, and keep the first
-     one that succeeds.*/
-  for(;addr!=NULL;addr=addr->ai_next){
-    /*Give IPv6 a slight edge by putting it first in the list.*/
-    if(addr->ai_family==AF_INET6){
+     one that succeeds.
+    Start by finding the first address from each family.
+    We order the first connection attempts in the same order the address
+     families were returned in the DNS records in accordance with RFC 6555.*/
+  for(addr=_addrs,nprotos=0;addr!=NULL&&nprotos<OP_NPROTOS;addr=addr->ai_next){
+    if(addr->ai_family==AF_INET6||addr->ai_family==AF_INET){
       OP_ASSERT(addr->ai_addrlen<=sizeof(struct sockaddr_in6));
-      if(addrs[0]==NULL)addrs[0]=addr;
-    }
-    else if(addr->ai_family==AF_INET){
       OP_ASSERT(addr->ai_addrlen<=sizeof(struct sockaddr_in));
-      if(addrs[1]==NULL)addrs[1]=addr;
+      /*If we've seen this address family before, skip this address for now.*/
+      for(pi=0;pi<nprotos;pi++)if(addrs[pi]->ai_family==addr->ai_family)break;
+      if(pi<nprotos)continue;
+      addrs[nprotos++]=addr;
     }
   }
-  /*Consolidate the list of addresses.*/
-  for(pi=nprotos=0;pi<OP_NPROTOS;pi++){
-    if(addrs[pi]!=NULL){
-      addrs[nprotos]=addrs[pi];
-      nprotos++;
-    }
-  }
   /*Pop the connection off the free list and put it on the LRU list.*/
   OP_ASSERT(_stream->free_head==_conn);
   _stream->free_head=_conn->next;
@@ -1928,7 +1936,12 @@
   *&_conn->read_time=*_start_time;
   _conn->read_bytes=0;
   _conn->read_rate=0;
-  /*Try to start a connection to each protocol.*/
+  /*Try to start a connection to each protocol.
+    RFC 6555 says it is RECOMMENDED that connection attempts be paced
+     150...250 ms apart "to balance human factors against network load", but
+     that "stateful algorithms" (that's us) "are expected to be more
+     aggressive".
+    We are definitely more aggressive: we don't pace at all.*/
   for(pi=0;pi<nprotos;pi++){
     ai_family=addrs[pi]->ai_family;
     fds[pi].fd=socket(ai_family,SOCK_STREAM,addrs[pi]->ai_protocol);
@@ -2020,6 +2033,29 @@
   return 0;
 }
 
+static int op_http_connect(OpusHTTPStream *_stream,OpusHTTPConn *_conn,
+ const struct addrinfo *_addrs,struct timeb *_start_time){
+  struct timeb     resolve_time;
+  struct addrinfo *new_addrs;
+  int              ret;
+  /*Re-resolve the host if we need to (RFC 6555 says we MUST do so
+     occasionally).*/
+  new_addrs=NULL;
+  ftime(&resolve_time);
+  if(_addrs!=&_stream->addr_info||op_time_diff_ms(&resolve_time,
+   &_stream->resolve_time)>=OP_RESOLVE_CACHE_TIMEOUT_MS){
+    new_addrs=op_resolve(_stream->connect_host,_stream->connect_port);
+    if(OP_LIKELY(new_addrs!=NULL)){
+      _addrs=new_addrs;
+      *&_stream->resolve_time=*&resolve_time;
+    }
+    else if(OP_LIKELY(_addrs==NULL))return OP_FALSE;
+  }
+  ret=op_http_connect_impl(_stream,_conn,_addrs,_start_time);
+  if(new_addrs!=NULL)freeaddrinfo(new_addrs);
+  return ret;
+}
+
 # define OP_BASE64_LENGTH(_len) (((_len)+2)/3*4)
 
 static const char BASE64_TABLE[64]={
@@ -2136,51 +2172,31 @@
  int _skip_certificate_check,const char *_proxy_host,unsigned _proxy_port,
  const char *_proxy_user,const char *_proxy_pass,OpusServerInfo *_info){
   struct addrinfo *addrs;
-  const char      *last_host;
-  unsigned         last_port;
   int              nredirs;
   int              ret;
-  if(_proxy_host!=NULL&&OP_UNLIKELY(_proxy_port>65535U))return OP_EINVAL;
-  last_host=NULL;
-  /*We shouldn't have to initialize last_port, but gcc is too dumb to figure
-     out that last_host!=NULL implies we've already taken one trip through the
-     loop.*/
-  last_port=0;
 #if defined(_WIN32)
   op_init_winsock();
 #endif
   ret=op_parse_url(&_stream->url,_url);
   if(OP_UNLIKELY(ret<0))return ret;
+  if(_proxy_host!=NULL){
+    if(OP_UNLIKELY(_proxy_port>65535U))return OP_EINVAL;
+    _stream->connect_host=op_string_dup(_proxy_host);
+    _stream->connect_port=_proxy_port;
+  }
+  else{
+    _stream->connect_host=_stream->url.host;
+    _stream->connect_port=_stream->url.port;
+  }
+  addrs=NULL;
   for(nredirs=0;nredirs<OP_REDIRECT_LIMIT;nredirs++){
-    struct timeb  start_time;
-    struct timeb  end_time;
-    char         *next;
-    char         *status_code;
-    const char   *host;
-    unsigned      port;
-    int           minor_version_pos;
-    int           v1_1_compat;
-    if(_proxy_host==NULL){
-      host=_stream->url.host;
-      port=_stream->url.port;
-    }
-    else{
-      host=_proxy_host;
-      port=_proxy_port;
-    }
-    /*If connecting to the same place as last time, don't re-resolve it.*/
-    addrs=NULL;
-    if(last_host!=NULL){
-      if(strcmp(last_host,host)==0&&last_port==port)addrs=&_stream->addr_info;
-      else if(_stream->ssl_session!=NULL){
-        /*Forget any cached SSL session from the last host.*/
-        SSL_SESSION_free(_stream->ssl_session);
-        _stream->ssl_session=NULL;
-      }
-      if(last_host!=_proxy_host)_ogg_free((void *)last_host);
-    }
-    last_host=host;
-    last_port=port;
+    OpusParsedURL  next_url;
+    struct timeb   start_time;
+    struct timeb   end_time;
+    char          *next;
+    char          *status_code;
+    int            minor_version_pos;
+    int            v1_1_compat;
     /*Initialize the SSL library if necessary.*/
     if(OP_URL_IS_SSL(&_stream->url)&&_stream->ssl_ctx==NULL){
       SSL_CTX *ssl_ctx;
@@ -2244,12 +2260,7 @@
       }
     }
     /*Actually make the connection.*/
-    if(addrs!=&_stream->addr_info){
-      addrs=op_resolve(host,port);
-      if(OP_UNLIKELY(addrs==NULL))return OP_FALSE;
-    }
     ret=op_http_connect(_stream,_stream->conns+0,addrs,&start_time);
-    if(addrs!=&_stream->addr_info)freeaddrinfo(addrs);
     if(OP_UNLIKELY(ret<0))return ret;
     /*Build the request to send.*/
     _stream->request.nbuf=0;
@@ -2524,15 +2535,33 @@
       if(strcmp(header,"location")==0&&OP_LIKELY(_url==NULL))_url=cdr;
     }
     if(OP_UNLIKELY(_url==NULL))return OP_FALSE;
-    /*Don't free last_host if it came from the last URL.*/
-    if(last_host!=_proxy_host)_stream->url.host=NULL;
-    op_parsed_url_clear(&_stream->url);
-    ret=op_parse_url(&_stream->url,_url);
-    if(OP_UNLIKELY(ret<0)){
-      if(ret==OP_EINVAL)ret=OP_FALSE;
-      if(last_host!=_proxy_host)_ogg_free((void *)last_host);
-      return ret;
+    ret=op_parse_url(&next_url,_url);
+    if(OP_UNLIKELY(ret<0))return ret;
+    if(_proxy_host==NULL||_stream->ssl_session!=NULL){
+      if(strcmp(_stream->url.host,next_url.host)==0
+       &&_stream->url.port==next_url.port){
+        /*Try to skip re-resolve when connecting to the same host.*/
+        addrs=&_stream->addr_info;
+      }
+      else{
+        if(_stream->ssl_session!=NULL){
+          /*Forget any cached SSL session from the last host.*/
+          SSL_SESSION_free(_stream->ssl_session);
+          _stream->ssl_session=NULL;
+        }
+      }
     }
+    if(_proxy_host==NULL){
+      OP_ASSERT(_stream->connect_host==_stream->url.host);
+      _stream->connect_host=next_url.host;
+      _stream->connect_port=next_url.port;
+    }
+    /*Always try to skip re-resolve for proxy connections.*/
+    else addrs=&_stream->addr_info;
+    op_parsed_url_clear(&_stream->url);
+    *&_stream->url=*&next_url;
+    /*TODO: On servers/proxies that support pipelining, we might be able to
+       re-use this connection.*/
     op_http_conn_close(_stream,_stream->conns+0,&_stream->lru_head,1);
   }
   /*Redirection limit reached.*/