3392 lines
123 KiB
C
3392 lines
123 KiB
C
/********************************************************************
|
|
* *
|
|
* THIS FILE IS PART OF THE libopusfile SOFTWARE CODEC SOURCE CODE. *
|
|
* USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS *
|
|
* GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE *
|
|
* IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING. *
|
|
* *
|
|
* THE libopusfile SOURCE CODE IS (C) COPYRIGHT 2012 *
|
|
* by the Xiph.Org Foundation and contributors http://www.xiph.org/ *
|
|
* *
|
|
********************************************************************/
|
|
#ifdef OPUS_HAVE_CONFIG_H
|
|
#include "opus_config.h"
|
|
#endif
|
|
|
|
#include "internal.h"
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <string.h>
|
|
|
|
/*RFCs referenced in this file:
|
|
RFC 761: DOD Standard Transmission Control Protocol
|
|
RFC 1535: A Security Problem and Proposed Correction With Widely Deployed DNS
|
|
Software
|
|
RFC 1738: Uniform Resource Locators (URL)
|
|
RFC 1945: Hypertext Transfer Protocol -- HTTP/1.0
|
|
RFC 2068: Hypertext Transfer Protocol -- HTTP/1.1
|
|
RFC 2145: Use and Interpretation of HTTP Version Numbers
|
|
RFC 2246: The TLS Protocol Version 1.0
|
|
RFC 2459: Internet X.509 Public Key Infrastructure Certificate and
|
|
Certificate Revocation List (CRL) Profile
|
|
RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
|
|
RFC 2617: HTTP Authentication: Basic and Digest Access Authentication
|
|
RFC 2817: Upgrading to TLS Within HTTP/1.1
|
|
RFC 2818: HTTP Over TLS
|
|
RFC 3492: Punycode: A Bootstring encoding of Unicode for Internationalized
|
|
Domain Names in Applications (IDNA)
|
|
RFC 3986: Uniform Resource Identifier (URI): Generic Syntax
|
|
RFC 3987: Internationalized Resource Identifiers (IRIs)
|
|
RFC 4343: Domain Name System (DNS) Case Insensitivity Clarification
|
|
RFC 5894: Internationalized Domain Names for Applications (IDNA):
|
|
Background, Explanation, and Rationale
|
|
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)
|
|
RFC 6555: Happy Eyeballs: Success with Dual-Stack Hosts*/
|
|
|
|
typedef struct OpusParsedURL OpusParsedURL;
|
|
typedef struct OpusStringBuf OpusStringBuf;
|
|
typedef struct OpusHTTPConn OpusHTTPConn;
|
|
typedef struct OpusHTTPStream OpusHTTPStream;
|
|
|
|
static char *op_string_range_dup(const char *_start,const char *_end){
|
|
size_t len;
|
|
char *ret;
|
|
OP_ASSERT(_start<=_end);
|
|
len=_end-_start;
|
|
/*This is to help avoid overflow elsewhere, later.*/
|
|
if(OP_UNLIKELY(len>=INT_MAX))return NULL;
|
|
ret=(char *)_ogg_malloc(sizeof(*ret)*(len+1));
|
|
if(OP_LIKELY(ret!=NULL)){
|
|
ret=(char *)memcpy(ret,_start,sizeof(*ret)*(len));
|
|
ret[len]='\0';
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static char *op_string_dup(const char *_s){
|
|
return op_string_range_dup(_s,_s+strlen(_s));
|
|
}
|
|
|
|
static char *op_string_tolower(char *_s){
|
|
int i;
|
|
for(i=0;_s[i]!='\0';i++){
|
|
int c;
|
|
c=_s[i];
|
|
if(c>='A'&&c<='Z')c+='a'-'A';
|
|
_s[i]=(char)c;
|
|
}
|
|
return _s;
|
|
}
|
|
|
|
/*URI character classes (from RFC 3986).*/
|
|
#define OP_URL_ALPHA \
|
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
#define OP_URL_DIGIT "0123456789"
|
|
#define OP_URL_HEXDIGIT "0123456789ABCDEFabcdef"
|
|
/*Not a character class, but the characters allowed in <scheme>.*/
|
|
#define OP_URL_SCHEME OP_URL_ALPHA OP_URL_DIGIT "+-."
|
|
#define OP_URL_GEN_DELIMS "#/:?@[]"
|
|
#define OP_URL_SUB_DELIMS "!$&'()*+,;="
|
|
#define OP_URL_RESERVED OP_URL_GEN_DELIMS OP_URL_SUB_DELIMS
|
|
#define OP_URL_UNRESERVED OP_URL_ALPHA OP_URL_DIGIT "-._~"
|
|
/*Not a character class, but the characters allowed in <pct-encoded>.*/
|
|
#define OP_URL_PCT_ENCODED "%"
|
|
/*Not a character class or production rule, but for convenience.*/
|
|
#define OP_URL_PCHAR_BASE \
|
|
OP_URL_UNRESERVED OP_URL_PCT_ENCODED OP_URL_SUB_DELIMS
|
|
#define OP_URL_PCHAR OP_URL_PCHAR_BASE ":@"
|
|
/*Not a character class, but the characters allowed in <userinfo> and
|
|
<IP-literal>.*/
|
|
#define OP_URL_PCHAR_NA OP_URL_PCHAR_BASE ":"
|
|
/*Not a character class, but the characters allowed in <segment-nz-nc>.*/
|
|
#define OP_URL_PCHAR_NC OP_URL_PCHAR_BASE "@"
|
|
/*Not a character clsss, but the characters allowed in <path>.*/
|
|
#define OP_URL_PATH OP_URL_PCHAR "/"
|
|
/*Not a character class, but the characters allowed in <query> / <fragment>.*/
|
|
#define OP_URL_QUERY_FRAG OP_URL_PCHAR "/?"
|
|
|
|
/*Check the <% HEXDIG HEXDIG> escapes of a URL for validity.
|
|
Return: 0 if valid, or a negative value on failure.*/
|
|
static int op_validate_url_escapes(const char *_s){
|
|
int i;
|
|
for(i=0;_s[i];i++){
|
|
if(_s[i]=='%'){
|
|
if(OP_UNLIKELY(!isxdigit(_s[i+1]))
|
|
||OP_UNLIKELY(!isxdigit(_s[i+2]))
|
|
/*RFC 3986 says %00 "should be rejected if the application is not
|
|
expecting to receive raw data within a component."*/
|
|
||OP_UNLIKELY(_s[i+1]=='0'&&_s[i+2]=='0')){
|
|
return OP_FALSE;
|
|
}
|
|
i+=2;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*Convert a hex digit to its actual value.
|
|
_c: The hex digit to convert.
|
|
Presumed to be valid ('0'...'9', 'A'...'F', or 'a'...'f').
|
|
Return: The value of the digit, in the range [0,15].*/
|
|
static int op_hex_value(int _c){
|
|
return _c>='a'?_c-'a'+10:_c>='A'?_c-'A'+10:_c-'0';
|
|
}
|
|
|
|
/*Unescape all the <% HEXDIG HEXDIG> sequences in a string in-place.
|
|
This does no validity checking.*/
|
|
static char *op_unescape_url_component(char *_s){
|
|
int i;
|
|
int j;
|
|
for(i=j=0;_s[i];i++,j++){
|
|
if(_s[i]=='%'){
|
|
_s[i]=(char)(op_hex_value(_s[i+1])<<4|op_hex_value(_s[i+2]));
|
|
i+=2;
|
|
}
|
|
}
|
|
return _s;
|
|
}
|
|
|
|
/*Parse a file: URL.
|
|
This code is not meant to be fast: strspn() with large sets is likely to be
|
|
slow, but it is very convenient.
|
|
It is meant to be RFC 1738-compliant (as updated by RFC 3986).*/
|
|
static const char *op_parse_file_url(const char *_src){
|
|
const char *scheme_end;
|
|
const char *path;
|
|
const char *path_end;
|
|
scheme_end=_src+strspn(_src,OP_URL_SCHEME);
|
|
if(OP_UNLIKELY(*scheme_end!=':')
|
|
||scheme_end-_src!=4||op_strncasecmp(_src,"file",4)!=0){
|
|
/*Unsupported protocol.*/
|
|
return NULL;
|
|
}
|
|
/*Make sure all escape sequences are valid to simplify unescaping later.*/
|
|
if(OP_UNLIKELY(op_validate_url_escapes(scheme_end+1)<0))return NULL;
|
|
if(scheme_end[1]=='/'&&scheme_end[2]=='/'){
|
|
const char *host;
|
|
/*file: URLs can have a host!
|
|
Yeah, I was surprised, too, but that's what RFC 1738 says.
|
|
It also says, "The file URL scheme is unusual in that it does not specify
|
|
an Internet protocol or access method for such files; as such, its
|
|
utility in network protocols between hosts is limited," which is a mild
|
|
understatement.*/
|
|
host=scheme_end+3;
|
|
/*The empty host is what we expect.*/
|
|
if(OP_LIKELY(*host=='/'))path=host;
|
|
else{
|
|
const char *host_end;
|
|
char host_buf[28];
|
|
/*RFC 1738 says localhost "is interpreted as `the machine from which the
|
|
URL is being interpreted,'" so let's check for it.*/
|
|
host_end=host+strspn(host,OP_URL_PCHAR_BASE);
|
|
/*No <port> allowed.
|
|
This also rejects IP-Literals.*/
|
|
if(*host_end!='/')return NULL;
|
|
/*An escaped "localhost" can take at most 27 characters.*/
|
|
if(OP_UNLIKELY(host_end-host>27))return NULL;
|
|
memcpy(host_buf,host,sizeof(*host_buf)*(host_end-host));
|
|
host_buf[host_end-host]='\0';
|
|
op_unescape_url_component(host_buf);
|
|
op_string_tolower(host_buf);
|
|
/*Some other host: give up.*/
|
|
if(OP_UNLIKELY(strcmp(host_buf,"localhost")!=0))return NULL;
|
|
path=host_end;
|
|
}
|
|
}
|
|
else path=scheme_end+1;
|
|
path_end=path+strspn(path,OP_URL_PATH);
|
|
/*This will reject a <query> or <fragment> component, too.
|
|
I don't know what to do with queries, but a temporal fragment would at
|
|
least make sense.
|
|
RFC 1738 pretty clearly defines a <searchpart> that's equivalent to the
|
|
RFC 3986 <query> component for other schemes, but not the file: scheme,
|
|
so I'm going to just reject it.*/
|
|
if(*path_end!='\0')return NULL;
|
|
return path;
|
|
}
|
|
|
|
#if defined(OP_ENABLE_HTTP)
|
|
# if defined(_WIN32)
|
|
# include <winsock2.h>
|
|
# include <ws2tcpip.h>
|
|
# include <openssl/ssl.h>
|
|
# include "winerrno.h"
|
|
|
|
typedef SOCKET op_sock;
|
|
|
|
# define OP_INVALID_SOCKET (INVALID_SOCKET)
|
|
|
|
/*Vista and later support WSAPoll(), but we don't want to rely on that.
|
|
Instead we re-implement it badly using select().
|
|
Unfortunately, they define a conflicting struct pollfd, so we only define our
|
|
own if it looks like that one has not already been defined.*/
|
|
# if !defined(POLLIN)
|
|
/*Equivalent to POLLIN.*/
|
|
# define POLLRDNORM (0x0100)
|
|
/*Priority band data can be read.*/
|
|
# define POLLRDBAND (0x0200)
|
|
/*There is data to read.*/
|
|
# define POLLIN (POLLRDNORM|POLLRDBAND)
|
|
/* There is urgent data to read.*/
|
|
# define POLLPRI (0x0400)
|
|
/*Equivalent to POLLOUT.*/
|
|
# define POLLWRNORM (0x0010)
|
|
/*Writing now will not block.*/
|
|
# define POLLOUT (POLLWRNORM)
|
|
/*Priority data may be written.*/
|
|
# define POLLWRBAND (0x0020)
|
|
/*Error condition (output only).*/
|
|
# define POLLERR (0x0001)
|
|
/*Hang up (output only).*/
|
|
# define POLLHUP (0x0002)
|
|
/*Invalid request: fd not open (output only).*/
|
|
# define POLLNVAL (0x0004)
|
|
|
|
struct pollfd{
|
|
/*File descriptor.*/
|
|
op_sock fd;
|
|
/*Requested events.*/
|
|
short events;
|
|
/*Returned events.*/
|
|
short revents;
|
|
};
|
|
# endif
|
|
|
|
/*But Winsock never defines nfds_t (it's simply hard-coded to ULONG).*/
|
|
typedef unsigned long nfds_t;
|
|
|
|
/*The usage of FD_SET() below is O(N^2).
|
|
This is okay because select() is limited to 64 sockets in Winsock, anyway.
|
|
In practice, we only ever call it with one or two sockets.*/
|
|
static int op_poll_win32(struct pollfd *_fds,nfds_t _nfds,int _timeout){
|
|
struct timeval tv;
|
|
fd_set ifds;
|
|
fd_set ofds;
|
|
fd_set efds;
|
|
nfds_t i;
|
|
int ret;
|
|
FD_ZERO(&ifds);
|
|
FD_ZERO(&ofds);
|
|
FD_ZERO(&efds);
|
|
for(i=0;i<_nfds;i++){
|
|
_fds[i].revents=0;
|
|
if(_fds[i].events&POLLIN)FD_SET(_fds[i].fd,&ifds);
|
|
if(_fds[i].events&POLLOUT)FD_SET(_fds[i].fd,&ofds);
|
|
FD_SET(_fds[i].fd,&efds);
|
|
}
|
|
if(_timeout>=0){
|
|
tv.tv_sec=_timeout/1000;
|
|
tv.tv_usec=(_timeout%1000)*1000;
|
|
}
|
|
ret=select(-1,&ifds,&ofds,&efds,_timeout<0?NULL:&tv);
|
|
if(ret>0){
|
|
for(i=0;i<_nfds;i++){
|
|
if(FD_ISSET(_fds[i].fd,&ifds))_fds[i].revents|=POLLIN;
|
|
if(FD_ISSET(_fds[i].fd,&ofds))_fds[i].revents|=POLLOUT;
|
|
/*This isn't correct: there are several different things that might have
|
|
happened to a fd in efds, but I don't know a good way to distinguish
|
|
them without more context from the caller.
|
|
It's okay, because we don't actually check any of these bits, we just
|
|
need _some_ bit set.*/
|
|
if(FD_ISSET(_fds[i].fd,&efds))_fds[i].revents|=POLLHUP;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/*We define op_errno() to make it clear that it's not an l-value like normal
|
|
errno is.*/
|
|
# define op_errno() (WSAGetLastError()?WSAGetLastError()-WSABASEERR:0)
|
|
# define op_reset_errno() (WSASetLastError(0))
|
|
|
|
/*The remaining functions don't get an op_ prefix even though they only
|
|
operate on sockets, because we don't use non-socket I/O here, and this
|
|
minimizes the changes needed to deal with Winsock.*/
|
|
# define close(_fd) closesocket(_fd)
|
|
/*This relies on sizeof(u_long)==sizeof(int), which is always true on both
|
|
Win32 and Win64.*/
|
|
# define ioctl(_fd,_req,_arg) ioctlsocket(_fd,_req,(u_long *)(_arg))
|
|
# define getsockopt(_fd,_level,_name,_val,_len) \
|
|
getsockopt(_fd,_level,_name,(char *)(_val),_len)
|
|
# define setsockopt(_fd,_level,_name,_val,_len) \
|
|
setsockopt(_fd,_level,_name,(const char *)(_val),_len)
|
|
# define poll(_fds,_nfds,_timeout) op_poll_win32(_fds,_nfds,_timeout)
|
|
|
|
# if defined(_MSC_VER)
|
|
typedef ptrdiff_t ssize_t;
|
|
# endif
|
|
|
|
/*Load certificates from the built-in certificate store.*/
|
|
int SSL_CTX_set_default_verify_paths_win32(SSL_CTX *_ssl_ctx);
|
|
# define SSL_CTX_set_default_verify_paths \
|
|
SSL_CTX_set_default_verify_paths_win32
|
|
|
|
# else
|
|
/*Normal Berkeley sockets.*/
|
|
# include <sys/ioctl.h>
|
|
# include <sys/types.h>
|
|
# include <sys/socket.h>
|
|
# include <arpa/inet.h>
|
|
# include <netinet/in.h>
|
|
# include <netinet/tcp.h>
|
|
# include <fcntl.h>
|
|
# include <netdb.h>
|
|
# include <poll.h>
|
|
# include <unistd.h>
|
|
# include <openssl/ssl.h>
|
|
|
|
typedef int op_sock;
|
|
|
|
# define OP_INVALID_SOCKET (-1)
|
|
|
|
# define op_errno() (errno)
|
|
# define op_reset_errno() (errno=0)
|
|
|
|
# endif
|
|
# include <sys/timeb.h>
|
|
# include <openssl/x509v3.h>
|
|
|
|
/*The maximum number of simultaneous connections.
|
|
RFC 2616 says this SHOULD NOT be more than 2, but everyone on the modern web
|
|
ignores that (e.g., IE 8 bumped theirs up from 2 to 6, Firefox uses 15).
|
|
If it makes you feel better, we'll only ever actively read from one of these
|
|
at a time.
|
|
The others are kept around mainly to avoid slow-starting a new connection
|
|
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
|
|
SHOULD detect infinite redirection loops."
|
|
Fortunately, 20 is less than infinity.*/
|
|
# define OP_REDIRECT_LIMIT (20)
|
|
|
|
/*The initial size of the buffer used to read a response message (before the
|
|
body).*/
|
|
# define OP_RESPONSE_SIZE_MIN (510)
|
|
/*The maximum size of a response message (before the body).
|
|
Responses larger than this will be discarded.
|
|
I've seen a real server return 20 kB of data for a 302 Found response.
|
|
Increasing this beyond 32kB will cause problems on platforms with a 16-bit
|
|
int.*/
|
|
# define OP_RESPONSE_SIZE_MAX (32766)
|
|
|
|
/*The number of milliseconds we will allow a connection to sit idle before we
|
|
refuse to resurrect it.
|
|
Apache as of 2.2 has reduced its default timeout to 5 seconds (from 15), so
|
|
that's what we'll use here.*/
|
|
# define OP_CONNECTION_IDLE_TIMEOUT_MS (5*1000)
|
|
|
|
/*The number of milliseconds we will wait to send or receive data before giving
|
|
up.*/
|
|
# define OP_POLL_TIMEOUT_MS (30*1000)
|
|
|
|
/*We will always attempt to read ahead at least this much in preference to
|
|
opening a new connection.*/
|
|
# define OP_READAHEAD_THRESH_MIN (32*(opus_int32)1024)
|
|
|
|
/*The amount of data to request after a seek.
|
|
This is a trade-off between read throughput after a seek vs. the the ability
|
|
to quickly perform another seek with the same connection.*/
|
|
# define OP_PIPELINE_CHUNK_SIZE (32*(opus_int32)1024)
|
|
/*Subsequent chunks are requested with larger and larger sizes until they pass
|
|
this threshold, after which we just ask for the rest of the resource.*/
|
|
# define OP_PIPELINE_CHUNK_SIZE_MAX (1024*(opus_int32)1024)
|
|
/*This is the maximum number of requests we'll make with a single connection.
|
|
Many servers will simply disconnect after we attempt some number of requests,
|
|
possibly without sending a Connection: close header, meaning we won't
|
|
discover it until we try to read beyond the end of the current chunk.
|
|
We can reconnect when that happens, but this is slow.
|
|
Instead, we impose a limit ourselves (set to the default for Apache
|
|
installations and thus likely the most common value in use).*/
|
|
# define OP_PIPELINE_MAX_REQUESTS (100)
|
|
/*This should be the number of requests, starting from a chunk size of
|
|
OP_PIPELINE_CHUNK_SIZE and doubling each time, until we exceed
|
|
OP_PIPELINE_CHUNK_SIZE_MAX and just request the rest of the file.
|
|
We won't reuse a connection when seeking unless it has at least this many
|
|
requests left, to reduce the chances we'll have to open a new connection
|
|
while reading forward afterwards.*/
|
|
# define OP_PIPELINE_MIN_REQUESTS (7)
|
|
|
|
/*Is this an https URL?
|
|
For now we can simply check the last letter of the scheme.*/
|
|
# define OP_URL_IS_SSL(_url) ((_url)->scheme[4]=='s')
|
|
|
|
/*Does this URL use the default port for its scheme?*/
|
|
# define OP_URL_IS_DEFAULT_PORT(_url) \
|
|
(!OP_URL_IS_SSL(_url)&&(_url)->port==80 \
|
|
||OP_URL_IS_SSL(_url)&&(_url)->port==443)
|
|
|
|
struct OpusParsedURL{
|
|
/*Either "http" or "https".*/
|
|
char *scheme;
|
|
/*The user name from the <userinfo> component, or NULL.*/
|
|
char *user;
|
|
/*The password from the <userinfo> component, or NULL.*/
|
|
char *pass;
|
|
/*The <host> component.
|
|
This may not be NULL.*/
|
|
char *host;
|
|
/*The <path> and <query> components.
|
|
This may not be NULL.*/
|
|
char *path;
|
|
/*The <port> component.
|
|
This is set to the default port if the URL did not contain one.*/
|
|
unsigned port;
|
|
};
|
|
|
|
/*Parse a URL.
|
|
This code is not meant to be fast: strspn() with large sets is likely to be
|
|
slow, but it is very convenient.
|
|
It is meant to be RFC 3986-compliant.
|
|
We currently do not support IRIs (Internationalized Resource Identifiers,
|
|
RFC 3987).
|
|
Callers should translate them to URIs first.*/
|
|
static int op_parse_url_impl(OpusParsedURL *_dst,const char *_src){
|
|
const char *scheme_end;
|
|
const char *authority;
|
|
const char *userinfo_end;
|
|
const char *user;
|
|
const char *user_end;
|
|
const char *pass;
|
|
const char *hostport;
|
|
const char *hostport_end;
|
|
const char *host_end;
|
|
const char *port;
|
|
opus_int32 port_num;
|
|
const char *port_end;
|
|
const char *path;
|
|
const char *path_end;
|
|
const char *uri_end;
|
|
scheme_end=_src+strspn(_src,OP_URL_SCHEME);
|
|
if(OP_UNLIKELY(*scheme_end!=':')
|
|
||OP_UNLIKELY(scheme_end-_src<4)||OP_UNLIKELY(scheme_end-_src>5)
|
|
||OP_UNLIKELY(op_strncasecmp(_src,"https",scheme_end-_src)!=0)){
|
|
/*Unsupported protocol.*/
|
|
return OP_EIMPL;
|
|
}
|
|
if(OP_UNLIKELY(scheme_end[1]!='/')||OP_UNLIKELY(scheme_end[2]!='/')){
|
|
/*We require an <authority> component.*/
|
|
return OP_EINVAL;
|
|
}
|
|
authority=scheme_end+3;
|
|
/*Make sure all escape sequences are valid to simplify unescaping later.*/
|
|
if(OP_UNLIKELY(op_validate_url_escapes(authority)<0))return OP_EINVAL;
|
|
/*Look for a <userinfo> component.*/
|
|
userinfo_end=authority+strspn(authority,OP_URL_PCHAR_NA);
|
|
if(*userinfo_end=='@'){
|
|
/*Found one.*/
|
|
user=authority;
|
|
/*Look for a password (yes, clear-text passwords are deprecated, I know,
|
|
but what else are people supposed to use? use SSL if you care).*/
|
|
user_end=authority+strspn(authority,OP_URL_PCHAR_BASE);
|
|
if(*user_end==':')pass=user_end+1;
|
|
else pass=NULL;
|
|
hostport=userinfo_end+1;
|
|
}
|
|
else{
|
|
/*We shouldn't have to initialize user_end, but gcc is too dumb to figure
|
|
out that user!=NULL below means we didn't take this else branch.*/
|
|
user=user_end=NULL;
|
|
pass=NULL;
|
|
hostport=authority;
|
|
}
|
|
/*Try to figure out where the <host> component ends.*/
|
|
if(hostport[0]=='['){
|
|
hostport++;
|
|
/*We have an <IP-literal>, which can contain colons.*/
|
|
hostport_end=host_end=hostport+strspn(hostport,OP_URL_PCHAR_NA);
|
|
if(OP_UNLIKELY(*hostport_end++!=']'))return OP_EINVAL;
|
|
}
|
|
/*Currently we don't support IDNA (RFC 5894), because I don't want to deal
|
|
with the policy about which domains should not be internationalized to
|
|
avoid confusing similarities.
|
|
Give this API Punycode (RFC 3492) domain names instead.*/
|
|
else hostport_end=host_end=hostport+strspn(hostport,OP_URL_PCHAR_BASE);
|
|
/*TODO: Validate host.*/
|
|
/*Is there a port number?*/
|
|
port_num=-1;
|
|
if(*hostport_end==':'){
|
|
int i;
|
|
port=hostport_end+1;
|
|
port_end=port+strspn(port,OP_URL_DIGIT);
|
|
path=port_end;
|
|
/*Not part of RFC 3986, but require port numbers in the range 0...65535.*/
|
|
if(OP_LIKELY(port_end-port>0)){
|
|
while(*port=='0')port++;
|
|
if(OP_UNLIKELY(port_end-port>5))return OP_EINVAL;
|
|
port_num=0;
|
|
for(i=0;i<port_end-port;i++)port_num=port_num*10+port[i]-'0';
|
|
if(OP_UNLIKELY(port_num>65535))return OP_EINVAL;
|
|
}
|
|
}
|
|
else path=hostport_end;
|
|
path_end=path+strspn(path,OP_URL_PATH);
|
|
/*If the path is not empty, it must begin with a '/'.*/
|
|
if(OP_LIKELY(path_end>path)&&OP_UNLIKELY(path[0]!='/'))return OP_EINVAL;
|
|
/*Consume the <query> component, if any (right now we don't split this out
|
|
from the <path> component).*/
|
|
if(*path_end=='?')path_end=path_end+strspn(path_end,OP_URL_QUERY_FRAG);
|
|
/*Discard the <fragment> component, if any.
|
|
This doesn't get sent to the server.
|
|
Some day we should add support for Media Fragment URIs
|
|
<http://www.w3.org/TR/media-frags/>.*/
|
|
if(*path_end=='#')uri_end=path_end+1+strspn(path_end+1,OP_URL_QUERY_FRAG);
|
|
else uri_end=path_end;
|
|
/*If there's anything left, this was not a valid URL.*/
|
|
if(OP_UNLIKELY(*uri_end!='\0'))return OP_EINVAL;
|
|
_dst->scheme=op_string_range_dup(_src,scheme_end);
|
|
if(OP_UNLIKELY(_dst->scheme==NULL))return OP_EFAULT;
|
|
op_string_tolower(_dst->scheme);
|
|
if(user!=NULL){
|
|
_dst->user=op_string_range_dup(user,user_end);
|
|
if(OP_UNLIKELY(_dst->user==NULL))return OP_EFAULT;
|
|
op_unescape_url_component(_dst->user);
|
|
/*Unescaping might have created a ':' in the username.
|
|
That's not allowed by RFC 2617's Basic Authentication Scheme.*/
|
|
if(OP_UNLIKELY(strchr(_dst->user,':')!=NULL))return OP_EINVAL;
|
|
}
|
|
else _dst->user=NULL;
|
|
if(pass!=NULL){
|
|
_dst->pass=op_string_range_dup(pass,userinfo_end);
|
|
if(OP_UNLIKELY(_dst->pass==NULL))return OP_EFAULT;
|
|
op_unescape_url_component(_dst->pass);
|
|
}
|
|
else _dst->pass=NULL;
|
|
_dst->host=op_string_range_dup(hostport,host_end);
|
|
if(OP_UNLIKELY(_dst->host==NULL))return OP_EFAULT;
|
|
if(port_num<0){
|
|
if(_src[4]=='s')port_num=443;
|
|
else port_num=80;
|
|
}
|
|
_dst->port=(unsigned)port_num;
|
|
/*RFC 2616 says an empty <abs-path> component is equivalent to "/", and we
|
|
MUST use the latter in the Request-URI.
|
|
Reserve space for the slash here.*/
|
|
if(path==path_end||path[0]=='?')path--;
|
|
_dst->path=op_string_range_dup(path,path_end);
|
|
if(OP_UNLIKELY(_dst->path==NULL))return OP_EFAULT;
|
|
/*And force-set it here.*/
|
|
_dst->path[0]='/';
|
|
return 0;
|
|
}
|
|
|
|
static void op_parsed_url_init(OpusParsedURL *_url){
|
|
memset(_url,0,sizeof(*_url));
|
|
}
|
|
|
|
static void op_parsed_url_clear(OpusParsedURL *_url){
|
|
_ogg_free(_url->scheme);
|
|
_ogg_free(_url->user);
|
|
_ogg_free(_url->pass);
|
|
_ogg_free(_url->host);
|
|
_ogg_free(_url->path);
|
|
}
|
|
|
|
static int op_parse_url(OpusParsedURL *_dst,const char *_src){
|
|
OpusParsedURL url;
|
|
int ret;
|
|
op_parsed_url_init(&url);
|
|
ret=op_parse_url_impl(&url,_src);
|
|
if(OP_UNLIKELY(ret<0))op_parsed_url_clear(&url);
|
|
else *_dst=*&url;
|
|
return ret;
|
|
}
|
|
|
|
/*A buffer to hold growing strings.
|
|
The main purpose of this is to consolidate allocation checks and simplify
|
|
cleanup on a failed allocation.*/
|
|
struct OpusStringBuf{
|
|
char *buf;
|
|
int nbuf;
|
|
int cbuf;
|
|
};
|
|
|
|
static void op_sb_init(OpusStringBuf *_sb){
|
|
_sb->buf=NULL;
|
|
_sb->nbuf=0;
|
|
_sb->cbuf=0;
|
|
}
|
|
|
|
static void op_sb_clear(OpusStringBuf *_sb){
|
|
_ogg_free(_sb->buf);
|
|
}
|
|
|
|
/*Make sure we have room for at least _capacity characters (plus 1 more for the
|
|
terminating NUL).*/
|
|
static int op_sb_ensure_capacity(OpusStringBuf *_sb,int _capacity){
|
|
char *buf;
|
|
int cbuf;
|
|
buf=_sb->buf;
|
|
cbuf=_sb->cbuf;
|
|
if(_capacity>=cbuf-1){
|
|
if(OP_UNLIKELY(cbuf>INT_MAX-1>>1))return OP_EFAULT;
|
|
if(OP_UNLIKELY(_capacity>=INT_MAX-1))return OP_EFAULT;
|
|
cbuf=OP_MAX(2*cbuf+1,_capacity+1);
|
|
buf=_ogg_realloc(buf,sizeof(*buf)*cbuf);
|
|
if(OP_UNLIKELY(buf==NULL))return OP_EFAULT;
|
|
_sb->buf=buf;
|
|
_sb->cbuf=cbuf;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*Increase the capacity of the buffer, but not to more than _max_size
|
|
characters (plus 1 more for the terminating NUL).*/
|
|
static int op_sb_grow(OpusStringBuf *_sb,int _max_size){
|
|
char *buf;
|
|
int cbuf;
|
|
buf=_sb->buf;
|
|
cbuf=_sb->cbuf;
|
|
OP_ASSERT(_max_size<=INT_MAX-1);
|
|
cbuf=cbuf<=_max_size-1>>1?2*cbuf+1:_max_size+1;
|
|
buf=_ogg_realloc(buf,sizeof(*buf)*cbuf);
|
|
if(OP_UNLIKELY(buf==NULL))return OP_EFAULT;
|
|
_sb->buf=buf;
|
|
_sb->cbuf=cbuf;
|
|
return 0;
|
|
}
|
|
|
|
static int op_sb_append(OpusStringBuf *_sb,const char *_s,int _len){
|
|
char *buf;
|
|
int nbuf;
|
|
int ret;
|
|
nbuf=_sb->nbuf;
|
|
if(OP_UNLIKELY(nbuf>INT_MAX-_len))return OP_EFAULT;
|
|
ret=op_sb_ensure_capacity(_sb,nbuf+_len);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
buf=_sb->buf;
|
|
memcpy(buf+nbuf,_s,sizeof(*buf)*_len);
|
|
nbuf+=_len;
|
|
buf[nbuf]='\0';
|
|
_sb->nbuf=nbuf;
|
|
return 0;
|
|
}
|
|
|
|
static int op_sb_append_string(OpusStringBuf *_sb,const char *_s){
|
|
return op_sb_append(_sb,_s,strlen(_s));
|
|
}
|
|
|
|
static int op_sb_append_port(OpusStringBuf *_sb,unsigned _port){
|
|
char port_buf[7];
|
|
OP_ASSERT(_port<=65535U);
|
|
sprintf(port_buf,":%u",_port);
|
|
return op_sb_append_string(_sb,port_buf);
|
|
}
|
|
|
|
static int op_sb_append_nonnegative_int64(OpusStringBuf *_sb,opus_int64 _i){
|
|
char digit;
|
|
int nbuf_start;
|
|
int ret;
|
|
OP_ASSERT(_i>=0);
|
|
nbuf_start=_sb->nbuf;
|
|
ret=0;
|
|
do{
|
|
digit='0'+_i%10;
|
|
ret|=op_sb_append(_sb,&digit,1);
|
|
_i/=10;
|
|
}
|
|
while(_i>0);
|
|
if(OP_LIKELY(ret>=0)){
|
|
char *buf;
|
|
int nbuf_end;
|
|
buf=_sb->buf;
|
|
nbuf_end=_sb->nbuf-1;
|
|
/*We've added the digits backwards.
|
|
Reverse them.*/
|
|
while(nbuf_start<nbuf_end){
|
|
digit=buf[nbuf_start];
|
|
buf[nbuf_start]=buf[nbuf_end];
|
|
buf[nbuf_end]=digit;
|
|
nbuf_start++;
|
|
nbuf_end--;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static struct addrinfo *op_resolve(const char *_host,unsigned _port){
|
|
struct addrinfo *addrs;
|
|
struct addrinfo hints;
|
|
char service[6];
|
|
memset(&hints,0,sizeof(hints));
|
|
hints.ai_socktype=SOCK_STREAM;
|
|
#if !defined(_WIN32)
|
|
hints.ai_flags=AI_NUMERICSERV;
|
|
#endif
|
|
OP_ASSERT(_port<=65535U);
|
|
sprintf(service,"%u",_port);
|
|
if(OP_LIKELY(!getaddrinfo(_host,service,&hints,&addrs)))return addrs;
|
|
return NULL;
|
|
}
|
|
|
|
static int op_sock_set_nonblocking(op_sock _fd,int _nonblocking){
|
|
#if !defined(_WIN32)
|
|
int flags;
|
|
flags=fcntl(_fd,F_GETFL);
|
|
if(OP_UNLIKELY(flags<0))return flags;
|
|
if(_nonblocking)flags|=O_NONBLOCK;
|
|
else flags&=~O_NONBLOCK;
|
|
return fcntl(_fd,F_SETFL,flags);
|
|
#else
|
|
return ioctl(_fd,FIONBIO,&_nonblocking);
|
|
#endif
|
|
}
|
|
|
|
/*Disable/enable write coalescing if we can.
|
|
We always send whole requests at once and always parse the response headers
|
|
before sending another one, so normally write coalescing just causes added
|
|
delay.*/
|
|
static void op_sock_set_tcp_nodelay(op_sock _fd,int _nodelay){
|
|
# if defined(TCP_NODELAY)&&(defined(IPPROTO_TCP)||defined(SOL_TCP))
|
|
# if defined(IPPROTO_TCP)
|
|
# define OP_SO_LEVEL IPPROTO_TCP
|
|
# else
|
|
# define OP_SO_LEVEL SOL_TCP
|
|
# endif
|
|
/*It doesn't really matter if this call fails, but it would be interesting
|
|
to hit a case where it does.*/
|
|
OP_ALWAYS_TRUE(!setsockopt(_fd,OP_SO_LEVEL,TCP_NODELAY,
|
|
&_nodelay,sizeof(_nodelay)));
|
|
# endif
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
static void op_init_winsock(){
|
|
static LONG count;
|
|
static WSADATA wsadata;
|
|
if(InterlockedIncrement(&count)==1)WSAStartup(0x0202,&wsadata);
|
|
}
|
|
#endif
|
|
|
|
/*A single physical connection to an HTTP server.
|
|
We may have several of these open at once.*/
|
|
struct OpusHTTPConn{
|
|
/*The current position indicator for this connection.*/
|
|
opus_int64 pos;
|
|
/*The position where the current request will end, or -1 if we're reading
|
|
until EOF (an unseekable stream or the initial HTTP/1.0 request).*/
|
|
opus_int64 end_pos;
|
|
/*The position where next request we've sent will start, or -1 if we haven't
|
|
sent the next request yet.*/
|
|
opus_int64 next_pos;
|
|
/*The end of the next request or -1 if we requested the rest of the resource.
|
|
This is only set to a meaningful value if next_pos is not -1.*/
|
|
opus_int64 next_end;
|
|
/*The SSL connection, if this is https.*/
|
|
SSL *ssl_conn;
|
|
/*The next connection in either the LRU or free list.*/
|
|
OpusHTTPConn *next;
|
|
/*The last time we blocked for reading from this connection.*/
|
|
struct timeb read_time;
|
|
/*The number of bytes we've read since the last time we blocked.*/
|
|
opus_int64 read_bytes;
|
|
/*The estimated throughput of this connection, in bytes/s.*/
|
|
opus_int64 read_rate;
|
|
/*The socket we're reading from.*/
|
|
op_sock fd;
|
|
/*The number of remaining requests we are allowed on this connection.*/
|
|
int nrequests_left;
|
|
/*The chunk size to use for pipelining requests.*/
|
|
opus_int32 chunk_size;
|
|
};
|
|
|
|
static void op_http_conn_init(OpusHTTPConn *_conn){
|
|
_conn->next_pos=-1;
|
|
_conn->ssl_conn=NULL;
|
|
_conn->next=NULL;
|
|
_conn->fd=OP_INVALID_SOCKET;
|
|
}
|
|
|
|
static void op_http_conn_clear(OpusHTTPConn *_conn){
|
|
if(_conn->ssl_conn!=NULL)SSL_free(_conn->ssl_conn);
|
|
/*SSL frees the BIO for us.*/
|
|
if(_conn->fd!=OP_INVALID_SOCKET)close(_conn->fd);
|
|
}
|
|
|
|
/*The global stream state.*/
|
|
struct OpusHTTPStream{
|
|
/*The list of connections.*/
|
|
OpusHTTPConn conns[OP_NCONNS_MAX];
|
|
/*The context object used as a framework for TLS/SSL functions.*/
|
|
SSL_CTX *ssl_ctx;
|
|
/*The cached session to reuse for future connections.*/
|
|
SSL_SESSION *ssl_session;
|
|
/*The LRU list (ordered from MRU to LRU) of currently connected
|
|
connections.*/
|
|
OpusHTTPConn *lru_head;
|
|
/*The free list.*/
|
|
OpusHTTPConn *free_head;
|
|
/*The URL to connect to.*/
|
|
OpusParsedURL url;
|
|
/*Information about the address we connected to.*/
|
|
struct addrinfo addr_info;
|
|
/*The address we connected to.*/
|
|
union{
|
|
struct sockaddr s;
|
|
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.*/
|
|
OpusStringBuf proxy_connect;
|
|
/*A buffer used to receive the response headers.*/
|
|
OpusStringBuf response;
|
|
/*The Content-Length, if specified, or -1 otherwise.
|
|
This will always be specified for seekable streams.*/
|
|
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;
|
|
/*Whether or not the server supports range requests.*/
|
|
int seekable;
|
|
/*Whether or not the server supports HTTP/1.1 with persistent connections.*/
|
|
int pipeline;
|
|
/*Whether or not we should skip certificate checks.*/
|
|
int skip_certificate_check;
|
|
/*The offset of the tail of the request.
|
|
Only the offset in the Range: header appears after this, allowing us to
|
|
quickly edit the request to ask for a new range.*/
|
|
int request_tail;
|
|
/*The estimated time required to open a new connection, in milliseconds.*/
|
|
opus_int32 connect_rate;
|
|
};
|
|
|
|
static void op_http_stream_init(OpusHTTPStream *_stream){
|
|
OpusHTTPConn **pnext;
|
|
int ci;
|
|
pnext=&_stream->free_head;
|
|
for(ci=0;ci<OP_NCONNS_MAX;ci++){
|
|
op_http_conn_init(_stream->conns+ci);
|
|
*pnext=_stream->conns+ci;
|
|
pnext=&_stream->conns[ci].next;
|
|
}
|
|
_stream->ssl_ctx=NULL;
|
|
_stream->ssl_session=NULL;
|
|
_stream->lru_head=NULL;
|
|
op_parsed_url_init(&_stream->url);
|
|
op_sb_init(&_stream->request);
|
|
op_sb_init(&_stream->proxy_connect);
|
|
op_sb_init(&_stream->response);
|
|
_stream->connect_host=NULL;
|
|
_stream->seekable=0;
|
|
}
|
|
|
|
/*Close the connection and move it to the free list.
|
|
_stream: The stream containing the free list.
|
|
_conn: The connection to close.
|
|
_penxt: The linked-list pointer currently pointing to this connection.
|
|
_gracefully: Whether or not to shut down cleanly.*/
|
|
static void op_http_conn_close(OpusHTTPStream *_stream,OpusHTTPConn *_conn,
|
|
OpusHTTPConn **_pnext,int _gracefully){
|
|
/*If we don't shut down gracefully, the server MUST NOT re-use our session
|
|
according to RFC 2246, because it can't tell the difference between an
|
|
abrupt close and a truncation attack.
|
|
So we shut down gracefully if we can.
|
|
However, we will not wait if this would block (it's not worth the savings
|
|
from session resumption to do so).
|
|
Clients (that's us) MAY resume a TLS session that ended with an incomplete
|
|
close, according to RFC 2818, so there's no reason to make sure the server
|
|
shut things down gracefully.*/
|
|
if(_gracefully&&_conn->ssl_conn!=NULL)SSL_shutdown(_conn->ssl_conn);
|
|
op_http_conn_clear(_conn);
|
|
_conn->next_pos=-1;
|
|
_conn->ssl_conn=NULL;
|
|
_conn->fd=OP_INVALID_SOCKET;
|
|
OP_ASSERT(*_pnext==_conn);
|
|
*_pnext=_conn->next;
|
|
_conn->next=_stream->free_head;
|
|
_stream->free_head=_conn;
|
|
}
|
|
|
|
static void op_http_stream_clear(OpusHTTPStream *_stream){
|
|
while(_stream->lru_head!=NULL){
|
|
op_http_conn_close(_stream,_stream->lru_head,&_stream->lru_head,0);
|
|
}
|
|
if(_stream->ssl_session!=NULL)SSL_SESSION_free(_stream->ssl_session);
|
|
if(_stream->ssl_ctx!=NULL)SSL_CTX_free(_stream->ssl_ctx);
|
|
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);
|
|
}
|
|
|
|
static int op_http_conn_write_fully(OpusHTTPConn *_conn,
|
|
const char *_buf,int _buf_size){
|
|
struct pollfd fd;
|
|
SSL *ssl_conn;
|
|
fd.fd=_conn->fd;
|
|
ssl_conn=_conn->ssl_conn;
|
|
while(_buf_size>0){
|
|
int err;
|
|
if(ssl_conn!=NULL){
|
|
int ret;
|
|
ret=SSL_write(ssl_conn,_buf,_buf_size);
|
|
if(ret>0){
|
|
/*Wrote some data.*/
|
|
_buf+=ret;
|
|
_buf_size-=ret;
|
|
continue;
|
|
}
|
|
/*Connection closed.*/
|
|
else if(ret==0)return OP_FALSE;
|
|
err=SSL_get_error(ssl_conn,ret);
|
|
/*Yes, renegotiations can cause SSL_write() to block for reading.*/
|
|
if(err==SSL_ERROR_WANT_READ)fd.events=POLLIN;
|
|
else if(err==SSL_ERROR_WANT_WRITE)fd.events=POLLOUT;
|
|
else return OP_FALSE;
|
|
}
|
|
else{
|
|
ssize_t ret;
|
|
op_reset_errno();
|
|
ret=send(fd.fd,_buf,_buf_size,0);
|
|
if(ret>0){
|
|
_buf+=ret;
|
|
_buf_size-=ret;
|
|
continue;
|
|
}
|
|
err=op_errno();
|
|
if(err!=EAGAIN&&err!=EWOULDBLOCK)return OP_FALSE;
|
|
fd.events=POLLOUT;
|
|
}
|
|
if(poll(&fd,1,OP_POLL_TIMEOUT_MS)<=0)return OP_FALSE;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int op_http_conn_estimate_available(OpusHTTPConn *_conn){
|
|
int available;
|
|
int ret;
|
|
ret=ioctl(_conn->fd,FIONREAD,&available);
|
|
if(ret<0)available=0;
|
|
/*This requires the SSL read_ahead flag to be unset to work.
|
|
We ignore partial records as well as the protocol overhead for any pending
|
|
bytes.
|
|
This means we might return somewhat less than can truly be read without
|
|
blocking (if there's a partial record).
|
|
This is okay, because we're using this value to estimate network transfer
|
|
time, and we _have_ already received those bytes.
|
|
We also might return slightly more (due to protocol overhead), but that's
|
|
small enough that it probably doesn't matter.*/
|
|
if(_conn->ssl_conn!=NULL)available+=SSL_pending(_conn->ssl_conn);
|
|
return available;
|
|
}
|
|
|
|
static opus_int32 op_time_diff_ms(const struct timeb *_end,
|
|
const struct timeb *_start){
|
|
opus_int64 dtime;
|
|
dtime=_end->time-(opus_int64)_start->time;
|
|
OP_ASSERT(_end->millitm<1000);
|
|
OP_ASSERT(_start->millitm<1000);
|
|
if(OP_UNLIKELY(dtime>(OP_INT32_MAX-1000)/1000))return OP_INT32_MAX;
|
|
if(OP_UNLIKELY(dtime<(OP_INT32_MIN+1000)/1000))return OP_INT32_MIN;
|
|
return (opus_int32)dtime*1000+_end->millitm-_start->millitm;
|
|
}
|
|
|
|
/*Update the read rate estimate for this connection.*/
|
|
static void op_http_conn_read_rate_update(OpusHTTPConn *_conn){
|
|
struct timeb read_time;
|
|
opus_int32 read_delta_ms;
|
|
opus_int64 read_delta_bytes;
|
|
opus_int64 read_rate;
|
|
read_delta_bytes=_conn->read_bytes;
|
|
if(read_delta_bytes<=0)return;
|
|
ftime(&read_time);
|
|
read_delta_ms=op_time_diff_ms(&read_time,&_conn->read_time);
|
|
read_rate=_conn->read_rate;
|
|
read_delta_ms=OP_MAX(read_delta_ms,1);
|
|
read_rate+=read_delta_bytes*1000/read_delta_ms-read_rate+4>>3;
|
|
*&_conn->read_time=*&read_time;
|
|
_conn->read_bytes=0;
|
|
_conn->read_rate=read_rate;
|
|
}
|
|
|
|
/*Tries to read from the given connection.
|
|
[out] _buf: Returns the data read.
|
|
_buf_size: The size of the buffer.
|
|
_blocking: Whether or not to block until some data is retrieved.
|
|
Return: A positive number of bytes read on success.
|
|
0: The read would block, or the connection was closed.
|
|
OP_EREAD: There was a fatal read error.*/
|
|
static int op_http_conn_read(OpusHTTPConn *_conn,
|
|
char *_buf,int _buf_size,int _blocking){
|
|
struct pollfd fd;
|
|
SSL *ssl_conn;
|
|
int nread;
|
|
int nread_unblocked;
|
|
fd.fd=_conn->fd;
|
|
ssl_conn=_conn->ssl_conn;
|
|
nread=nread_unblocked=0;
|
|
/*RFC 2818 says "client implementations MUST treat any premature closes as
|
|
errors and the data received as potentially truncated," so we make very
|
|
sure to report read errors upwards.*/
|
|
do{
|
|
int err;
|
|
if(ssl_conn!=NULL){
|
|
int ret;
|
|
ret=SSL_read(ssl_conn,_buf+nread,_buf_size-nread);
|
|
OP_ASSERT(ret<=_buf_size-nread);
|
|
if(ret>0){
|
|
/*Read some data.
|
|
Keep going to see if there's more.*/
|
|
nread+=ret;
|
|
nread_unblocked+=ret;
|
|
continue;
|
|
}
|
|
/*If we already read some data, return it right now.*/
|
|
if(nread>0)break;
|
|
err=SSL_get_error(ssl_conn,ret);
|
|
if(ret==0){
|
|
/*Connection close.
|
|
Check for a clean shutdown to prevent truncation attacks.
|
|
This check always succeeds for SSLv2, as it has no "close notify"
|
|
message and thus can't verify an orderly shutdown.*/
|
|
return err==SSL_ERROR_ZERO_RETURN?0:OP_EREAD;
|
|
}
|
|
if(err==SSL_ERROR_WANT_READ)fd.events=POLLIN;
|
|
/*Yes, renegotiations can cause SSL_read() to block for writing.*/
|
|
else if(err==SSL_ERROR_WANT_WRITE)fd.events=POLLOUT;
|
|
/*Some other error.*/
|
|
else return OP_EREAD;
|
|
}
|
|
else{
|
|
ssize_t ret;
|
|
op_reset_errno();
|
|
ret=recv(fd.fd,_buf+nread,_buf_size-nread,0);
|
|
OP_ASSERT(ret<=_buf_size-nread);
|
|
if(ret>0){
|
|
/*Read some data.
|
|
Keep going to see if there's more.*/
|
|
nread+=ret;
|
|
nread_unblocked+=ret;
|
|
continue;
|
|
}
|
|
/*If we already read some data or the connection was closed, return
|
|
right now.*/
|
|
if(ret==0||nread>0)break;
|
|
err=op_errno();
|
|
if(err!=EAGAIN&&err!=EWOULDBLOCK)return OP_EREAD;
|
|
fd.events=POLLIN;
|
|
}
|
|
_conn->read_bytes+=nread_unblocked;
|
|
op_http_conn_read_rate_update(_conn);
|
|
nread_unblocked=0;
|
|
if(!_blocking)break;
|
|
/*Need to wait to get any data at all.*/
|
|
if(poll(&fd,1,OP_POLL_TIMEOUT_MS)<=0)return OP_EREAD;
|
|
}
|
|
while(nread<_buf_size);
|
|
_conn->read_bytes+=nread_unblocked;
|
|
return nread;
|
|
}
|
|
|
|
/*Tries to look at the pending data for a connection without consuming it.
|
|
[out] _buf: Returns the data at which we're peeking.
|
|
_buf_size: The size of the buffer.*/
|
|
static int op_http_conn_peek(OpusHTTPConn *_conn,char *_buf,int _buf_size){
|
|
struct pollfd fd;
|
|
SSL *ssl_conn;
|
|
int ret;
|
|
fd.fd=_conn->fd;
|
|
ssl_conn=_conn->ssl_conn;
|
|
for(;;){
|
|
int err;
|
|
if(ssl_conn!=NULL){
|
|
ret=SSL_peek(ssl_conn,_buf,_buf_size);
|
|
/*Either saw some data or the connection was closed.*/
|
|
if(ret>=0)return ret;
|
|
err=SSL_get_error(ssl_conn,ret);
|
|
if(err==SSL_ERROR_WANT_READ)fd.events=POLLIN;
|
|
/*Yes, renegotiations can cause SSL_peek() to block for writing.*/
|
|
else if(err==SSL_ERROR_WANT_WRITE)fd.events=POLLOUT;
|
|
else return 0;
|
|
}
|
|
else{
|
|
op_reset_errno();
|
|
ret=(int)recv(fd.fd,_buf,_buf_size,MSG_PEEK);
|
|
/*Either saw some data or the connection was closed.*/
|
|
if(ret>=0)return ret;
|
|
err=op_errno();
|
|
if(err!=EAGAIN&&err!=EWOULDBLOCK)return 0;
|
|
fd.events=POLLIN;
|
|
}
|
|
/*Need to wait to get any data at all.*/
|
|
if(poll(&fd,1,OP_POLL_TIMEOUT_MS)<=0)return 0;
|
|
}
|
|
}
|
|
|
|
/*When parsing response headers, RFC 2616 mandates that all lines end in CR LF.
|
|
However, even in the year 2012, I have seen broken servers use just a LF.
|
|
This is the evil that Postel's advice from RFC 761 breeds.*/
|
|
|
|
/*Reads the entirety of a response to an HTTP request into the response buffer.
|
|
Actual parsing and validation is done later.
|
|
Return: The number of bytes in the response on success, OP_EREAD if the
|
|
connection was closed before reading any data, or another negative
|
|
value on any other error.*/
|
|
static int op_http_conn_read_response(OpusHTTPConn *_conn,
|
|
OpusStringBuf *_response){
|
|
int ret;
|
|
_response->nbuf=0;
|
|
ret=op_sb_ensure_capacity(_response,OP_RESPONSE_SIZE_MIN);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
for(;;){
|
|
char *buf;
|
|
int size;
|
|
int capacity;
|
|
int read_limit;
|
|
int terminated;
|
|
size=_response->nbuf;
|
|
capacity=_response->cbuf-1;
|
|
if(OP_UNLIKELY(size>=capacity)){
|
|
ret=op_sb_grow(_response,OP_RESPONSE_SIZE_MAX);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
capacity=_response->cbuf-1;
|
|
/*The response was too large.
|
|
This prevents a bad server from running us out of memory.*/
|
|
if(OP_UNLIKELY(size>=capacity))return OP_EIMPL;
|
|
}
|
|
buf=_response->buf;
|
|
ret=op_http_conn_peek(_conn,buf+size,capacity-size);
|
|
if(OP_UNLIKELY(ret<=0))return size<=0?OP_EREAD:OP_FALSE;
|
|
/*We read some data.*/
|
|
/*Make sure the starting characters are "HTTP".
|
|
Otherwise we could wind up waiting forever for a response from
|
|
something that is not an HTTP server.*/
|
|
if(size<4&&op_strncasecmp(buf,"HTTP",OP_MIN(size+ret,4))!=0){
|
|
return OP_FALSE;
|
|
}
|
|
/*How far can we read without passing the "\r\n\r\n" terminator?*/
|
|
buf[size+ret]='\0';
|
|
terminated=0;
|
|
for(read_limit=OP_MAX(size-3,0);read_limit<size+ret;read_limit++){
|
|
/*We don't look for the leading '\r' thanks to broken servers.*/
|
|
if(buf[read_limit]=='\n'){
|
|
if(buf[read_limit+1]=='\r'&&OP_LIKELY(buf[read_limit+2]=='\n')){
|
|
terminated=3;
|
|
break;
|
|
}
|
|
/*This case is for broken servers.*/
|
|
else if(OP_UNLIKELY(buf[read_limit+1]=='\n')){
|
|
terminated=2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
read_limit+=terminated;
|
|
OP_ASSERT(size<=read_limit);
|
|
OP_ASSERT(read_limit<=size+ret);
|
|
/*Actually consume that data.*/
|
|
ret=op_http_conn_read(_conn,buf+size,read_limit-size,1);
|
|
if(OP_UNLIKELY(ret<=0))return OP_FALSE;
|
|
size+=ret;
|
|
buf[size]='\0';
|
|
_response->nbuf=size;
|
|
/*We found the terminator and read all the data up to and including it.*/
|
|
if(terminated&&OP_LIKELY(size>=read_limit))return size;
|
|
}
|
|
return OP_EIMPL;
|
|
}
|
|
|
|
# define OP_HTTP_DIGIT "0123456789"
|
|
|
|
/*The Reason-Phrase is not allowed to contain control characters, except
|
|
horizontal tab (HT: \011).*/
|
|
# define OP_HTTP_CREASON_PHRASE \
|
|
"\001\002\003\004\005\006\007\010\012\013\014\015\016\017\020\021" \
|
|
"\022\023\024\025\026\027\030\031\032\033\034\035\036\037\177"
|
|
|
|
# define OP_HTTP_CTLS \
|
|
"\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020" \
|
|
"\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037\177"
|
|
|
|
/*This also includes '\t', but we get that from OP_HTTP_CTLS.*/
|
|
# define OP_HTTP_SEPARATORS " \"(),/:;<=>?@[\\]{}"
|
|
|
|
/*TEXT can also include LWS, but that has structure, so we parse it
|
|
separately.*/
|
|
# define OP_HTTP_CTOKEN OP_HTTP_CTLS OP_HTTP_SEPARATORS
|
|
|
|
/*Return: The amount of linear white space (LWS) at the start of _s.*/
|
|
static int op_http_lwsspn(const char *_s){
|
|
int i;
|
|
for(i=0;;){
|
|
if(_s[0]=='\r'&&_s[1]=='\n'&&(_s[2]=='\t'||_s[2]==' '))i+=3;
|
|
/*This case is for broken servers.*/
|
|
else if(_s[0]=='\n'&&(_s[1]=='\t'||_s[1]==' '))i+=2;
|
|
else if(_s[i]=='\t'||_s[i]==' ')i++;
|
|
else return i;
|
|
}
|
|
}
|
|
|
|
static char *op_http_parse_status_line(int *_v1_1_compat,
|
|
char **_status_code,char *_response){
|
|
char *next;
|
|
char *status_code;
|
|
int v1_1_compat;
|
|
size_t d;
|
|
/*RFC 2616 Section 6.1 does not say that the tokens in the Status-Line cannot
|
|
be separated by optional LWS, but since it specifically calls out where
|
|
spaces are to be placed and that CR and LF are not allowed except at the
|
|
end, I am assuming this to be true.*/
|
|
/*We already validated that this starts with "HTTP"*/
|
|
OP_ASSERT(op_strncasecmp(_response,"HTTP",4)==0);
|
|
next=_response+4;
|
|
if(OP_UNLIKELY(*next++!='/'))return NULL;
|
|
d=strspn(next,OP_HTTP_DIGIT);
|
|
/*"Leading zeros MUST be ignored by recipients."*/
|
|
while(*next=='0'){
|
|
next++;
|
|
OP_ASSERT(d>0);
|
|
d--;
|
|
}
|
|
/*We only support version 1.x*/
|
|
if(OP_UNLIKELY(d!=1)||OP_UNLIKELY(*next++!='1'))return NULL;
|
|
if(OP_UNLIKELY(*next++!='.'))return NULL;
|
|
d=strspn(next,OP_HTTP_DIGIT);
|
|
if(OP_UNLIKELY(d<=0))return NULL;
|
|
/*"Leading zeros MUST be ignored by recipients."*/
|
|
while(*next=='0'){
|
|
next++;
|
|
OP_ASSERT(d>0);
|
|
d--;
|
|
}
|
|
/*We don't need to parse the version number.
|
|
Any non-zero digit means it's greater than 1.*/
|
|
v1_1_compat=d>0;
|
|
next+=d;
|
|
if(OP_UNLIKELY(*next++!=' '))return NULL;
|
|
status_code=next;
|
|
d=strspn(next,OP_HTTP_DIGIT);
|
|
if(OP_UNLIKELY(d!=3))return NULL;
|
|
next+=d;
|
|
/*The Reason-Phrase can be empty, but the space must be here.*/
|
|
if(OP_UNLIKELY(*next++!=' '))return NULL;
|
|
next+=strcspn(next,OP_HTTP_CREASON_PHRASE);
|
|
/*We are not mandating this be present thanks to broken servers.*/
|
|
if(OP_LIKELY(*next=='\r'))next++;
|
|
if(OP_UNLIKELY(*next++!='\n'))return NULL;
|
|
if(_v1_1_compat!=NULL)*_v1_1_compat=v1_1_compat;
|
|
*_status_code=status_code;
|
|
return next;
|
|
}
|
|
|
|
/*Get the next response header.
|
|
[out] _header: The header token, NUL-terminated, with leading and trailing
|
|
whitespace stripped, and converted to lower case (to simplify
|
|
case-insensitive comparisons), or NULL if there are no more
|
|
response headers.
|
|
[out] _cdr: The remaining contents of the header, excluding the initial
|
|
colon (':') and the terminating CRLF ("\r\n"),
|
|
NUL-terminated, and with leading and trailing whitespace
|
|
stripped, or NULL if there are no more response headers.
|
|
[inout] _s: On input, this points to the start of the current line of the
|
|
response headers.
|
|
On output, it points to the start of the first line following
|
|
this header, or NULL if there are no more response headers.
|
|
Return: 0 on success, or a negative value on failure.*/
|
|
static int op_http_get_next_header(char **_header,char **_cdr,char **_s){
|
|
char *header;
|
|
char *header_end;
|
|
char *cdr;
|
|
char *cdr_end;
|
|
char *next;
|
|
size_t d;
|
|
next=*_s;
|
|
/*The second case is for broken servers.*/
|
|
if(next[0]=='\r'&&next[1]=='\n'||OP_UNLIKELY(next[0]=='\n')){
|
|
/*No more headers.*/
|
|
*_header=NULL;
|
|
*_cdr=NULL;
|
|
*_s=NULL;
|
|
return 0;
|
|
}
|
|
header=next+op_http_lwsspn(next);
|
|
d=strcspn(header,OP_HTTP_CTOKEN);
|
|
if(OP_UNLIKELY(d<=0))return OP_FALSE;
|
|
header_end=header+d;
|
|
next=header_end+op_http_lwsspn(header_end);
|
|
if(OP_UNLIKELY(*next++!=':'))return OP_FALSE;
|
|
next+=op_http_lwsspn(next);
|
|
cdr=next;
|
|
do{
|
|
cdr_end=next+strcspn(next,OP_HTTP_CTLS);
|
|
next=cdr_end+op_http_lwsspn(cdr_end);
|
|
}
|
|
while(next>cdr_end);
|
|
/*We are not mandating this be present thanks to broken servers.*/
|
|
if(OP_LIKELY(*next=='\r'))next++;
|
|
if(OP_UNLIKELY(*next++!='\n'))return OP_FALSE;
|
|
*header_end='\0';
|
|
*cdr_end='\0';
|
|
/*Field names are case-insensitive.*/
|
|
op_string_tolower(header);
|
|
*_header=header;
|
|
*_cdr=cdr;
|
|
*_s=next;
|
|
return 0;
|
|
}
|
|
|
|
static opus_int64 op_http_parse_nonnegative_int64(const char **_next,
|
|
const char *_cdr){
|
|
const char *next;
|
|
opus_int64 ret;
|
|
int i;
|
|
next=_cdr+strspn(_cdr,OP_HTTP_DIGIT);
|
|
*_next=next;
|
|
if(OP_UNLIKELY(next<=_cdr))return OP_FALSE;
|
|
while(*_cdr=='0')_cdr++;
|
|
if(OP_UNLIKELY(next-_cdr>19))return OP_EIMPL;
|
|
ret=0;
|
|
for(i=0;i<next-_cdr;i++){
|
|
int digit;
|
|
digit=_cdr[i]-'0';
|
|
/*Check for overflow.*/
|
|
if(OP_UNLIKELY(ret>(OP_INT64_MAX-9)/10+(digit<=7)))return OP_EIMPL;
|
|
ret=ret*10+digit;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static opus_int64 op_http_parse_content_length(const char *_cdr){
|
|
const char *next;
|
|
opus_int64 content_length;
|
|
content_length=op_http_parse_nonnegative_int64(&next,_cdr);
|
|
if(OP_UNLIKELY(*next!='\0'))return OP_FALSE;
|
|
return content_length;
|
|
}
|
|
|
|
static int op_http_parse_content_range(opus_int64 *_first,opus_int64 *_last,
|
|
opus_int64 *_length,const char *_cdr){
|
|
opus_int64 first;
|
|
opus_int64 last;
|
|
opus_int64 length;
|
|
size_t d;
|
|
if(OP_UNLIKELY(op_strncasecmp(_cdr,"bytes",5)!=0))return OP_FALSE;
|
|
_cdr+=5;
|
|
d=op_http_lwsspn(_cdr);
|
|
if(OP_UNLIKELY(d<=0))return OP_FALSE;
|
|
_cdr+=d;
|
|
if(*_cdr!='*'){
|
|
first=op_http_parse_nonnegative_int64(&_cdr,_cdr);
|
|
if(OP_UNLIKELY(first<0))return (int)first;
|
|
_cdr+=op_http_lwsspn(_cdr);
|
|
if(*_cdr++!='-')return OP_FALSE;
|
|
_cdr+=op_http_lwsspn(_cdr);
|
|
last=op_http_parse_nonnegative_int64(&_cdr,_cdr);
|
|
if(OP_UNLIKELY(last<0))return (int)last;
|
|
_cdr+=op_http_lwsspn(_cdr);
|
|
}
|
|
else{
|
|
/*This is for a 416 response (Requested range not satisfiable).*/
|
|
first=last=-1;
|
|
_cdr++;
|
|
}
|
|
if(OP_UNLIKELY(*_cdr++!='/'))return OP_FALSE;
|
|
if(*_cdr!='*'){
|
|
length=op_http_parse_nonnegative_int64(&_cdr,_cdr);
|
|
if(OP_UNLIKELY(length<0))return (int)length;
|
|
}
|
|
else{
|
|
/*The total length is unspecified.*/
|
|
_cdr++;
|
|
length=-1;
|
|
}
|
|
if(OP_UNLIKELY(*_cdr!='\0'))return OP_FALSE;
|
|
if(OP_UNLIKELY(last<first))return OP_FALSE;
|
|
if(length>=0&&OP_UNLIKELY(last>=length))return OP_FALSE;
|
|
*_first=first;
|
|
*_last=last;
|
|
*_length=length;
|
|
return 0;
|
|
}
|
|
|
|
/*Parse the Connection response header and look for a "close" token.
|
|
Return: 1 if a "close" token is found, 0 if it's not found, and a negative
|
|
value on error.*/
|
|
static int op_http_parse_connection(char *_cdr){
|
|
size_t d;
|
|
int ret;
|
|
ret=0;
|
|
for(;;){
|
|
d=strcspn(_cdr,OP_HTTP_CTOKEN);
|
|
if(OP_UNLIKELY(d<=0))return OP_FALSE;
|
|
if(op_strncasecmp(_cdr,"close",(int)d)==0)ret=1;
|
|
/*We're supposed to strip and ignore any headers mentioned in the
|
|
Connection header if this response is from an HTTP/1.0 server (to
|
|
work around forwarding of hop-by-hop headers by old proxies), but the
|
|
only hop-by-hop header we look at is Connection itself.
|
|
Everything else is a well-defined end-to-end header, and going back and
|
|
undoing the things we did based on already-examined headers would be
|
|
hard (since we only scan them once, in a destructive manner).
|
|
Therefore we just ignore all the other tokens.*/
|
|
_cdr+=d;
|
|
d=op_http_lwsspn(_cdr);
|
|
if(d<=0)break;
|
|
_cdr+=d;
|
|
}
|
|
return OP_UNLIKELY(*_cdr!='\0')?OP_FALSE:ret;
|
|
}
|
|
|
|
typedef int (*op_ssl_step_func)(SSL *_ssl_conn);
|
|
|
|
/*Try to run an SSL function to completion (blocking if necessary).*/
|
|
static int op_do_ssl_step(SSL *_ssl_conn,op_sock _fd,op_ssl_step_func _step){
|
|
struct pollfd fd;
|
|
fd.fd=_fd;
|
|
for(;;){
|
|
int ret;
|
|
int err;
|
|
ret=(*_step)(_ssl_conn);
|
|
if(ret>=0)return ret;
|
|
err=SSL_get_error(_ssl_conn,ret);
|
|
if(err==SSL_ERROR_WANT_READ)fd.events=POLLIN;
|
|
else if(err==SSL_ERROR_WANT_WRITE)fd.events=POLLOUT;
|
|
else return OP_FALSE;
|
|
if(poll(&fd,1,OP_POLL_TIMEOUT_MS)<=0)return OP_FALSE;
|
|
}
|
|
}
|
|
|
|
/*Implement a BIO type that just indicates every operation should be retried.
|
|
We use this when initializing an SSL connection via a proxy to allow the
|
|
initial handshake to proceed all the way up to the first read attempt, and
|
|
then return.
|
|
This allows the TLS client hello message to be pipelined with the HTTP
|
|
CONNECT request.*/
|
|
|
|
static int op_bio_retry_write(BIO *_b,const char *_buf,int _num){
|
|
(void)_buf;
|
|
(void)_num;
|
|
BIO_clear_retry_flags(_b);
|
|
BIO_set_retry_write(_b);
|
|
return -1;
|
|
}
|
|
|
|
static int op_bio_retry_read(BIO *_b,char *_buf,int _num){
|
|
(void)_buf;
|
|
(void)_num;
|
|
BIO_clear_retry_flags(_b);
|
|
BIO_set_retry_read(_b);
|
|
return -1;
|
|
}
|
|
|
|
static int op_bio_retry_puts(BIO *_b,const char *_str){
|
|
return op_bio_retry_write(_b,_str,0);
|
|
}
|
|
|
|
static long op_bio_retry_ctrl(BIO *_b,int _cmd,long _num,void *_ptr){
|
|
long ret;
|
|
(void)_b;
|
|
(void)_num;
|
|
(void)_ptr;
|
|
ret=0;
|
|
switch(_cmd){
|
|
case BIO_CTRL_RESET:
|
|
case BIO_C_RESET_READ_REQUEST:{
|
|
BIO_clear_retry_flags(_b);
|
|
/*Fall through.*/
|
|
}
|
|
case BIO_CTRL_EOF:
|
|
case BIO_CTRL_SET:
|
|
case BIO_CTRL_SET_CLOSE:
|
|
case BIO_CTRL_FLUSH:
|
|
case BIO_CTRL_DUP:{
|
|
ret=1;
|
|
}break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static int op_bio_retry_new(BIO *_b){
|
|
_b->init=1;
|
|
_b->num=0;
|
|
_b->ptr=NULL;
|
|
return 1;
|
|
}
|
|
|
|
static int op_bio_retry_free(BIO *_b){
|
|
return _b!=NULL;
|
|
}
|
|
|
|
/*This is not const because OpenSSL doesn't allow it, even though it won't
|
|
write to it.*/
|
|
static BIO_METHOD op_bio_retry_method={
|
|
BIO_TYPE_NULL,
|
|
"retry",
|
|
op_bio_retry_write,
|
|
op_bio_retry_read,
|
|
op_bio_retry_puts,
|
|
NULL,
|
|
op_bio_retry_ctrl,
|
|
op_bio_retry_new,
|
|
op_bio_retry_free,
|
|
NULL
|
|
};
|
|
|
|
/*Establish a CONNECT tunnel and pipeline the start of the TLS handshake for
|
|
proxying https URL requests.*/
|
|
static int op_http_conn_establish_tunnel(OpusHTTPStream *_stream,
|
|
OpusHTTPConn *_conn,op_sock _fd,SSL *_ssl_conn,BIO *_ssl_bio){
|
|
BIO *retry_bio;
|
|
char *status_code;
|
|
char *next;
|
|
int ret;
|
|
_conn->ssl_conn=NULL;
|
|
_conn->fd=_fd;
|
|
OP_ASSERT(_stream->proxy_connect.nbuf>0);
|
|
ret=op_http_conn_write_fully(_conn,
|
|
_stream->proxy_connect.buf,_stream->proxy_connect.nbuf);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
retry_bio=BIO_new(&op_bio_retry_method);
|
|
if(OP_UNLIKELY(retry_bio==NULL))return OP_EFAULT;
|
|
SSL_set_bio(_ssl_conn,retry_bio,_ssl_bio);
|
|
SSL_set_connect_state(_ssl_conn);
|
|
/*This shouldn't succeed, since we can't read yet.*/
|
|
OP_ALWAYS_TRUE(SSL_connect(_ssl_conn)<0);
|
|
SSL_set_bio(_ssl_conn,_ssl_bio,_ssl_bio);
|
|
/*Only now do we disable write coalescing, to allow the CONNECT
|
|
request and the start of the TLS handshake to be combined.*/
|
|
op_sock_set_tcp_nodelay(_fd,1);
|
|
ret=op_http_conn_read_response(_conn,&_stream->response);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
next=op_http_parse_status_line(NULL,&status_code,_stream->response.buf);
|
|
/*According to RFC 2817, "Any successful (2xx) response to a
|
|
CONNECT request indicates that the proxy has established a
|
|
connection to the requested host and port.*/
|
|
if(OP_UNLIKELY(next==NULL)||OP_UNLIKELY(status_code[0]!='2'))return OP_FALSE;
|
|
return 0;
|
|
}
|
|
|
|
/*Match a host name against a host with a possible wildcard pattern according
|
|
to the rules of RFC 6125 Section 6.4.3.
|
|
Return: 0 if the pattern doesn't match, and a non-zero value if it does.*/
|
|
static int op_http_hostname_match(const char *_host,size_t _host_len,
|
|
ASN1_STRING *_pattern){
|
|
const char *pattern;
|
|
size_t host_label_len;
|
|
size_t host_suffix_len;
|
|
size_t pattern_len;
|
|
size_t pattern_label_len;
|
|
size_t pattern_prefix_len;
|
|
size_t pattern_suffix_len;
|
|
pattern=(const char *)ASN1_STRING_data(_pattern);
|
|
pattern_len=strlen(pattern);
|
|
/*Check the pattern for embedded NULs.*/
|
|
if(OP_UNLIKELY(pattern_len!=(size_t)ASN1_STRING_length(_pattern)))return 0;
|
|
pattern_label_len=strcspn(pattern,".");
|
|
OP_ASSERT(pattern_label_len<=pattern_len);
|
|
pattern_prefix_len=strcspn(pattern,"*");
|
|
if(pattern_prefix_len>=pattern_label_len){
|
|
/*"The client SHOULD NOT attempt to match a presented identifier in which
|
|
the wildcard character comprises a label other than the left-most label
|
|
(e.g., do not match bar.*.example.net)." [RFC 6125 Section 6.4.3]*/
|
|
if(pattern_prefix_len<pattern_len)return 0;
|
|
/*If the pattern does not contain a wildcard in the first element, do an
|
|
exact match.
|
|
Don't use the system strcasecmp here, as that uses the locale and
|
|
RFC 4343 makes clear that DNS's case-insensitivity only applies to
|
|
the ASCII range.*/
|
|
return _host_len==pattern_len&&op_strncasecmp(_host,pattern,_host_len)==0;
|
|
}
|
|
/*"However, the client SHOULD NOT attempt to match a presented identifier
|
|
where the wildcard character is embedded within an A-label or U-label of
|
|
an internationalized domain name." [RFC 6125 Section 6.4.3]*/
|
|
if(op_strncasecmp(pattern,"xn--",4)==0)return 0;
|
|
host_label_len=strcspn(_host,".");
|
|
/*Make sure the host has at least two dots, to prevent the wildcard match
|
|
from being ridiculously wide.
|
|
We should have already checked to ensure it had at least one.*/
|
|
if(OP_UNLIKELY(_host[host_label_len]!='.')
|
|
||strchr(_host+host_label_len+1,'.')==NULL){
|
|
return 0;
|
|
}
|
|
OP_ASSERT(host_label_len<_host_len);
|
|
/*"If the wildcard character is the only character of the left-most label in
|
|
the presented identifier, the client SHOULD NOT compare against anything
|
|
but the left-most label of the reference identifier (e.g., *.example.com
|
|
would match foo.example.com but not bar.foo.example.com)." [RFC 6125
|
|
Section 6.4.3]
|
|
This is really confusingly worded, as we check this by actually comparing
|
|
the rest of the pattern for an exact match.
|
|
We also use the fact that the wildcard must match at least one character,
|
|
so the left-most label of the hostname must be at least as large as the
|
|
left-most label of the pattern.*/
|
|
if(host_label_len<pattern_label_len)return 0;
|
|
OP_ASSERT(pattern[pattern_prefix_len]=='*');
|
|
/*"The client MAY match a presented identifier in which the wildcard
|
|
character is not the only character of the label (e.g., baz*.example.net
|
|
and *baz.example.net and b*z.example.net would be taken to match
|
|
baz1.example.net and foobaz.example.net and buzz.example.net,
|
|
respectively)." [RFC 6125 Section 6.4.3]*/
|
|
pattern_suffix_len=pattern_len-pattern_prefix_len-1;
|
|
host_suffix_len=_host_len-host_label_len
|
|
+pattern_label_len-pattern_prefix_len-1;
|
|
return pattern_suffix_len==host_suffix_len
|
|
&&op_strncasecmp(_host,pattern,pattern_prefix_len)==0
|
|
&&op_strncasecmp(_host+_host_len-host_suffix_len,
|
|
pattern+pattern_prefix_len+1,host_suffix_len)==0;
|
|
}
|
|
|
|
/*Convert a host to a numeric address, if possible.
|
|
Return: A struct addrinfo containing the address, if it was numeric, and NULL
|
|
otherise.*/
|
|
static struct addrinfo *op_inet_pton(const char *_host){
|
|
struct addrinfo *addrs;
|
|
struct addrinfo hints;
|
|
memset(&hints,0,sizeof(hints));
|
|
hints.ai_socktype=SOCK_STREAM;
|
|
hints.ai_flags=AI_NUMERICHOST;
|
|
if(!getaddrinfo(_host,NULL,&hints,&addrs))return addrs;
|
|
return NULL;
|
|
}
|
|
|
|
/*Verify the server's hostname matches the certificate they presented using
|
|
the procedure from Section 6 of RFC 6125.
|
|
Return: 0 if the certificate doesn't match, and a non-zero value if it does.*/
|
|
static int op_http_verify_hostname(OpusHTTPStream *_stream,SSL *_ssl_conn){
|
|
X509 *peer_cert;
|
|
STACK_OF(GENERAL_NAME) *san_names;
|
|
char *host;
|
|
size_t host_len;
|
|
int ret;
|
|
host=_stream->url.host;
|
|
host_len=strlen(host);
|
|
peer_cert=SSL_get_peer_certificate(_ssl_conn);
|
|
/*We set VERIFY_PEER, so we shouldn't get here without a certificate.*/
|
|
if(OP_UNLIKELY(peer_cert==NULL))return 0;
|
|
ret=0;
|
|
OP_ASSERT(host_len<INT_MAX);
|
|
/*RFC 2818 says (after correcting for Eratta 1077): "If a subjectAltName
|
|
extension of type dNSName is present, that MUST be used as the identity.
|
|
Otherwise, the (most specific) Common Name field in the Subject field of
|
|
the certificate MUST be used.
|
|
Although the use of the Common Name is existing practice, it is deprecated
|
|
and Certification Authorities are encouraged to use the dNSName
|
|
instead."
|
|
"Matching is performed using the matching rules specified by RFC 2459.
|
|
If more than one identity of a given type is present in the certificate
|
|
(e.g., more than one dNSName name), a match in any one of the set is
|
|
considered acceptable.
|
|
Names may contain the wildcard character * which is condered to match any
|
|
single domain name component or component fragment.
|
|
E.g., *.a.com matches foo.a.com but not bar.foo.a.com.
|
|
f*.com matches foo.com but not bar.com."
|
|
"In some cases, the URI is specified as an IP address rather than a
|
|
hostname.
|
|
In this case, the iPAddress subjectAltName must be present in the
|
|
certificate and must exactly match the IP in the URI."*/
|
|
san_names=X509_get_ext_d2i(peer_cert,NID_subject_alt_name,NULL,NULL);
|
|
if(san_names!=NULL){
|
|
struct addrinfo *addr;
|
|
unsigned char *ip;
|
|
int ip_len;
|
|
int nsan_names;
|
|
int sni;
|
|
/*Check to see if the host was specified as a simple IP address.*/
|
|
addr=op_inet_pton(host);
|
|
ip=NULL;
|
|
ip_len=0;
|
|
if(addr!=NULL){
|
|
switch(addr->ai_family){
|
|
case AF_INET:{
|
|
struct sockaddr_in *s;
|
|
s=(struct sockaddr_in *)addr->ai_addr;
|
|
OP_ASSERT(addr->ai_addrlen>=sizeof(*s));
|
|
ip=(unsigned char *)&s->sin_addr;
|
|
ip_len=sizeof(s->sin_addr);
|
|
}break;
|
|
case AF_INET6:{
|
|
struct sockaddr_in6 *s;
|
|
s=(struct sockaddr_in6 *)addr->ai_addr;
|
|
OP_ASSERT(addr->ai_addrlen>=sizeof(*s));
|
|
ip=(unsigned char *)&s->sin6_addr;
|
|
ip_len=sizeof(s->sin6_addr);
|
|
}break;
|
|
}
|
|
}
|
|
/*We can only verify fully-qualified domain names.
|
|
To quote RFC 6125: "The extracted data MUST include only information that
|
|
can be securely parsed out of the inputs (e.g., parsing the fully
|
|
qualified DNS domain name out of the "host" component (or its
|
|
equivalent) of a URI or deriving the application service type from the
|
|
scheme of a URI) ..."
|
|
We don't have a way to check (without relying on DNS records, which might
|
|
be subverted) if this address is fully-qualified.
|
|
This is particularly problematic when using a CONNECT tunnel, as it is
|
|
the server that does DNS lookup, not us.
|
|
However, we are certain that if the hostname has no '.', it is definitely
|
|
not a fully-qualified domain name (with the exception of crazy TLDs that
|
|
actually resolve, like "uz", but I am willing to ignore those).
|
|
RFC 1535 says "...in any event where a '.' exists in a specified name it
|
|
should be assumed to be a fully qualified domain name (FQDN) and SHOULD
|
|
be tried as a rooted name first."
|
|
That doesn't give us any security guarantees, of course (a subverted DNS
|
|
could fail the original query and our resolver might still retry with a
|
|
local domain appended).
|
|
If we don't have a FQDN, just set the number of names to 0, so we'll fail
|
|
and clean up any resources we allocated.*/
|
|
if(ip==NULL&&strchr(host,'.')==NULL)nsan_names=0;
|
|
/*RFC 2459 says there MUST be at least one, but we don't depend on it.*/
|
|
else nsan_names=sk_GENERAL_NAME_num(san_names);
|
|
for(sni=0;sni<nsan_names;sni++){
|
|
const GENERAL_NAME *name;
|
|
name=sk_GENERAL_NAME_value(san_names,sni);
|
|
if(ip==NULL){
|
|
if(name->type==GEN_DNS
|
|
&&op_http_hostname_match(host,host_len,name->d.dNSName)){
|
|
ret=1;
|
|
break;
|
|
}
|
|
}
|
|
else if(name->type==GEN_IPADD){
|
|
unsigned char *cert_ip;
|
|
/*If we do have an IP address, compare it directly.
|
|
RFC 6125: "When the reference identity is an IP address, the identity
|
|
MUST be converted to the 'network byte order' octet string
|
|
representation.
|
|
For IP Version 4, as specified in RFC 791, the octet string will
|
|
contain exactly four octets.
|
|
For IP Version 6, as specified in RFC 2460, the octet string will
|
|
contain exactly sixteen octets.
|
|
This octet string is then compared against subjectAltName values of
|
|
type iPAddress.
|
|
A match occurs if the reference identity octet string and the value
|
|
octet strings are identical."*/
|
|
cert_ip=ASN1_STRING_data(name->d.iPAddress);
|
|
if(ip_len==ASN1_STRING_length(name->d.iPAddress)
|
|
&&memcmp(ip,cert_ip,ip_len)==0){
|
|
ret=1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
sk_GENERAL_NAME_pop_free(san_names,GENERAL_NAME_free);
|
|
if(addr!=NULL)freeaddrinfo(addr);
|
|
}
|
|
/*Do the same FQDN check we did above.
|
|
We don't do this once in advance for both cases, because in the
|
|
subjectAltName case we might have an IPv6 address without a dot.*/
|
|
else if(strchr(host,'.')!=NULL){
|
|
int last_cn_loc;
|
|
int cn_loc;
|
|
/*If there is no subjectAltName, match against commonName.
|
|
RFC 6125 says that at least one significant CA is known to issue certs
|
|
with multiple CNs, although it SHOULD NOT.
|
|
It also says: "The server's identity may also be verified by comparing
|
|
the reference identity to the Common Name (CN) value in the last
|
|
Relative Distinguished Name (RDN) of the subject field of the server's
|
|
certificate (where "last" refers to the DER-encoded order...)."
|
|
So find the last one and check it.*/
|
|
cn_loc=-1;
|
|
do{
|
|
last_cn_loc=cn_loc;
|
|
cn_loc=X509_NAME_get_index_by_NID(X509_get_subject_name(peer_cert),
|
|
NID_commonName,last_cn_loc);
|
|
}
|
|
while(cn_loc>=0);
|
|
ret=last_cn_loc>=0
|
|
&&op_http_hostname_match(host,host_len,
|
|
X509_NAME_ENTRY_get_data(
|
|
X509_NAME_get_entry(X509_get_subject_name(peer_cert),last_cn_loc)));
|
|
}
|
|
X509_free(peer_cert);
|
|
return ret;
|
|
}
|
|
|
|
/*Perform the TLS handshake on a new connection.*/
|
|
static int op_http_conn_start_tls(OpusHTTPStream *_stream,OpusHTTPConn *_conn,
|
|
op_sock _fd,SSL *_ssl_conn){
|
|
SSL_SESSION *ssl_session;
|
|
BIO *ssl_bio;
|
|
int skip_certificate_check;
|
|
int ret;
|
|
ssl_bio=BIO_new_socket(_fd,BIO_NOCLOSE);
|
|
if(OP_LIKELY(ssl_bio==NULL))return OP_FALSE;
|
|
# if !defined(OPENSSL_NO_TLSEXT)
|
|
/*Support for RFC 6066 Server Name Indication.*/
|
|
SSL_set_tlsext_host_name(_ssl_conn,_stream->url.host);
|
|
# endif
|
|
/*Resume a previous session if available.*/
|
|
if(_stream->ssl_session!=NULL){
|
|
SSL_set_session(_ssl_conn,_stream->ssl_session);
|
|
}
|
|
/*If we're proxying, establish the CONNECT tunnel.*/
|
|
if(_stream->proxy_connect.nbuf>0){
|
|
ret=op_http_conn_establish_tunnel(_stream,_conn,
|
|
_fd,_ssl_conn,ssl_bio);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
else{
|
|
/*Otherwise, just use this socket directly.*/
|
|
op_sock_set_tcp_nodelay(_fd,1);
|
|
SSL_set_bio(_ssl_conn,ssl_bio,ssl_bio);
|
|
SSL_set_connect_state(_ssl_conn);
|
|
}
|
|
ret=op_do_ssl_step(_ssl_conn,_fd,SSL_connect);
|
|
if(OP_UNLIKELY(ret<=0))return OP_FALSE;
|
|
ssl_session=_stream->ssl_session;
|
|
skip_certificate_check=_stream->skip_certificate_check;
|
|
if(ssl_session==NULL||!skip_certificate_check){
|
|
ret=op_do_ssl_step(_ssl_conn,_fd,SSL_do_handshake);
|
|
if(OP_UNLIKELY(ret<=0))return OP_FALSE;
|
|
/*OpenSSL does not do hostname verification, despite the fact that we just
|
|
passed it the hostname above in the call to SSL_set_tlsext_host_name(),
|
|
because they are morons.
|
|
Do it for them.*/
|
|
if(!skip_certificate_check&&!op_http_verify_hostname(_stream,_ssl_conn)){
|
|
return OP_FALSE;
|
|
}
|
|
if(ssl_session==NULL){
|
|
/*Save the session for later resumption.*/
|
|
_stream->ssl_session=SSL_get1_session(_ssl_conn);
|
|
}
|
|
}
|
|
_conn->ssl_conn=_ssl_conn;
|
|
_conn->fd=_fd;
|
|
_conn->nrequests_left=OP_PIPELINE_MAX_REQUESTS;
|
|
return 0;
|
|
}
|
|
|
|
/*Try to start a connection to the next address in the given list of a given
|
|
type.
|
|
_fd: The socket to connect with.
|
|
[inout] _addr: A pointer to the list of addresses.
|
|
This will be advanced to the first one that matches the given
|
|
address family (possibly the current one).
|
|
_ai_family: The address family to connect to.
|
|
Return: 1 If the connection was successful.
|
|
0 If the connection is in progress.
|
|
OP_FALSE If the connection failed and there were no more addresses
|
|
left to try.
|
|
*_addr will be set to NULL in this case.*/
|
|
static int op_sock_connect_next(op_sock _fd,
|
|
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.*/
|
|
for(;addr!=NULL&&addr->ai_family!=_ai_family;addr=addr->ai_next);
|
|
*_addr=addr;
|
|
/*No more: failure.*/
|
|
if(addr==NULL)return OP_FALSE;
|
|
if(connect(_fd,addr->ai_addr,addr->ai_addrlen)>=0)return 1;
|
|
err=op_errno();
|
|
/*Winsock will set WSAEWOULDBLOCK.*/
|
|
if(OP_LIKELY(err==EINPROGRESS||err==EWOULDBLOCK))return 0;
|
|
addr=addr->ai_next;
|
|
}
|
|
}
|
|
|
|
/*The number of address families to try connecting to simultaneously.*/
|
|
# define OP_NPROTOS (2)
|
|
|
|
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;
|
|
/*Try connecting via both IPv4 and IPv6 simultaneously, and keep the first
|
|
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));
|
|
OP_ASSERT(addr->ai_addrlen<=sizeof(struct sockaddr_in));
|
|
/*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;
|
|
}
|
|
}
|
|
/*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;
|
|
_conn->next=_stream->lru_head;
|
|
_stream->lru_head=_conn;
|
|
ftime(_start_time);
|
|
*&_conn->read_time=*_start_time;
|
|
_conn->read_bytes=0;
|
|
_conn->read_rate=0;
|
|
/*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);
|
|
fds[pi].events=POLLOUT;
|
|
if(OP_LIKELY(fds[pi].fd!=OP_INVALID_SOCKET)){
|
|
if(OP_LIKELY(op_sock_set_nonblocking(fds[pi].fd,1)>=0)){
|
|
ret=op_sock_connect_next(fds[pi].fd,addrs+pi,ai_family);
|
|
if(OP_UNLIKELY(ret>0)){
|
|
/*It succeeded right away (technically possible), so stop.*/
|
|
nprotos=pi+1;
|
|
break;
|
|
}
|
|
/*Otherwise go on to the next protocol, and skip the clean-up below.*/
|
|
else if(ret==0)continue;
|
|
/*Tried all the addresses for this protocol.*/
|
|
}
|
|
/*Clean up the socket.*/
|
|
close(fds[pi].fd);
|
|
}
|
|
/*Remove this protocol from the list.*/
|
|
memmove(addrs+pi,addrs+pi+1,sizeof(*addrs)*(nprotos-pi-1));
|
|
nprotos--;
|
|
pi--;
|
|
}
|
|
/*Wait for one of the connections to finish.*/
|
|
while(pi>=nprotos&&nprotos>0&&poll(fds,nprotos,OP_POLL_TIMEOUT_MS)>0){
|
|
for(pi=0;pi<nprotos;pi++){
|
|
socklen_t errlen;
|
|
int err;
|
|
/*Still waiting...*/
|
|
if(!fds[pi].revents)continue;
|
|
errlen=sizeof(err);
|
|
/*Some platforms will return the pending error in &err and return 0.
|
|
Others will put it in errno and return -1.*/
|
|
ret=getsockopt(fds[pi].fd,SOL_SOCKET,SO_ERROR,&err,&errlen);
|
|
if(ret<0)err=op_errno();
|
|
/*Success!*/
|
|
if(err==0||err==EISCONN)break;
|
|
/*Move on to the next address for this protocol.*/
|
|
ai_family=addrs[pi]->ai_family;
|
|
addrs[pi]=addrs[pi]->ai_next;
|
|
ret=op_sock_connect_next(fds[pi].fd,addrs+pi,ai_family);
|
|
/*It succeeded right away, so stop.*/
|
|
if(ret>0)break;
|
|
/*Otherwise go on to the next protocol, and skip the clean-up below.*/
|
|
else if(ret==0)continue;
|
|
/*Tried all the addresses for this protocol.
|
|
Remove it from the list.*/
|
|
close(fds[pi].fd);
|
|
memmove(fds+pi,fds+pi+1,sizeof(*fds)*(nprotos-pi-1));
|
|
memmove(addrs+pi,addrs+pi+1,sizeof(*addrs)*(nprotos-pi-1));
|
|
nprotos--;
|
|
pi--;
|
|
}
|
|
}
|
|
/*Close all the other sockets.*/
|
|
for(pj=0;pj<nprotos;pj++)if(pi!=pj)close(fds[pj].fd);
|
|
/*If none of them succeeded, we're done.*/
|
|
if(pi>=nprotos)return OP_FALSE;
|
|
/*Save this address for future connection attempts.*/
|
|
if(addrs[pi]!=&_stream->addr_info){
|
|
memcpy(&_stream->addr_info,addrs[pi],sizeof(_stream->addr_info));
|
|
_stream->addr_info.ai_addr=&_stream->addr.s;
|
|
_stream->addr_info.ai_next=NULL;
|
|
memcpy(&_stream->addr,addrs[pi]->ai_addr,addrs[pi]->ai_addrlen);
|
|
}
|
|
if(OP_URL_IS_SSL(&_stream->url)){
|
|
SSL *ssl_conn;
|
|
/*Start the SSL connection.*/
|
|
OP_ASSERT(_stream->ssl_ctx!=NULL);
|
|
ssl_conn=SSL_new(_stream->ssl_ctx);
|
|
if(OP_LIKELY(ssl_conn!=NULL)){
|
|
ret=op_http_conn_start_tls(_stream,_conn,fds[pi].fd,ssl_conn);
|
|
if(OP_LIKELY(ret>=0))return ret;
|
|
SSL_free(ssl_conn);
|
|
}
|
|
close(fds[pi].fd);
|
|
_conn->fd=OP_INVALID_SOCKET;
|
|
return OP_FALSE;
|
|
}
|
|
/*Just a normal non-SSL connection.*/
|
|
_conn->ssl_conn=NULL;
|
|
_conn->fd=fds[pi].fd;
|
|
_conn->nrequests_left=OP_PIPELINE_MAX_REQUESTS;
|
|
/*Disable write coalescing.
|
|
We always send whole requests at once and always parse the response headers
|
|
before sending another one.*/
|
|
op_sock_set_tcp_nodelay(fds[pi].fd,1);
|
|
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]={
|
|
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
|
|
'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
|
|
'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
|
|
'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
|
|
};
|
|
|
|
static char *op_base64_encode(char *_dst,const char *_src,int _len){
|
|
unsigned s0;
|
|
unsigned s1;
|
|
unsigned s2;
|
|
int ngroups;
|
|
int i;
|
|
ngroups=_len/3;
|
|
for(i=0;i<ngroups;i++){
|
|
s0=_src[3*i+0];
|
|
s1=_src[3*i+1];
|
|
s2=_src[3*i+2];
|
|
_dst[4*i+0]=BASE64_TABLE[s0>>2];
|
|
_dst[4*i+1]=BASE64_TABLE[(s0&3)<<4|s1>>4];
|
|
_dst[4*i+2]=BASE64_TABLE[(s1&15)<<2|s2>>6];
|
|
_dst[4*i+3]=BASE64_TABLE[s2&63];
|
|
}
|
|
_len-=3*i;
|
|
if(_len==1){
|
|
s0=_src[3*i+0];
|
|
_dst[4*i+0]=BASE64_TABLE[s0>>2];
|
|
_dst[4*i+1]=BASE64_TABLE[(s0&3)<<4];
|
|
_dst[4*i+2]='=';
|
|
_dst[4*i+3]='=';
|
|
i++;
|
|
}
|
|
else if(_len==2){
|
|
s0=_src[3*i+0];
|
|
s1=_src[3*i+1];
|
|
_dst[4*i+0]=BASE64_TABLE[s0>>2];
|
|
_dst[4*i+1]=BASE64_TABLE[(s0&3)<<4|s1>>4];
|
|
_dst[4*i+2]=BASE64_TABLE[(s1&15)<<2];
|
|
_dst[4*i+3]='=';
|
|
i++;
|
|
}
|
|
_dst[4*i]='\0';
|
|
return _dst+4*i;
|
|
}
|
|
|
|
/*Construct an HTTP authorization header using RFC 2617's Basic Authentication
|
|
Scheme and append it to the given string buffer.*/
|
|
static int op_sb_append_basic_auth_header(OpusStringBuf *_sb,
|
|
const char *_header,const char *_user,const char *_pass){
|
|
int user_len;
|
|
int pass_len;
|
|
int user_pass_len;
|
|
int base64_len;
|
|
int nbuf_total;
|
|
int ret;
|
|
ret=op_sb_append_string(_sb,_header);
|
|
ret|=op_sb_append(_sb,": Basic ",8);
|
|
user_len=strlen(_user);
|
|
pass_len=strlen(_pass);
|
|
if(OP_UNLIKELY(pass_len>INT_MAX-user_len))return OP_EFAULT;
|
|
if(OP_UNLIKELY(user_len+pass_len>(INT_MAX>>2)*3-3))return OP_EFAULT;
|
|
user_pass_len=user_len+1+pass_len;
|
|
base64_len=OP_BASE64_LENGTH(user_pass_len);
|
|
/*Stick "user:pass" at the end of the buffer so we can Base64 encode it
|
|
in-place.*/
|
|
nbuf_total=_sb->nbuf;
|
|
if(OP_UNLIKELY(base64_len>INT_MAX-nbuf_total))return OP_EFAULT;
|
|
nbuf_total+=base64_len;
|
|
ret|=op_sb_ensure_capacity(_sb,nbuf_total);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
_sb->nbuf=nbuf_total-user_pass_len;
|
|
OP_ALWAYS_TRUE(!op_sb_append(_sb,_user,user_len));
|
|
OP_ALWAYS_TRUE(!op_sb_append(_sb,":",1));
|
|
OP_ALWAYS_TRUE(!op_sb_append(_sb,_pass,pass_len));
|
|
op_base64_encode(_sb->buf+nbuf_total-base64_len,
|
|
_sb->buf+nbuf_total-user_pass_len,user_pass_len);
|
|
return op_sb_append(_sb,"\r\n",2);
|
|
}
|
|
|
|
static int op_http_allow_pipelining(const char *_server){
|
|
/*Servers known to do bad things with pipelined requests.
|
|
This list is taken from Gecko's nsHttpConnection::SupportsPipelining() (in
|
|
netwerk/protocol/http/nsHttpConnection.cpp).*/
|
|
static const char *BAD_SERVERS[]={
|
|
"EFAServer/",
|
|
"Microsoft-IIS/4.",
|
|
"Microsoft-IIS/5.",
|
|
"Netscape-Enterprise/3.",
|
|
"Netscape-Enterprise/4.",
|
|
"Netscape-Enterprise/5.",
|
|
"Netscape-Enterprise/6.",
|
|
"WebLogic 3.",
|
|
"WebLogic 4.",
|
|
"WebLogic 5.",
|
|
"WebLogic 6.",
|
|
"Winstone Servlet Engine v0."
|
|
};
|
|
# define NBAD_SERVERS ((int)(sizeof(BAD_SERVERS)/sizeof(*BAD_SERVERS)))
|
|
if(*_server>='E'&&*_server<='W'){
|
|
int si;
|
|
for(si=0;si<NBAD_SERVERS;si++){
|
|
if(strncmp(_server,BAD_SERVERS[si],strlen(BAD_SERVERS[si]))==0){
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
return 1;
|
|
# undef NBAD_SERVERS
|
|
}
|
|
|
|
static int op_http_stream_open(OpusHTTPStream *_stream,const char *_url,
|
|
int _skip_certificate_check,const char *_proxy_host,unsigned _proxy_port,
|
|
const char *_proxy_user,const char *_proxy_pass,OpusServerInfo *_info){
|
|
struct addrinfo *addrs;
|
|
int nredirs;
|
|
int ret;
|
|
#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++){
|
|
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;
|
|
# if !defined(OPENSSL_NO_LOCKING)
|
|
/*The documentation says SSL_library_init() is not reentrant.
|
|
We don't want to add our own depenencies on a threading library, and it
|
|
appears that it's safe to call OpenSSL's locking functions before the
|
|
library is initialized, so that's what we'll do (really OpenSSL should
|
|
do this for us).
|
|
This doesn't guarantee that _other_ threads in the application aren't
|
|
calling SSL_library_init() at the same time, but there's not much we
|
|
can do about that.*/
|
|
CRYPTO_w_lock(CRYPTO_LOCK_SSL);
|
|
# endif
|
|
SSL_library_init();
|
|
/*Needed to get SHA2 algorithms with old OpenSSL versions.*/
|
|
OpenSSL_add_ssl_algorithms();
|
|
# if !defined(OPENSSL_NO_LOCKING)
|
|
CRYPTO_w_unlock(CRYPTO_LOCK_SSL);
|
|
# endif
|
|
ssl_ctx=SSL_CTX_new(SSLv23_client_method());
|
|
if(ssl_ctx==NULL)return OP_EFAULT;
|
|
if(!_skip_certificate_check){
|
|
/*We don't do anything if this fails, since it just means we won't load
|
|
any certificates (and thus all checks will fail).
|
|
However, as that is probably the result of a system
|
|
mis-configuration, assert here to make it easier to identify.*/
|
|
OP_ALWAYS_TRUE(SSL_CTX_set_default_verify_paths(ssl_ctx));
|
|
SSL_CTX_set_verify(ssl_ctx,SSL_VERIFY_PEER,NULL);
|
|
}
|
|
_stream->ssl_ctx=ssl_ctx;
|
|
_stream->skip_certificate_check=_skip_certificate_check;
|
|
if(_proxy_host!=NULL){
|
|
/*We need to establish a CONNECT tunnel to handle https proxying.
|
|
Build the request we'll send to do so.*/
|
|
_stream->proxy_connect.nbuf=0;
|
|
ret=op_sb_append(&_stream->proxy_connect,"CONNECT ",8);
|
|
ret|=op_sb_append_string(&_stream->proxy_connect,_stream->url.host);
|
|
ret|=op_sb_append_port(&_stream->proxy_connect,_stream->url.port);
|
|
/*CONNECT requires at least HTTP 1.1.*/
|
|
ret|=op_sb_append(&_stream->proxy_connect," HTTP/1.1\r\n",11);
|
|
ret|=op_sb_append(&_stream->proxy_connect,"Host: ",6);
|
|
ret|=op_sb_append_string(&_stream->proxy_connect,_stream->url.host);
|
|
/*The example in RFC 2817 Section 5.2 specifies an explicit port even
|
|
when connecting to the default port.
|
|
Given that the proxy doesn't know whether we're trying to connect to
|
|
an http or an https URL except by the port number, this seems like a
|
|
good idea.*/
|
|
ret|=op_sb_append_port(&_stream->proxy_connect,_stream->url.port);
|
|
ret|=op_sb_append(&_stream->proxy_connect,"\r\n",2);
|
|
ret|=op_sb_append(&_stream->proxy_connect,"User-Agent: .\r\n",15);
|
|
if(_proxy_user!=NULL&&_proxy_pass!=NULL){
|
|
ret|=op_sb_append_basic_auth_header(&_stream->proxy_connect,
|
|
"Proxy-Authorization",_proxy_user,_proxy_pass);
|
|
}
|
|
/*For backwards compatibility.*/
|
|
ret|=op_sb_append(&_stream->proxy_connect,
|
|
"Proxy-Connection: keep-alive\r\n",30);
|
|
ret|=op_sb_append(&_stream->proxy_connect,"\r\n",2);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
}
|
|
/*Actually make the connection.*/
|
|
ret=op_http_connect(_stream,_stream->conns+0,addrs,&start_time);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*Build the request to send.*/
|
|
_stream->request.nbuf=0;
|
|
ret=op_sb_append(&_stream->request,"GET ",4);
|
|
ret|=op_sb_append_string(&_stream->request,
|
|
_proxy_host!=NULL?_url:_stream->url.path);
|
|
/*Send HTTP/1.0 by default for maximum compatibility (so we don't have to
|
|
re-try if HTTP/1.1 fails, though it shouldn't, even for a 1.0 server).
|
|
This means we aren't conditionally compliant with RFC 2145, because we
|
|
violate the requirement that "An HTTP client SHOULD send a request
|
|
version equal to the highest version for which the client is at least
|
|
conditionally compliant...".
|
|
According to RFC 2145, that means we can't claim any compliance with any
|
|
IETF HTTP specification.*/
|
|
ret|=op_sb_append(&_stream->request," HTTP/1.0\r\n",11);
|
|
/*Remember where this is so we can upgrade to HTTP/1.1 if the server
|
|
supports it.*/
|
|
minor_version_pos=_stream->request.nbuf-3;
|
|
ret|=op_sb_append(&_stream->request,"Host: ",6);
|
|
ret|=op_sb_append_string(&_stream->request,_stream->url.host);
|
|
if(!OP_URL_IS_DEFAULT_PORT(&_stream->url)){
|
|
ret|=op_sb_append_port(&_stream->request,_stream->url.port);
|
|
}
|
|
ret|=op_sb_append(&_stream->request,"\r\n",2);
|
|
/*User-Agents have been a bad idea, so send as little as possible.
|
|
RFC 2616 requires at least one token in the User-Agent, which must have
|
|
at least one character.*/
|
|
ret|=op_sb_append(&_stream->request,"User-Agent: .\r\n",15);
|
|
if(_proxy_host!=NULL&&!OP_URL_IS_SSL(&_stream->url)
|
|
&&_proxy_user!=NULL&&_proxy_pass!=NULL){
|
|
ret|=op_sb_append_basic_auth_header(&_stream->request,
|
|
"Proxy-Authorization",_proxy_user,_proxy_pass);
|
|
}
|
|
if(_stream->url.user!=NULL&&_stream->url.pass!=NULL){
|
|
ret|=op_sb_append_basic_auth_header(&_stream->request,
|
|
"Authorization",_stream->url.user,_stream->url.pass);
|
|
}
|
|
/*Always send a Referer [sic] header.
|
|
It's common to refuse to serve a resource unless one is present.
|
|
We just use the relative "/" URI to suggest we came from the same domain,
|
|
as this is the most common check.
|
|
This might violate RFC 2616's mandate that the field "MUST NOT be sent if
|
|
the Request-URI was obtained from a source that does not have its own
|
|
URI, such as input from the user keyboard," but we don't really have any
|
|
way to know.*/
|
|
/*TODO: Should we update this on redirects?*/
|
|
ret|=op_sb_append(&_stream->request,"Referer: /\r\n",12);
|
|
/*Always send a Range request header to find out if we're seekable.
|
|
This requires an HTTP/1.1 server to succeed, but we'll still get what we
|
|
want with an HTTP/1.0 server that ignores this request header.*/
|
|
ret|=op_sb_append(&_stream->request,"Range: bytes=0-\r\n",17);
|
|
/*Remember where this is so we can append offsets to it later.*/
|
|
_stream->request_tail=_stream->request.nbuf-4;
|
|
ret|=op_sb_append(&_stream->request,"\r\n",2);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
ret=op_http_conn_write_fully(_stream->conns+0,
|
|
_stream->request.buf,_stream->request.nbuf);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
ret=op_http_conn_read_response(_stream->conns+0,&_stream->response);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
ftime(&end_time);
|
|
next=op_http_parse_status_line(&v1_1_compat,&status_code,
|
|
_stream->response.buf);
|
|
if(OP_UNLIKELY(next==NULL))return OP_FALSE;
|
|
if(status_code[0]=='2'){
|
|
opus_int64 content_length;
|
|
opus_int64 range_length;
|
|
int pipeline_supported;
|
|
int pipeline_disabled;
|
|
/*We only understand 20x codes.*/
|
|
if(status_code[1]!='0')return OP_FALSE;
|
|
content_length=-1;
|
|
range_length=-1;
|
|
/*Pipelining must be explicitly enabled.*/
|
|
pipeline_supported=0;
|
|
pipeline_disabled=0;
|
|
for(;;){
|
|
char *header;
|
|
char *cdr;
|
|
ret=op_http_get_next_header(&header,&cdr,&next);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
if(header==NULL)break;
|
|
if(strcmp(header,"content-length")==0){
|
|
/*Two Content-Length headers?*/
|
|
if(OP_UNLIKELY(content_length>=0))return OP_FALSE;
|
|
content_length=op_http_parse_content_length(cdr);
|
|
if(OP_UNLIKELY(content_length<0))return (int)content_length;
|
|
/*Make sure the Content-Length and Content-Range headers match.*/
|
|
if(range_length>=0&&OP_UNLIKELY(content_length!=range_length)){
|
|
return OP_FALSE;
|
|
}
|
|
}
|
|
else if(strcmp(header,"content-range")==0){
|
|
opus_int64 range_first;
|
|
opus_int64 range_last;
|
|
/*Two Content-Range headers?*/
|
|
if(OP_UNLIKELY(range_length>=0))return OP_FALSE;
|
|
ret=op_http_parse_content_range(&range_first,&range_last,
|
|
&range_length,cdr);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*"A response with satus code 206 (Partial Content) MUST NOT
|
|
include a Content-Range field with a byte-range-resp-spec of
|
|
'*'."*/
|
|
if(status_code[2]=='6'
|
|
&&(OP_UNLIKELY(range_first<0)||OP_UNLIKELY(range_last<0))){
|
|
return OP_FALSE;
|
|
}
|
|
/*We asked for the entire resource.*/
|
|
if(range_length>=0){
|
|
/*Quit if we didn't get it.*/
|
|
if(range_last>=0&&OP_UNLIKELY(range_last!=range_length-1)){
|
|
return OP_FALSE;
|
|
}
|
|
}
|
|
/*If there was no length, use the end of the range.*/
|
|
else if(range_last>=0)range_length=range_last+1;
|
|
/*Make sure the Content-Length and Content-Range headers match.*/
|
|
if(content_length>=0&&OP_UNLIKELY(content_length!=range_length)){
|
|
return OP_FALSE;
|
|
}
|
|
}
|
|
else if(strcmp(header,"connection")==0){
|
|
/*According to RFC 2616, if an HTTP/1.1 application does not support
|
|
pipelining, it "MUST include the 'close' connection option in
|
|
every message."
|
|
Therefore, if we receive one in the initial response, disable
|
|
pipelining entirely.
|
|
The server still might support it (e.g., we might just have hit the
|
|
request limit for a temporary child process), but if it doesn't
|
|
and we assume it does, every time we cross a chunk boundary we'll
|
|
error out and reconnect, adding lots of latency.*/
|
|
ret=op_http_parse_connection(cdr);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
pipeline_disabled|=ret;
|
|
}
|
|
else if(strcmp(header,"server")==0){
|
|
/*If we got a Server response header, and it wasn't from a known-bad
|
|
server, enable pipelining, as long as it's at least HTTP/1.1.
|
|
According to RFC 2145, the server is supposed to respond with the
|
|
highest minor version number it supports unless it is known or
|
|
suspected that we incorrectly implement the HTTP specification.
|
|
So it should send back at least HTTP/1.1, despite our HTTP/1.0
|
|
request.*/
|
|
pipeline_supported=v1_1_compat;
|
|
if(v1_1_compat)pipeline_disabled|=!op_http_allow_pipelining(cdr);
|
|
if(_info!=NULL&&_info->server==NULL)_info->server=op_string_dup(cdr);
|
|
}
|
|
/*Collect station information headers if the caller requested it.
|
|
If there's more than one copy of a header, the first one wins.*/
|
|
else if(_info!=NULL){
|
|
if(strcmp(header,"content-type")==0){
|
|
if(_info->content_type==NULL){
|
|
_info->content_type=op_string_dup(cdr);
|
|
}
|
|
}
|
|
else if(header[0]=='i'&&header[1]=='c'
|
|
&&(header[2]=='e'||header[2]=='y')&&header[3]=='-'){
|
|
if(strcmp(header+4,"name")==0){
|
|
if(_info->name==NULL)_info->name=op_string_dup(cdr);
|
|
}
|
|
else if(strcmp(header+4,"description")==0){
|
|
if(_info->description==NULL)_info->description=op_string_dup(cdr);
|
|
}
|
|
else if(strcmp(header+4,"genre")==0){
|
|
if(_info->genre==NULL)_info->genre=op_string_dup(cdr);
|
|
}
|
|
else if(strcmp(header+4,"url")==0){
|
|
if(_info->url==NULL)_info->url=op_string_dup(cdr);
|
|
}
|
|
else if(strcmp(header,"icy-br")==0
|
|
||strcmp(header,"ice-bitrate")==0){
|
|
if(_info->bitrate_kbps<0){
|
|
opus_int64 bitrate_kbps;
|
|
/*Just re-using this function to parse a random unsigned
|
|
integer field.*/
|
|
bitrate_kbps=op_http_parse_content_length(cdr);
|
|
if(bitrate_kbps>=0&&bitrate_kbps<=OP_INT32_MAX){
|
|
_info->bitrate_kbps=(opus_int32)bitrate_kbps;
|
|
}
|
|
}
|
|
}
|
|
else if(strcmp(header,"icy-pub")==0
|
|
||strcmp(header,"ice-public")==0){
|
|
if(_info->is_public<0&&(cdr[0]=='0'||cdr[0]=='1')&&cdr[1]=='\0'){
|
|
_info->is_public=cdr[0]-'0';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
switch(status_code[2]){
|
|
/*200 OK*/
|
|
case '0':break;
|
|
/*203 Non-Authoritative Information*/
|
|
case '3':break;
|
|
/*204 No Content*/
|
|
case '4':{
|
|
if(content_length>=0&&OP_UNLIKELY(content_length!=0)){
|
|
return OP_FALSE;
|
|
}
|
|
}break;
|
|
/*206 Partial Content*/
|
|
case '6':{
|
|
/*No Content-Range header.*/
|
|
if(OP_UNLIKELY(range_length<0))return OP_FALSE;
|
|
content_length=range_length;
|
|
/*The server supports range requests for this resource.
|
|
We can seek.*/
|
|
_stream->seekable=1;
|
|
}break;
|
|
/*201 Created: the response "SHOULD include an entity containing a list
|
|
of resource characteristics and location(s)," but not an Opus file.
|
|
202 Accepted: the response "SHOULD include an indication of request's
|
|
current status and either a pointer to a status monitor or some
|
|
estimate of when the user can expect the request to be fulfilled,"
|
|
but not an Opus file.
|
|
205 Reset Content: this "MUST NOT include an entity," meaning no Opus
|
|
file.
|
|
207...209 are not yet defined, so we don't know how to handle them.*/
|
|
default:return OP_FALSE;
|
|
}
|
|
_stream->content_length=content_length;
|
|
_stream->pipeline=pipeline_supported&&!pipeline_disabled;
|
|
/*Pipelining requires HTTP/1.1 persistent connections.*/
|
|
if(_stream->pipeline)_stream->request.buf[minor_version_pos]='1';
|
|
_stream->conns[0].pos=0;
|
|
_stream->conns[0].end_pos=_stream->seekable?content_length:-1;
|
|
_stream->conns[0].chunk_size=-1;
|
|
_stream->cur_conni=0;
|
|
_stream->connect_rate=op_time_diff_ms(&end_time,&start_time);
|
|
_stream->connect_rate=OP_MAX(_stream->connect_rate,1);
|
|
if(_info!=NULL)_info->is_ssl=OP_URL_IS_SSL(&_stream->url);
|
|
/*The URL has been successfully opened.*/
|
|
return 0;
|
|
}
|
|
/*Shouldn't get 1xx; 4xx and 5xx are both failures (and we don't retry).
|
|
Everything else is undefined.*/
|
|
else if(status_code[0]!='3')return OP_FALSE;
|
|
/*We have some form of redirect request.*/
|
|
/*We only understand 30x codes.*/
|
|
if(status_code[1]!='0')return OP_FALSE;
|
|
switch(status_code[2]){
|
|
/*300 Multiple Choices: "If the server has a preferred choice of
|
|
representation, it SHOULD include the specific URI for that
|
|
representation in the Location field," otherwise we'll fail.*/
|
|
case '0':
|
|
/*301 Moved Permanently*/
|
|
case '1':
|
|
/*302 Found*/
|
|
case '2':
|
|
/*307 Temporary Redirect*/
|
|
case '7':
|
|
/*308 Permanent Redirect (defined by draft-reschke-http-status-308-07).*/
|
|
case '8':break;
|
|
/*305 Use Proxy: "The Location field gives the URI of the proxy."
|
|
TODO: This shouldn't actually be that hard to do.*/
|
|
case '5':return OP_EIMPL;
|
|
/*303 See Other: "The new URI is not a substitute reference for the
|
|
originally requested resource."
|
|
304 Not Modified: "The 304 response MUST NOT contain a message-body."
|
|
306 (Unused)
|
|
309 is not yet defined, so we don't know how to handle it.*/
|
|
default:return OP_FALSE;
|
|
}
|
|
_url=NULL;
|
|
for(;;){
|
|
char *header;
|
|
char *cdr;
|
|
ret=op_http_get_next_header(&header,&cdr,&next);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
if(header==NULL)break;
|
|
if(strcmp(header,"location")==0&&OP_LIKELY(_url==NULL))_url=cdr;
|
|
}
|
|
if(OP_UNLIKELY(_url==NULL))return OP_FALSE;
|
|
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.*/
|
|
return OP_FALSE;
|
|
}
|
|
|
|
static int op_http_conn_send_request(OpusHTTPStream *_stream,
|
|
OpusHTTPConn *_conn,opus_int64 _pos,opus_int32 _chunk_size,
|
|
int _try_not_to_block){
|
|
opus_int64 next_end;
|
|
int ret;
|
|
/*We shouldn't have another request outstanding.*/
|
|
OP_ASSERT(_conn->next_pos<0);
|
|
/*Build the request to send.*/
|
|
OP_ASSERT(_stream->request.nbuf>=_stream->request_tail);
|
|
_stream->request.nbuf=_stream->request_tail;
|
|
ret=op_sb_append_nonnegative_int64(&_stream->request,_pos);
|
|
ret|=op_sb_append(&_stream->request,"-",1);
|
|
if(_chunk_size>0&&OP_ADV_OFFSET(_pos,2*_chunk_size)<_stream->content_length){
|
|
/*We shouldn't be pipelining requests with non-HTTP/1.1 servers.*/
|
|
OP_ASSERT(_stream->pipeline);
|
|
next_end=_pos+_chunk_size;
|
|
ret|=op_sb_append_nonnegative_int64(&_stream->request,next_end-1);
|
|
/*Use a larger chunk size for our next request.*/
|
|
_chunk_size<<=1;
|
|
/*But after a while, just request the rest of the resource.*/
|
|
if(_chunk_size>OP_PIPELINE_CHUNK_SIZE_MAX)_chunk_size=-1;
|
|
}
|
|
else{
|
|
/*Either this was a non-pipelined request or we were close enough to the
|
|
end to just ask for the rest.*/
|
|
next_end=-1;
|
|
_chunk_size=-1;
|
|
}
|
|
ret|=op_sb_append(&_stream->request,"\r\n\r\n",4);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*If we don't want to block, check to see if there's enough space in the send
|
|
queue.
|
|
There's still a chance we might block, even if there is enough space, but
|
|
it's a much slimmer one.
|
|
Blocking at all is pretty unlikely, as we won't have any requests queued
|
|
when _try_not_to_block is set, so if FIONSPACE isn't available (e.g., on
|
|
Linux), just skip the test.*/
|
|
if(_try_not_to_block){
|
|
# if defined(FIONSPACE)
|
|
int available;
|
|
ret=ioctl(_conn->fd,FIONSPACE,&available);
|
|
if(ret<0||available<_stream->request.nbuf)return 1;
|
|
# endif
|
|
}
|
|
ret=op_http_conn_write_fully(_conn,
|
|
_stream->request.buf,_stream->request.nbuf);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
_conn->next_pos=_pos;
|
|
_conn->next_end=next_end;
|
|
/*Save the chunk size to use for the next request.*/
|
|
_conn->chunk_size=_chunk_size;
|
|
_conn->nrequests_left--;
|
|
return ret;
|
|
}
|
|
|
|
/*Handles the response to all requests after the first one.
|
|
Return: 1 if the connection was closed or timed out, 0 on success, or a
|
|
negative value on any other error.*/
|
|
static int op_http_conn_handle_response(OpusHTTPStream *_stream,
|
|
OpusHTTPConn *_conn){
|
|
char *next;
|
|
char *status_code;
|
|
opus_int64 range_length;
|
|
opus_int64 next_pos;
|
|
opus_int64 next_end;
|
|
int ret;
|
|
ret=op_http_conn_read_response(_conn,&_stream->response);
|
|
/*If the server just closed the connection on us, we may have just hit a
|
|
connection re-use limit, so we might want to retry.*/
|
|
if(OP_UNLIKELY(ret<0))return ret==OP_EREAD?1:ret;
|
|
next=op_http_parse_status_line(NULL,&status_code,_stream->response.buf);
|
|
if(OP_UNLIKELY(next==NULL))return OP_FALSE;
|
|
/*We _need_ a 206 Partial Content response.
|
|
Nothing else will do.*/
|
|
if(strncmp(status_code,"206",3)!=0){
|
|
/*But on a 408 Request Timeout, we might want to re-try.*/
|
|
return strncmp(status_code,"408",3)==0?1:OP_FALSE;
|
|
}
|
|
next_pos=_conn->next_pos;
|
|
next_end=_conn->next_end;
|
|
range_length=-1;
|
|
for(;;){
|
|
char *header;
|
|
char *cdr;
|
|
ret=op_http_get_next_header(&header,&cdr,&next);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
if(header==NULL)break;
|
|
if(strcmp(header,"content-range")==0){
|
|
opus_int64 range_first;
|
|
opus_int64 range_last;
|
|
/*Two Content-Range headers?*/
|
|
if(OP_UNLIKELY(range_length>=0))return OP_FALSE;
|
|
ret=op_http_parse_content_range(&range_first,&range_last,
|
|
&range_length,cdr);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*"A response with satus code 206 (Partial Content) MUST NOT
|
|
include a Content-Range field with a byte-range-resp-spec of
|
|
'*'."*/
|
|
if(OP_UNLIKELY(range_first<0)||OP_UNLIKELY(range_last<0))return OP_FALSE;
|
|
/*We also don't want range_last to overflow.*/
|
|
if(OP_UNLIKELY(range_last>=OP_INT64_MAX))return OP_FALSE;
|
|
range_last++;
|
|
/*Quit if we didn't get the offset we asked for.*/
|
|
if(range_first!=next_pos)return OP_FALSE;
|
|
if(next_end<0){
|
|
/*We asked for the rest of the resource.*/
|
|
if(range_length>=0){
|
|
/*Quit if we didn't get it.*/
|
|
if(OP_UNLIKELY(range_last!=range_length))return OP_FALSE;
|
|
}
|
|
/*If there was no length, use the end of the range.*/
|
|
else range_length=range_last;
|
|
next_end=range_last;
|
|
}
|
|
else{
|
|
if(range_last!=next_end)return OP_FALSE;
|
|
/*If there was no length, use the larger of the content length or the
|
|
end of this chunk.*/
|
|
if(range_length<0){
|
|
range_length=OP_MAX(range_last,_stream->content_length);
|
|
}
|
|
}
|
|
}
|
|
else if(strcmp(header,"content-length")==0){
|
|
opus_int64 content_length;
|
|
/*Validate the Content-Length header, if present, against the request we
|
|
made.*/
|
|
content_length=op_http_parse_content_length(cdr);
|
|
if(OP_UNLIKELY(content_length<0))return (int)content_length;
|
|
if(next_end<0){
|
|
/*If we haven't seen the Content-Range header yet and we asked for the
|
|
rest of the resource, set next_end, so we can make sure they match
|
|
when we do find the Content-Range header.*/
|
|
if(OP_UNLIKELY(next_pos>OP_INT64_MAX-content_length))return OP_FALSE;
|
|
next_end=next_pos+content_length;
|
|
}
|
|
/*Otherwise, make sure they match now.*/
|
|
else if(OP_UNLIKELY(next_end-next_pos!=content_length))return OP_FALSE;
|
|
}
|
|
else if(strcmp(header,"connection")==0){
|
|
ret=op_http_parse_connection(cdr);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*If the server told us it was going to close the connection, don't make
|
|
any more requests.*/
|
|
if(OP_UNLIKELY(ret>0))_conn->nrequests_left=0;
|
|
}
|
|
}
|
|
/*No Content-Range header.*/
|
|
if(OP_UNLIKELY(range_length<0))return OP_FALSE;
|
|
/*Update the content_length if necessary.*/
|
|
_stream->content_length=range_length;
|
|
_conn->pos=next_pos;
|
|
_conn->end_pos=next_end;
|
|
_conn->next_pos=-1;
|
|
return 0;
|
|
}
|
|
|
|
/*Open a new connection that will start reading at byte offset _pos.
|
|
_pos: The byte offset to start reading from.
|
|
_chunk_size: The number of bytes to ask for in the initial request, or -1 to
|
|
request the rest of the resource.
|
|
This may be more bytes than remain, in which case it will be
|
|
converted into a request for the rest.*/
|
|
static int op_http_conn_open_pos(OpusHTTPStream *_stream,
|
|
OpusHTTPConn *_conn,opus_int64 _pos,opus_int32 _chunk_size){
|
|
struct timeb start_time;
|
|
struct timeb end_time;
|
|
opus_int32 connect_rate;
|
|
opus_int32 connect_time;
|
|
int ret;
|
|
ret=op_http_connect(_stream,_conn,&_stream->addr_info,&start_time);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
ret=op_http_conn_send_request(_stream,_conn,_pos,_chunk_size,0);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
ret=op_http_conn_handle_response(_stream,_conn);
|
|
if(OP_UNLIKELY(ret!=0))return OP_FALSE;
|
|
ftime(&end_time);
|
|
_stream->cur_conni=_conn-_stream->conns;
|
|
OP_ASSERT(_stream->cur_conni>=0&&_stream->cur_conni<OP_NCONNS_MAX);
|
|
/*The connection has been successfully opened.
|
|
Update the connection time estimate.*/
|
|
connect_time=op_time_diff_ms(&end_time,&start_time);
|
|
connect_rate=_stream->connect_rate;
|
|
connect_rate+=OP_MAX(connect_time,1)-connect_rate+8>>4;
|
|
_stream->connect_rate=connect_rate;
|
|
return 0;
|
|
}
|
|
|
|
/*Read data from the current response body.
|
|
If we're pipelining and we get close to the end of this response, queue
|
|
another request.
|
|
If we've reached the end of this response body, parse the next response and
|
|
keep going.
|
|
[out] _buf: Returns the data read.
|
|
_buf_size: The size of the buffer.
|
|
Return: A positive number of bytes read on success.
|
|
0: The connection was closed.
|
|
OP_EREAD: There was a fatal read error.*/
|
|
static int op_http_conn_read_body(OpusHTTPStream *_stream,
|
|
OpusHTTPConn *_conn,unsigned char *_buf,int _buf_size){
|
|
opus_int64 pos;
|
|
opus_int64 end_pos;
|
|
opus_int64 next_pos;
|
|
opus_int64 content_length;
|
|
int nread;
|
|
int pipeline;
|
|
int ret;
|
|
/*Currently this function can only be called on the LRU head.
|
|
Otherwise, we'd need a _pnext pointer if we needed to close the connection,
|
|
and re-opening it would re-organize the lists.*/
|
|
OP_ASSERT(_stream->lru_head==_conn);
|
|
/*We should have filterd out empty reads by this point.*/
|
|
OP_ASSERT(_buf_size>0);
|
|
pos=_conn->pos;
|
|
end_pos=_conn->end_pos;
|
|
next_pos=_conn->next_pos;
|
|
pipeline=_stream->pipeline;
|
|
content_length=_stream->content_length;
|
|
if(end_pos>=0){
|
|
/*Have we reached the end of the current response body?*/
|
|
if(pos>=end_pos){
|
|
OP_ASSERT(content_length>=0);
|
|
/*If this was the end of the stream, we're done.
|
|
Also return early if a non-blocking read was requested (regardless of
|
|
whether we might be able to parse the next response without
|
|
blocking).*/
|
|
if(content_length<=end_pos)return 0;
|
|
/*Otherwise, start on the next response.*/
|
|
if(next_pos<0){
|
|
/*We haven't issued another request yet.*/
|
|
if(!pipeline||_conn->nrequests_left<=0){
|
|
/*There are two ways to get here: either the server told us it was
|
|
going to close the connection after the last request, or we
|
|
thought we were reading the whole resource, but it grew while we
|
|
were reading it.
|
|
The only way the latter could have happened is if content_length
|
|
changed while seeking.
|
|
Open a new request to read the rest.*/
|
|
OP_ASSERT(_stream->seekable);
|
|
/*Try to open a new connection to read another chunk.*/
|
|
op_http_conn_close(_stream,_conn,&_stream->lru_head,1);
|
|
/*If we're not pipelining, we should be requesting the rest.*/
|
|
OP_ASSERT(pipeline||_conn->chunk_size==-1);
|
|
ret=op_http_conn_open_pos(_stream,_conn,end_pos,_conn->chunk_size);
|
|
if(OP_UNLIKELY(ret<0))return OP_EREAD;
|
|
}
|
|
else{
|
|
/*Issue the request now (better late than never).*/
|
|
ret=op_http_conn_send_request(_stream,_conn,pos,_conn->chunk_size,0);
|
|
if(OP_UNLIKELY(ret<0))return OP_EREAD;
|
|
next_pos=_conn->next_pos;
|
|
OP_ASSERT(next_pos>=0);
|
|
}
|
|
}
|
|
if(next_pos>=0){
|
|
/*We shouldn't be trying to read past the current request body if we're
|
|
seeking somewhere else.*/
|
|
OP_ASSERT(next_pos==end_pos);
|
|
ret=op_http_conn_handle_response(_stream,_conn);
|
|
if(OP_UNLIKELY(ret<0))return OP_EREAD;
|
|
if(OP_UNLIKELY(ret>0)&&pipeline){
|
|
opus_int64 next_end;
|
|
next_end=_conn->next_end;
|
|
/*Our request timed out or the server closed the connection.
|
|
Try re-connecting.*/
|
|
op_http_conn_close(_stream,_conn,&_stream->lru_head,1);
|
|
/*Unless there's a bug, we should be able to convert
|
|
(next_pos,next_end) into valid (_pos,_chunk_size) parameters.*/
|
|
OP_ASSERT(next_end<0
|
|
||next_end-next_pos>=0&&next_end-next_pos<=OP_INT32_MAX);
|
|
ret=op_http_conn_open_pos(_stream,_conn,next_pos,
|
|
next_end<0?-1:(opus_int32)(next_end-next_pos));
|
|
if(OP_UNLIKELY(ret<0))return OP_EREAD;
|
|
}
|
|
else if(OP_UNLIKELY(ret!=0))return OP_EREAD;
|
|
}
|
|
pos=_conn->pos;
|
|
end_pos=_conn->end_pos;
|
|
content_length=_stream->content_length;
|
|
}
|
|
OP_ASSERT(end_pos>pos);
|
|
_buf_size=OP_MIN(_buf_size,end_pos-pos);
|
|
}
|
|
nread=op_http_conn_read(_conn,(char *)_buf,_buf_size,1);
|
|
if(OP_UNLIKELY(nread<0))return nread;
|
|
pos+=nread;
|
|
_conn->pos=pos;
|
|
OP_ASSERT(end_pos<0||content_length>=0);
|
|
/*TODO: If nrequests_left<=0, we can't make a new request, and there will be
|
|
a big pause after we hit the end of the chunk while we open a new
|
|
connection.
|
|
It would be nice to be able to start that process now, but we have no way
|
|
to do it in the background without blocking (even if we could start it, we
|
|
have no guarantee the application will return control to us in a
|
|
sufficiently timely manner to allow us to complete it, and this is
|
|
uncommon enough that it's not worth using threads just for this).*/
|
|
if(end_pos>=0&&end_pos<content_length&&next_pos<0
|
|
&&pipeline&&OP_LIKELY(_conn->nrequests_left>0)){
|
|
opus_int64 request_thresh;
|
|
opus_int32 chunk_size;
|
|
/*Are we getting close to the end of the current response body?
|
|
If so, we should request more data.*/
|
|
request_thresh=_stream->connect_rate*_conn->read_rate>>12;
|
|
/*But don't commit ourselves too quickly.*/
|
|
chunk_size=_conn->chunk_size;
|
|
if(chunk_size>=0)request_thresh=OP_MIN(chunk_size>>2,request_thresh);
|
|
if(end_pos-pos<request_thresh){
|
|
ret=op_http_conn_send_request(_stream,_conn,end_pos,_conn->chunk_size,1);
|
|
if(OP_UNLIKELY(ret<0))return OP_EREAD;
|
|
}
|
|
}
|
|
return nread;
|
|
}
|
|
|
|
static int op_http_stream_read(void *_stream,
|
|
unsigned char *_ptr,int _buf_size){
|
|
OpusHTTPStream *stream;
|
|
ptrdiff_t nread;
|
|
opus_int64 size;
|
|
opus_int64 pos;
|
|
int ci;
|
|
stream=(OpusHTTPStream *)_stream;
|
|
/*Check for an empty read.*/
|
|
if(_buf_size<=0)return 0;
|
|
ci=stream->cur_conni;
|
|
/*No current connection => EOF.*/
|
|
if(ci<0)return 0;
|
|
pos=stream->conns[ci].pos;
|
|
size=stream->content_length;
|
|
/*Check for EOF.*/
|
|
if(size>=0){
|
|
if(pos>=size)return 0;
|
|
/*Check for a short read.*/
|
|
if(_buf_size>size-pos)_buf_size=(int)(size-pos);
|
|
}
|
|
nread=op_http_conn_read_body(stream,stream->conns+ci,_ptr,_buf_size);
|
|
if(OP_UNLIKELY(nread<=0)){
|
|
/*We hit an error or EOF.
|
|
Either way, we're done with this connection.*/
|
|
op_http_conn_close(stream,stream->conns+ci,&stream->lru_head,1);
|
|
stream->cur_conni=-1;
|
|
stream->pos=pos;
|
|
}
|
|
return nread;
|
|
}
|
|
|
|
/*Discard data until we reach the _target position.
|
|
This destroys the contents of _stream->response.buf, as we need somewhere to
|
|
read this data, and that is a convenient place.
|
|
_just_read_ahead: Whether or not this is a plain fast-forward.
|
|
If 0, we need to issue a new request for a chunk at _target
|
|
and discard all the data from our current request(s).
|
|
Otherwise, we should be able to reach _target without
|
|
issuing any new requests.
|
|
_target: The stream position to which to read ahead.*/
|
|
static int op_http_conn_read_ahead(OpusHTTPStream *_stream,
|
|
OpusHTTPConn *_conn,int _just_read_ahead,opus_int64 _target){
|
|
opus_int64 pos;
|
|
opus_int64 end_pos;
|
|
opus_int64 next_pos;
|
|
opus_int64 next_end;
|
|
ptrdiff_t nread;
|
|
int ret;
|
|
pos=_conn->pos;
|
|
end_pos=_conn->end_pos;
|
|
next_pos=_conn->next_pos;
|
|
next_end=_conn->next_end;
|
|
if(!_just_read_ahead){
|
|
/*We need to issue a new pipelined request.
|
|
This is the only case where we allow more than one outstanding request
|
|
at a time, so we need to reset next_pos (we'll restore it below if we
|
|
did have an outstanding request).*/
|
|
OP_ASSERT(_stream->pipeline);
|
|
_conn->next_pos=-1;
|
|
ret=op_http_conn_send_request(_stream,_conn,_target,
|
|
OP_PIPELINE_CHUNK_SIZE,0);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
/*We can reach the target position by reading forward in the current chunk.*/
|
|
if(_just_read_ahead&&(end_pos<0||_target<end_pos))end_pos=_target;
|
|
else if(next_pos>=0){
|
|
opus_int64 next_next_pos;
|
|
opus_int64 next_next_end;
|
|
/*We already have a request outstanding.
|
|
Finish off the current chunk.*/
|
|
while(pos<end_pos){
|
|
nread=op_http_conn_read(_conn,_stream->response.buf,
|
|
(int)OP_MIN(end_pos-pos,_stream->response.cbuf),1);
|
|
/*We failed to read ahead.*/
|
|
if(nread<=0)return OP_FALSE;
|
|
pos+=nread;
|
|
}
|
|
OP_ASSERT(pos==end_pos);
|
|
if(_just_read_ahead){
|
|
next_next_pos=next_next_end=-1;
|
|
end_pos=_target;
|
|
}
|
|
else{
|
|
OP_ASSERT(_conn->next_pos==_target);
|
|
next_next_pos=_target;
|
|
next_next_end=_conn->next_end;
|
|
_conn->next_pos=next_pos;
|
|
_conn->next_end=next_end;
|
|
end_pos=next_end;
|
|
}
|
|
ret=op_http_conn_handle_response(_stream,_conn);
|
|
if(OP_UNLIKELY(ret!=0))return OP_FALSE;
|
|
_conn->next_pos=next_next_pos;
|
|
_conn->next_end=next_next_end;
|
|
}
|
|
while(pos<end_pos){
|
|
nread=op_http_conn_read(_conn,_stream->response.buf,
|
|
(int)OP_MIN(end_pos-pos,_stream->response.cbuf),1);
|
|
/*We failed to read ahead.*/
|
|
if(nread<=0)return OP_FALSE;
|
|
pos+=nread;
|
|
}
|
|
OP_ASSERT(pos==end_pos);
|
|
if(!_just_read_ahead){
|
|
ret=op_http_conn_handle_response(_stream,_conn);
|
|
if(OP_UNLIKELY(ret!=0))return OP_FALSE;
|
|
}
|
|
else _conn->pos=end_pos;
|
|
OP_ASSERT(_conn->pos==_target);
|
|
return 0;
|
|
}
|
|
|
|
static int op_http_stream_seek(void *_stream,opus_int64 _offset,int _whence){
|
|
struct timeb seek_time;
|
|
OpusHTTPStream *stream;
|
|
OpusHTTPConn *conn;
|
|
OpusHTTPConn **pnext;
|
|
OpusHTTPConn *close_conn;
|
|
OpusHTTPConn **close_pnext;
|
|
opus_int64 content_length;
|
|
opus_int64 pos;
|
|
int pipeline;
|
|
int ci;
|
|
int ret;
|
|
stream=(OpusHTTPStream *)_stream;
|
|
if(!stream->seekable)return -1;
|
|
content_length=stream->content_length;
|
|
/*If we're seekable, we should have gotten a Content-Length.*/
|
|
OP_ASSERT(content_length>=0);
|
|
ci=stream->cur_conni;
|
|
pos=ci<0?content_length:stream->conns[ci].pos;
|
|
switch(_whence){
|
|
case SEEK_SET:{
|
|
/*Check for overflow:*/
|
|
if(_offset<0)return -1;
|
|
pos=_offset;
|
|
}break;
|
|
case SEEK_CUR:{
|
|
/*Check for overflow:*/
|
|
if(_offset<-pos||_offset>OP_INT64_MAX-pos)return -1;
|
|
pos+=_offset;
|
|
}break;
|
|
case SEEK_END:{
|
|
/*Check for overflow:*/
|
|
if(_offset>content_length||_offset<content_length-OP_INT64_MAX)return -1;
|
|
pos=content_length-_offset;
|
|
}break;
|
|
default:return -1;
|
|
}
|
|
/*Mark when we deactivated the active connection.*/
|
|
if(ci>=0){
|
|
op_http_conn_read_rate_update(stream->conns+ci);
|
|
*&seek_time=*&stream->conns[ci].read_time;
|
|
}
|
|
else ftime(&seek_time);
|
|
/*If we seeked past the end of the stream, just disable the active
|
|
connection.*/
|
|
if(pos>=content_length){
|
|
stream->cur_conni=-1;
|
|
stream->pos=pos;
|
|
return 0;
|
|
}
|
|
/*First try to find a connection we can use without waiting.*/
|
|
pnext=&stream->lru_head;
|
|
conn=stream->lru_head;
|
|
while(conn!=NULL){
|
|
opus_int64 conn_pos;
|
|
opus_int64 end_pos;
|
|
int available;
|
|
/*If this connection has been dormant too long or has made too many
|
|
requests, close it.
|
|
This is to prevent us from hitting server limits/firewall timeouts.*/
|
|
if(op_time_diff_ms(&seek_time,&conn->read_time)>
|
|
OP_CONNECTION_IDLE_TIMEOUT_MS
|
|
||conn->nrequests_left<OP_PIPELINE_MIN_REQUESTS){
|
|
op_http_conn_close(stream,conn,pnext,1);
|
|
conn=*pnext;
|
|
continue;
|
|
}
|
|
available=op_http_conn_estimate_available(conn);
|
|
conn_pos=conn->pos;
|
|
end_pos=conn->end_pos;
|
|
if(conn->next_pos>=0){
|
|
OP_ASSERT(end_pos>=0);
|
|
OP_ASSERT(conn->next_pos==end_pos);
|
|
end_pos=conn->next_end;
|
|
}
|
|
OP_ASSERT(end_pos<0||conn_pos<=end_pos);
|
|
/*Can we quickly read ahead without issuing a new request or waiting for
|
|
any more data?
|
|
If we have an oustanding request, we'll over-estimate the amount of data
|
|
it has available (because we'll count the response headers, too), but
|
|
that probably doesn't matter.*/
|
|
if(conn_pos<=pos&&pos-conn_pos<=available&&(end_pos<0||pos<end_pos)){
|
|
/*Found a suitable connection to re-use.*/
|
|
ret=op_http_conn_read_ahead(stream,conn,1,pos);
|
|
if(OP_UNLIKELY(ret<0)){
|
|
/*The connection might have become stale, so close it and keep going.*/
|
|
op_http_conn_close(stream,conn,pnext,1);
|
|
conn=*pnext;
|
|
continue;
|
|
}
|
|
/*Sucessfully resurrected this connection.*/
|
|
*pnext=conn->next;
|
|
conn->next=stream->lru_head;
|
|
stream->lru_head=conn;
|
|
stream->cur_conni=conn-stream->conns;
|
|
return 0;
|
|
}
|
|
pnext=&conn->next;
|
|
conn=conn->next;
|
|
}
|
|
/*Chances are that didn't work, so now try to find one we can use by reading
|
|
ahead a reasonable amount and/or by issuing a new request.*/
|
|
close_pnext=NULL;
|
|
close_conn=NULL;
|
|
pnext=&stream->lru_head;
|
|
conn=stream->lru_head;
|
|
pipeline=stream->pipeline;
|
|
while(conn!=NULL){
|
|
opus_int64 conn_pos;
|
|
opus_int64 end_pos;
|
|
opus_int64 read_ahead_thresh;
|
|
int available;
|
|
int just_read_ahead;
|
|
/*Dividing by 2048 instead of 1000 scales this by nearly 1/2, biasing away
|
|
from connection re-use (and roughly compensating for the lag required to
|
|
reopen the TCP window of a connection that's been idle).
|
|
There's no overflow checking here, because it's vanishingly unlikely, and
|
|
all it would do is cause us to make poor decisions.*/
|
|
read_ahead_thresh=OP_MAX(OP_READAHEAD_THRESH_MIN,
|
|
stream->connect_rate*conn->read_rate>>11);
|
|
available=op_http_conn_estimate_available(conn);
|
|
conn_pos=conn->pos;
|
|
end_pos=conn->end_pos;
|
|
if(conn->next_pos>=0){
|
|
OP_ASSERT(end_pos>=0);
|
|
OP_ASSERT(conn->next_pos==end_pos);
|
|
end_pos=conn->next_end;
|
|
}
|
|
OP_ASSERT(end_pos<0||conn_pos<=end_pos);
|
|
/*Can we quickly read ahead without issuing a new request?*/
|
|
just_read_ahead=conn_pos<=pos&&pos-conn_pos-available<=read_ahead_thresh
|
|
&&(end_pos<0||pos<end_pos);
|
|
if(just_read_ahead||pipeline&&end_pos>=0
|
|
&&end_pos-conn_pos-available<=read_ahead_thresh){
|
|
/*Found a suitable connection to re-use.*/
|
|
ret=op_http_conn_read_ahead(stream,conn,just_read_ahead,pos);
|
|
if(OP_UNLIKELY(ret<0)){
|
|
/*The connection might have become stale, so close it and keep going.*/
|
|
op_http_conn_close(stream,conn,pnext,1);
|
|
conn=*pnext;
|
|
continue;
|
|
}
|
|
/*Sucessfully resurrected this connection.*/
|
|
*pnext=conn->next;
|
|
conn->next=stream->lru_head;
|
|
stream->lru_head=conn;
|
|
stream->cur_conni=conn-stream->conns;
|
|
return 0;
|
|
}
|
|
close_pnext=pnext;
|
|
close_conn=conn;
|
|
pnext=&conn->next;
|
|
conn=conn->next;
|
|
}
|
|
/*No suitable connections.
|
|
Open a new one.*/
|
|
if(stream->free_head==NULL){
|
|
/*All connections in use.
|
|
Expire one of them (we should have already picked which one when scanning
|
|
the list).*/
|
|
OP_ASSERT(close_conn!=NULL);
|
|
OP_ASSERT(close_pnext!=NULL);
|
|
op_http_conn_close(stream,close_conn,close_pnext,1);
|
|
}
|
|
OP_ASSERT(stream->free_head!=NULL);
|
|
conn=stream->free_head;
|
|
/*If we can pipeline, only request a chunk of data.
|
|
If we're seeking now, there's a good chance we will want to seek again
|
|
soon, and this avoids committing this connection to reading the rest of
|
|
the stream.
|
|
Particularly with SSL or proxies, issuing a new request on the same
|
|
connection can be substantially faster than opening a new one.
|
|
This also limits the amount of data the server will blast at us on this
|
|
connection if we later seek elsewhere and start reading from a different
|
|
connection.*/
|
|
ret=op_http_conn_open_pos(stream,conn,pos,
|
|
pipeline?OP_PIPELINE_CHUNK_SIZE:-1);
|
|
if(OP_UNLIKELY(ret<0)){
|
|
op_http_conn_close(stream,conn,&stream->lru_head,1);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static opus_int64 op_http_stream_tell(void *_stream){
|
|
OpusHTTPStream *stream;
|
|
int ci;
|
|
stream=(OpusHTTPStream *)_stream;
|
|
ci=stream->cur_conni;
|
|
return ci<0?stream->pos:stream->conns[ci].pos;
|
|
}
|
|
|
|
static int op_http_stream_close(void *_stream){
|
|
OpusHTTPStream *stream;
|
|
stream=(OpusHTTPStream *)_stream;
|
|
if(OP_LIKELY(stream!=NULL)){
|
|
op_http_stream_clear(stream);
|
|
_ogg_free(stream);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static const OpusFileCallbacks OP_HTTP_CALLBACKS={
|
|
op_http_stream_read,
|
|
op_http_stream_seek,
|
|
op_http_stream_tell,
|
|
op_http_stream_close
|
|
};
|
|
#endif
|
|
|
|
void opus_server_info_init(OpusServerInfo *_info){
|
|
_info->name=NULL;
|
|
_info->description=NULL;
|
|
_info->genre=NULL;
|
|
_info->url=NULL;
|
|
_info->server=NULL;
|
|
_info->content_type=NULL;
|
|
_info->bitrate_kbps=-1;
|
|
_info->is_public=-1;
|
|
_info->is_ssl=0;
|
|
}
|
|
|
|
void opus_server_info_clear(OpusServerInfo *_info){
|
|
_ogg_free(_info->content_type);
|
|
_ogg_free(_info->server);
|
|
_ogg_free(_info->url);
|
|
_ogg_free(_info->genre);
|
|
_ogg_free(_info->description);
|
|
_ogg_free(_info->name);
|
|
}
|
|
|
|
/*The actual URL stream creation function.
|
|
This one isn't extensible like the application-level interface, but because
|
|
it isn't public, we're free to change it in the future.*/
|
|
static void *op_url_stream_create_impl(OpusFileCallbacks *_cb,const char *_url,
|
|
int _skip_certificate_check,const char *_proxy_host,unsigned _proxy_port,
|
|
const char *_proxy_user,const char *_proxy_pass,OpusServerInfo *_info){
|
|
const char *path;
|
|
/*Check to see if this is a valid file: URL.*/
|
|
path=op_parse_file_url(_url);
|
|
if(path!=NULL){
|
|
char *unescaped_path;
|
|
void *ret;
|
|
unescaped_path=op_string_dup(path);
|
|
if(OP_UNLIKELY(unescaped_path==NULL))return NULL;
|
|
ret=op_fopen(_cb,op_unescape_url_component(unescaped_path),"rb");
|
|
_ogg_free(unescaped_path);
|
|
return ret;
|
|
}
|
|
#if defined(OP_ENABLE_HTTP)
|
|
/*If not, try http/https.*/
|
|
else{
|
|
OpusHTTPStream *stream;
|
|
int ret;
|
|
stream=(OpusHTTPStream *)_ogg_malloc(sizeof(*stream));
|
|
if(OP_UNLIKELY(stream==NULL))return NULL;
|
|
op_http_stream_init(stream);
|
|
ret=op_http_stream_open(stream,_url,_skip_certificate_check,
|
|
_proxy_host,_proxy_port,_proxy_user,_proxy_pass,_info);
|
|
if(OP_UNLIKELY(ret<0)){
|
|
op_http_stream_clear(stream);
|
|
_ogg_free(stream);
|
|
return NULL;
|
|
}
|
|
*_cb=*&OP_HTTP_CALLBACKS;
|
|
return stream;
|
|
}
|
|
#else
|
|
(void)_skip_certificate_check;
|
|
(void)_proxy_host;
|
|
(void)_proxy_port;
|
|
(void)_proxy_user;
|
|
(void)_proxy_pass;
|
|
(void)_info;
|
|
return NULL;
|
|
#endif
|
|
}
|
|
|
|
void *op_url_stream_vcreate(OpusFileCallbacks *_cb,
|
|
const char *_url,va_list _ap){
|
|
int skip_certificate_check;
|
|
const char *proxy_host;
|
|
opus_int32 proxy_port;
|
|
const char *proxy_user;
|
|
const char *proxy_pass;
|
|
OpusServerInfo *pinfo;
|
|
skip_certificate_check=0;
|
|
proxy_host=NULL;
|
|
proxy_port=8080;
|
|
proxy_user=NULL;
|
|
proxy_pass=NULL;
|
|
pinfo=NULL;
|
|
for(;;){
|
|
ptrdiff_t request;
|
|
request=va_arg(_ap,char *)-(char *)NULL;
|
|
/*If we hit NULL, we're done processing options.*/
|
|
if(!request)break;
|
|
switch(request){
|
|
case OP_SSL_SKIP_CERTIFICATE_CHECK_REQUEST:{
|
|
skip_certificate_check=!!va_arg(_ap,opus_int32);
|
|
}break;
|
|
case OP_HTTP_PROXY_HOST_REQUEST:{
|
|
proxy_host=va_arg(_ap,const char *);
|
|
}break;
|
|
case OP_HTTP_PROXY_PORT_REQUEST:{
|
|
proxy_port=va_arg(_ap,opus_int32);
|
|
if(proxy_port<0||proxy_port>(opus_int32)65535)return NULL;
|
|
}break;
|
|
case OP_HTTP_PROXY_USER_REQUEST:{
|
|
proxy_user=va_arg(_ap,const char *);
|
|
}break;
|
|
case OP_HTTP_PROXY_PASS_REQUEST:{
|
|
proxy_pass=va_arg(_ap,const char *);
|
|
}break;
|
|
case OP_GET_SERVER_INFO_REQUEST:{
|
|
pinfo=va_arg(_ap,OpusServerInfo *);
|
|
}break;
|
|
/*Some unknown option.*/
|
|
default:return NULL;
|
|
}
|
|
}
|
|
/*If the caller has requested server information, proxy it to a local copy to
|
|
simplify error handling.*/
|
|
if(pinfo!=NULL){
|
|
OpusServerInfo info;
|
|
void *ret;
|
|
opus_server_info_init(&info);
|
|
ret=op_url_stream_create_impl(_cb,_url,skip_certificate_check,
|
|
proxy_host,proxy_port,proxy_user,proxy_pass,&info);
|
|
if(ret!=NULL)*pinfo=*&info;
|
|
else opus_server_info_clear(&info);
|
|
return ret;
|
|
}
|
|
return op_url_stream_create_impl(_cb,_url,skip_certificate_check,
|
|
proxy_host,proxy_port,proxy_user,proxy_pass,NULL);
|
|
}
|
|
|
|
void *op_url_stream_create(OpusFileCallbacks *_cb,
|
|
const char *_url,...){
|
|
va_list ap;
|
|
void *ret;
|
|
va_start(ap,_url);
|
|
ret=op_url_stream_vcreate(_cb,_url,ap);
|
|
va_end(ap);
|
|
return ret;
|
|
}
|
|
|
|
/*Convenience routines to open/test URLs in a single step.*/
|
|
|
|
OggOpusFile *op_vopen_url(const char *_url,int *_error,va_list _ap){
|
|
OpusFileCallbacks cb;
|
|
OggOpusFile *of;
|
|
void *source;
|
|
source=op_url_stream_vcreate(&cb,_url,_ap);
|
|
if(OP_UNLIKELY(source==NULL)){
|
|
if(_error!=NULL)*_error=OP_EFAULT;
|
|
return NULL;
|
|
}
|
|
of=op_open_callbacks(source,&cb,NULL,0,_error);
|
|
if(OP_UNLIKELY(of==NULL))(*cb.close)(source);
|
|
return of;
|
|
}
|
|
|
|
OggOpusFile *op_open_url(const char *_url,int *_error,...){
|
|
OggOpusFile *ret;
|
|
va_list ap;
|
|
va_start(ap,_error);
|
|
ret=op_vopen_url(_url,_error,ap);
|
|
va_end(ap);
|
|
return ret;
|
|
}
|
|
|
|
OggOpusFile *op_vtest_url(const char *_url,int *_error,va_list _ap){
|
|
OpusFileCallbacks cb;
|
|
OggOpusFile *of;
|
|
void *source;
|
|
source=op_url_stream_vcreate(&cb,_url,_ap);
|
|
if(OP_UNLIKELY(source==NULL)){
|
|
if(_error!=NULL)*_error=OP_EFAULT;
|
|
return NULL;
|
|
}
|
|
of=op_test_callbacks(source,&cb,NULL,0,_error);
|
|
if(OP_UNLIKELY(of==NULL))(*cb.close)(source);
|
|
return of;
|
|
}
|
|
|
|
OggOpusFile *op_test_url(const char *_url,int *_error,...){
|
|
OggOpusFile *ret;
|
|
va_list ap;
|
|
va_start(ap,_error);
|
|
ret=op_vtest_url(_url,_error,ap);
|
|
va_end(ap);
|
|
return ret;
|
|
}
|