ref: e63503349f3fb5d72030e9e7ea3fbc43bf743762
parent: bad75759948a72946a24831d66ef951d3e982558
author: Timothy B. Terriberry <[email protected]>
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.*/